Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,17 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
createReturnStatement = t.returnStatement;

createTaggedTemplate(tag: t.Expression, template: TemplateLiteral<t.Expression>): t.Expression {
return t.taggedTemplateExpression(tag, this.createTemplateLiteral(template));
}

createTemplateLiteral(template: TemplateLiteral<t.Expression>): t.TemplateLiteral {
const elements = template.elements.map((element, i) =>
this.setSourceMapRange(
t.templateElement(element, i === template.elements.length - 1),
element.range,
),
);
return t.taggedTemplateExpression(tag, t.templateLiteral(elements, template.expressions));
return t.templateLiteral(elements, template.expressions);
}

createThrowStatement = t.throwStatement;
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ export interface AstFactory<TStatement, TExpression> {
*/
createTaggedTemplate(tag: TExpression, template: TemplateLiteral<TExpression>): TExpression;

/**
* Create an untagged template literal
*
* ```
* `str1${expr1}str2${expr2}str3`
* ```
*
* @param template the collection of strings and expressions that constitute an interpolated
* template literal.
*/
createTemplateLiteral(template: TemplateLiteral<TExpression>): TExpression;

/**
* Create a throw statement (e.g. `throw expr;`).
*
Expand Down
43 changes: 32 additions & 11 deletions packages/compiler-cli/src/ngtsc/translator/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,19 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
);
}

visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression {
visitTaggedTemplateLiteralExpr(ast: o.TaggedTemplateLiteralExpr, context: Context): TExpression {
return this.setSourceMapRange(
this.createTaggedTemplateExpression(ast.tag.visitExpression(this, context), {
elements: ast.template.elements.map((e) =>
createTemplateElement({
cooked: e.text,
raw: e.rawText,
range: e.sourceSpan ?? ast.sourceSpan,
}),
),
expressions: ast.template.expressions.map((e) => e.visitExpression(this, context)),
}),
this.createTaggedTemplateExpression(
ast.tag.visitExpression(this, context),
this.getTemplateLiteralFromAst(ast.template, context),
),
ast.sourceSpan,
);
}

visitTemplateLiteralExpr(ast: o.TemplateLiteralExpr, context: Context): TExpression {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't bother implementing the downleveling code here, because all the browsers we support also support template literals natively.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. There wasn't downleveling here before anyway right? (even with tagged template literals)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the tagged ones we only downleveled "known" ones like $localize and not in a standards-based way.

return this.setSourceMapRange(
this.factory.createTemplateLiteral(this.getTemplateLiteralFromAst(ast, context)),
ast.sourceSpan,
);
}
Expand Down Expand Up @@ -433,6 +434,10 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
throw new Error('Method not implemented.');
}

visitTemplateLiteralElementExpr(ast: o.TemplateLiteralElementExpr, context: any) {
throw new Error('Method not implemented');
}

visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, _context: Context): any {
this.recordWrappedNode(ast);
return ast.node;
Expand Down Expand Up @@ -474,6 +479,22 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
}
return statement;
}

private getTemplateLiteralFromAst(
ast: o.TemplateLiteralExpr,
context: Context,
): TemplateLiteral<TExpression> {
return {
elements: ast.elements.map((e) =>
createTemplateElement({
cooked: e.text,
raw: e.rawText,
range: e.sourceSpan ?? ast.sourceSpan,
}),
),
expressions: ast.expressions.map((e) => e.visitExpression(this, context)),
};
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,15 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
throw new Error('Method not implemented.');
}

visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): never {
visitTaggedTemplateLiteralExpr(ast: o.TaggedTemplateLiteralExpr, context: Context): never {
throw new Error('Method not implemented.');
}

visitTemplateLiteralExpr(ast: o.TemplateLiteralExpr, context: any) {
throw new Error('Method not implemented.');
}

visitTemplateLiteralElementExpr(ast: o.TemplateLiteralElementExpr, context: any) {
throw new Error('Method not implemented.');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express
tag: ts.Expression,
template: TemplateLiteral<ts.Expression>,
): ts.Expression {
return ts.factory.createTaggedTemplateExpression(
tag,
undefined,
this.createTemplateLiteral(template),
);
}

createTemplateLiteral(template: TemplateLiteral<ts.Expression>): ts.TemplateLiteral {
let templateLiteral: ts.TemplateLiteral;
const length = template.elements.length;
const head = template.elements[0];
Expand Down Expand Up @@ -276,7 +284,7 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express
if (head.range !== null) {
this.setSourceMapRange(templateLiteral, head.range);
}
return ts.factory.createTaggedTemplateExpression(tag, undefined, templateLiteral);
return templateLiteral;
}

createThrowStatement = ts.factory.createThrowStatement;
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
SafePropertyRead,
ThisReceiver,
Unary,
TemplateLiteral,
TemplateLiteralElement,
} from '@angular/compiler';
import ts from 'typescript';

Expand Down Expand Up @@ -445,6 +447,34 @@ class AstTranslator implements AstVisitor {
return node;
}

visitTemplateLiteral(ast: TemplateLiteral): ts.TemplateLiteral {
const length = ast.elements.length;
const head = ast.elements[0];
let result: ts.TemplateLiteral;

if (length === 1) {
result = ts.factory.createNoSubstitutionTemplateLiteral(head.text);
} else {
const spans: ts.TemplateSpan[] = [];
const tailIndex = length - 1;

for (let i = 1; i < tailIndex; i++) {
const middle = ts.factory.createTemplateMiddle(ast.elements[i].text);
spans.push(ts.factory.createTemplateSpan(this.translate(ast.expressions[i - 1]), middle));
}
const resolvedExpression = this.translate(ast.expressions[tailIndex - 1]);
const templateTail = ts.factory.createTemplateTail(ast.elements[tailIndex].text);
spans.push(ts.factory.createTemplateSpan(resolvedExpression, templateTail));
result = ts.factory.createTemplateExpression(ts.factory.createTemplateHead(head.text), spans);
}

return result;
}

visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
throw new Error('Method not implemented');
}

private convertToSafeCall(
ast: Call | SafeCall,
expr: ts.Expression,
Expand Down Expand Up @@ -567,4 +597,10 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
visitSafeKeyedRead(ast: SafeKeyedRead): boolean {
return false;
}
visitTemplateLiteral(ast: TemplateLiteral, context: any) {
return false;
}
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('_t2(1)');
});

it('should handle template literals', () => {
expect(tcb('{{ `hello world` }}')).toContain('"" + (`hello world`);');
expect(tcb('{{ `hello \\${name}!!!` }}')).toContain('"" + (`hello \\${name}!!!`);');
expect(tcb('{{ `${a} - ${b} - ${c}` }}')).toContain(
'"" + (`${((this).a)} - ${((this).b)} - ${((this).c)}`);',
);
});

describe('type constructors', () => {
it('should handle missing property bindings', () => {
const TEMPLATE = `<div dir [inputA]="foo"></div>`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,60 @@ export declare class MyModule {
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}

/****************************************************************************************************
* PARTIAL FILE: template_literals.js
****************************************************************************************************/
import { Component, Pipe } from '@angular/core';
import * as i0 from "@angular/core";
export class UppercasePipe {
transform(value) {
return value.toUpperCase();
}
}
UppercasePipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
UppercasePipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, isStandalone: true, name: "uppercase" });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, decorators: [{
type: Pipe,
args: [{ name: 'uppercase' }]
}] });
export class MyApp {
constructor() {
this.name = 'Frodo';
this.timeOfDay = 'morning';
}
}
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "my-app", ngImport: i0, template: `
<div>No interpolations: {{ \`hello world \` }}</div>
<span>With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
<p>With pipe: {{\`hello \${name}\` | uppercase}}</p>
`, 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,
args: [{
selector: 'my-app',
template: `
<div>No interpolations: {{ \`hello world \` }}</div>
<span>With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
<p>With pipe: {{\`hello \${name}\` | uppercase}}</p>
`,
imports: [UppercasePipe],
}]
}] });

