diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 4cf410fbf4a..c6a6b83ebe4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -351,17 +351,19 @@ class TcbExprTranslator implements AstVisitor { let result: string; if (length === 1) { - result = `\`${head.text}\``; + result = `\`${this.escapeTemplateLiteral(head.text)}\``; } else { - let parts = [`\`${head.text}`]; + let parts = [`\`${this.escapeTemplateLiteral(head.text)}`]; const tailIndex = length - 1; for (let i = 1; i < tailIndex; i++) { const expr = this.translate(ast.expressions[i - 1]); - parts.push(`\${${expr.print()}}${ast.elements[i].text}`); + parts.push(`\${${expr.print()}}${this.escapeTemplateLiteral(ast.elements[i].text)}`); } const resolvedExpression = this.translate(ast.expressions[tailIndex - 1]); - parts.push(`\${${resolvedExpression.print()}}${ast.elements[tailIndex].text}\``); + parts.push( + `\${${resolvedExpression.print()}}${this.escapeTemplateLiteral(ast.elements[tailIndex].text)}\``, + ); result = parts.join(''); } return new TcbExpr(result); @@ -446,6 +448,10 @@ class TcbExprTranslator implements AstVisitor { // (a!.method(...) as any) return new TcbExpr(`(${expr}!(${args}) as any)`); } + + private escapeTemplateLiteral(value: string) { + return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '$\\{'); + } } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 007ee7dde44..317e98fc360 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -220,6 +220,7 @@ describe('type check blocks', () => { expect(tcb('{{ `${a} - ${b} - ${c}` }}')).toContain( '"" + (`${((this).a)} - ${((this).b)} - ${((this).c)}`);', ); + expect(tcb('{{ `a${`hello ${name}`}b` }}')).toContain('(`a${`hello ${((this).name)}`}b`)'); }); it('should handle tagged template literals', () => { @@ -232,6 +233,13 @@ describe('type check blocks', () => { ); }); + it('should escape characters in template literals', () => { + expect(tcb('{{ `a\\`b` }}')).toContain('(`a\\`b`)'); + expect(tcb('{{ `a\\${b}` }}')).toContain('(`a$\\{b}`)'); + expect(tcb('{{ `a\\\\b` }}')).toContain('(`a\\\\b`)'); + expect(tcb('{{ `a\\\`${middle}\\\`b` }}')).toContain('(`a\\\`${((this).middle)}\\\`b`)'); + }); + it('should generate regular expressions', () => { const TEMPLATE = '{{ /\\d+/g.test("123") }}'; expect(tcb(TEMPLATE)).toContain('(((/\\d+/g)).test("123"))'); diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 7e812496311..f1982b876c0 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -3484,6 +3484,31 @@ runInEachFileSystem(() => { `Argument of type 'number' is not assignable to parameter of type 'string'.`, ); }); + + it('should check template literals with escaped characters', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + @Component({ + template: '{{\\\`Hello \\\\\`\${check(name)}\\\\\`\\\`}}', + }) + export class Main { + name = 'test'; + check(input: number): string { + return String(input); + } + } + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toBe( + `Argument of type 'string' is not assignable to parameter of type 'number'.`, + ); + }); }); describe('tagged template literals', () => {