Skip to content

Commit b7fc7fd

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(compiler): integrate let declarations into the template binder (angular#56199)
Integrates the let declarations into the template binder which will be used to power the scoping behavior of the new syntax. PR Close angular#56199
1 parent 6cef0ed commit b7fc7fd

File tree

3 files changed

+166
-33
lines changed

3 files changed

+166
-33
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ForLoopBlock,
2121
ForLoopBlockEmpty,
2222
IfBlockBranch,
23+
LetDeclaration,
2324
Node,
2425
Reference,
2526
SwitchBlockCase,
@@ -50,6 +51,9 @@ export type ReferenceTarget<DirectiveT> =
5051
| Element
5152
| Template;
5253

54+
/** Entity that is local to the template and defined within the template. */
55+
export type TemplateEntity = Reference | Variable | LetDeclaration;
56+
5357
/*
5458
* t2 is the replacement for the `TemplateDefinitionBuilder`. It handles the operations of
5559
* analyzing Angular templates, extracting semantic info, and ultimately producing a template
@@ -200,15 +204,15 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
200204
* This is only defined for `AST` expressions that read or write to a property of an
201205
* `ImplicitReceiver`.
202206
*/
203-
getExpressionTarget(expr: AST): Reference | Variable | null;
207+
getExpressionTarget(expr: AST): TemplateEntity | null;
204208

205209
/**
206210
* Given a particular `Reference` or `Variable`, get the `ScopedNode` which created it.
207211
*
208212
* All `Variable`s are defined on node, so this will always return a value for a `Variable`
209213
* from the `Target`. Returns `null` otherwise.
210214
*/
211-
getDefinitionNodeOfSymbol(symbol: Reference | Variable): ScopedNode | null;
215+
getDefinitionNodeOfSymbol(symbol: TemplateEntity): ScopedNode | null;
212216

213217
/**
214218
* Get the nesting level of a particular `ScopedNode`.
@@ -222,7 +226,7 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
222226
* Get all `Reference`s and `Variables` visible within the given `ScopedNode` (or at the top
223227
* level, if `null` is passed).
224228
*/
225-
getEntitiesInScope(node: ScopedNode | null): ReadonlySet<Reference | Variable>;
229+
getEntitiesInScope(node: ScopedNode | null): ReadonlySet<TemplateEntity>;
226230

227231
/**
228232
* Get a list of all the directives used by the target,

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

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

9+
import {TmplAstLetDeclaration} from '../../compiler';
910
import {
1011
AST,
1112
BindingPipe,
@@ -14,6 +15,7 @@ import {
1415
PropertyWrite,
1516
RecursiveAstVisitor,
1617
SafePropertyRead,
18+
ThisReceiver,
1719
} from '../../expression_parser/ast';
1820
import {SelectorMatcher} from '../../selector';
1921
import {
@@ -56,6 +58,7 @@ import {
5658
ScopedNode,
5759
Target,
5860
TargetBinder,
61+
TemplateEntity,
5962
} from './t2_api';
6063
import {createCssSelectorFromNode} from './util';
6164

@@ -125,7 +128,7 @@ class Scope implements Visitor {
125128
/**
126129
* Named members of the `Scope`, such as `Reference`s or `Variable`s.
127130
*/
128-
readonly namedEntities = new Map<string, Reference | Variable>();
131+
readonly namedEntities = new Map<string, TemplateEntity>();
129132

130133
/**
131134
* Set of elements that belong to this scope.
@@ -275,7 +278,7 @@ class Scope implements Visitor {
275278
}
276279

277280
visitLetDeclaration(decl: LetDeclaration) {
278-
// TODO(crisbeto): needs further integration
281+
this.maybeDeclare(decl);
279282
}
280283

281284
// Unused visitors.
@@ -288,7 +291,7 @@ class Scope implements Visitor {
288291
visitDeferredTrigger(trigger: DeferredTrigger) {}
289292
visitUnknownBlock(block: UnknownBlock) {}
290293

291-
private maybeDeclare(thing: Reference | Variable) {
294+
private maybeDeclare(thing: TemplateEntity) {
292295
// Declare something with a name, as long as that name isn't taken.
293296
if (!this.namedEntities.has(thing.name)) {
294297
this.namedEntities.set(thing.name, thing);
@@ -300,7 +303,7 @@ class Scope implements Visitor {
300303
*
301304
* This can recurse into a parent `Scope` if it's available.
302305
*/
303-
lookup(name: string): Reference | Variable | null {
306+
lookup(name: string): TemplateEntity | null {
304307
if (this.namedEntities.has(name)) {
305308
// Found in the local scope.
306309
return this.namedEntities.get(name)!;
@@ -541,10 +544,6 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
541544
content.children.forEach((child) => child.visit(this));
542545
}
543546

544-
visitLetDeclaration(decl: LetDeclaration) {
545-
// TODO(crisbeto): needs further integration
546-
}
547-
548547
// Unused visitors.
549548
visitVariable(variable: Variable): void {}
550549
visitReference(reference: Reference): void {}
@@ -557,6 +556,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
557556
visitIcu(icu: Icu): void {}
558557
visitDeferredTrigger(trigger: DeferredTrigger): void {}
559558
visitUnknownBlock(block: UnknownBlock) {}
559+
visitLetDeclaration(decl: LetDeclaration) {}
560560
}
561561

562562
/**
@@ -572,8 +572,8 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
572572
private visitNode: (node: Node) => void;
573573

574574
private constructor(
575-
private bindings: Map<AST, Reference | Variable>,
576-
private symbols: Map<Reference | Variable, ScopedNode>,
575+
private bindings: Map<AST, TemplateEntity>,
576+
private symbols: Map<TemplateEntity, ScopedNode>,
577577
private usedPipes: Set<string>,
578578
private eagerPipes: Set<string>,
579579
private deferBlocks: [DeferredBlock, Scope][],
@@ -615,15 +615,15 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
615615
nodes: Node[],
616616
scope: Scope,
617617
): {
618-
expressions: Map<AST, Reference | Variable>;
619-
symbols: Map<Variable | Reference, Template>;
618+
expressions: Map<AST, TemplateEntity>;
619+
symbols: Map<TemplateEntity, Template>;
620620
nestingLevel: Map<ScopedNode, number>;
621621
usedPipes: Set<string>;
622622
eagerPipes: Set<string>;
623623
deferBlocks: [DeferredBlock, Scope][];
624624
} {
625-
const expressions = new Map<AST, Reference | Variable>();
626-
const symbols = new Map<Variable | Reference, Template>();
625+
const expressions = new Map<AST, TemplateEntity>();
626+
const symbols = new Map<TemplateEntity, Template>();
627627
const nestingLevel = new Map<ScopedNode, number>();
628628
const usedPipes = new Set<string>();
629629
const eagerPipes = new Set<string>();
@@ -803,8 +803,11 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
803803
}
804804

805805
visitLetDeclaration(decl: LetDeclaration) {
806-
// TODO(crisbeto): needs further integration
807806
decl.value.visit(this);
807+
808+
if (this.rootNode !== null) {
809+
this.symbols.set(decl, this.rootNode);
810+
}
808811
}
809812

810813
override visitPipe(ast: BindingPipe, context: any): any {
@@ -858,7 +861,15 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
858861

859862
// Check whether the name exists in the current scope. If so, map it. Otherwise, the name is
860863
// probably a property on the top-level component context.
861-
let target = this.scope.lookup(name);
864+
const target = this.scope.lookup(name);
865+
866+
// It's not allowed to read template entities via `this`, however it previously worked by
867+
// accident (see #55115). Since `@let` declarations are new, we can fix it from the beginning,
868+
// whereas pre-existing template entities will be fixed in #55115.
869+
if (target instanceof TmplAstLetDeclaration && ast.receiver instanceof ThisReceiver) {
870+
return;
871+
}
872+
862873
if (target !== null) {
863874
this.bindings.set(ast, target);
864875
}
@@ -889,10 +900,10 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
889900
BoundAttribute | BoundEvent | Reference | TextAttribute,
890901
{directive: DirectiveT; node: Element | Template} | Element | Template
891902
>,
892-
private exprTargets: Map<AST, Reference | Variable>,
893-
private symbols: Map<Reference | Variable, Template>,
903+
private exprTargets: Map<AST, TemplateEntity>,
904+
private symbols: Map<TemplateEntity, Template>,
894905
private nestingLevel: Map<ScopedNode, number>,
895-
private scopedNodeEntities: Map<ScopedNode | null, ReadonlySet<Reference | Variable>>,
906+
private scopedNodeEntities: Map<ScopedNode | null, ReadonlySet<TemplateEntity>>,
896907
private usedPipes: Set<string>,
897908
private eagerPipes: Set<string>,
898909
rawDeferred: [DeferredBlock, Scope][],
@@ -901,7 +912,7 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
901912
this.deferredScopes = new Map(rawDeferred);
902913
}
903914

904-
getEntitiesInScope(node: ScopedNode | null): ReadonlySet<Reference | Variable> {
915+
getEntitiesInScope(node: ScopedNode | null): ReadonlySet<TemplateEntity> {
905916
return this.scopedNodeEntities.get(node) ?? new Set();
906917
}
907918

@@ -919,11 +930,11 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
919930
return this.bindings.get(binding) || null;
920931
}
921932

922-
getExpressionTarget(expr: AST): Reference | Variable | null {
933+
getExpressionTarget(expr: AST): TemplateEntity | null {
923934
return this.exprTargets.get(expr) || null;
924935
}
925936

926-
getDefinitionNodeOfSymbol(symbol: Reference | Variable): ScopedNode | null {
937+
getDefinitionNodeOfSymbol(symbol: TemplateEntity): ScopedNode | null {
927938
return this.symbols.get(symbol) || null;
928939
}
929940

@@ -1046,7 +1057,7 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
10461057
* @param rootNode Root node of the scope.
10471058
* @param name Name of the entity.
10481059
*/
1049-
private findEntityInScope(rootNode: ScopedNode, name: string): Reference | Variable | null {
1060+
private findEntityInScope(rootNode: ScopedNode, name: string): TemplateEntity | null {
10501061
const entities = this.getEntitiesInScope(rootNode);
10511062

10521063
for (const entity of entities) {
@@ -1072,19 +1083,17 @@ export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTar
10721083
}
10731084
}
10741085

1075-
function extractScopedNodeEntities(
1076-
rootScope: Scope,
1077-
): Map<ScopedNode | null, Set<Reference | Variable>> {
1078-
const entityMap = new Map<ScopedNode | null, Map<string, Reference | Variable>>();
1086+
function extractScopedNodeEntities(rootScope: Scope): Map<ScopedNode | null, Set<TemplateEntity>> {
1087+
const entityMap = new Map<ScopedNode | null, Map<string, TemplateEntity>>();
10791088

1080-
function extractScopeEntities(scope: Scope): Map<string, Reference | Variable> {
1089+
function extractScopeEntities(scope: Scope): Map<string, TemplateEntity> {
10811090
if (entityMap.has(scope.rootNode)) {
10821091
return entityMap.get(scope.rootNode)!;
10831092
}
10841093

10851094
const currentEntities = scope.namedEntities;
10861095

1087-
let entities: Map<string, Reference | Variable>;
1096+
let entities: Map<string, TemplateEntity>;
10881097
if (scope.parentScope !== null) {
10891098
entities = new Map([...extractScopeEntities(scope.parentScope), ...currentEntities]);
10901099
} else {
@@ -1104,7 +1113,7 @@ function extractScopedNodeEntities(
11041113
extractScopeEntities(scope);
11051114
}
11061115

1107-
const templateEntities = new Map<ScopedNode | null, Set<Reference | Variable>>();
1116+
const templateEntities = new Map<ScopedNode | null, Set<TemplateEntity>>();
11081117
for (const [template, entities] of entityMap) {
11091118
templateEntities.set(template, new Set(entities.values()));
11101119
}

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

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,126 @@ describe('t2 binding', () => {
212212
expect(elDirectives[0].name).toBe('Dir');
213213
});
214214

215+
it('should get @let declarations when resolving entities at the root', () => {
216+
const template = parseTemplate(
217+
`
218+
@let one = 1;
219+
@let two = 2;
220+
@let sum = one + two;
221+
`,
222+
'',
223+
{
224+
enableLetSyntax: true,
225+
},
226+
);
227+
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
228+
const res = binder.bind({template: template.nodes});
229+
const entities = Array.from(res.getEntitiesInScope(null));
230+
231+
expect(entities.map((entity) => entity.name)).toEqual(['one', 'two', 'sum']);
232+
});
233+
234+
it('should scope @let declarations to their current view', () => {
235+
const template = parseTemplate(
236+
`
237+
@let one = 1;
238+
239+
@if (true) {
240+
@let two = 2;
241+
}
242+
243+
@if (true) {
244+
@let three = 3;
245+
}
246+
`,
247+
'',
248+
{
249+
enableLetSyntax: true,
250+
},
251+
);
252+
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
253+
const res = binder.bind({template: template.nodes});
254+
const rootEntities = Array.from(res.getEntitiesInScope(null));
255+
const firstBranchEntities = Array.from(
256+
res.getEntitiesInScope((template.nodes[1] as a.IfBlock).branches[0]),
257+
);
258+
const secondBranchEntities = Array.from(
259+
res.getEntitiesInScope((template.nodes[2] as a.IfBlock).branches[0]),
260+
);
261+
262+
expect(rootEntities.map((entity) => entity.name)).toEqual(['one']);
263+
expect(firstBranchEntities.map((entity) => entity.name)).toEqual(['one', 'two']);
264+
expect(secondBranchEntities.map((entity) => entity.name)).toEqual(['one', 'three']);
265+
});
266+
267+
it('should resolve expressions to an @let declaration', () => {
268+
const template = parseTemplate(
269+
`
270+
@let value = 1;
271+
{{value}}
272+
`,
273+
'',
274+
{
275+
enableLetSyntax: true,
276+
},
277+
);
278+
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
279+
const res = binder.bind({template: template.nodes});
280+
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
281+
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
282+
const target = res.getExpressionTarget(propertyRead);
283+
284+
expect(target instanceof a.LetDeclaration).toBe(true);
285+
expect((target as a.LetDeclaration)?.name).toBe('value');
286+
});
287+
288+
it('should not resolve a `this` access to a `@let` declaration', () => {
289+
const template = parseTemplate(
290+
`
291+
@let value = 1;
292+
{{this.value}}
293+
`,
294+
'',
295+
{
296+
enableLetSyntax: true,
297+
},
298+
);
299+
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
300+
const res = binder.bind({template: template.nodes});
301+
const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource;
302+
const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0];
303+
const target = res.getExpressionTarget(propertyRead);
304+
305+
expect(target).toBe(null);
306+
});
307+
308+
it('should resolve the definition node of let declarations', () => {
309+
const template = parseTemplate(
310+
`
311+
@if (true) {
312+
@let one = 1;
313+
}
314+
315+
@if (true) {
316+
@let two = 2;
317+
}
318+
`,
319+
'',
320+
{
321+
enableLetSyntax: true,
322+
},
323+
);
324+
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta[]>());
325+
const res = binder.bind({template: template.nodes});
326+
const firstBranch = (template.nodes[0] as a.IfBlock).branches[0];
327+
const firstLet = firstBranch.children[0] as a.LetDeclaration;
328+
const secondBranch = (template.nodes[1] as a.IfBlock).branches[0];
329+
const secondLet = secondBranch.children[0] as a.LetDeclaration;
330+
331+
expect(res.getDefinitionNodeOfSymbol(firstLet)).toBe(firstBranch);
332+
expect(res.getDefinitionNodeOfSymbol(secondLet)).toBe(secondBranch);
333+
});
334+
215335
describe('matching inputs to consuming directives', () => {
216336
it('should work for bound attributes', () => {
217337
const template = parseTemplate('<div hasInput [inputBinding]="myValue"></div>', '', {});

0 commit comments

Comments
 (0)