Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix isExhaustiveSwitchStatement to better handle circularities (#51095)
* Fix isExhaustiveSwitchStatement to better handle circularities

* Add regression test
  • Loading branch information
ahejlsberg committed Oct 12, 2022
1 parent 503604c commit 4f54e7e
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 4 deletions.
16 changes: 13 additions & 3 deletions src/compiler/checker.ts
Expand Up @@ -33328,7 +33328,17 @@ namespace ts {

function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
const links = getNodeLinks(node);
return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node));
if (links.isExhaustive === undefined) {
links.isExhaustive = 0; // Indicate resolution is in process
const exhaustive = computeExhaustiveSwitchStatement(node);
if (links.isExhaustive === 0) {
links.isExhaustive = exhaustive;
}
}
else if (links.isExhaustive === 0) {
links.isExhaustive = false; // Resolve circularity to false
}
return links.isExhaustive;
}

function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean {
Expand All @@ -33337,7 +33347,7 @@ namespace ts {
if (!witnesses) {
return false;
}
const operandConstraint = getBaseConstraintOrType(getTypeOfExpression((node.expression as TypeOfExpression).expression));
const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression));
// Get the not-equal flags for all handled cases.
const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses);
if (operandConstraint.flags & TypeFlags.AnyOrUnknown) {
Expand All @@ -33347,7 +33357,7 @@ namespace ts {
// A missing not-equal flag indicates that the type wasn't handled by some case.
return !someType(operandConstraint, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts);
}
const type = getTypeOfExpression(node.expression);
const type = checkExpressionCached(node.expression);
if (!isLiteralType(type)) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Expand Up @@ -5571,7 +5571,7 @@ namespace ts {
deferredNodes?: Set<Node>; // Set of nodes whose checking has been deferred
capturedBlockScopeBindings?: Symbol[]; // Block-scoped bindings captured beneath this part of an IterationStatement
outerTypeParameters?: TypeParameter[]; // Outer type parameters of anonymous object type
isExhaustive?: boolean; // Is node an exhaustive switch statement
isExhaustive?: boolean | 0; // Is node an exhaustive switch statement (0 indicates in-process resolution)
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
declarationRequiresScopeChange?: boolean; // Set by `useOuterVariableScopeInParameter` in checker when downlevel emit would change the name resolution scope inside of a parameter.
serializedTypes?: ESMap<string, TypeNode & {truncating?: boolean, addedLength: number}>; // Collection of types serialized at this location
Expand Down
@@ -0,0 +1,25 @@
tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts(14,26): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.


==== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts (1 errors) ====
// Repro from #47539

declare function isNever(n: never): boolean;

function f() {
let foo: "aaa" | "bbb" = "aaa";
while (true) {
switch (foo) {
case "aaa":
}
if (foo === "aaa") {
foo = "bbb";
}
else if (isNever(foo)) { // Error expected
~~~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
break;
}
}
}

38 changes: 38 additions & 0 deletions tests/baselines/reference/exhaustiveSwitchCheckCircularity.js
@@ -0,0 +1,38 @@
//// [exhaustiveSwitchCheckCircularity.ts]
// Repro from #47539

declare function isNever(n: never): boolean;

function f() {
let foo: "aaa" | "bbb" = "aaa";
while (true) {
switch (foo) {
case "aaa":
}
if (foo === "aaa") {
foo = "bbb";
}
else if (isNever(foo)) { // Error expected
break;
}
}
}


//// [exhaustiveSwitchCheckCircularity.js]
"use strict";
// Repro from #47539
function f() {
var foo = "aaa";
while (true) {
switch (foo) {
case "aaa":
}
if (foo === "aaa") {
foo = "bbb";
}
else if (isNever(foo)) { // Error expected
break;
}
}
}
34 changes: 34 additions & 0 deletions tests/baselines/reference/exhaustiveSwitchCheckCircularity.symbols
@@ -0,0 +1,34 @@
=== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts ===
// Repro from #47539

declare function isNever(n: never): boolean;
>isNever : Symbol(isNever, Decl(exhaustiveSwitchCheckCircularity.ts, 0, 0))
>n : Symbol(n, Decl(exhaustiveSwitchCheckCircularity.ts, 2, 25))

function f() {
>f : Symbol(f, Decl(exhaustiveSwitchCheckCircularity.ts, 2, 44))

let foo: "aaa" | "bbb" = "aaa";
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))

while (true) {
switch (foo) {
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))

case "aaa":
}
if (foo === "aaa") {
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))

foo = "bbb";
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))
}
else if (isNever(foo)) { // Error expected
>isNever : Symbol(isNever, Decl(exhaustiveSwitchCheckCircularity.ts, 0, 0))
>foo : Symbol(foo, Decl(exhaustiveSwitchCheckCircularity.ts, 5, 7))

break;
}
}
}

43 changes: 43 additions & 0 deletions tests/baselines/reference/exhaustiveSwitchCheckCircularity.types
@@ -0,0 +1,43 @@
=== tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts ===
// Repro from #47539

declare function isNever(n: never): boolean;
>isNever : (n: never) => boolean
>n : never

function f() {
>f : () => void

let foo: "aaa" | "bbb" = "aaa";
>foo : "aaa" | "bbb"
>"aaa" : "aaa"

while (true) {
>true : true

switch (foo) {
>foo : "aaa" | "bbb"

case "aaa":
>"aaa" : "aaa"
}
if (foo === "aaa") {
>foo === "aaa" : boolean
>foo : "aaa" | "bbb"
>"aaa" : "aaa"

foo = "bbb";
>foo = "bbb" : "bbb"
>foo : "aaa" | "bbb"
>"bbb" : "bbb"
}
else if (isNever(foo)) { // Error expected
>isNever(foo) : boolean
>isNever : (n: never) => boolean
>foo : "bbb"

break;
}
}
}

20 changes: 20 additions & 0 deletions tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts
@@ -0,0 +1,20 @@
// @strict: true

// Repro from #47539

declare function isNever(n: never): boolean;

function f() {
let foo: "aaa" | "bbb" = "aaa";
while (true) {
switch (foo) {
case "aaa":
}
if (foo === "aaa") {
foo = "bbb";
}
else if (isNever(foo)) { // Error expected
break;
}
}
}

0 comments on commit 4f54e7e

Please sign in to comment.