From d84a79c9927aa8d93998ddae5c6e5d244a1492d4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 24 Mar 2025 14:34:33 +0100 Subject: [PATCH] fix(compiler): error if rawText isn't estimated correctly The `TemplateLiteralElementExpr` has some logic where it tries to estimate the `rawText` if one isn't provided by looking at the node's source span. The problem with this approach is that we have some long-standing issues with our expression AST parser (see https://github.com/angular/angular/pull/60267#discussion_r1986402524) where it might not produce accurate spans if escape sequences are involved. This in turn can lead to unrecoverable errors, because TypeScript will throw an error if the raw string doesn't match the cooked one when constructing a TypeScript AST node. These changes remove the logic that depends on the source span and relies purely on the secondary fallback that inserts escaped characters manually. It's also worth noting that the `rawText` doesn't seem to matter much at this point, because the main usage of it is when downlevelling template literals to ES5 which we no longer support. Fixes #60528. --- .../value_composition/GOLDEN_PARTIAL.js | 2 ++ .../value_composition/template_literals.js | 5 ++++- .../value_composition/template_literals.ts | 1 + packages/compiler/src/output/output_ast.ts | 13 ++++++------- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js index 31a087d83051..3112b22cbba9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js @@ -750,6 +750,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-
No interpolations: {{ \`hello world \` }}
With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}

With pipe: {{\`hello \${name}\` | uppercase}}

+

@let insideLet = \`Hello \${name}\`; Inside let: {{insideLet}}

`, isInline: true, dependencies: [{ kind: "pipe", type: UppercasePipe, name: "uppercase" }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ type: Component, @@ -759,6 +760,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
No interpolations: {{ \`hello world \` }}
With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}

With pipe: {{\`hello \${name}\` | uppercase}}

+

@let insideLet = \`Hello \${name}\`; Inside let: {{insideLet}}

`, imports: [UppercasePipe], }] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.js index 3e0e315e44d0..835cee887e78 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.js @@ -4,5 +4,8 @@ if (rf & 2) { $r3$.ɵɵadvance(2); $r3$.ɵɵtextInterpolate1("With interpolations: ", `hello ${ctx.name}, it is currently ${ctx.timeOfDay}!`); $r3$.ɵɵadvance(2); - $r3$.ɵɵtextInterpolate1("With pipe: ", $r3$.ɵɵpipeBind1(6, 3, `hello ${ctx.name}`)); + $r3$.ɵɵtextInterpolate1("With pipe: ", $r3$.ɵɵpipeBind1(6, 4, `hello ${ctx.name}`)); + const $insideLet_r1$ = `Hello ${ctx.name}`; + $r3$.ɵɵadvance(4); + $r3$.ɵɵtextInterpolate1(" Inside let: ", $insideLet_r1$); } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.ts index 70ef4e7044ee..8b0a3b3a179c 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/template_literals.ts @@ -13,6 +13,7 @@ export class UppercasePipe {
No interpolations: {{ \`hello world \` }}
With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}

With pipe: {{\`hello \${name}\` | uppercase}}

+

@let insideLet = \`Hello \${name}\`; Inside let: {{insideLet}}

`, imports: [UppercasePipe], }) diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index aba3d5c1fd33..6085b20590ef 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -705,23 +705,22 @@ export class TemplateLiteralExpr extends Expression { } } export class TemplateLiteralElementExpr extends Expression { - rawText: string; + readonly rawText: string; constructor( - public text: string, + readonly text: string, sourceSpan?: ParseSourceSpan | null, rawText?: string, ) { super(STRING_TYPE, sourceSpan); - // If `rawText` is not provided, try to extract the raw string from its - // associated `sourceSpan`. If that is also not available, "fake" the raw - // string instead by escaping the following control sequences: + // If `rawText` is not provided, "fake" the raw string by escaping the following sequences: // - "\" would otherwise indicate that the next character is a control character. // - "`" and "${" are template string control sequences that would otherwise prematurely // indicate the end of the template literal element. - this.rawText = - rawText ?? sourceSpan?.toString() ?? escapeForTemplateLiteral(escapeSlashes(text)); + // Note that we can't rely on the `sourceSpan` here, because it may be incorrect (see + // https://github.com/angular/angular/pull/60267#discussion_r1986402524). + this.rawText = rawText ?? escapeForTemplateLiteral(escapeSlashes(text)); } override visitExpression(visitor: ExpressionVisitor, context: any) {