Skip to content

Commit 8bc31a5

Browse files
JeanMecheleonsenft
authored andcommitted
feat(core): Allow other expression for exhaustive typechecking
When the switched expression is nested within a union, exhaustive typechecking needs to know which expression to check. This change adds the possibility of specifying the expression to check: ``` @component({ selector: 'app-root', imports: [], template: ` @switch (state.mode) { @case ('show') { {{ state.menu }}; } @case ('hide') {} @default never(state); } `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class App { state!: { mode: 'hide' } | { mode: 'show'; menu: number };; } ``` fixes #67406
1 parent 0eeb1b5 commit 8bc31a5

File tree

13 files changed

+147
-22
lines changed

13 files changed

+147
-22
lines changed

adev/src/content/guide/templates/control-flow.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,20 @@ export class AppComponent {
160160
state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut';
161161
}
162162
```
163+
164+
When the switched expression is nested within a union, you must explicitly specify the expression to check for exhaustiveness.
165+
166+
```angular-ts
167+
@Component({
168+
template: `
169+
@switch (state.mode) {
170+
@case ('show') { {{ state.menu }}; }
171+
@case ('hide') {}
172+
@default never(state);
173+
}
174+
`,
175+
})
176+
export class App {
177+
state!: {mode: 'hide'} | {mode: 'show'; menu: number};
178+
}
179+
```

packages/compiler-cli/src/ngtsc/typecheck/src/ops/switch_block.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ export class TcbSwitchOp extends TcbOp {
6060
});
6161

6262
if (this.block.exhaustiveCheck) {
63-
const switchValue = tcbExpression(this.block.expression, this.tcb, this.scope);
63+
let translateExpression = this.block.expression;
64+
if (this.block.exhaustiveCheck.expression) {
65+
translateExpression = this.block.exhaustiveCheck.expression;
66+
}
67+
68+
const switchValue = tcbExpression(translateExpression, this.tcb, this.scope);
6469
const exhaustiveId = this.tcb.allocateId();
6570

6671
clauses.push(

packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,25 @@ class TestComponent {
14531453

14541454
expect(messages).toEqual([]);
14551455
});
1456+
1457+
it('should narrow union when switching on a nested prop', () => {
1458+
const messages = diagnose(
1459+
`
1460+
@switch (state.mode) {
1461+
@case ('show') { {{ state.menu }}; }
1462+
@case ('hide') {}
1463+
@default never(state);
1464+
}
1465+
`,
1466+
`
1467+
export class TestComponent {
1468+
state: { mode: 'hide' } | { mode: 'show'; menu: number };
1469+
}
1470+
`,
1471+
);
1472+
1473+
expect(messages).toEqual([]);
1474+
});
14561475
});
14571476

14581477
// https://github.com/angular/angular/issues/43970

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2202,6 +2202,27 @@ describe('type check blocks', () => {
22022202
);
22032203
});
22042204

2205+
it('should generate a switch block with exhaustiveness checking with param', () => {
2206+
const TEMPLATE = `
2207+
@switch (expr) {
2208+
@case (1) {
2209+
{{one()}}
2210+
}
2211+
@case (2) {
2212+
{{two()}}
2213+
}
2214+
@default never(expr.prop);
2215+
}
2216+
`;
2217+
2218+
expect(tcb(TEMPLATE)).toContain(
2219+
'switch (((this).expr)) { ' +
2220+
'case 1: "" + ((this).one()); break; ' +
2221+
'case 2: "" + ((this).two()); break; ' +
2222+
'default: const tcbExhaustive_t1: never = ((((this).expr)).prop);',
2223+
);
2224+
});
2225+
22052226
it('should not report unused locals for exhaustiveness check variable', () => {
22062227
const TEMPLATE = `
22072228
@switch (expr) {

packages/compiler/src/ml_parser/lexer.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,14 +299,6 @@ class _Tokenizer {
299299
this._beginToken(TokenType.BLOCK_OPEN_START, start);
300300
const startToken = this._endToken([this._getBlockName()]);
301301

302-
if (startToken.parts[0] === 'default never' && this._attemptCharCode(chars.$SEMICOLON)) {
303-
this._beginToken(TokenType.BLOCK_OPEN_END);
304-
this._endToken([]);
305-
this._beginToken(TokenType.BLOCK_CLOSE);
306-
this._endToken([]);
307-
return;
308-
}
309-
310302
if (this._cursor.peek() === chars.$LPAREN) {
311303
// Advance past the opening paren.
312304
this._cursor.advance();
@@ -324,6 +316,14 @@ class _Tokenizer {
324316
}
325317
}
326318

319+
if (startToken.parts[0] === 'default never' && this._attemptCharCode(chars.$SEMICOLON)) {
320+
this._beginToken(TokenType.BLOCK_OPEN_END);
321+
this._endToken([]);
322+
this._beginToken(TokenType.BLOCK_CLOSE);
323+
this._endToken([]);
324+
return;
325+
}
326+
327327
if (this._attemptCharCode(chars.$LBRACE)) {
328328
this._beginToken(TokenType.BLOCK_OPEN_END);
329329
this._endToken([]);

packages/compiler/src/render3/r3_ast.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ export class SwitchBlockCaseGroup extends BlockNode implements Node {
471471

472472
export class SwitchExhaustiveCheck extends BlockNode implements Node {
473473
constructor(
474+
public expression: AST | null,
474475
sourceSpan: ParseSourceSpan,
475476
startSourceSpan: ParseSourceSpan,
476477
endSourceSpan: ParseSourceSpan | null,

packages/compiler/src/render3/r3_control_flow.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ export function createSwitchBlock(
264264
if (isCase) {
265265
expression = parseBlockParameterToBinding(node.parameters[0], bindingParser);
266266
} else if (node.name === 'default never') {
267+
if (node.parameters.length > 0) {
268+
expression = parseBlockParameterToBinding(node.parameters[0], bindingParser);
269+
}
270+
267271
if (
268272
node.children.length > 0 ||
269273
(node.endSourceSpan !== null &&
@@ -287,6 +291,7 @@ export function createSwitchBlock(
287291
}
288292

289293
exhaustiveCheck = new t.SwitchExhaustiveCheck(
294+
expression,
290295
node.sourceSpan,
291296
node.startSourceSpan,
292297
node.endSourceSpan,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ class TemplateBinder extends CombinedRecursiveAstVisitor {
953953
}
954954

955955
override visitSwitchExhaustiveCheck(block: SwitchExhaustiveCheck) {
956-
// There are no bindings/references in the exhaustive check block.
956+
block.expression?.visit(this);
957957
}
958958

959959
override visitForLoopBlock(block: ForLoopBlock) {

packages/compiler/test/ml_parser/lexer_spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3396,6 +3396,16 @@ describe('HtmlLexer', () => {
33963396
]);
33973397
});
33983398

3399+
it('should parse @default never(expr);', () => {
3400+
expect(tokenizeAndHumanizeParts('@default never(expr);')).toEqual([
3401+
[TokenType.BLOCK_OPEN_START, 'default never'],
3402+
[TokenType.BLOCK_PARAMETER, 'expr'],
3403+
[TokenType.BLOCK_OPEN_END],
3404+
[TokenType.BLOCK_CLOSE],
3405+
[TokenType.EOF],
3406+
]);
3407+
});
3408+
33993409
it('should parse @default never ;', () => {
34003410
expect(tokenizeAndHumanizeParts('@default never ;')).toEqual([
34013411
[TokenType.BLOCK_OPEN_START, 'default never'],

packages/language-service/src/template_target.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,9 @@ class TemplateTargetVisitor implements TmplAstVisitor {
677677
this.visitAll(block.children);
678678
}
679679

680-
visitSwitchExhaustiveCheck(block: TmplAstSwitchExhaustiveCheck) {}
680+
visitSwitchExhaustiveCheck(block: TmplAstSwitchExhaustiveCheck) {
681+
block.expression && this.visitBinding(block.expression);
682+
}
681683

682684
visitForLoopBlock(block: TmplAstForLoopBlock) {
683685
this.visit(block.item);

0 commit comments

Comments
 (0)