/****************************************************************************************************
* PARTIAL FILE: template_literals.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class UppercasePipe {
transform(value: string): string;
static ɵfac: i0.ɵɵFactoryDeclaration<UppercasePipe, never>;
static ɵpipe: i0.ɵɵPipeDeclaration<UppercasePipe, "uppercase", true>;
}
export declare class MyApp {
name: string;
timeOfDay: string;
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, true, never>;
}

Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@
]
}
]
},
{
"description": "should support template literals",
"inputFiles": [
"template_literals.ts"
],
"expectations": [
{
"failureMessage": "Invalid template literal binding",
"files": [
"template_literals.js"
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
if (rf & 2) {
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate1("No interpolations: ", `hello world `, "");
$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}`), "");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Component, Pipe} from '@angular/core';

@Pipe({name: 'uppercase'})
export class UppercasePipe {
transform(value: string) {
return value.toUpperCase();
}
}

@Component({
selector: 'my-app',
template: `
<div>No interpolations: {{ \`hello world \` }}</div>
<span>With interpolations: {{ \`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
<p>With pipe: {{\`hello \${name}\` | uppercase}}</p>
`,
imports: [UppercasePipe],
})
export class MyApp {
name = 'Frodo';
timeOfDay = 'morning';
}
52 changes: 52 additions & 0 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3352,6 +3352,58 @@ runInEachFileSystem(() => {
expect(diags.length).toBe(0);
});

describe('template literals', () => {
it('should treat template literals as strings', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';

@Component({
template: 'Result: {{getValue(\`foo\`)}}',
standalone: true,
})
export class Main {
getValue(value: number) {
return value;
}
}
`,
);

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'.`,
);
});

it('should check interpolations inside template literals', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';

@Component({
template: '{{\`Hello \${getName(123)}\`}}',
standalone: true,
})
export class Main {
getName(value: string) {
return value;
}
}
`,
);

const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
);
});
});

describe('legacy schema checking with the DOM schema', () => {
beforeEach(() => {
env.tsconfig({fullTemplateTypeCheck: false});
Expand Down
Loading
Loading