diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 64ab5ef6b2e..c6ed86463ca 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -17,12 +17,14 @@ interface SwitchMetadata { readonly missingBranchTypes: ts.Type[]; readonly defaultCase: TSESTree.SwitchCase | undefined; readonly isUnion: boolean; + readonly containsNonLiteralType: boolean; } type Options = [ { /** - * If `true`, allow `default` cases on switch statements with exhaustive cases. + * If `true`, allow `default` cases on switch statements with exhaustive + * cases. * * @default true */ @@ -104,12 +106,16 @@ export default createRule({ | string | undefined; + const containsNonLiteralType = + doesTypeContainNonLiteralType(discriminantType); + if (!discriminantType.isUnion()) { return { symbolName, missingBranchTypes: [], defaultCase, isUnion: false, + containsNonLiteralType, }; } @@ -138,9 +144,29 @@ export default createRule({ missingBranchTypes, 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, @@ -272,9 +298,14 @@ export default createRule({ return; } - const { missingBranchTypes, defaultCase } = switchMetadata; + const { missingBranchTypes, defaultCase, containsNonLiteralType } = + switchMetadata; - if (missingBranchTypes.length === 0 && defaultCase !== undefined) { + if ( + missingBranchTypes.length === 0 && + defaultCase !== undefined && + !containsNonLiteralType + ) { context.report({ node: defaultCase, messageId: 'dangerousDefaultCase', @@ -322,3 +353,7 @@ export default createRule({ }; }, }); + +function isFlagSet(flags: number, flag: number): boolean { + return (flags & flag) !== 0; +} 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 9a06ace1e0a..5d58d057605 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -229,6 +229,110 @@ switch (value) { }, ], }, + // switch with default clause on string type + + // "allowDefaultCaseForExhaustiveSwitch" option + { + code: ` +declare const value: string; +switch (value) { + case 'foo': + return 0; + case 'bar': + return 1; + default: + return -1; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + // switch with default clause on number type + + // "allowDefaultCaseForExhaustiveSwitch" option + { + code: ` +declare const value: number; +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: + return -1; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + // switch with default clause on bigint type + + // "allowDefaultCaseForExhaustiveSwitch" option + { + code: ` +declare const value: bigint; +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: + return -1; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + // switch with default clause on symbol type + + // "allowDefaultCaseForExhaustiveSwitch" option + { + code: ` +declare const value: symbol; +const foo = Symbol('foo'); +switch (value) { + case foo: + return 0; + default: + return -1; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, + // switch with default clause on union with number + + // "allowDefaultCaseForExhaustiveSwitch" option + { + code: ` +declare const value: 0 | 1 | number; +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: + return -1; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], + }, ], invalid: [ { @@ -735,6 +839,7 @@ switch (value) { ], }, { + // superfluous switch with a string-based union code: ` type MyUnion = 'foo' | 'bar' | 'baz'; @@ -746,6 +851,186 @@ switch (myUnion) { 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; }