Skip to content

Commit 4f54e7e

Browse files
authoredOct 12, 2022
Fix isExhaustiveSwitchStatement to better handle circularities (#51095)
* Fix isExhaustiveSwitchStatement to better handle circularities * Add regression test
1 parent 503604c commit 4f54e7e

7 files changed

+174
-4
lines changed
 

‎src/compiler/checker.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -33328,7 +33328,17 @@ namespace ts {
3332833328

3332933329
function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
3333033330
const links = getNodeLinks(node);
33331-
return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node));
33331+
if (links.isExhaustive === undefined) {
33332+
links.isExhaustive = 0; // Indicate resolution is in process
33333+
const exhaustive = computeExhaustiveSwitchStatement(node);
33334+
if (links.isExhaustive === 0) {
33335+
links.isExhaustive = exhaustive;
33336+
}
33337+
}
33338+
else if (links.isExhaustive === 0) {
33339+
links.isExhaustive = false; // Resolve circularity to false
33340+
}
33341+
return links.isExhaustive;
3333233342
}
3333333343

3333433344
function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean {
@@ -33337,7 +33347,7 @@ namespace ts {
3333733347
if (!witnesses) {
3333833348
return false;
3333933349
}
33340-
const operandConstraint = getBaseConstraintOrType(getTypeOfExpression((node.expression as TypeOfExpression).expression));
33350+
const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression));
3334133351
// Get the not-equal flags for all handled cases.
3334233352
const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses);
3334333353
if (operandConstraint.flags & TypeFlags.AnyOrUnknown) {
@@ -33347,7 +33357,7 @@ namespace ts {
3334733357
// A missing not-equal flag indicates that the type wasn't handled by some case.
3334833358
return !someType(operandConstraint, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts);
3334933359
}
33350-
const type = getTypeOfExpression(node.expression);
33360+
const type = checkExpressionCached(node.expression);
3335133361
if (!isLiteralType(type)) {
3335233362
return false;
3335333363
}

‎src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5571,7 +5571,7 @@ namespace ts {
55715571
deferredNodes?: Set<Node>; // Set of nodes whose checking has been deferred
55725572
capturedBlockScopeBindings?: Symbol[]; // Block-scoped bindings captured beneath this part of an IterationStatement
55735573
outerTypeParameters?: TypeParameter[]; // Outer type parameters of anonymous object type
5574-
isExhaustive?: boolean; // Is node an exhaustive switch statement
5574+
isExhaustive?: boolean | 0; // Is node an exhaustive switch statement (0 indicates in-process resolution)
55755575
skipDirectInference?: true; // Flag set by the API `getContextualType` call on a node when `Completions` is passed to force the checker to skip making inferences to a node's type
55765576
declarationRequiresScopeChange?: boolean; // Set by `useOuterVariableScopeInParameter` in checker when downlevel emit would change the name resolution scope inside of a parameter.
55775577
serializedTypes?: ESMap<string, TypeNode & {truncating?: boolean, addedLength: number}>; // Collection of types serialized at this location
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts(14,26): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
2+
3+
4+
==== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts (1 errors) ====
5+
// Repro from #47539
6+
7+
declare function isNever(n: never): boolean;
8+
9+
function f() {
10+
let foo: "aaa" | "bbb" = "aaa";
11+
while (true) {
12+
switch (foo) {
13+
case "aaa":
14+
}
15+
if (foo === "aaa") {
16+
foo = "bbb";
17+
}
18+
else if (isNever(foo)) { // Error expected
19+
~~~
20+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
21+
break;
22+
}
23+
}
24+
}
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//// [exhaustiveSwitchCheckCircularity.ts]
2+
// Repro from #47539
3+
4+
declare function isNever(n: never): boolean;
5+
6+
function f() {
7+
let foo: "aaa" | "bbb" = "aaa";
8+
while (true) {
9+
switch (foo) {
10+
case "aaa":
11+
}
12+
if (foo === "aaa") {
13+
foo = "bbb";
14+
}
15+
else if (isNever(foo)) { // Error expected
16+
break;
17+
}
18+
}
19+
}
20+
21+
22+
//// [exhaustiveSwitchCheckCircularity.js]
23+
"use strict";
24+
// Repro from #47539
25+
function f() {
26+
var foo = "aaa";
27+
while (true) {
28+
switch (foo) {
29+
case "aaa":
30+
}
31+
if (foo === "aaa") {
32+
foo = "bbb";
33+
}
34+
else if (isNever(foo)) { // Error expected
35+
break;
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
=== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts ===
2+
// Repro from #47539
3+
4+
declare function isNever(n: never): boolean;
5+
>isNever : Symbol(isNever, Decl(exhaustiveSwitchCheckCircularity.ts, 0, 0))
6+
>n : Symbol(n, Decl(exhaustiveSwitchCheckCircularity.ts, 2, 25))
7+
8+
function f() {
9+
>f : Symbol(f, Decl(exhaustiveSwitchCheckCircularity.ts, 2, 44))
10+
11+
let foo: "aaa" | "bbb" = "aaa";
12+
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
13+
14+
while (true) {
15+
switch (foo) {
16+
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
17+
18+
case "aaa":
19+
}
20+
if (foo === "aaa") {
21+
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
22+
23+
foo = "bbb";
24+
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
25+
}
26+
else if (isNever(foo)) { // Error expected
27+
>isNever : Symbol(isNever, Decl(exhaustiveSwitchCheckCircularity.ts, 0, 0))
28+
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
29+
30+
break;
31+
}
32+
}
33+
}
34+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
=== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts ===
2+
// Repro from #47539
3+
4+
declare function isNever(n: never): boolean;
5+
>isNever : (n: never) => boolean
6+
>n : never
7+
8+
function f() {
9+
>f : () => void
10+
11+
let foo: "aaa" | "bbb" = "aaa";
12+
>foo : "aaa" | "bbb"
13+
>"aaa" : "aaa"
14+
15+
while (true) {
16+
>true : true
17+
18+
switch (foo) {
19+
>foo : "aaa" | "bbb"
20+
21+
case "aaa":
22+
>"aaa" : "aaa"
23+
}
24+
if (foo === "aaa") {
25+
>foo === "aaa" : boolean
26+
>foo : "aaa" | "bbb"
27+
>"aaa" : "aaa"
28+
29+
foo = "bbb";
30+
>foo = "bbb" : "bbb"
31+
>foo : "aaa" | "bbb"
32+
>"bbb" : "bbb"
33+
}
34+
else if (isNever(foo)) { // Error expected
35+
>isNever(foo) : boolean
36+
>isNever : (n: never) => boolean
37+
>foo : "bbb"
38+
39+
break;
40+
}
41+
}
42+
}
43+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @strict: true
2+
3+
// Repro from #47539
4+
5+
declare function isNever(n: never): boolean;
6+
7+
function f() {
8+
let foo: "aaa" | "bbb" = "aaa";
9+
while (true) {
10+
switch (foo) {
11+
case "aaa":
12+
}
13+
if (foo === "aaa") {
14+
foo = "bbb";
15+
}
16+
else if (isNever(foo)) { // Error expected
17+
break;
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)