Skip to content

Commit

Permalink
refactor(compiler): implement let declarations in render3 ast
Browse files Browse the repository at this point in the history
Introduces a new `LetDeclaration` into the Render3 AST, simiarly to the HTML AST, and adds an initial integration into the various visitors.
  • Loading branch information
crisbeto committed May 20, 2024
1 parent 5513685 commit 4fbc6ff
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 5 deletions.
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/indexer/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
TmplAstForLoopBlockEmpty,
TmplAstIfBlock,
TmplAstIfBlockBranch,
TmplAstLetDeclaration,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstReference,
Expand Down Expand Up @@ -351,6 +352,10 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
this.visitAll(block.children);
}

override visitLetDeclaration(decl: TmplAstLetDeclaration): void {
this.visitExpression(decl.value);
}

/** Creates an identifier for a template element or template node. */
private elementOrTemplateToIdentifier(
node: TmplAstElement | TmplAstTemplate,
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
TmplAstIcu,
TmplAstIfBlock,
TmplAstIfBlockBranch,
TmplAstLetDeclaration,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstReference,
Expand Down Expand Up @@ -264,6 +265,10 @@ class TemplateVisitor<Code extends ErrorCode>
this.visitAllNodes(block.children);
}

visitLetDeclaration(decl: TmplAstLetDeclaration): void {
this.visitAst(decl.value);
}

getDiagnostics(template: TmplAstNode[]): NgTemplateDiagnostic<Code>[] {
this.diagnostics = [];
this.visitAllNodes(template);
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export {
IfBlockBranch as TmplAstIfBlockBranch,
DeferredBlockTriggers as TmplAstDeferredBlockTriggers,
UnknownBlock as TmplAstUnknownBlock,
LetDeclaration as TmplAstLetDeclaration,
} from './render3/r3_ast';
export * from './render3/view/t2_api';
export * from './render3/view/t2_binder';
Expand Down
16 changes: 16 additions & 0 deletions packages/compiler/src/render3/r3_ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,20 @@ export class UnknownBlock implements Node {
}
}

export class LetDeclaration implements Node {
constructor(
public name: string,
public value: AST,
public sourceSpan: ParseSourceSpan,
public nameSpan: ParseSourceSpan,
public valueSpan: ParseSourceSpan,
) {}

visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitLetDeclaration(this);
}
}

export class Template implements Node {
constructor(
// tagName is the name of the container element, if applicable.
Expand Down Expand Up @@ -613,6 +627,7 @@ export interface Visitor<Result = any> {
visitIfBlock(block: IfBlock): Result;
visitIfBlockBranch(block: IfBlockBranch): Result;
visitUnknownBlock(block: UnknownBlock): Result;
visitLetDeclaration(decl: LetDeclaration): Result;
}

export class RecursiveVisitor implements Visitor<void> {
Expand Down Expand Up @@ -678,6 +693,7 @@ export class RecursiveVisitor implements Visitor<void> {
visitIcu(icu: Icu): void {}
visitDeferredTrigger(trigger: DeferredTrigger): void {}
visitUnknownBlock(block: UnknownBlock): void {}
visitLetDeclaration(decl: LetDeclaration): void {}
}

export function visitAll<Result>(visitor: Visitor<Result>, nodes: Node[]): Result[] {
Expand Down
17 changes: 14 additions & 3 deletions packages/compiler/src/render3/r3_template_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
import {EmptyExpr, ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
import * as i18n from '../i18n/i18n_ast';
import * as html from '../ml_parser/ast';
import {replaceNgsp} from '../ml_parser/html_whitespaces';
Expand Down Expand Up @@ -390,7 +390,18 @@ class HtmlAstToIvyAst implements html.Visitor {
}

visitLetDeclaration(decl: html.LetDeclaration, context: any) {
throw new Error('TODO: implement R3 LetDeclaration');
const value = this.bindingParser.parseBinding(
decl.value,
false,
decl.valueSpan,
decl.valueSpan.start.offset,
);

if (value.errors.length === 0 && value.ast instanceof EmptyExpr) {
this.reportError('@let declaration value cannot be empty', decl.valueSpan);
}

return new t.LetDeclaration(decl.name, value, decl.sourceSpan, decl.nameSpan, decl.valueSpan);
}

visitBlockParameter() {
Expand Down Expand Up @@ -894,7 +905,7 @@ class NonBindableVisitor implements html.Visitor {
}

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

Expand Down
15 changes: 15 additions & 0 deletions packages/compiler/src/render3/view/t2_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
IfBlock,
IfBlockBranch,
InteractionDeferredTrigger,
LetDeclaration,
Node,
Reference,
SwitchBlock,
Expand Down Expand Up @@ -273,6 +274,10 @@ class Scope implements Visitor {
this.ingestScopedNode(content);
}

visitLetDeclaration(decl: LetDeclaration) {
// TODO(crisbeto): needs further integration
}

// Unused visitors.
visitBoundAttribute(attr: BoundAttribute) {}
visitBoundEvent(event: BoundEvent) {}
Expand Down Expand Up @@ -536,6 +541,10 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
content.children.forEach((child) => child.visit(this));
}

visitLetDeclaration(decl: LetDeclaration) {
// TODO(crisbeto): needs further integration
}

// Unused visitors.
visitVariable(variable: Variable): void {}
visitReference(reference: Reference): void {}
Expand Down Expand Up @@ -792,6 +801,12 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
visitBoundText(text: BoundText) {
text.value.visit(this);
}

visitLetDeclaration(decl: LetDeclaration) {
// TODO(crisbeto): needs further integration
decl.value.visit(this);
}

override visitPipe(ast: BindingPipe, context: any): any {
this.usedPipes.add(ast.name);
if (!this.scope.isDeferred) {
Expand Down
21 changes: 19 additions & 2 deletions packages/compiler/test/render3/r3_ast_spans_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ class R3AstSourceSpans implements t.Visitor<void> {
this.result.push(['UnknownBlock', humanizeSpan(block.sourceSpan)]);
}

visitLetDeclaration(decl: t.LetDeclaration): void {
this.result.push([
'LetDeclaration',
humanizeSpan(decl.sourceSpan),
humanizeSpan(decl.nameSpan),
humanizeSpan(decl.valueSpan),
]);
}

private visitAll(nodes: t.Node[][]) {
nodes.forEach((node) => t.visitAll(this, node));
}
Expand All @@ -259,8 +268,8 @@ function humanizeSpan(span: ParseSourceSpan | null | undefined): string {
return span.toString();
}

function expectFromHtml(html: string) {
return expectFromR3Nodes(parse(html).nodes);
function expectFromHtml(html: string, options?: {tokenizeLet?: boolean}) {
return expectFromR3Nodes(parse(html, options).nodes);
}

function expectFromR3Nodes(nodes: t.Node[]) {
Expand Down Expand Up @@ -817,4 +826,12 @@ describe('R3 AST source spans', () => {
]);
});
});

describe('@let declaration', () => {
it('is correct for a let declaration', () => {
expectFromHtml('@let foo = 123;', {tokenizeLet: true}).toEqual([
['LetDeclaration', '@let foo = 123', 'foo', '123'],
]);
});
});
});
39 changes: 39 additions & 0 deletions packages/compiler/test/render3/r3_template_transform_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ class R3AstHumanizer implements t.Visitor<void> {
this.result.push(['UnknownBlock', block.name]);
}

visitLetDeclaration(decl: t.LetDeclaration) {
this.result.push(['LetDeclaration', decl.name, unparse(decl.value)]);
}

private visitAll(nodes: t.Node[][]) {
nodes.forEach((node) => t.visitAll(this, node));
}
Expand Down Expand Up @@ -2179,4 +2183,39 @@ describe('R3 template transform', () => {
expectFromHtml('@unknown {}', true /* ignoreError */).toEqual([['UnknownBlock', 'unknown']]);
});
});

describe('@let declarations', () => {
function parseLet(html: string, ignoreError = false) {
return parse(html, {ignoreError, tokenizeLet: true});
}

function expectLet(html: string, ignoreError = false) {
const res = parseLet(html, ignoreError);
return expectFromR3Nodes(res.nodes);
}

it('should parse a let declaration', () => {
expectLet('@let foo = 123 + 456;').toEqual([['LetDeclaration', 'foo', '123 + 456']]);
});

it('should report syntax errors in the let declaration value', () => {
expect(() => parseLet('@let foo = {one: 1;')).toThrowError(
/Parser Error: Missing expected } at the end of the expression \[\{one: 1]/,
);
});

it('should report a let declaration with no value', () => {
expect(() => parseLet('@let foo = ;')).toThrowError(
/@let declaration value cannot be empty/,
);
});

it('should produce a text node when @let is used inside ngNonBindable', () => {
expectLet('<div ngNonBindable>@let foo = 123;</div>').toEqual([
['Element', 'div'],
['TextAttribute', 'ngNonBindable', ''],
['Text', '@let foo = 123;'],
]);
});
});
});
4 changes: 4 additions & 0 deletions packages/compiler/test/render3/util/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit
block.expressionAlias?.visit(this);
t.visitAll(this, block.children);
}

visitLetDeclaration(decl: t.LetDeclaration) {
decl.value.visit(this);
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/test/render3/view/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ export function parseR3(
preserveWhitespaces?: boolean;
leadingTriviaChars?: string[];
ignoreError?: boolean;
tokenizeLet?: boolean;
} = {},
): Render3ParseResult {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(input, 'path:://to/template', {
tokenizeExpansionForms: true,
leadingTriviaChars: options.leadingTriviaChars ?? LEADING_TRIVIA_CHARS,
tokenizeLet: options.tokenizeLet,
});

if (parseResult.errors.length > 0 && !options.ignoreError) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/schematics/utils/template_ast_visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
TmplAstTextAttribute,
TmplAstVariable,
TmplAstUnknownBlock,
TmplAstLetDeclaration,
} from '@angular/compiler';

/**
Expand Down Expand Up @@ -79,6 +80,7 @@ export class TemplateAstVisitor implements TmplAstRecursiveVisitor {
visitForLoopBlockEmpty(block: TmplAstForLoopBlockEmpty): void {}
visitIfBlock(block: TmplAstIfBlock): void {}
visitIfBlockBranch(block: TmplAstIfBlockBranch): void {}
visitLetDeclaration(decl: TmplAstLetDeclaration): void {}

/**
* Visits all the provided nodes in order using this Visitor's visit methods.
Expand Down
5 changes: 5 additions & 0 deletions packages/language-service/src/template_target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
TmplAstIcu,
TmplAstIfBlock,
TmplAstIfBlockBranch,
TmplAstLetDeclaration,
TmplAstNode,
TmplAstReference,
TmplAstSwitchBlock,
Expand Down Expand Up @@ -610,6 +611,10 @@ class TemplateTargetVisitor implements TmplAstVisitor {

visitUnknownBlock(block: TmplAstUnknownBlock) {}

visitLetDeclaration(decl: TmplAstLetDeclaration) {
this.visitBinding(decl.value);
}

visitAll(nodes: TmplAstNode[]) {
for (const node of nodes) {
this.visit(node);
Expand Down

0 comments on commit 4fbc6ff

Please sign in to comment.