Skip to content

Commit 2a1291e

Browse files
crisbetothePunderWoman
authored andcommittedJul 1, 2024·
fix(compiler): give precedence to local let declarations over parent ones (#56752)
Currently the logic that maps a name to a variable looks at the variables in their definition order. This means that `@let` declarations from parent views will always come before local ones, because the local ones are declared inline whereas the parent ones are hoisted to the top of the function. These changes resolve the issue by giving precedence to the local variables. Fixes #56737. PR Close #56752
1 parent 4d18c5b commit 2a1291e

File tree

9 files changed

+146
-1
lines changed

9 files changed

+146
-1
lines changed
 

‎packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_let/GOLDEN_PARTIAL.js

+40
Original file line numberDiff line numberDiff line change
@@ -715,3 +715,43 @@ export declare class MyApp {
715715
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
716716
}
717717

718+
/****************************************************************************************************
719+
* PARTIAL FILE: shadowed_let.js
720+
****************************************************************************************************/
721+
import { Component } from '@angular/core';
722+
import * as i0 from "@angular/core";
723+
export class MyApp {
724+
}
725+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
726+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
727+
@let value = 'parent';
728+
729+
@if (true) {
730+
@let value = 'local';
731+
The value comes from {{value}}
732+
}
733+
`, isInline: true });
734+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
735+
type: Component,
736+
args: [{
737+
template: `
738+
@let value = 'parent';
739+
740+
@if (true) {
741+
@let value = 'local';
742+
The value comes from {{value}}
743+
}
744+
`,
745+
standalone: true,
746+
}]
747+
}] });
748+
749+
/****************************************************************************************************
750+
* PARTIAL FILE: shadowed_let.d.ts
751+
****************************************************************************************************/
752+
import * as i0 from "@angular/core";
753+
export declare class MyApp {
754+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
755+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
756+
}
757+

‎packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_let/TEST_CASES.json

+17
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,23 @@
275275
"failureMessage": "Incorrect template"
276276
}
277277
]
278+
},
279+
{
280+
"description": "should give precedence to local @let definition over one from a parent view",
281+
"inputFiles": [
282+
"shadowed_let.ts"
283+
],
284+
"expectations": [
285+
{
286+
"files": [
287+
{
288+
"expected": "shadowed_let_template.js",
289+
"generated": "shadowed_let.js"
290+
}
291+
],
292+
"failureMessage": "Incorrect template"
293+
}
294+
]
278295
}
279296
]
280297
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@let value = 'parent';
6+
7+
@if (true) {
8+
@let value = 'local';
9+
The value comes from {{value}}
10+
}
11+
`,
12+
standalone: true,
13+
})
14+
export class MyApp {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
function MyApp_Conditional_1_Template(rf, ctx) {
2+
if (rf & 1) {
3+
$r3$.ɵɵdeclareLet(0);
4+
$r3$.ɵɵtext(1);
5+
}
6+
if (rf & 2) {
7+
const $value_r1$ = "local";
8+
$r3$.ɵɵadvance();
9+
$r3$.ɵɵtextInterpolate1(" The value comes from ", $value_r1$, " ");
10+
}
11+
}
12+
13+
14+
15+
$r3$.ɵɵdefineComponent({
16+
17+
decls: 2,
18+
vars: 1,
19+
template: function MyApp_Template(rf, ctx) {
20+
if (rf & 1) {
21+
$r3$.ɵɵdeclareLet(0);
22+
$r3$.ɵɵtemplate(1, MyApp_Conditional_1_Template, 2, 1);
23+
}
24+
if (rf & 2) {
25+
"parent";
26+
$r3$.ɵɵadvance();
27+
$r3$.ɵɵconditional(true ? 1 : -1);
28+
}
29+
},
30+
31+
});

‎packages/compiler/src/template/pipeline/ir/src/variable.ts

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export interface IdentifierVariable extends SemanticVariableBase {
5656
* The identifier whose value in the template is tracked in this variable.
5757
*/
5858
identifier: string;
59+
60+
/**
61+
* Whether the variable was declared locally within the same view or somewhere else.
62+
*/
63+
local: boolean;
5964
}
6065

6166
/**

‎packages/compiler/src/template/pipeline/src/phases/generate_local_let_references.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function generateLocalLetReferences(job: ComponentCompilationJob): void {
2525
kind: ir.SemanticVariableKind.Identifier,
2626
name: null,
2727
identifier: op.declaredName,
28+
local: true,
2829
};
2930

3031
ir.OpList.replace<ir.UpdateOp>(

‎packages/compiler/src/template/pipeline/src/phases/generate_variables.ts

+3
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
167167
kind: ir.SemanticVariableKind.Identifier,
168168
name: null,
169169
identifier,
170+
local: false,
170171
});
171172
}
172173

@@ -189,6 +190,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
189190
kind: ir.SemanticVariableKind.Identifier,
190191
name: null,
191192
identifier: op.localRefs[offset].name,
193+
local: false,
192194
},
193195
});
194196
}
@@ -202,6 +204,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
202204
kind: ir.SemanticVariableKind.Identifier,
203205
name: null,
204206
identifier: op.declaredName,
207+
local: false,
205208
},
206209
});
207210
break;

‎packages/compiler/src/template/pipeline/src/phases/resolve_names.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ function processLexicalScope(
3636
// identifiers from parent templates) only local variables need be considered here.
3737
const scope = new Map<string, ir.XrefId>();
3838

39+
// Symbols defined within the current scope. They take precedence over ones defined outside.
40+
const localDefinitions = new Map<string, ir.XrefId>();
41+
3942
// First, step through the operations list and:
4043
// 1) build up the `scope` mapping
4144
// 2) recurse into any listener functions
@@ -44,6 +47,16 @@ function processLexicalScope(
4447
case ir.OpKind.Variable:
4548
switch (op.variable.kind) {
4649
case ir.SemanticVariableKind.Identifier:
50+
if (op.variable.local) {
51+
if (localDefinitions.has(op.variable.identifier)) {
52+
continue;
53+
}
54+
localDefinitions.set(op.variable.identifier, op.xref);
55+
} else if (scope.has(op.variable.identifier)) {
56+
continue;
57+
}
58+
scope.set(op.variable.identifier, op.xref);
59+
break;
4760
case ir.SemanticVariableKind.Alias:
4861
// This variable represents some kind of identifier which can be used in the template.
4962
if (scope.has(op.variable.identifier)) {
@@ -85,7 +98,9 @@ function processLexicalScope(
8598
// `expr` is a read of a name within the lexical scope of this view.
8699
// Either that name is defined within the current view, or it represents a property from the
87100
// main component context.
88-
if (scope.has(expr.name)) {
101+
if (localDefinitions.has(expr.name)) {
102+
return new ir.ReadVariableExpr(localDefinitions.get(expr.name)!);
103+
} else if (scope.has(expr.name)) {
89104
// This was a defined variable in the current scope.
90105
return new ir.ReadVariableExpr(scope.get(expr.name)!);
91106
} else {

‎packages/core/test/acceptance/let_spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,25 @@ describe('@let declarations', () => {
426426
expect(fixture.nativeElement.textContent).toContain('The value comes from @let');
427427
});
428428

429+
it('should give precedence to local @let definition over one from a parent view', () => {
430+
@Component({
431+
standalone: true,
432+
template: `
433+
@let value = 'parent';
434+
435+
@if (true) {
436+
@let value = 'local';
437+
The value comes from {{value}}
438+
}
439+
`,
440+
})
441+
class TestComponent {}
442+
443+
const fixture = TestBed.createComponent(TestComponent);
444+
fixture.detectChanges();
445+
expect(fixture.nativeElement.textContent).toContain('The value comes from local');
446+
});
447+
429448
it('should be able to use @for loop variables in @let declarations', () => {
430449
@Component({
431450
standalone: true,

0 commit comments

Comments
 (0)
Please sign in to comment.