From 4f54e7e947298162d29f3104265e74dcfbc90d82 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 12 Oct 2022 07:22:06 -0700 Subject: [PATCH] Fix isExhaustiveSwitchStatement to better handle circularities (#51095) * Fix isExhaustiveSwitchStatement to better handle circularities * Add regression test --- src/compiler/checker.ts | 16 +++++-- src/compiler/types.ts | 2 +- ...xhaustiveSwitchCheckCircularity.errors.txt | 25 +++++++++++ .../exhaustiveSwitchCheckCircularity.js | 38 ++++++++++++++++ .../exhaustiveSwitchCheckCircularity.symbols | 34 +++++++++++++++ .../exhaustiveSwitchCheckCircularity.types | 43 +++++++++++++++++++ .../exhaustiveSwitchCheckCircularity.ts | 20 +++++++++ 7 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 tests/baselines/reference/exhaustiveSwitchCheckCircularity.errors.txt create mode 100644 tests/baselines/reference/exhaustiveSwitchCheckCircularity.js create mode 100644 tests/baselines/reference/exhaustiveSwitchCheckCircularity.symbols create mode 100644 tests/baselines/reference/exhaustiveSwitchCheckCircularity.types create mode 100644 tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9ebaca719f2b9..b81e741e55c37 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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 { @@ -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) { @@ -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; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1214f822d87a0..38984dffb6543 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5571,7 +5571,7 @@ namespace ts { deferredNodes?: Set; // 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; // Collection of types serialized at this location diff --git a/tests/baselines/reference/exhaustiveSwitchCheckCircularity.errors.txt b/tests/baselines/reference/exhaustiveSwitchCheckCircularity.errors.txt new file mode 100644 index 0000000000000..96eea203d5f99 --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchCheckCircularity.errors.txt @@ -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; + } + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/exhaustiveSwitchCheckCircularity.js b/tests/baselines/reference/exhaustiveSwitchCheckCircularity.js new file mode 100644 index 0000000000000..89a043ec4a2db --- /dev/null +++ b/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; + } + } +} diff --git a/tests/baselines/reference/exhaustiveSwitchCheckCircularity.symbols b/tests/baselines/reference/exhaustiveSwitchCheckCircularity.symbols new file mode 100644 index 0000000000000..a3d1cac8a9ebd --- /dev/null +++ b/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; + } + } +} + diff --git a/tests/baselines/reference/exhaustiveSwitchCheckCircularity.types b/tests/baselines/reference/exhaustiveSwitchCheckCircularity.types new file mode 100644 index 0000000000000..16051e2046124 --- /dev/null +++ b/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; + } + } +} + diff --git a/tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts b/tests/cases/compiler/exhaustiveSwitchCheckCircularity.ts new file mode 100644 index 0000000000000..7db35fd1d000c --- /dev/null +++ b/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; + } + } +}