diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 538b911049c3..e35aaed0d072 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -23,7 +23,7 @@ This rule reports when a `switch` statement over a value typed as a union of lit If set to false, this rule will also report when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. Thus, by setting this option to false, the rule becomes stricter. When a `switch` statement over a union type is exhaustive, a final `default` case would be a form of dead code. -Additionally, if a new value is added to the union type, a `default` would prevent the `switch-exhaustiveness-check` rule from reporting on the new case not being handled in the `switch` statement. +Additionally, if a new value is added to the union type and you're using [`considerDefaultExhaustiveForUnions`](#considerDefaultExhaustiveForUnions), a `default` would prevent the `switch-exhaustiveness-check` rule from reporting on the new case not being handled in the `switch` statement. #### `allowDefaultCaseForExhaustiveSwitch` Caveats @@ -57,6 +57,57 @@ switch (value) { Since `value` is a non-union type it requires the switch case to have a default clause only with `requireDefaultForNonUnion` enabled. +### `considerDefaultExhaustiveForUnions` + +{/* insert option description */} + +If set to true, a `switch` statement over a union type that includes a `default` case is considered exhaustive. +Otherwise, the rule enforces explicitly handling every constituent of the union type with their own explicit `case`. +Keeping this option disabled can be useful if you want to make sure every value added to the union receives explicit handling, with the `default` case reserved for reporting an error. + +Examples of code with `{ considerDefaultExhaustiveForUnions: true }`: + + + + +```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + default: + break; +} +``` + + + + + +```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + case 'b': + break; + default: + break; +} + +switch (literal) { + case 'a': + break; + case 'b': + break; +} +``` + + + + ## Examples When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint. diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index c5ebc2970175..6aa6f9f78fdb 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -37,6 +37,13 @@ type Options = [ * @default false */ requireDefaultForNonUnion?: boolean; + + /** + * If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types. + * + * @default false + */ + considerDefaultExhaustiveForUnions?: boolean; }, ]; @@ -70,6 +77,10 @@ export default createRule({ type: 'boolean', description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`, }, + considerDefaultExhaustiveForUnions: { + type: 'boolean', + description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`, + }, requireDefaultForNonUnion: { type: 'boolean', description: `If 'true', require a 'default' clause for switches on non-union types.`, @@ -81,12 +92,19 @@ export default createRule({ defaultOptions: [ { allowDefaultCaseForExhaustiveSwitch: true, + considerDefaultExhaustiveForUnions: false, requireDefaultForNonUnion: false, }, ], create( context, - [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }], + [ + { + allowDefaultCaseForExhaustiveSwitch, + considerDefaultExhaustiveForUnions, + requireDefaultForNonUnion, + }, + ], ) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -156,10 +174,13 @@ export default createRule({ const { defaultCase, missingLiteralBranchTypes, symbolName } = 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 (missingLiteralBranchTypes.length > 0 && defaultCase === undefined) { + // Unless considerDefaultExhaustiveForUnions is enabled, the presence of a default case + // always makes the switch exhaustive. + if (!considerDefaultExhaustiveForUnions && defaultCase != null) { + return; + } + + if (missingLiteralBranchTypes.length > 0) { context.report({ node: node.discriminant, messageId: 'switchIsNotExhaustive', @@ -197,6 +218,8 @@ export default createRule({ ): TSESLint.RuleFix { const lastCase = node.cases.length > 0 ? node.cases[node.cases.length - 1] : null; + const defaultCase = node.cases.find(caseEl => caseEl.test == null); + const caseIndent = lastCase ? ' '.repeat(lastCase.loc.start.column) : // If there are no cases, use indentation of the switch statement and @@ -244,6 +267,13 @@ export default createRule({ .join('\n'); if (lastCase) { + if (defaultCase) { + const beforeFixString = missingCases + .map(code => `${code}\n${caseIndent}`) + .join(''); + + return fixer.insertTextBefore(defaultCase, beforeFixString); + } return fixer.insertTextAfter(lastCase, `\n${fixString}`); } diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot index 1b2fd682199f..f763ba7e0ed7 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot @@ -17,6 +17,46 @@ switch (value) { exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 2`] = ` "Incorrect +Options: { "considerDefaultExhaustiveForUnions": true } + +declare const literal: 'a' | 'b'; + +switch (literal) { + ~~~~~~~ Switch is not exhaustive. Cases not matched: "b" + case 'a': + break; + default: + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = ` +"Correct +Options: { "considerDefaultExhaustiveForUnions": true } + +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + case 'b': + break; + default: + break; +} + +switch (literal) { + case 'a': + break; + case 'b': + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = ` +"Incorrect type Day = | 'Monday' @@ -39,7 +79,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` "Correct type Day = @@ -80,7 +120,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` "Correct type Day = @@ -105,7 +145,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` "Incorrect enum Fruit { @@ -125,7 +165,7 @@ switch (fruit) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = ` "Correct enum Fruit { @@ -152,7 +192,7 @@ switch (fruit) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 9`] = ` "Correct enum Fruit { 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 13869b62c859..3bcbd37a491a 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -810,6 +810,104 @@ switch (value) { }, ], }, + { + code: ` +declare const literal: 'a' | 'b'; +switch (literal) { + case 'a': + break; + case 'b': + break; +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const literal: 'a' | 'b'; +switch (literal) { + case 'a': + break; + case 'b': + break; + default: + break; +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const literal: 'a' | 'b'; +switch (literal) { + case 'a': + break; + case 'b': + break; +} + `, + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +enum MyEnum { + Foo = 'Foo', + Bar = 'Bar', + Baz = 'Baz', +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + break; + case MyEnum.Bar: + break; + case MyEnum.Baz: + break; + default: { + break; + } +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const value: boolean; +switch (value) { + case false: + break; + case true: + break; + default: { + break; + } +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, ], invalid: [ { @@ -2373,5 +2471,252 @@ switch (myValue) { }, ], }, + { + code: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + default: + break; +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + case "b": { throw new Error('Not implemented yet: "b" case') } + default: + break; +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + case 'a': + break; + case "b": { throw new Error('Not implemented yet: "b" case') } +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + default: + case 'a': + break; +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b'; + +switch (literal) { + case "b": { throw new Error('Not implemented yet: "b" case') } + default: + case 'a': + break; +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + default: + break; +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + case "b": { throw new Error('Not implemented yet: "b" case') } + case "c": { throw new Error('Not implemented yet: "c" case') } + default: + break; +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +enum MyEnum { + Foo = 'Foo', + Bar = 'Bar', + Baz = 'Baz', +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + break; + default: { + break; + } +} + `, + errors: [ + { + column: 9, + line: 10, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +enum MyEnum { + Foo = 'Foo', + Bar = 'Bar', + Baz = 'Baz', +} + +declare const myEnum: MyEnum; + +switch (myEnum) { + case MyEnum.Foo: + break; + case MyEnum.Bar: { throw new Error('Not implemented yet: MyEnum.Bar case') } + case MyEnum.Baz: { throw new Error('Not implemented yet: MyEnum.Baz case') } + default: { + break; + } +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const value: boolean; +switch (value) { + default: { + break; + } +} + `, + errors: [ + { + column: 9, + line: 3, + messageId: 'switchIsNotExhaustive', + 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') } + default: { + break; + } +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot index 6146dadc128b..05cbeedd6162 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -12,6 +12,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "description": "If 'true', allow 'default' cases on switch statements with exhaustive cases.", "type": "boolean" }, + "considerDefaultExhaustiveForUnions": { + "description": "If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type", + "type": "boolean" + }, "requireDefaultForNonUnion": { "description": "If 'true', require a 'default' clause for switches on non-union types.", "type": "boolean" @@ -28,6 +32,8 @@ type Options = [ { /** If 'true', allow 'default' cases on switch statements with exhaustive cases. */ allowDefaultCaseForExhaustiveSwitch?: boolean; + /** If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type */ + considerDefaultExhaustiveForUnions?: boolean; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; },