Skip to content

Commit cdebf75

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(compiler-cli): used before declared diagnostic not firing for control flow blocks (#56843)
When we process `@if` and `@for` blocks, we create a scope around their expressions in order to encapsulate the aliases to them. The problem is that this doesn't represent the actual structure since the expression is part of the outer scope. This surfaces by not raising the "used before declared" diagnostic for `@let` declarations. These changes resolve the issue by processing the expression as a part of the parent scope. Fixes #56842. PR Close #56843
1 parent 66e5825 commit cdebf75

File tree

2 files changed

+92
-10
lines changed

2 files changed

+92
-10
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,23 +1647,23 @@ class TcbIfOp extends TcbOp {
16471647
return ts.factory.createBlock(branchScope.render());
16481648
}
16491649

1650-
// We need to process the expression first so it gets its own scope that the body of the
1651-
// conditional will inherit from. We do this, because we need to declare a separate variable
1650+
// We process the expression first in the parent scope, but create a scope around the block
1651+
// that the body will inherit from. We do this, because we need to declare a separate variable
16521652
// for the case where the expression has an alias _and_ because we need the processed
16531653
// expression when generating the guard for the body.
1654-
const expressionScope = Scope.forNodes(this.tcb, this.scope, branch, [], null);
1655-
expressionScope.render().forEach((stmt) => this.scope.addStatement(stmt));
1656-
this.expressionScopes.set(branch, expressionScope);
1654+
const outerScope = Scope.forNodes(this.tcb, this.scope, branch, [], null);
1655+
outerScope.render().forEach((stmt) => this.scope.addStatement(stmt));
1656+
this.expressionScopes.set(branch, outerScope);
16571657

1658-
let expression = tcbExpression(branch.expression, this.tcb, expressionScope);
1658+
let expression = tcbExpression(branch.expression, this.tcb, this.scope);
16591659
if (branch.expressionAlias !== null) {
16601660
expression = ts.factory.createBinaryExpression(
16611661
ts.factory.createParenthesizedExpression(expression),
16621662
ts.SyntaxKind.AmpersandAmpersandToken,
1663-
expressionScope.resolve(branch.expressionAlias),
1663+
outerScope.resolve(branch.expressionAlias),
16641664
);
16651665
}
1666-
const bodyScope = this.getBranchScope(expressionScope, branch, index);
1666+
const bodyScope = this.getBranchScope(outerScope, branch, index);
16671667

16681668
return ts.factory.createIfStatement(
16691669
expression,
@@ -1889,7 +1889,7 @@ class TcbForOfOp extends TcbOp {
18891889
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
18901890
// Add a non-null expression to allow such values to be assigned.
18911891
const expression = ts.factory.createNonNullExpression(
1892-
tcbExpression(this.block.expression, this.tcb, loopScope),
1892+
tcbExpression(this.block.expression, this.tcb, this.scope),
18931893
);
18941894
const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block);
18951895
const trackExpression = trackTranslator.translate(this.block.trackBy);

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5818,7 +5818,7 @@ suppress
58185818
]);
58195819
});
58205820

5821-
it('should not allow usages of aliased `if` block variables inside the tracking exprssion', () => {
5821+
it('should not allow usages of aliased `if` block variables inside the tracking expression', () => {
58225822
env.write(
58235823
'/test.ts',
58245824
`
@@ -7342,6 +7342,88 @@ suppress
73427342
const diags = env.driveDiagnostics();
73437343
expect(diags.length).toBe(0);
73447344
});
7345+
7346+
it('should report @let declaration used in the expression of a @if block before it is defined', () => {
7347+
env.write(
7348+
'test.ts',
7349+
`
7350+
import {Component} from '@angular/core';
7351+
7352+
@Component({
7353+
template: \`
7354+
@if (value) {
7355+
Hello
7356+
}
7357+
@let value = 123;
7358+
\`,
7359+
standalone: true,
7360+
})
7361+
export class Main {}
7362+
`,
7363+
);
7364+
7365+
const diags = env.driveDiagnostics();
7366+
expect(diags.length).toBe(1);
7367+
expect(diags[0].messageText).toBe(
7368+
`Cannot read @let declaration 'value' before it has been defined.`,
7369+
);
7370+
});
7371+
7372+
it('should report @let declaration used in the expression of a @for block before it is defined', () => {
7373+
env.write(
7374+
'test.ts',
7375+
`
7376+
import {Component} from '@angular/core';
7377+
7378+
@Component({
7379+
template: \`
7380+
@for (current of value; track $index) {
7381+
{{current}}
7382+
}
7383+
7384+
@let value = [1, 2, 3];
7385+
\`,
7386+
standalone: true,
7387+
})
7388+
export class Main {}
7389+
`,
7390+
);
7391+
7392+
const diags = env.driveDiagnostics();
7393+
expect(diags.length).toBe(1);
7394+
expect(diags[0].messageText).toBe(
7395+
`Cannot read @let declaration 'value' before it has been defined.`,
7396+
);
7397+
});
7398+
7399+
it('should report @let declaration used in the expression of a @switch block before it is defined', () => {
7400+
env.write(
7401+
'test.ts',
7402+
`
7403+
import {Component} from '@angular/core';
7404+
7405+
@Component({
7406+
template: \`
7407+
@switch (value) {
7408+
@case (123) {
7409+
Hello
7410+
}
7411+
}
7412+
7413+
@let value = [1, 2, 3];
7414+
\`,
7415+
standalone: true,
7416+
})
7417+
export class Main {}
7418+
`,
7419+
);
7420+
7421+
const diags = env.driveDiagnostics();
7422+
expect(diags.length).toBe(1);
7423+
expect(diags[0].messageText).toBe(
7424+
`Cannot read @let declaration 'value' before it has been defined.`,
7425+
);
7426+
});
73457427
});
73467428
});
73477429
});

0 commit comments

Comments
 (0)