diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 64ab5ef6b2e2..c6ed86463ca0 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 9a06ace1e0a9..5d58d0576052 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; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy