From ade7deabbcf2978feabfbccb96e0e8ba532dfdac Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 08:40:54 +0000 Subject: [PATCH 1/6] feat(eslint-plugin): [switch-exhaustiveness-check] better support for intersections, infinite types, non-union values --- .../src/rules/switch-exhaustiveness-check.ts | 121 ++- .../rules/switch-exhaustiveness-check.test.ts | 901 +++++++++++++++--- 2 files changed, 806 insertions(+), 216 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index c6ed86463ca0..243876fcc323 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -14,9 +14,8 @@ import { interface SwitchMetadata { readonly symbolName: string | undefined; - readonly missingBranchTypes: ts.Type[]; readonly defaultCase: TSESTree.SwitchCase | undefined; - readonly isUnion: boolean; + readonly missingLiteralBranchTypes: ts.Type[]; readonly containsNonLiteralType: boolean; } @@ -109,16 +108,6 @@ export default createRule({ const containsNonLiteralType = doesTypeContainNonLiteralType(discriminantType); - if (!discriminantType.isUnion()) { - return { - symbolName, - missingBranchTypes: [], - defaultCase, - isUnion: false, - containsNonLiteralType, - }; - } - const caseTypes = new Set(); for (const switchCase of node.cases) { // If the `test` property of the switch case is `null`, then we are on a @@ -134,54 +123,51 @@ export default createRule({ caseTypes.add(caseType); } - const unionTypes = tsutils.unionTypeParts(discriminantType); - const missingBranchTypes = unionTypes.filter( - unionType => !caseTypes.has(unionType), - ); + function findMissingBranchTypes(): ts.Type[] { + const missingLiteralTypes: ts.Type[] = []; + + for (const unionPart of tsutils.unionTypeParts(discriminantType)) { + for (const intersectionPart of tsutils.intersectionTypeParts( + unionPart, + )) { + if ( + caseTypes.has(intersectionPart) || + !isTypeLiteralLikeType(intersectionPart) + ) { + continue; + } + + missingLiteralTypes.push(intersectionPart); + } + } + + return missingLiteralTypes; + } return { symbolName, - missingBranchTypes, + missingLiteralBranchTypes: findMissingBranchTypes(), defaultCase, - isUnion: true, containsNonLiteralType, }; } - /** - * For example: - * - * - `"foo" | "bar"` is a type with all literal types. - * - `"foo" | number` is a type that contains non-literal types. - * - * Default cases are never superfluous in switches with non-literal types. - */ - function doesTypeContainNonLiteralType(type: ts.Type): boolean { - const types = tsutils.unionTypeParts(type); - return types.some( - type => - !isFlagSet( - type.getFlags(), - ts.TypeFlags.Literal | ts.TypeFlags.Undefined | ts.TypeFlags.Null, - ), - ); - } - function checkSwitchExhaustive( node: TSESTree.SwitchStatement, switchMetadata: SwitchMetadata, ): void { - const { missingBranchTypes, symbolName, defaultCase } = switchMetadata; + const { missingLiteralBranchTypes, symbolName, defaultCase } = + switchMetadata; // We only trigger the rule if a `default` case does not exist, since that // would disqualify the switch statement from having cases that exactly // match the members of a union. - if (missingBranchTypes.length > 0 && defaultCase === undefined) { + if (missingLiteralBranchTypes.length > 0 && defaultCase === undefined) { context.report({ node: node.discriminant, messageId: 'switchIsNotExhaustive', data: { - missingBranches: missingBranchTypes + missingBranches: missingLiteralBranchTypes .map(missingType => tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) ? `typeof ${missingType.getSymbol()?.escapedName as string}` @@ -196,7 +182,7 @@ export default createRule({ return fixSwitch( fixer, node, - missingBranchTypes, + missingLiteralBranchTypes, symbolName?.toString(), ); }, @@ -227,22 +213,6 @@ export default createRule({ continue; } - // While running this rule on the "checker.ts" file of TypeScript, the - // the fix introduced a compiler error due to: - // - // ```ts - // type __String = (string & { - // __escapedIdentifier: void; - // }) | (void & { - // __escapedIdentifier: void; - // }) | InternalSymbolName; - // ``` - // - // The following check fixes it. - if (missingBranchType.isIntersection()) { - continue; - } - const missingBranchName = missingBranchType.getSymbol()?.escapedName; let caseTest = checker.typeToString(missingBranchType); @@ -298,11 +268,11 @@ export default createRule({ return; } - const { missingBranchTypes, defaultCase, containsNonLiteralType } = + const { missingLiteralBranchTypes, defaultCase, containsNonLiteralType } = switchMetadata; if ( - missingBranchTypes.length === 0 && + missingLiteralBranchTypes.length === 0 && defaultCase !== undefined && !containsNonLiteralType ) { @@ -321,9 +291,9 @@ export default createRule({ return; } - const { isUnion, defaultCase } = switchMetadata; + const { defaultCase, containsNonLiteralType } = switchMetadata; - if (!isUnion && defaultCase === undefined) { + if (containsNonLiteralType && defaultCase === undefined) { context.report({ node: node.discriminant, messageId: 'switchIsNotExhaustive', @@ -354,6 +324,31 @@ export default createRule({ }, }); -function isFlagSet(flags: number, flag: number): boolean { - return (flags & flag) !== 0; +function isTypeLiteralLikeType(type: ts.Type): boolean { + return tsutils.isTypeFlagSet( + type, + ts.TypeFlags.Literal | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.ESSymbolLike, + ); +} + +/** + * For example: + * + * - `"foo" | "bar"` is a type with all literal types. + * - `"foo" | number` is a type that contains non-literal types. + * - `"foo" & { bar: 1 }` is a type that contains non-literal types. + * + * Default cases are never superfluous in switches with non-literal types. + */ +function doesTypeContainNonLiteralType(type: ts.Type): boolean { + return tsutils + .unionTypeParts(type) + .some(type => + tsutils + .intersectionTypeParts(type) + .every(subType => !isTypeLiteralLikeType(subType)), + ); } diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index 5d58d0576052..8d19c666a74b 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -333,6 +333,322 @@ switch (value) { }, ], }, + { + code: ` +declare const value: 'literal'; +switch (value) { + case 'literal': + return 0; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: null; +switch (value) { + case null: + return 0; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: undefined; +switch (value) { + case undefined: + return 0; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: null | undefined; +switch (value) { + case null: + return 0; + case undefined: + return 0; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: 'literal' & { _brand: true }; +switch (value) { + case 'literal': + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: ('literal' & { _brand: true }) | 1; +switch (value) { + case 'literal': + break; + case 1: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: (1 & { _brand: true }) | 'literal' | null; +switch (value) { + case 'literal': + break; + case 1: + break; + case null: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case '2': + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case '2': + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case '2': + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | (number & { foo: 'bar' }); +switch (value) { + case '1': + break; + case '2': + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case '2': + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: number | null | undefined; +switch (value) { + case null: + break; + case undefined: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: (string & { foo: void }) | 'bar'; +switch (value) { + case 'bar': + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +const a = Symbol('a'); +declare const value: typeof a | 2; +switch (value) { + case a: + break; + case 2: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: string | number; +switch (value) { + case 1: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: string | number; +switch (value) { +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: string | number; +switch (value) { + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, ], invalid: [ { @@ -477,48 +793,22 @@ type Day = | 'Wednesday' | 'Thursday' | 'Friday' - | 'Saturday' - | 'Sunday'; - -const day = 'Monday' as Day; - -switch (day) { -} - `, - errors: [ - { - messageId: 'switchIsNotExhaustive', - line: 13, - column: 9, - data: { - missingBranches: - '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', - }, - }, - ], - }, - { - // Still complains with union intersection part - code: ` -type FooBar = (string & { foo: void }) | 'bar'; + | 'Saturday' + | 'Sunday'; -const foobar = 'bar' as FooBar; -let result = 0; +const day = 'Monday' as Day; -switch (foobar) { - case 'bar': { - result = 42; - break; - } +switch (day) { } `, errors: [ { messageId: 'switchIsNotExhaustive', - line: 7, + line: 13, column: 9, data: { - missingBranches: 'string & { foo: void; }', + missingBranches: + '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', }, }, ], @@ -821,17 +1111,258 @@ switch (value) { { messageId: 'addMissingCases', output: ` - enum Enum { - 'a' = 1, - "'a' \`b\` \\"c\\"" = 2, - } - - declare const a: Enum; - - switch (a) { - case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } - } + enum Enum { + 'a' = 1, + "'a' \`b\` \\"c\\"" = 2, + } + + declare const a: Enum; + + switch (a) { + case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } + case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } + } + `, + }, + ], + }, + ], + }, + { + // superfluous switch with a string-based union + code: ` +type MyUnion = 'foo' | 'bar' | 'baz'; + +declare const myUnion: MyUnion; + +switch (myUnion) { + case 'foo': + case 'bar': + case 'baz': { + break; + } + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with a string-based enum + code: ` +enum MyEnum { + Foo = 'Foo', + Bar = 'Bar', + Baz = 'Baz', +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + case MyEnum.Bar: + case MyEnum.Baz: { + break; + } + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with a number-based enum + code: ` +enum MyEnum { + Foo, + Bar, + Baz, +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + case MyEnum.Bar: + case MyEnum.Baz: { + break; + } + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with a boolean + code: ` +declare const myBoolean: boolean; + +switch (myBoolean) { + case true: + case false: { + break; + } + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with undefined + code: ` +declare const myValue: undefined; + +switch (myValue) { + case undefined: { + break; + } + + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with null + code: ` +declare const myValue: null; + +switch (myValue) { + case null: { + break; + } + + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with union of various types + code: ` +declare const myValue: 'foo' | boolean | undefined | null; + +switch (myValue) { + case 'foo': + case true: + case false: + case undefined: + case null: { + break; + } + + default: { + break; + } +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + code: ` +declare const value: 'literal'; +switch (value) { +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: 'literal'; +switch (value) { +case "literal": { throw new Error('Not implemented yet: "literal" case') } +} `, }, ], @@ -839,212 +1370,276 @@ switch (value) { ], }, { - // superfluous switch with a string-based union code: ` -type MyUnion = 'foo' | 'bar' | 'baz'; - -declare const myUnion: MyUnion; - -switch (myUnion) { - case 'foo': - case 'bar': - case 'baz': { - break; - } - default: { - break; - } +declare const value: 'literal' & { _brand: true }; +switch (value) { } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: 'literal' & { _brand: true }; +switch (value) { +case "literal": { throw new Error('Not implemented yet: "literal" case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with a string-based enum code: ` -enum MyEnum { - Foo = 'Foo', - Bar = 'Bar', - Baz = 'Baz', -} - -declare const myEnum: MyEnum; - -switch (myEnum) { - case MyEnum.Foo: - case MyEnum.Bar: - case MyEnum.Baz: { - break; - } - default: { +declare const value: ('literal' & { _brand: true }) | 1; +switch (value) { + case 'literal': break; - } } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: ('literal' & { _brand: true }) | 1; +switch (value) { + case 'literal': + break; + case 1: { throw new Error('Not implemented yet: 1 case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with a number-based enum code: ` -enum MyEnum { - Foo, - Bar, - Baz, -} - -declare const myEnum: MyEnum; - -switch (myEnum) { - case MyEnum.Foo: - case MyEnum.Bar: - case MyEnum.Baz: { - break; - } - default: { +declare const value: '1' | '2' | number; +switch (value) { + case '1': break; - } } `, options: [ { - allowDefaultCaseForExhaustiveSwitch: false, + allowDefaultCaseForExhaustiveSwitch: true, requireDefaultForNonUnion: false, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case "2": { throw new Error('Not implemented yet: "2" case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with a boolean code: ` -declare const myBoolean: boolean; - -switch (myBoolean) { - case true: - case false: { - break; - } - default: { +declare const value: '1' | '2' | number; +switch (value) { + case '1': break; - } } `, options: [ { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case "2": { throw new Error('Not implemented yet: "2" case') } +} + `, + }, + ], + }, + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + default: { throw new Error('default case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with undefined code: ` -declare const myValue: undefined; - -switch (myValue) { - case undefined: { - break; - } - - default: { +declare const value: (string & { foo: void }) | 'bar'; +switch (value) { + case 'bar': break; - } } `, options: [ { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: (string & { foo: void }) | 'bar'; +switch (value) { + case 'bar': + break; + default: { throw new Error('default case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with null code: ` -declare const myValue: null; - -switch (myValue) { - case null: { - break; - } - - default: { - break; - } +declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; +switch (value) { } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; +switch (value) { +case undefined: { throw new Error('Not implemented yet: undefined case') } +case null: { throw new Error('Not implemented yet: null case') } +case "bar": { throw new Error('Not implemented yet: "bar" case') } +case 1: { throw new Error('Not implemented yet: 1 case') } +} + `, + }, + ], + }, + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; +switch (value) { +default: { throw new Error('default case') } +} + `, + }, + ], }, ], }, { - // superfluous switch with union of various types code: ` -declare const myValue: 'foo' | boolean | undefined | null; - -switch (myValue) { - case 'foo': - case true: - case false: - case undefined: - case null: { - break; - } - - default: { +declare const value: string | number; +switch (value) { + case 1: break; - } } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, + requireDefaultForNonUnion: true, }, ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: string | number; +switch (value) { + case 1: + break; + default: { throw new Error('default case') } +} + `, + }, + ], }, ], }, From 8d52a3a658092c2554707302afacaf54e61c468a Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 09:01:55 +0000 Subject: [PATCH 2/6] chore: try to fix weird diff with main --- .../rules/switch-exhaustiveness-check.test.ts | 1170 ++++++++--------- 1 file changed, 585 insertions(+), 585 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index 8d19c666a74b..b99fa550619f 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -575,9 +575,9 @@ switch (value) { }, { code: ` -declare const value: (string & { foo: void }) | 'bar'; +declare const value: (string & { foo: 'bar' }) | '1'; switch (value) { - case 'bar': + case '1': break; } `, @@ -652,221 +652,130 @@ switch (value) { ], invalid: [ { - // Matched only one branch out of seven. code: ` -type Day = - | 'Monday' - | 'Tuesday' - | 'Wednesday' - | 'Thursday' - | 'Friday' - | 'Saturday' - | 'Sunday'; - -const day = 'Monday' as Day; -let result = 0; - -switch (day) { - case 'Monday': { - result = 1; - break; - } +declare const value: 'literal'; +switch (value) { } `, - errors: [ + options: [ { - messageId: 'switchIsNotExhaustive', - line: 14, - column: 9, - data: { - missingBranches: - '"Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', - }, + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, }, ], - }, - { - // Didn't match all enum variants - code: ` -enum Enum { - A, - B, -} - -function test(value: Enum): number { - switch (value) { - case Enum.A: - return 1; - } -} - `, errors: [ { messageId: 'switchIsNotExhaustive', - line: 8, - column: 11, - data: { - missingBranches: 'Enum.B', - }, + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: 'literal'; +switch (value) { +case "literal": { throw new Error('Not implemented yet: "literal" case') } +} + `, + }, + ], }, ], }, { code: ` -type A = 'a'; -type B = 'b'; -type C = 'c'; -type Union = A | B | C; - -function test(value: Union): number { - switch (value) { - case 'a': - return 1; - } +declare const value: 'literal' & { _brand: true }; +switch (value) { } `, - errors: [ + options: [ { - messageId: 'switchIsNotExhaustive', - line: 8, - column: 11, - data: { - missingBranches: '"b" | "c"', - }, + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, }, ], - }, - { - code: ` -const A = 'a'; -const B = 1; -const C = true; - -type Union = typeof A | typeof B | typeof C; - -function test(value: Union): number { - switch (value) { - case 'a': - return 1; - } -} - `, errors: [ { messageId: 'switchIsNotExhaustive', - line: 9, - column: 11, - data: { - missingBranches: 'true | 1', - }, + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: 'literal' & { _brand: true }; +switch (value) { +case "literal": { throw new Error('Not implemented yet: "literal" case') } +} + `, + }, + ], }, ], }, { code: ` -type DiscriminatedUnion = { type: 'A'; a: 1 } | { type: 'B'; b: 2 }; - -function test(value: DiscriminatedUnion): number { - switch (value.type) { - case 'A': - return 1; - } +declare const value: ('literal' & { _brand: true }) | 1; +switch (value) { + case 'literal': + break; } `, - errors: [ + options: [ { - messageId: 'switchIsNotExhaustive', - line: 5, - column: 11, - data: { - missingBranches: '"B"', - }, + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, }, ], - }, - { - // Still complains with empty switch - code: ` -type Day = - | 'Monday' - | 'Tuesday' - | 'Wednesday' - | 'Thursday' - | 'Friday' - | 'Saturday' - | 'Sunday'; - -const day = 'Monday' as Day; - -switch (day) { -} - `, errors: [ { messageId: 'switchIsNotExhaustive', - line: 13, + line: 3, column: 9, - data: { - missingBranches: - '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', - }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: ('literal' & { _brand: true }) | 1; +switch (value) { + case 'literal': + break; + case 1: { throw new Error('Not implemented yet: 1 case') } +} + `, + }, + ], }, ], }, { code: ` -const a = Symbol('a'); -const b = Symbol('b'); -const c = Symbol('c'); - -type T = typeof a | typeof b | typeof c; - -function test(value: T): number { - switch (value) { - case a: - return 1; - } +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; } `, - errors: [ + options: [ { - messageId: 'switchIsNotExhaustive', - line: 9, - column: 11, - data: { - missingBranches: 'typeof b | typeof c', - }, + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, }, ], - }, - // Provides suggestions to add missing cases - { - // with existing cases present - code: ` -type T = 1 | 2; - -function test(value: T): number { - switch (value) { - case 1: - return 1; - } -} - `, errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -type T = 1 | 2; - -function test(value: T): number { - switch (value) { - case 1: - return 1; - case 2: { throw new Error('Not implemented yet: 2 case') } - } +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case "2": { throw new Error('Not implemented yet: "2" case') } } `, }, @@ -875,66 +784,51 @@ function test(value: T): number { ], }, { - // without existing cases code: ` -type T = 1 | 2; - -function test(value: T): number { - switch (value) { - } +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; } `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -type T = 1 | 2; - -function test(value: T): number { - switch (value) { - case 1: { throw new Error('Not implemented yet: 1 case') } - case 2: { throw new Error('Not implemented yet: 2 case') } - } +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + case "2": { throw new Error('Not implemented yet: "2" case') } } `, }, ], }, - ], - }, - { - // keys include special characters - code: ` -export enum Enum { - 'test-test' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - } -} - `, - errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -export enum Enum { - 'test-test' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - case Enum['test-test']: { throw new Error('Not implemented yet: Enum[\\'test-test\\'] case') } - case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } - } +declare const value: '1' | '2' | number; +switch (value) { + case '1': + break; + default: { throw new Error('default case') } } `, }, @@ -943,72 +837,33 @@ function test(arg: Enum): string { ], }, { - // keys include empty string code: ` -export enum Enum { - '' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - } +declare const value: (string & { foo: 'bar' }) | '1'; +switch (value) { + case '1': + break; } `, - errors: [ + options: [ { - messageId: 'switchIsNotExhaustive', - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -export enum Enum { - '' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - case Enum['']: { throw new Error('Not implemented yet: Enum[\\'\\'] case') } - case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } - } -} - `, - }, - ], + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, }, ], - }, - { - // keys include number as first character - code: ` -export enum Enum { - '9test' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - } -} - `, errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -export enum Enum { - '9test' = 'test-test', - 'test' = 'test', -} - -function test(arg: Enum): string { - switch (arg) { - case Enum['9test']: { throw new Error('Not implemented yet: Enum[\\'9test\\'] case') } - case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } - } +declare const value: (string & { foo: 'bar' }) | '1'; +switch (value) { + case '1': + break; + default: { throw new Error('default case') } } `, }, @@ -1018,75 +873,48 @@ function test(arg: Enum): string { }, { code: ` -const value: number = Math.floor(Math.random() * 3); +declare const value: (string & { foo: 'bar' }) | '1' | 1 | null | undefined; switch (value) { - case 0: - return 0; - case 1: - return 1; } `, options: [ { - allowDefaultCaseForExhaustiveSwitch: true, + allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true, }, ], errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -const value: number = Math.floor(Math.random() * 3); +declare const value: (string & { foo: 'bar' }) | '1' | 1 | null | undefined; switch (value) { - case 0: - return 0; - case 1: - return 1; - default: { throw new Error('default case') } +case undefined: { throw new Error('Not implemented yet: undefined case') } +case null: { throw new Error('Not implemented yet: null case') } +case "1": { throw new Error('Not implemented yet: "1" case') } +case 1: { throw new Error('Not implemented yet: 1 case') } } `, }, ], }, - ], - }, - { - code: ` - enum Enum { - 'a' = 1, - [\`key-with - - new-line\`] = 2, - } - - declare const a: Enum; - - switch (a) { - } - `, - errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` - enum Enum { - 'a' = 1, - [\`key-with - - new-line\`] = 2, - } - - declare const a: Enum; - - switch (a) { - case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\n\\n new-line\\'] case') } - } +declare const value: (string & { foo: 'bar' }) | '1' | 1 | null | undefined; +switch (value) { +default: { throw new Error('default case') } +} `, }, ], @@ -1094,274 +922,256 @@ switch (value) { ], }, { - code: noFormat` - enum Enum { - 'a' = 1, - "'a' \`b\` \\"c\\"" = 2, - } - - declare const a: Enum; - - switch (a) {} + code: ` +declare const value: string | number; +switch (value) { + case 1: + 1; +} `, errors: [ { messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` - enum Enum { - 'a' = 1, - "'a' \`b\` \\"c\\"" = 2, - } - - declare const a: Enum; - - switch (a) { - case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } - case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } - } +declare const value: string | number; +switch (value) { + case 1: + break; + default: { throw new Error('default case') } +} `, }, ], }, ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], }, { - // superfluous switch with a string-based union + // Matched only one branch out of seven. code: ` -type MyUnion = 'foo' | 'bar' | 'baz'; +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; -declare const myUnion: MyUnion; +const day = 'Monday' as Day; +let result = 0; -switch (myUnion) { - case 'foo': - case 'bar': - case 'baz': { - break; - } - default: { +switch (day) { + case 'Monday': { + result = 1; break; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 14, + column: 9, + data: { + missingBranches: + '"Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', + }, }, ], }, { - // superfluous switch with a string-based enum + // Didn't match all enum variants code: ` -enum MyEnum { - Foo = 'Foo', - Bar = 'Bar', - Baz = 'Baz', +enum Enum { + A, + B, } -declare const myEnum: MyEnum; - -switch (myEnum) { - case MyEnum.Foo: - case MyEnum.Bar: - case MyEnum.Baz: { - break; - } - default: { - break; +function test(value: Enum): number { + switch (value) { + case Enum.A: + return 1; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 8, + column: 11, + data: { + missingBranches: 'Enum.B', + }, }, ], }, { - // superfluous switch with a number-based enum code: ` -enum MyEnum { - Foo, - Bar, - Baz, -} - -declare const myEnum: MyEnum; +type A = 'a'; +type B = 'b'; +type C = 'c'; +type Union = A | B | C; -switch (myEnum) { - case MyEnum.Foo: - case MyEnum.Bar: - case MyEnum.Baz: { - break; - } - default: { - break; +function test(value: Union): number { + switch (value) { + case 'a': + return 1; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 8, + column: 11, + data: { + missingBranches: '"b" | "c"', + }, }, ], }, { - // superfluous switch with a boolean code: ` -declare const myBoolean: boolean; +const A = 'a'; +const B = 1; +const C = true; -switch (myBoolean) { - case true: - case false: { - break; - } - default: { - break; +type Union = typeof A | typeof B | typeof C; + +function test(value: Union): number { + switch (value) { + case 'a': + return 1; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 9, + column: 11, + data: { + missingBranches: 'true | 1', + }, }, ], }, { - // superfluous switch with undefined code: ` -declare const myValue: undefined; - -switch (myValue) { - case undefined: { - break; - } +type DiscriminatedUnion = { type: 'A'; a: 1 } | { type: 'B'; b: 2 }; - default: { - break; +function test(value: DiscriminatedUnion): number { + switch (value.type) { + case 'A': + return 1; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 5, + column: 11, + data: { + missingBranches: '"B"', + }, }, ], }, { - // superfluous switch with null + // Still complains with empty switch code: ` -declare const myValue: null; +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; -switch (myValue) { - case null: { - break; - } +const day = 'Monday' as Day; - default: { - break; - } +switch (day) { } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 13, + column: 9, + data: { + missingBranches: + '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', + }, }, ], }, { - // superfluous switch with union of various types code: ` -declare const myValue: 'foo' | boolean | undefined | null; +const a = Symbol('a'); +const b = Symbol('b'); +const c = Symbol('c'); -switch (myValue) { - case 'foo': - case true: - case false: - case undefined: - case null: { - break; - } +type T = typeof a | typeof b | typeof c; - default: { - break; +function test(value: T): number { + switch (value) { + case a: + return 1; } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: false, - }, - ], errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', + line: 9, + column: 11, + data: { + missingBranches: 'typeof b | typeof c', + }, }, ], }, + // Provides suggestions to add missing cases { + // with existing cases present code: ` -declare const value: 'literal'; -switch (value) { +type T = 1 | 2; + +function test(value: T): number { + switch (value) { + case 1: + return 1; + } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: 'literal'; -switch (value) { -case "literal": { throw new Error('Not implemented yet: "literal" case') } +type T = 1 | 2; + +function test(value: T): number { + switch (value) { + case 1: + return 1; + case 2: { throw new Error('Not implemented yet: 2 case') } + } } `, }, @@ -1370,29 +1180,29 @@ case "literal": { throw new Error('Not implemented yet: "literal" case') } ], }, { + // without existing cases code: ` -declare const value: 'literal' & { _brand: true }; -switch (value) { +type T = 1 | 2; + +function test(value: T): number { + switch (value) { + } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: 'literal' & { _brand: true }; -switch (value) { -case "literal": { throw new Error('Not implemented yet: "literal" case') } +type T = 1 | 2; + +function test(value: T): number { + switch (value) { + case 1: { throw new Error('Not implemented yet: 1 case') } + case 2: { throw new Error('Not implemented yet: 2 case') } + } } `, }, @@ -1401,33 +1211,35 @@ case "literal": { throw new Error('Not implemented yet: "literal" case') } ], }, { + // keys include special characters code: ` -declare const value: ('literal' & { _brand: true }) | 1; -switch (value) { - case 'literal': - break; +export enum Enum { + 'test-test' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + } } `, - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: ('literal' & { _brand: true }) | 1; -switch (value) { - case 'literal': - break; - case 1: { throw new Error('Not implemented yet: 1 case') } +export enum Enum { + 'test-test' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + case Enum['test-test']: { throw new Error('Not implemented yet: Enum[\\'test-test\\'] case') } + case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } + } } `, }, @@ -1436,33 +1248,72 @@ switch (value) { ], }, { + // keys include empty string code: ` -declare const value: '1' | '2' | number; -switch (value) { - case '1': - break; +export enum Enum { + '' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + } } `, - options: [ + errors: [ { - allowDefaultCaseForExhaustiveSwitch: true, - requireDefaultForNonUnion: false, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +export enum Enum { + '' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + case Enum['']: { throw new Error('Not implemented yet: Enum[\\'\\'] case') } + case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } + } +} + `, + }, + ], }, ], + }, + { + // keys include number as first character + code: ` +export enum Enum { + '9test' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + } +} + `, errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: '1' | '2' | number; -switch (value) { - case '1': - break; - case "2": { throw new Error('Not implemented yet: "2" case') } +export enum Enum { + '9test' = 'test-test', + 'test' = 'test', +} + +function test(arg: Enum): string { + switch (arg) { + case Enum['9test']: { throw new Error('Not implemented yet: Enum[\\'9test\\'] case') } + case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } + } } `, }, @@ -1472,10 +1323,12 @@ switch (value) { }, { code: ` -declare const value: '1' | '2' | number; +const value: number = Math.floor(Math.random() * 3); switch (value) { - case '1': - break; + case 0: + return 0; + case 1: + return 1; } `, options: [ @@ -1487,36 +1340,58 @@ switch (value) { errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: '1' | '2' | number; +const value: number = Math.floor(Math.random() * 3); switch (value) { - case '1': - break; - case "2": { throw new Error('Not implemented yet: "2" case') } + case 0: + return 0; + case 1: + return 1; + default: { throw new Error('default case') } } `, }, ], }, + ], + }, + { + code: ` + enum Enum { + 'a' = 1, + [\`key-with + + new-line\`] = 2, + } + + declare const a: Enum; + + switch (a) { + } + `, + errors: [ { messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, suggestions: [ { messageId: 'addMissingCases', output: ` -declare const value: '1' | '2' | number; -switch (value) { - case '1': - break; - default: { throw new Error('default case') } -} + enum Enum { + 'a' = 1, + [\`key-with + + new-line\`] = 2, + } + + declare const a: Enum; + + switch (a) { + case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } + case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\n\\n new-line\\'] case') } + } `, }, ], @@ -1524,122 +1399,247 @@ switch (value) { ], }, { + code: noFormat` + enum Enum { + 'a' = 1, + "'a' \`b\` \\"c\\"" = 2, + } + + declare const a: Enum; + + switch (a) {} + `, + errors: [ + { + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` + enum Enum { + 'a' = 1, + "'a' \`b\` \\"c\\"" = 2, + } + + declare const a: Enum; + + switch (a) { + case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } + case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } + } + `, + }, + ], + }, + ], + }, + { + // superfluous switch with a string-based union code: ` -declare const value: (string & { foo: void }) | 'bar'; -switch (value) { +type MyUnion = 'foo' | 'bar' | 'baz'; + +declare const myUnion: MyUnion; + +switch (myUnion) { + case 'foo': case 'bar': + case 'baz': { + break; + } + default: { break; + } } `, options: [ { - allowDefaultCaseForExhaustiveSwitch: true, - requireDefaultForNonUnion: true, + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, }, ], errors: [ { - messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -declare const value: (string & { foo: void }) | 'bar'; -switch (value) { - case 'bar': + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with a string-based enum + code: ` +enum MyEnum { + Foo = 'Foo', + Bar = 'Bar', + Baz = 'Baz', +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + case MyEnum.Bar: + case MyEnum.Baz: { break; - default: { throw new Error('default case') } + } + default: { + break; + } } `, - }, - ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', }, ], }, { + // superfluous switch with a number-based enum code: ` -declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; -switch (value) { +enum MyEnum { + Foo, + Bar, + Baz, +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + case MyEnum.Bar: + case MyEnum.Baz: { + break; + } + default: { + break; + } } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, + requireDefaultForNonUnion: false, }, ], errors: [ { - messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; -switch (value) { -case undefined: { throw new Error('Not implemented yet: undefined case') } -case null: { throw new Error('Not implemented yet: null case') } -case "bar": { throw new Error('Not implemented yet: "bar" case') } -case 1: { throw new Error('Not implemented yet: 1 case') } + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with a boolean + code: ` +declare const myBoolean: boolean; + +switch (myBoolean) { + case true: + case false: { + break; + } + default: { + break; + } } `, - }, - ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, }, + ], + errors: [ { - messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -declare const value: (string & { foo: void }) | 'bar' | 1 | null | undefined; -switch (value) { -default: { throw new Error('default case') } + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with undefined + code: ` +declare const myValue: undefined; + +switch (myValue) { + case undefined: { + break; + } + + default: { + break; + } } `, - }, - ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', }, ], }, { + // superfluous switch with null code: ` -declare const value: string | number; -switch (value) { - case 1: +declare const myValue: null; + +switch (myValue) { + case null: { + break; + } + + default: { break; + } } `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, + requireDefaultForNonUnion: false, }, ], errors: [ { - messageId: 'switchIsNotExhaustive', - line: 3, - column: 9, - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -declare const value: string | number; -switch (value) { - case 1: + messageId: 'dangerousDefaultCase', + }, + ], + }, + { + // superfluous switch with union of various types + code: ` +declare const myValue: 'foo' | boolean | undefined | null; + +switch (myValue) { + case 'foo': + case true: + case false: + case undefined: + case null: { break; - default: { throw new Error('default case') } + } + + default: { + break; + } } `, - }, - ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'dangerousDefaultCase', }, ], }, From 7850ed455d1ed7b4db37d58e46ff0f7a5b28a6ea Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 09:03:22 +0000 Subject: [PATCH 3/6] --- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index b99fa550619f..b90682b27ee8 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -926,7 +926,7 @@ default: { throw new Error('default case') } declare const value: string | number; switch (value) { case 1: - 1; + break; } `, errors: [ From 7d40deb4228673c5d6d80785856233cb14871d1c Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 09:07:00 +0000 Subject: [PATCH 4/6] refactor: no need to collect missing branches in function --- .../src/rules/switch-exhaustiveness-check.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 243876fcc323..4a2984c2a06d 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -123,30 +123,26 @@ export default createRule({ caseTypes.add(caseType); } - function findMissingBranchTypes(): ts.Type[] { - const missingLiteralTypes: ts.Type[] = []; - - for (const unionPart of tsutils.unionTypeParts(discriminantType)) { - for (const intersectionPart of tsutils.intersectionTypeParts( - unionPart, - )) { - if ( - caseTypes.has(intersectionPart) || - !isTypeLiteralLikeType(intersectionPart) - ) { - continue; - } - - missingLiteralTypes.push(intersectionPart); + const missingLiteralBranchTypes: ts.Type[] = []; + + for (const unionPart of tsutils.unionTypeParts(discriminantType)) { + for (const intersectionPart of tsutils.intersectionTypeParts( + unionPart, + )) { + if ( + caseTypes.has(intersectionPart) || + !isTypeLiteralLikeType(intersectionPart) + ) { + continue; } - } - return missingLiteralTypes; + missingLiteralBranchTypes.push(intersectionPart); + } } return { symbolName, - missingLiteralBranchTypes: findMissingBranchTypes(), + missingLiteralBranchTypes, defaultCase, containsNonLiteralType, }; From b7b890c14a340cc4f554f74aa1c76ca756ff6b07 Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 10:02:49 +0000 Subject: [PATCH 5/6] fix: provide valid fixes for unique symbols --- .../src/rules/switch-exhaustiveness-check.ts | 16 +- .../rules/switch-exhaustiveness-check.test.ts | 480 ++++++++++++++++++ 2 files changed, 489 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 4a2984c2a06d..1fd8d96eecc2 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -91,6 +91,12 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); + function stringifyType(type: ts.Type): string { + return tsutils.isTypeFlagSet(type, ts.TypeFlags.UniqueESSymbol) + ? `typeof ${type.getSymbol()?.escapedName as string}` + : checker.typeToString(type); + } + function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata { const defaultCase = node.cases.find( switchCase => switchCase.test == null, @@ -164,11 +170,7 @@ export default createRule({ messageId: 'switchIsNotExhaustive', data: { missingBranches: missingLiteralBranchTypes - .map(missingType => - tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) - ? `typeof ${missingType.getSymbol()?.escapedName as string}` - : checker.typeToString(missingType), - ) + .map(stringifyType) .join(' | '), }, suggest: [ @@ -210,7 +212,7 @@ export default createRule({ } const missingBranchName = missingBranchType.getSymbol()?.escapedName; - let caseTest = checker.typeToString(missingBranchType); + let caseTest = stringifyType(missingBranchType); if ( symbolName && @@ -326,7 +328,7 @@ function isTypeLiteralLikeType(type: ts.Type): boolean { ts.TypeFlags.Literal | ts.TypeFlags.Undefined | ts.TypeFlags.Null | - ts.TypeFlags.ESSymbolLike, + ts.TypeFlags.UniqueESSymbol, ); } diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index b90682b27ee8..9937d246df5f 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -649,6 +649,143 @@ switch (value) { }, ], }, + { + code: ` +declare const value: number; +declare const a: number; +switch (value) { + case a: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: bigint; +switch (value) { + case 10n: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: symbol; +const a = Symbol('a'); +switch (value) { + case a: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +declare const value: symbol; +const a = Symbol('a'); +switch (value) { + case a: + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +const a = Symbol('a'); +declare const value: typeof a | string; +switch (value) { + case a: + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +const a = Symbol('a'); +declare const value: typeof a | string; +switch (value) { + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: boolean | 1; +switch (value) { + case 1: + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: boolean | 1; +switch (value) { + case 1: + break; + case true: + break; + case false: + break; + default: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, ], invalid: [ { @@ -929,6 +1066,12 @@ switch (value) { break; } `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], errors: [ { messageId: 'switchIsNotExhaustive', @@ -949,12 +1092,349 @@ switch (value) { ], }, ], + }, + { + code: ` +declare const value: number; +declare const a: number; +switch (value) { + case a: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 4, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: number; +declare const a: number; +switch (value) { + case a: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: bigint; +switch (value) { + case 10n: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: bigint; +switch (value) { + case 10n: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: symbol; +const a = Symbol('a'); +switch (value) { + case a: + break; +} + `, options: [ { allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true, }, ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 4, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: symbol; +const a = Symbol('a'); +switch (value) { + case a: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +const a = Symbol('a'); +const b = Symbol('b'); +declare const value: typeof a | typeof b | 1; +switch (value) { + case 1: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 5, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const a = Symbol('a'); +const b = Symbol('b'); +declare const value: typeof a | typeof b | 1; +switch (value) { + case 1: + break; + case typeof a: { throw new Error('Not implemented yet: typeof a case') } + case typeof b: { throw new Error('Not implemented yet: typeof b case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +const a = Symbol('a'); +declare const value: typeof a | string; +switch (value) { + case a: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 4, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const a = Symbol('a'); +declare const value: typeof a | string; +switch (value) { + case a: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: boolean; +switch (value) { +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: boolean; +switch (value) { +case false: { throw new Error('Not implemented yet: false case') } +case true: { throw new Error('Not implemented yet: true case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: boolean | 1; +switch (value) { + case false: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: boolean | 1; +switch (value) { + case false: + break; + case true: { throw new Error('Not implemented yet: true case') } + case 1: { throw new Error('Not implemented yet: 1 case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: boolean | number; +switch (value) { + case 1: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: boolean | number; +switch (value) { + case 1: + break; + case false: { throw new Error('Not implemented yet: false case') } + case true: { throw new Error('Not implemented yet: true case') } +} + `, + }, + ], + }, + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: boolean | number; +switch (value) { + case 1: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const value: object; +switch (value) { + case 1: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 3, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const value: object; +switch (value) { + case 1: + break; + default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], }, { // Matched only one branch out of seven. From e343dcead170ffe333ddbbcd996c2a38e30d85f5 Mon Sep 17 00:00:00 2001 From: auvred Date: Sun, 14 Jan 2024 12:33:58 +0000 Subject: [PATCH 6/6] fix: valid fixes for unique symbols + few test cases for enums --- .../src/rules/switch-exhaustiveness-check.ts | 19 ++-- .../rules/switch-exhaustiveness-check.test.ts | 106 +++++++++++++++++- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 1fd8d96eecc2..3247982dc2ad 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -91,12 +91,6 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); - function stringifyType(type: ts.Type): string { - return tsutils.isTypeFlagSet(type, ts.TypeFlags.UniqueESSymbol) - ? `typeof ${type.getSymbol()?.escapedName as string}` - : checker.typeToString(type); - } - function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata { const defaultCase = node.cases.find( switchCase => switchCase.test == null, @@ -170,7 +164,11 @@ export default createRule({ messageId: 'switchIsNotExhaustive', data: { missingBranches: missingLiteralBranchTypes - .map(stringifyType) + .map(missingType => + tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) + ? `typeof ${missingType.getSymbol()?.escapedName as string}` + : checker.typeToString(missingType), + ) .join(' | '), }, suggest: [ @@ -212,7 +210,12 @@ export default createRule({ } const missingBranchName = missingBranchType.getSymbol()?.escapedName; - let caseTest = stringifyType(missingBranchType); + let caseTest = tsutils.isTypeFlagSet( + missingBranchType, + ts.TypeFlags.ESSymbolLike, + ) + ? missingBranchName! + : checker.typeToString(missingBranchType); if ( symbolName && diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index 9937d246df5f..b7919942e011 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -777,6 +777,29 @@ switch (value) { break; default: break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + }, + { + code: ` +enum Aaa { + Foo, + Bar, +} +declare const value: Aaa | 1; +switch (value) { + case 1: + break; + case Aaa.Foo: + break; + case Aaa.Bar: + break; } `, options: [ @@ -1204,8 +1227,8 @@ switch (value) { }, { code: ` -const a = Symbol('a'); -const b = Symbol('b'); +const a = Symbol('aa'); +const b = Symbol('bb'); declare const value: typeof a | typeof b | 1; switch (value) { case 1: @@ -1227,14 +1250,14 @@ switch (value) { { messageId: 'addMissingCases', output: ` -const a = Symbol('a'); -const b = Symbol('b'); +const a = Symbol('aa'); +const b = Symbol('bb'); declare const value: typeof a | typeof b | 1; switch (value) { case 1: break; - case typeof a: { throw new Error('Not implemented yet: typeof a case') } - case typeof b: { throw new Error('Not implemented yet: typeof b case') } + case a: { throw new Error('Not implemented yet: a case') } + case b: { throw new Error('Not implemented yet: b case') } } `, }, @@ -1429,6 +1452,77 @@ switch (value) { case 1: break; default: { throw new Error('default case') } +} + `, + }, + ], + }, + ], + }, + { + code: ` +enum Aaa { + Foo, + Bar, +} +declare const value: Aaa | 1 | string; +switch (value) { + case 1: + break; + case Aaa.Foo: + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: true, + }, + ], + errors: [ + { + messageId: 'switchIsNotExhaustive', + line: 7, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +enum Aaa { + Foo, + Bar, +} +declare const value: Aaa | 1 | string; +switch (value) { + case 1: + break; + case Aaa.Foo: + break; + case Aaa.Bar: { throw new Error('Not implemented yet: Aaa.Bar case') } +} + `, + }, + ], + }, + { + messageId: 'switchIsNotExhaustive', + line: 7, + column: 9, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +enum Aaa { + Foo, + Bar, +} +declare const value: Aaa | 1 | string; +switch (value) { + case 1: + break; + case Aaa.Foo: + break; + default: { throw new Error('default case') } } `, },