From b4eb016e561b1fd9b5395318553a3dfa09f9611c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 31 Aug 2020 16:27:44 +0100 Subject: [PATCH] fix(compiler-cli): compute source-mappings for localized strings (#38747) Previously, localized strings had very limited or incorrect source-mapping information available. Now the i18n AST nodes and related output AST nodes include source-span information about message-parts and placeholders - including closing tag placeholders. This information is then used when generating the final localized string ASTs to ensure that the correct source-mapping is rendered. See #38588 (comment) PR Close #38747 --- .../src/ngtsc/translator/src/translator.ts | 225 ++++++++++-------- .../test/ngtsc/template_mapping_spec.ts | 87 ++++++- packages/compiler/src/i18n/i18n_ast.ts | 6 +- packages/compiler/src/i18n/i18n_parser.ts | 4 +- packages/compiler/src/i18n/message_bundle.ts | 2 +- .../src/render3/view/i18n/localize_utils.ts | 4 +- .../test/i18n/serializers/i18n_ast_spec.ts | 2 +- 7 files changed, 217 insertions(+), 113 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 195cae9f67009..6a24078d3b186 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast'; import * as ts from 'typescript'; @@ -212,7 +212,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor visitReadVarExpr(ast: ReadVarExpr, context: Context): ts.Identifier { const identifier = ts.createIdentifier(ast.name!); - this.setSourceMapRange(identifier, ast); + this.setSourceMapRange(identifier, ast.sourceSpan); return identifier; } @@ -244,7 +244,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor const call = ts.createCall( ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined, ast.args.map(arg => arg.visitExpression(this, context))); - this.setSourceMapRange(call, ast); + this.setSourceMapRange(call, ast.sourceSpan); return call; } @@ -255,7 +255,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor if (ast.pure) { ts.addSyntheticLeadingComment(expr, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false); } - this.setSourceMapRange(expr, ast); + this.setSourceMapRange(expr, ast.sourceSpan); return expr; } @@ -274,15 +274,15 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor } else { expr = ts.createLiteral(ast.value); } - this.setSourceMapRange(expr, ast); + this.setSourceMapRange(expr, ast.sourceSpan); return expr; } visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression { const localizedString = this.scriptTarget >= ts.ScriptTarget.ES2015 ? - createLocalizedStringTaggedTemplate(ast, context, this) : - createLocalizedStringFunctionCall(ast, context, this, this.imports); - this.setSourceMapRange(localizedString, ast); + this.createLocalizedStringTaggedTemplate(ast, context) : + this.createLocalizedStringFunctionCall(ast, context); + this.setSourceMapRange(localizedString, ast.sourceSpan); return localizedString; } @@ -395,7 +395,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): ts.ArrayLiteralExpression { const expr = ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context))); - this.setSourceMapRange(expr, ast); + this.setSourceMapRange(expr, ast.sourceSpan); return expr; } @@ -405,7 +405,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key), entry.value.visitExpression(this, context))); const expr = ts.createObjectLiteral(entries); - this.setSourceMapRange(expr, ast); + this.setSourceMapRange(expr, ast.sourceSpan); return expr; } @@ -424,9 +424,111 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor return ts.createTypeOf(ast.expr.visitExpression(this, context)); } - private setSourceMapRange(expr: ts.Expression, ast: Expression) { - if (ast.sourceSpan) { - const {start, end} = ast.sourceSpan; + /** + * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted + * output. + */ + private createLocalizedStringTaggedTemplate(ast: LocalizedString, context: Context): + ts.TaggedTemplateExpression { + let template: ts.TemplateLiteral; + const length = ast.messageParts.length; + const metaBlock = ast.serializeI18nHead(); + if (length === 1) { + template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw); + this.setSourceMapRange(template, ast.getMessagePartSourceSpan(0)); + } else { + // Create the head part + const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw); + this.setSourceMapRange(head, ast.getMessagePartSourceSpan(0)); + const spans: ts.TemplateSpan[] = []; + // Create the middle parts + for (let i = 1; i < length - 1; i++) { + const resolvedExpression = ast.expressions[i - 1].visitExpression(this, context); + this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(i - 1)); + const templatePart = ast.serializeI18nTemplatePart(i); + const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw); + this.setSourceMapRange(templateMiddle, ast.getMessagePartSourceSpan(i)); + const templateSpan = ts.createTemplateSpan(resolvedExpression, templateMiddle); + spans.push(templateSpan); + } + // Create the tail part + const resolvedExpression = ast.expressions[length - 2].visitExpression(this, context); + this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(length - 2)); + const templatePart = ast.serializeI18nTemplatePart(length - 1); + const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw); + this.setSourceMapRange(templateTail, ast.getMessagePartSourceSpan(length - 1)); + spans.push(ts.createTemplateSpan(resolvedExpression, templateTail)); + // Put it all together + template = ts.createTemplateExpression(head, spans); + } + const expression = ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); + this.setSourceMapRange(expression, ast.sourceSpan); + return expression; + } + + /** + * Translate the `LocalizedString` node into a `$localize` call using the imported + * `__makeTemplateObject` helper for ES5 formatted output. + */ + private createLocalizedStringFunctionCall(ast: LocalizedString, context: Context) { + // A `$localize` message consists `messageParts` and `expressions`, which get interleaved + // together. The interleaved pieces look like: + // `[messagePart0, expression0, messagePart1, expression1, messagePart2]` + // + // Note that there is always a message part at the start and end, and so therefore + // `messageParts.length === expressions.length + 1`. + // + // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters. + // The metadata is attached to the first and subsequent message parts by calls to + // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively. + + // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` + // array. + const messageParts = [ast.serializeI18nHead()]; + const expressions: any[] = []; + + // The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed + // into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]` + for (let i = 1; i < ast.messageParts.length; i++) { + expressions.push(ast.expressions[i - 1].visitExpression(this, context)); + messageParts.push(ast.serializeI18nTemplatePart(i)); + } + + // The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()` + // helper, so we must ensure it has been imported. + const {moduleImport, symbol} = + this.imports.generateNamedImport('tslib', '__makeTemplateObject'); + const __makeTemplateObjectHelper = (moduleImport === null) ? + ts.createIdentifier(symbol) : + ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol)); + + // Generate the call in the form: + // `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);` + const cookedLiterals = messageParts.map( + (messagePart, i) => + this.createLiteral(messagePart.cooked, ast.getMessagePartSourceSpan(i))); + const rawLiterals = messageParts.map( + (messagePart, i) => this.createLiteral(messagePart.raw, ast.getMessagePartSourceSpan(i))); + return ts.createCall( + /* expression */ ts.createIdentifier('$localize'), + /* typeArguments */ undefined, + /* argumentsArray */[ + ts.createCall( + /* expression */ __makeTemplateObjectHelper, + /* typeArguments */ undefined, + /* argumentsArray */ + [ + ts.createArrayLiteral(cookedLiterals), + ts.createArrayLiteral(rawLiterals), + ]), + ...expressions, + ]); + } + + + private setSourceMapRange(expr: ts.Node, sourceSpan: ParseSourceSpan|null) { + if (sourceSpan) { + const {start, end} = sourceSpan; const {url, content} = start.file; if (url) { if (!this.externalSourceFiles.has(url)) { @@ -437,6 +539,12 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor } } } + + private createLiteral(text: string, span: ParseSourceSpan|null) { + const literal = ts.createStringLiteral(text); + this.setSourceMapRange(literal, span); + return literal; + } } export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { @@ -662,40 +770,6 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { } } -/** - * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted - * output. - */ -function createLocalizedStringTaggedTemplate( - ast: LocalizedString, context: Context, visitor: ExpressionVisitor) { - let template: ts.TemplateLiteral; - const length = ast.messageParts.length; - const metaBlock = ast.serializeI18nHead(); - if (length === 1) { - template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw); - } else { - // Create the head part - const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw); - const spans: ts.TemplateSpan[] = []; - // Create the middle parts - for (let i = 1; i < length - 1; i++) { - const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context); - const templatePart = ast.serializeI18nTemplatePart(i); - const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw); - spans.push(ts.createTemplateSpan(resolvedExpression, templateMiddle)); - } - // Create the tail part - const resolvedExpression = ast.expressions[length - 2].visitExpression(visitor, context); - const templatePart = ast.serializeI18nTemplatePart(length - 1); - const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw); - spans.push(ts.createTemplateSpan(resolvedExpression, templateTail)); - // Put it all together - template = ts.createTemplateExpression(head, spans); - } - return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); -} - - // HACK: Use this in place of `ts.createTemplateMiddle()`. // Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle { @@ -710,59 +784,4 @@ function createTemplateTail(cooked: string, raw: string): ts.TemplateTail { const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw); (node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail; return node as ts.TemplateTail; -} - -/** - * Translate the `LocalizedString` node into a `$localize` call using the imported - * `__makeTemplateObject` helper for ES5 formatted output. - */ -function createLocalizedStringFunctionCall( - ast: LocalizedString, context: Context, visitor: ExpressionVisitor, imports: ImportManager) { - // A `$localize` message consists `messageParts` and `expressions`, which get interleaved - // together. The interleaved pieces look like: - // `[messagePart0, expression0, messagePart1, expression1, messagePart2]` - // - // Note that there is always a message part at the start and end, and so therefore - // `messageParts.length === expressions.length + 1`. - // - // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters. - // The metadata is attached to the first and subsequent message parts by calls to - // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively. - - // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` array. - const messageParts = [ast.serializeI18nHead()]; - const expressions: any[] = []; - - // The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed - // into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]` - for (let i = 1; i < ast.messageParts.length; i++) { - expressions.push(ast.expressions[i - 1].visitExpression(visitor, context)); - messageParts.push(ast.serializeI18nTemplatePart(i)); - } - - // The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()` - // helper, so we must ensure it has been imported. - const {moduleImport, symbol} = imports.generateNamedImport('tslib', '__makeTemplateObject'); - const __makeTemplateObjectHelper = (moduleImport === null) ? - ts.createIdentifier(symbol) : - ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol)); - - // Generate the call in the form: - // `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);` - return ts.createCall( - /* expression */ ts.createIdentifier('$localize'), - /* typeArguments */ undefined, - /* argumentsArray */[ - ts.createCall( - /* expression */ __makeTemplateObjectHelper, - /* typeArguments */ undefined, - /* argumentsArray */ - [ - ts.createArrayLiteral( - messageParts.map(messagePart => ts.createStringLiteral(messagePart.cooked))), - ts.createArrayLiteral( - messageParts.map(messagePart => ts.createStringLiteral(messagePart.raw))), - ]), - ...expressions, - ]); -} +} \ No newline at end of file diff --git a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts index c6865f8efac6f..b93026c79e6b0 100644 --- a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts @@ -24,7 +24,7 @@ runInEachFileSystem((os) => { beforeEach(() => { env = NgtscTestEnvironment.setup(testFiles); - env.tsconfig(); + env.tsconfig({sourceMap: true, target: 'es2015', enableI18nLegacyMessageIdFormat: false}); }); describe('Inline templates', () => { @@ -360,6 +360,90 @@ runInEachFileSystem((os) => { }); }); + describe('$localize', () => { + it('should create simple i18n message source-mapping', () => { + const mappings = compileAndMap(`
Hello, World!
`); + expect(mappings).toContain({ + source: '
', + generated: 'i0.ɵɵelementStart(0, "div")', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: 'Hello, World!', + generated: '`Hello, World!`', + sourceUrl: '../test.ts', + }); + }); + + it('should create placeholder source-mappings', () => { + const mappings = compileAndMap(`
Hello, {{name}}!
`); + expect(mappings).toContain({ + source: '
', + generated: 'i0.ɵɵelementStart(0, "div")', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '
', + generated: 'i0.ɵɵelementEnd()', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: 'Hello, ', + generated: '`Hello, ${', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '{{name}}', + generated: '"\\uFFFD0\\uFFFD"', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '!', + generated: '}:INTERPOLATION:!`', + sourceUrl: '../test.ts', + }); + }); + + it('should create tag (container) placeholder source-mappings', () => { + const mappings = compileAndMap(`
Hello, World!
`); + expect(mappings).toContain({ + source: '
', + generated: 'i0.ɵɵelementStart(0, "div")', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '
', + generated: 'i0.ɵɵelementEnd()', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: 'Hello, ', + generated: '`Hello, ${', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '', + generated: '"\\uFFFD#2\\uFFFD"', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: 'World', + generated: '}:START_BOLD_TEXT:World${', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '', + generated: '"\\uFFFD/#2\\uFFFD"', + sourceUrl: '../test.ts', + }); + expect(mappings).toContain({ + source: '!', + generated: '}:CLOSE_BOLD_TEXT:!`', + sourceUrl: '../test.ts', + }); + }); + }); + it('should create (simple string) inline template source-mapping', () => { const mappings = compileAndMap('
this is a test
{{ 1 + 2 }}
'); @@ -520,7 +604,6 @@ runInEachFileSystem((os) => { function compileAndMap(template: string, templateUrl: string|null = null) { const templateConfig = templateUrl ? `templateUrl: '${templateUrl}'` : ('template: `' + template.replace(/`/g, '\\`') + '`'); - env.tsconfig({sourceMap: true}); env.write('test.ts', ` import {Component} from '@angular/core'; diff --git a/packages/compiler/src/i18n/i18n_ast.ts b/packages/compiler/src/i18n/i18n_ast.ts index 0f7c412205bb0..9bd6259bd30cc 100644 --- a/packages/compiler/src/i18n/i18n_ast.ts +++ b/packages/compiler/src/i18n/i18n_ast.ts @@ -87,7 +87,9 @@ export class TagPlaceholder implements Node { constructor( public tag: string, public attrs: {[k: string]: string}, public startName: string, public closeName: string, public children: Node[], public isVoid: boolean, - public sourceSpan: ParseSourceSpan, public closeSourceSpan: ParseSourceSpan|null) {} + // TODO sourceSpan should cover all (we need a startSourceSpan and endSourceSpan) + public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null, + public endSourceSpan: ParseSourceSpan|null) {} visit(visitor: Visitor, context?: any): any { return visitor.visitTagPlaceholder(this, context); @@ -152,7 +154,7 @@ export class CloneVisitor implements Visitor { const children = ph.children.map(n => n.visit(this, context)); return new TagPlaceholder( ph.tag, ph.attrs, ph.startName, ph.closeName, children, ph.isVoid, ph.sourceSpan, - ph.closeSourceSpan); + ph.startSourceSpan, ph.endSourceSpan); } visitPlaceholder(ph: Placeholder, context?: any): Placeholder { diff --git a/packages/compiler/src/i18n/i18n_parser.ts b/packages/compiler/src/i18n/i18n_parser.ts index 923a9d8dff628..e8d4d7f54c80f 100644 --- a/packages/compiler/src/i18n/i18n_parser.ts +++ b/packages/compiler/src/i18n/i18n_parser.ts @@ -93,8 +93,8 @@ class _I18nVisitor implements html.Visitor { } const node = new i18n.TagPlaceholder( - el.name, attrs, startPhName, closePhName, children, isVoid, el.startSourceSpan!, - el.endSourceSpan); + el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan, + el.startSourceSpan, el.endSourceSpan); return context.visitNodeFn(el, node); } diff --git a/packages/compiler/src/i18n/message_bundle.ts b/packages/compiler/src/i18n/message_bundle.ts index 036b6d45fb175..b3fa5895384b8 100644 --- a/packages/compiler/src/i18n/message_bundle.ts +++ b/packages/compiler/src/i18n/message_bundle.ts @@ -95,7 +95,7 @@ class MapPlaceholderNames extends i18n.CloneVisitor { const children = ph.children.map(n => n.visit(this, mapper)); return new i18n.TagPlaceholder( ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan, - ph.closeSourceSpan); + ph.startSourceSpan, ph.endSourceSpan); } visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder { diff --git a/packages/compiler/src/render3/view/i18n/localize_utils.ts b/packages/compiler/src/render3/view/i18n/localize_utils.ts index 4c4f29eff27db..444139ecacb61 100644 --- a/packages/compiler/src/render3/view/i18n/localize_utils.ts +++ b/packages/compiler/src/render3/view/i18n/localize_utils.ts @@ -48,10 +48,10 @@ class LocalizeSerializerVisitor implements i18n.Visitor { } visitTagPlaceholder(ph: i18n.TagPlaceholder, context: o.MessagePiece[]): any { - context.push(this.createPlaceholderPiece(ph.startName, ph.sourceSpan)); + context.push(this.createPlaceholderPiece(ph.startName, ph.startSourceSpan ?? ph.sourceSpan)); if (!ph.isVoid) { ph.children.forEach(child => child.visit(this, context)); - context.push(this.createPlaceholderPiece(ph.closeName, ph.closeSourceSpan ?? ph.sourceSpan)); + context.push(this.createPlaceholderPiece(ph.closeName, ph.endSourceSpan ?? ph.sourceSpan)); } } diff --git a/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts b/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts index 910bb735334f2..7b2fe3b7b715e 100644 --- a/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts +++ b/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts @@ -41,7 +41,7 @@ import {_extractMessages} from '../i18n_parser_spec'; new i18n.IcuPlaceholder(null!, '', null!), ], null!); - const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null!, null); + const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null!, null, null); const icu = new i18n.Icu('', '', {tag}, null!); icu.visit(visitor);