From 6f7f6d6b9c43b07ce195642597299f05aa353a25 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 4 Nov 2023 19:33:34 +0100 Subject: [PATCH 1/5] rule(switch-exhaustiveness-check): add requireDefaultForNonUnion option --- .../docs/rules/switch-exhaustiveness-check.md | 23 ++++++- .../src/rules/switch-exhaustiveness-check.ts | 62 ++++++++++++++++--- .../rules/switch-exhaustiveness-check.test.ts | 50 +++++++++++++++ .../switch-exhaustiveness-check.shot | 22 ++++++- 4 files changed, 146 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 9320624924b9..283061f799b7 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 type.' +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 changes, it's easy to forget to modify the cases to a This rule reports when a `switch` statement over a value typed as a union of literals 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 @@ -101,6 +103,25 @@ switch (day) { } ``` +## 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. + ## When Not To Use It If you don't frequently `switch` over union types 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 6abdbf27fc6e..39a4c16edc6f 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -11,25 +11,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 type', + 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 = context.getSourceCode(); const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -38,7 +60,7 @@ 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 { const lastCase = @@ -51,6 +73,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: // @@ -163,6 +189,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 | null { + 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 b250a09e2bb2..2f22a047e67f 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,22 @@ function test(value: ObjectUnion): number { } } `, + // switch with default clause on non-union type + { + code: ` +const value: number = Math.floor(Math.random() * 3); + +switch (value) { + case 0: + return 0; + case 1: + return 1; + default: + return -1; +} + `, + options: [{ requireDefaultForNonUnion: true }], + }, ], invalid: [ { @@ -595,6 +611,40 @@ 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 b9d9916dc427..10996a21371f 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; + }, +]; +" `; From e85158d56d8223cc4c01a768c5430605901f150c Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 12 Nov 2023 18:49:28 +0100 Subject: [PATCH 2/5] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 2 +- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 283061f799b7..0d599ca3bc5e 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -120,7 +120,7 @@ switch (value) { } ``` -Since `value` is a non-union type it requires the switch case to have a default clause. +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 diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 39a4c16edc6f..d59aa9d0849c 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -204,7 +204,7 @@ export default createRule({ suggest: [ { messageId: 'addMissingCases', - fix(fixer): TSESLint.RuleFix | null { + 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 2f22a047e67f..b1e9c3430436 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -212,7 +212,7 @@ function test(value: ObjectUnion): number { // switch with default clause on non-union type { code: ` -const value: number = Math.floor(Math.random() * 3); +declare const value: number; switch (value) { case 0: From 172792dce49c2be137acce25a020aae748970a6d Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 12 Nov 2023 18:54:32 +0100 Subject: [PATCH 3/5] chore: apply suggestions to more locations --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index d59aa9d0849c..27acebe8ad25 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -62,7 +62,7 @@ export default createRule({ node: TSESTree.SwitchStatement, 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 @@ -178,7 +178,7 @@ export default createRule({ suggest: [ { messageId: 'addMissingCases', - fix(fixer): TSESLint.RuleFix | null { + fix(fixer): TSESLint.RuleFix { return fixSwitch( fixer, node, From a0c6aff11635e3e2e16b5649ec6299beddd9ac4a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 19 Nov 2023 15:45:04 +0100 Subject: [PATCH 4/5] fix lint --- .../docs/rules/switch-exhaustiveness-check.md | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index e40efa37483a..4b306f75a57e 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -107,29 +107,6 @@ switch (day) { } ``` -<<<<<<< HEAD - -## 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. - - - Likewise, here are some examples of code working with an enum: @@ -202,7 +179,26 @@ switch (fruit) { -> > > > > > > main +## 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 From 9a8ae4cf10f2c9b539591372920a89cfb1122410 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 19 Nov 2023 15:46:46 +0100 Subject: [PATCH 5/5] Fix linting and ordering of md --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 4b306f75a57e..267a2199ed11 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -107,6 +107,8 @@ switch (day) { } ``` + + Likewise, here are some examples of code working with an enum: