diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts index 2906b32b355..77846bd2e11 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts @@ -28,6 +28,7 @@ import { TmplAstForLoopBlockEmpty, TmplAstIfBlock, TmplAstIfBlockBranch, + TmplAstLetDeclaration, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, @@ -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, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts index 37bf8131e4b..f33da482c24 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts @@ -27,6 +27,7 @@ import { TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, + TmplAstLetDeclaration, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, @@ -264,6 +265,10 @@ class TemplateVisitor this.visitAllNodes(block.children); } + visitLetDeclaration(decl: TmplAstLetDeclaration): void { + this.visitAst(decl.value); + } + getDiagnostics(template: TmplAstNode[]): NgTemplateDiagnostic[] { this.diagnostics = []; this.visitAllNodes(template); diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 902eac9b8ea..3c287cc4bc4 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -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'; diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index aa63b857181..919f040dfa2 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -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(visitor: Visitor): Result { + return visitor.visitLetDeclaration(this); + } +} + export class Template implements Node { constructor( // tagName is the name of the container element, if applicable. @@ -613,6 +627,7 @@ export interface Visitor { visitIfBlock(block: IfBlock): Result; visitIfBlockBranch(block: IfBlockBranch): Result; visitUnknownBlock(block: UnknownBlock): Result; + visitLetDeclaration(decl: LetDeclaration): Result; } export class RecursiveVisitor implements Visitor { @@ -678,6 +693,7 @@ export class RecursiveVisitor implements Visitor { visitIcu(icu: Icu): void {} visitDeferredTrigger(trigger: DeferredTrigger): void {} visitUnknownBlock(block: UnknownBlock): void {} + visitLetDeclaration(decl: LetDeclaration): void {} } export function visitAll(visitor: Visitor, nodes: Node[]): Result[] { diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index 78eaaf49baa..bdaf7a4e81f 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -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'; @@ -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() { @@ -899,7 +910,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); } } diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index e398df99d80..c209048e82b 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -35,6 +35,7 @@ import { IfBlock, IfBlockBranch, InteractionDeferredTrigger, + LetDeclaration, Node, Reference, SwitchBlock, @@ -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) {} @@ -536,6 +541,10 @@ class DirectiveBinder 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 {} @@ -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) { diff --git a/packages/compiler/test/render3/r3_ast_spans_spec.ts b/packages/compiler/test/render3/r3_ast_spans_spec.ts index d08434b25c5..a72af359246 100644 --- a/packages/compiler/test/render3/r3_ast_spans_spec.ts +++ b/packages/compiler/test/render3/r3_ast_spans_spec.ts @@ -247,6 +247,15 @@ class R3AstSourceSpans implements t.Visitor { 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)); } @@ -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[]) { @@ -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'], + ]); + }); + }); }); diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts index 84e2f9b2273..d1be158d977 100644 --- a/packages/compiler/test/render3/r3_template_transform_spec.ts +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -166,6 +166,10 @@ class R3AstHumanizer implements t.Visitor { 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)); } @@ -2223,4 +2227,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('
@let foo = 123;
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Text', '@let foo = 123;'], + ]); + }); + }); }); diff --git a/packages/compiler/test/render3/util/expression.ts b/packages/compiler/test/render3/util/expression.ts index 50b57a91174..b5cbaf6a36a 100644 --- a/packages/compiler/test/render3/util/expression.ts +++ b/packages/compiler/test/render3/util/expression.ts @@ -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); + } } /** diff --git a/packages/compiler/test/render3/view/util.ts b/packages/compiler/test/render3/view/util.ts index 02099311669..e9830604cb8 100644 --- a/packages/compiler/test/render3/view/util.ts +++ b/packages/compiler/test/render3/view/util.ts @@ -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) { diff --git a/packages/core/schematics/utils/template_ast_visitor.ts b/packages/core/schematics/utils/template_ast_visitor.ts index b136ea5e7c6..e2f38c4fa14 100644 --- a/packages/core/schematics/utils/template_ast_visitor.ts +++ b/packages/core/schematics/utils/template_ast_visitor.ts @@ -32,6 +32,7 @@ import type { TmplAstTextAttribute, TmplAstVariable, TmplAstUnknownBlock, + TmplAstLetDeclaration, } from '@angular/compiler'; /** @@ -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. diff --git a/packages/language-service/src/template_target.ts b/packages/language-service/src/template_target.ts index 7a5d57c1219..fda0c8d8c49 100644 --- a/packages/language-service/src/template_target.ts +++ b/packages/language-service/src/template_target.ts @@ -33,6 +33,7 @@ import { TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, + TmplAstLetDeclaration, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, @@ -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);