Skip to content

Commit 989a8e1

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(compiler): implement let declarations in render3 ast (#55848)
Introduces a new `LetDeclaration` into the Render3 AST, simiarly to the HTML AST, and adds an initial integration into the various visitors. PR Close #55848
1 parent d44c8ee commit 989a8e1

File tree

12 files changed

+127
-5
lines changed

12 files changed

+127
-5
lines changed

packages/compiler-cli/src/ngtsc/indexer/src/template.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
TmplAstForLoopBlockEmpty,
2929
TmplAstIfBlock,
3030
TmplAstIfBlockBranch,
31+
TmplAstLetDeclaration,
3132
TmplAstNode,
3233
TmplAstRecursiveVisitor,
3334
TmplAstReference,
@@ -351,6 +352,10 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
351352
this.visitAll(block.children);
352353
}
353354

355+
override visitLetDeclaration(decl: TmplAstLetDeclaration): void {
356+
this.visitExpression(decl.value);
357+
}
358+
354359
/** Creates an identifier for a template element or template node. */
355360
private elementOrTemplateToIdentifier(
356361
node: TmplAstElement | TmplAstTemplate,

packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
TmplAstIcu,
2828
TmplAstIfBlock,
2929
TmplAstIfBlockBranch,
30+
TmplAstLetDeclaration,
3031
TmplAstNode,
3132
TmplAstRecursiveVisitor,
3233
TmplAstReference,
@@ -264,6 +265,10 @@ class TemplateVisitor<Code extends ErrorCode>
264265
this.visitAllNodes(block.children);
265266
}
266267

268+
visitLetDeclaration(decl: TmplAstLetDeclaration): void {
269+
this.visitAst(decl.value);
270+
}
271+
267272
getDiagnostics(template: TmplAstNode[]): NgTemplateDiagnostic<Code>[] {
268273
this.diagnostics = [];
269274
this.visitAllNodes(template);

packages/compiler/src/compiler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export {
158158
IfBlockBranch as TmplAstIfBlockBranch,
159159
DeferredBlockTriggers as TmplAstDeferredBlockTriggers,
160160
UnknownBlock as TmplAstUnknownBlock,
161+
LetDeclaration as TmplAstLetDeclaration,
161162
} from './render3/r3_ast';
162163
export * from './render3/view/t2_api';
163164
export * from './render3/view/t2_binder';

packages/compiler/src/render3/r3_ast.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,20 @@ export class UnknownBlock implements Node {
508508
}
509509
}
510510

