fix(compiler-cli): escape template literal in TCB

Fixes a regression introduced by #67381 where we weren't escaping backticks in template literals.

Fixes #67675.
This commit is contained in:
Kristiyan Kostadinov
2026-03-15 22:27:34 +01:00
committed by Matthew Beck (Berry)
parent dc0446552a
commit 2bd708fb6b
3 changed files with 43 additions and 4 deletions
@@ -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, '$\\{');
}
}
/**
@@ -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"))');
@@ -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', () => {