diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 11998bfc5f2..267a2199ed1 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -1,5 +1,5 @@ --- -description: 'Require switch-case statements to be exhaustive with union types and enums.' +description: 'Require switch-case statements to be exhaustive.' --- > 🛑 This file is source code, not the primary documentation location! 🛑 @@ -11,6 +11,8 @@ However, if the union type or the enum changes, it's easy to forget to modify th This rule reports when a `switch` statement over a value typed as a union of literals or as an enum is missing a case for any of those literal types and does not have a `default` clause. +There is also an option to check the exhaustiveness of switches on non-union types by requiring a default clause. + ## Examples When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint. @@ -179,6 +181,27 @@ switch (fruit) { +## Options + +### `requireDefaultForNonUnion` + +Examples of additional **incorrect** code for this rule with `{ requireDefaultForNonUnion: true }`: + +```ts option='{ "requireDefaultForNonUnion": true }' showPlaygroundButton +const value: number = Math.floor(Math.random() * 3); + +switch (value) { + case 0: + return 0; + case 1: + return 1; +} +``` + +Since `value` is a non-union type it requires the switch case to have a default clause only with `requireDefaultForNonUnion` enabled. + + + ## When Not To Use It If you don't frequently `switch` over union types or enums with many parts, or intentionally wish to leave out some parts. diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 91ca9b555a7..90172006589 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -12,25 +12,47 @@ import { requiresQuoting, } from '../util'; -export default createRule({ +type MessageIds = 'switchIsNotExhaustive' | 'addMissingCases'; +type Options = [ + { + /** + * If `true`, require a `default` clause for switches on non-union types. + * + * @default false + */ + requireDefaultForNonUnion?: boolean; + }, +]; + +export default createRule({ name: 'switch-exhaustiveness-check', meta: { type: 'suggestion', docs: { - description: - 'Require switch-case statements to be exhaustive with union types and enums', + description: 'Require switch-case statements to be exhaustive', requiresTypeChecking: true, }, hasSuggestions: true, - schema: [], + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + requireDefaultForNonUnion: { + description: `If 'true', require a 'default' clause for switches on non-union types.`, + type: 'boolean', + }, + }, + }, + ], messages: { switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', addMissingCases: 'Add branches for missing cases.', }, }, - defaultOptions: [], - create(context) { + defaultOptions: [{ requireDefaultForNonUnion: false }], + create(context, [{ requireDefaultForNonUnion }]) { const sourceCode = getSourceCode(context); const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -39,9 +61,9 @@ export default createRule({ function fixSwitch( fixer: TSESLint.RuleFixer, node: TSESTree.SwitchStatement, - missingBranchTypes: ts.Type[], + missingBranchTypes: (ts.Type | null)[], // null means default branch symbolName?: string, - ): TSESLint.RuleFix | null { + ): TSESLint.RuleFix { const lastCase = node.cases.length > 0 ? node.cases[node.cases.length - 1] : null; const caseIndent = lastCase @@ -52,6 +74,10 @@ export default createRule({ const missingCases = []; for (const missingBranchType of missingBranchTypes) { + if (missingBranchType == null) { + missingCases.push(`default: { throw new Error('default case') }`); + continue; + } // While running this rule on checker.ts of TypeScript project // the fix introduced a compiler error due to: // @@ -159,7 +185,7 @@ export default createRule({ suggest: [ { messageId: 'addMissingCases', - fix(fixer): TSESLint.RuleFix | null { + fix(fixer): TSESLint.RuleFix { return fixSwitch( fixer, node, @@ -170,6 +196,28 @@ export default createRule({ }, ], }); + } else if (requireDefaultForNonUnion) { + const hasDefault = node.cases.some( + switchCase => switchCase.test == null, + ); + + if (!hasDefault) { + context.report({ + node: node.discriminant, + messageId: 'switchIsNotExhaustive', + data: { + missingBranches: 'default', + }, + suggest: [ + { + messageId: 'addMissingCases', + fix(fixer): TSESLint.RuleFix { + return fixSwitch(fixer, node, [null]); + }, + }, + ], + }); + } } } 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 9852e0d543d..cf7f6791753 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -209,6 +209,21 @@ function test(value: ObjectUnion): number { } } `, + // switch with default clause on non-union type + { + code: ` +declare const value: number; +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: + return -1; +} + `, + options: [{ requireDefaultForNonUnion: true }], + }, ], invalid: [ { @@ -595,6 +610,38 @@ function test(arg: Enum): string { case Enum['9test']: { throw new Error('Not implemented yet: Enum[\\'9test\\'] case') } case Enum.test: { throw new Error('Not implemented yet: Enum.test case') } } +} + `, + }, + ], + }, + ], + }, + { + code: ` +const value: number = Math.floor(Math.random() * 3); +switch (value) { + case 0: + return 0; + case 1: + return 1; +} + `, + options: [{ requireDefaultForNonUnion: true }], + errors: [ + { + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const value: number = Math.floor(Math.random() * 3); +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: { throw new Error('default case') } } `, }, 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 b9d9916dc42..10996a21371 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -4,11 +4,27 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos " # SCHEMA: -[] +[ + { + "additionalProperties": false, + "properties": { + "requireDefaultForNonUnion": { + "description": "If 'true', require a 'default' clause for switches on non-union types.", + "type": "boolean" + } + }, + "type": "object" + } +] # TYPES: -/** No options declared */ -type Options = [];" +type Options = [ + { + /** If 'true', require a 'default' clause for switches on non-union types. */ + requireDefaultForNonUnion?: boolean; + }, +]; +" `;