511+
export class LetDeclaration implements Node {
512+
constructor(
513+
public name: string,
514+
public value: AST,
515+
public sourceSpan: ParseSourceSpan,
516+
public nameSpan: ParseSourceSpan,
517+
public valueSpan: ParseSourceSpan,
518+
) {}
519+
520+
visit<Result>(visitor: Visitor<Result>): Result {
521+
return visitor.visitLetDeclaration(this);
522+
}
523+
}
524+
511525
export class Template implements Node {
512526
constructor(
513527
// tagName is the name of the container element, if applicable.
@@ -613,6 +627,7 @@ export interface Visitor<Result = any> {
613627
visitIfBlock(block: IfBlock): Result;
614628
visitIfBlockBranch(block: IfBlockBranch): Result;
615629
visitUnknownBlock(block: UnknownBlock): Result;
630+
visitLetDeclaration(decl: LetDeclaration): Result;
616631
}
617632

618633
export class RecursiveVisitor implements Visitor<void> {
@@ -678,6 +693,7 @@ export class RecursiveVisitor implements Visitor<void> {
678693
visitIcu(icu: Icu): void {}
679694
visitDeferredTrigger(trigger: DeferredTrigger): void {}
680695
visitUnknownBlock(block: UnknownBlock): void {}
696+
visitLetDeclaration(decl: LetDeclaration): void {}
681697
}
682698

683699
export function visitAll<Result>(visitor: Visitor<Result>, nodes: Node[]): Result[] {

packages/compiler/src/render3/r3_template_transform.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
9+
import {EmptyExpr, ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
1010
import * as i18n from '../i18n/i18n_ast';
1111
import * as html from '../ml_parser/ast';
1212
import {replaceNgsp} from '../ml_parser/html_whitespaces';
@@ -390,7 +390,18 @@ class HtmlAstToIvyAst implements html.Visitor {
390390
}
391391

392392
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
393-
throw new Error('TODO: implement R3 LetDeclaration');
393+
const value = this.bindingParser.parseBinding(
394+
decl.value,
395+
false,
396+
decl.valueSpan,
397+
decl.valueSpan.start.offset,
398+
);
399+
400+
if (value.errors.length === 0 && value.ast instanceof EmptyExpr) {
401+
this.reportError('@let declaration value cannot be empty', decl.valueSpan);
402+
}
403+
404+
return new t.LetDeclaration(decl.name, value, decl.sourceSpan, decl.nameSpan, decl.valueSpan);
394405
}
395406

396407
visitBlockParameter() {
@@ -899,7 +910,7 @@ class NonBindableVisitor implements html.Visitor {
899910
}
900911

901912
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
902-
throw new Error('TODO: implement R3 LetDeclaration');
913+
return new t.Text(`@let ${decl.name} = ${decl.value};`, decl.sourceSpan);
903914
}
904915
}
905916

packages/compiler/src/render3/view/t2_binder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
IfBlock,
3636
IfBlockBranch,
3737
InteractionDeferredTrigger,
38+
LetDeclaration,
3839
Node,
3940
Reference,
4041
SwitchBlock,
@@ -273,6 +274,10 @@ class Scope implements Visitor {
273274
this.ingestScopedNode(content);
274275
}
275276

277+
visitLetDeclaration(decl: LetDeclaration) {
278+
// TODO(crisbeto): needs further integration
279+
}
280+
276281
// Unused visitors.
277282
visitBoundAttribute(attr: BoundAttribute) {}
278283
visitBoundEvent(event: BoundEvent) {}
@@ -536,6 +541,10 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
536541
content.children.forEach((child) => child.visit(this));
537542
}
538543

544+
visitLetDeclaration(decl: LetDeclaration) {
545+
// TODO(crisbeto): needs further integration
546+
}
547+
539548
// Unused visitors.
540549
visitVariable(variable: Variable): void {}
541550
visitReference(reference: Reference): void {}
@@ -792,6 +801,12 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
792801
visitBoundText(text: BoundText) {
793802
text.value.visit(this);
794803
}
804+
805+
visitLetDeclaration(decl: LetDeclaration) {
806+
// TODO(crisbeto): needs further integration
807+
decl.value.visit(this);
808+
}
809+
795810
override visitPipe(ast: BindingPipe, context: any): any {
796811
this.usedPipes.add(ast.name);
797812
if (!this.scope.isDeferred) {

packages/compiler/test/render3/r3_ast_spans_spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,15 @@ class R3AstSourceSpans implements t.Visitor<void> {
247247
this.result.push(['UnknownBlock', humanizeSpan(block.sourceSpan)]);
248248
}
249249

250+
visitLetDeclaration(decl: t.LetDeclaration): void {
251+
this.result.push([
252+
'LetDeclaration',
253+
humanizeSpan(decl.sourceSpan),
254+
humanizeSpan(decl.nameSpan),
255+
humanizeSpan(decl.valueSpan),
256+
]);
257+
}
258+
250259
private visitAll(nodes: t.Node[][]) {
251260
nodes.forEach((node) => t.visitAll(this, node));
252261
}
@@ -259,8 +268,8 @@ function humanizeSpan(span: ParseSourceSpan | null | undefined): string {
259268
return span.toString();
260269
}
261270

262-
function expectFromHtml(html: string) {
263-
return expectFromR3Nodes(parse(html).nodes);
271+
function expectFromHtml(html: string, options?: {tokenizeLet?: boolean}) {
272+
return expectFromR3Nodes(parse(html, options).nodes);
264273
}
265274

266275
function expectFromR3Nodes(nodes: t.Node[]) {
@@ -817,4 +826,12 @@ describe('R3 AST source spans', () => {
817826
]);
818827
});
819828
});
829+
830+
describe('@let declaration', () => {
831+
it('is correct for a let declaration', () => {
832+
expectFromHtml('@let foo = 123;', {tokenizeLet: true}).toEqual([
833+
['LetDeclaration', '@let foo = 123', 'foo', '123'],
834+
]);
835+
});
836+
});
820837
});

packages/compiler/test/render3/r3_template_transform_spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ class R3AstHumanizer implements t.Visitor<void> {
166166
this.result.push(['UnknownBlock', block.name]);
167167
}
168168

169+
visitLetDeclaration(decl: t.LetDeclaration) {
170+
this.result.push(['LetDeclaration', decl.name, unparse(decl.value)]);
171+
}
172+
169173
private visitAll(nodes: t.Node[][]) {
170174
nodes.forEach((node) => t.visitAll(this, node));
171175
}
@@ -2223,4 +2227,39 @@ describe('R3 template transform', () => {
22232227
expectFromHtml('@unknown {}', true /* ignoreError */).toEqual([['UnknownBlock', 'unknown']]);
22242228
});
22252229
});
2230+
2231+
describe('@let declarations', () => {
2232+
function parseLet(html: string, ignoreError = false) {
2233+
return parse(html, {ignoreError, tokenizeLet: true});
2234+
}
2235+
2236+
function expectLet(html: string, ignoreError = false) {
2237+
const res = parseLet(html, ignoreError);
2238+
return expectFromR3Nodes(res.nodes);
2239+
}
2240+
2241+
it('should parse a let declaration', () => {
2242+
expectLet('@let foo = 123 + 456;').toEqual([['LetDeclaration', 'foo', '123 + 456']]);
2243+
});
2244+
2245+
it('should report syntax errors in the let declaration value', () => {
2246+
expect(() => parseLet('@let foo = {one: 1;')).toThrowError(
2247+
/Parser Error: Missing expected } at the end of the expression \[\{one: 1]/,
2248+
);
2249+
});
2250+
2251+
it('should report a let declaration with no value', () => {
2252+
expect(() => parseLet('@let foo = ;')).toThrowError(
2253+
/@let declaration value cannot be empty/,
2254+
);
2255+
});
2256+
2257+
it('should produce a text node when @let is used inside ngNonBindable', () => {
2258+
expectLet('<div ngNonBindable>@let foo = 123;</div>').toEqual([
2259+
['Element', 'div'],
2260+
['TextAttribute', 'ngNonBindable', ''],
2261+
['Text', '@let foo = 123;'],
2262+
]);
2263+
});
2264+
});
22262265
});

packages/compiler/test/render3/util/expression.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit
203203
block.expressionAlias?.visit(this);
204204
t.visitAll(this, block.children);
205205
}
206+
207+
visitLetDeclaration(decl: t.LetDeclaration) {
208+
decl.value.visit(this);
209+
}
206210
}
207211

208212
/**

packages/compiler/test/render3/view/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,14 @@ export function parseR3(
148148
preserveWhitespaces?: boolean;
149149
leadingTriviaChars?: string[];
150150
ignoreError?: boolean;
151+
tokenizeLet?: boolean;
151152
} = {},
152153
): Render3ParseResult {
153154
const htmlParser = new HtmlParser();
154155
const parseResult = htmlParser.parse(input, 'path:://to/template', {
155156
tokenizeExpansionForms: true,
156157
leadingTriviaChars: options.leadingTriviaChars ?? LEADING_TRIVIA_CHARS,
158+
tokenizeLet: options.tokenizeLet,
157159
});
158160

159161
if (parseResult.errors.length > 0 && !options.ignoreError) {

0 commit comments

Comments
 (0)