From af5eda0e190661bf3e563f696f7f226950324c32 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Fri, 25 Aug 2023 22:45:24 -0400 Subject: [PATCH 01/43] feat: switch-exhaustiveness-check checks for dangerous default case --- .../docs/rules/switch-exhaustiveness-check.md | 5 +- .../src/rules/switch-exhaustiveness-check.ts | 187 +++++++++++++----- .../rules/switch-exhaustiveness-check.test.ts | 24 +++ 3 files changed, 160 insertions(+), 56 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..c951dd12a436 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -6,11 +6,12 @@ description: 'Require switch-case statements to be exhaustive with union type.' > > See **https://typescript-eslint.io/rules/switch-exhaustiveness-check** for documentation. -When working with union types in TypeScript, it's common to want to write a `switch` statement intended to contain a `case` for each constituent (possible type in the union). -However, if the union type changes, it's easy to forget to modify the cases to account for any new types. +When working with union types in TypeScript, it's common to want to write a `switch` statement intended to contain a `case` for each constituent (possible type in the union). However, if the union type changes, it's easy to forget to modify the cases to account for any new types. 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. +Additionally, this also reports when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. `default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases are actively harmful: if a new value is added to the switch statement union, the `default` statement would prevent this rule from alerting you that you need to handle the new case. + ## Examples diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 6abdbf27fc6e..42307fae17a4 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -11,6 +11,22 @@ import { requiresQuoting, } from '../util'; +interface SwitchStatementMetadata { + /** The name of the union that is inside of the switch statement. */ + symbolName: string | undefined; + + /** + * If the length of this array is equal to 0, then the switch statement is + * exhaustive. + */ + missingBranchTypes: ts.Type[]; + + /** + * The node representing the `default` case on the switch statement, if any. + */ + defaultCase: TSESTree.SwitchCase | undefined; +} + export default createRule({ name: 'switch-exhaustiveness-check', meta: { @@ -25,6 +41,8 @@ export default createRule({ messages: { switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', + dangerousDefaultCase: + 'The switch statement is exhaustive, so the default case is superfluous and will obfucate future additions to the union.', addMissingCases: 'Add branches for missing cases.', }, }, @@ -35,6 +53,104 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); + /** + * @returns Metadata about whether the switch is exhaustive (or `undefined` + * if the switch case is not a union). + */ + function getSwitchStatementMetadata( + node: TSESTree.SwitchStatement, + ): SwitchStatementMetadata | undefined { + const discriminantType = getConstrainedTypeAtLocation( + services, + node.discriminant, + ); + if (!discriminantType.isUnion()) { + return undefined; + } + + const caseTypes = new Set(); + for (const switchCase of node.cases) { + // If the `test` property of the switch case is `null`, then we are on a + // `default` case. + if (switchCase.test == null) { + continue; + } + + const caseType = getConstrainedTypeAtLocation( + services, + switchCase.test, + ); + caseTypes.add(caseType); + } + + const unionTypes = tsutils.unionTypeParts(discriminantType); + const missingBranchTypes = unionTypes.filter( + unionType => !caseTypes.has(unionType), + ); + + /** + * Convert `ts.__String | undefined` to `string | undefined` for + * simplicity. + */ + const symbolName = discriminantType.getSymbol()?.escapedName as + | string + | undefined; + + /** + * The `test` property of a `SwitchCase` node will usually be a `Literal` + * node. However, on a `default` case, it will be equal to `null`. + */ + const defaultCase = node.cases.find( + switchCase => switchCase.test == null, + ); + + return { + missingBranchTypes, + symbolName, + defaultCase, + }; + } + + function checkSwitchExhaustive( + node: TSESTree.SwitchStatement, + switchStatementMetadata: SwitchStatementMetadata, + ): void { + const { missingBranchTypes, symbolName, defaultCase } = + switchStatementMetadata; + + // We only trigger the rule if a `default` case does not exist, since that + // would disqualifies the switch statement from having cases that exactly + // match the members of a union. + if (missingBranchTypes.length > 0 && defaultCase === undefined) { + context.report({ + node: node.discriminant, + messageId: 'switchIsNotExhaustive', + data: { + missingBranches: missingBranchTypes + .map(missingType => + tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) + ? `typeof ${missingType.getSymbol()?.escapedName as string}` + : checker.typeToString(missingType), + ) + .join(' | '), + }, + suggest: [ + { + messageId: 'addMissingCases', + fix(fixer): TSESLint.RuleFix | null { + return fixSwitch( + fixer, + node, + missingBranchTypes, + symbolName?.toString(), + ); + }, + }, + ], + }); + } + } + function fixSwitch( fixer: TSESLint.RuleFixer, node: TSESTree.SwitchStatement, @@ -107,67 +223,30 @@ export default createRule({ ); } - function checkSwitchExhaustive(node: TSESTree.SwitchStatement): void { - const discriminantType = getConstrainedTypeAtLocation( - services, - node.discriminant, - ); - const symbolName = discriminantType.getSymbol()?.escapedName; - - if (discriminantType.isUnion()) { - const unionTypes = tsutils.unionTypeParts(discriminantType); - const caseTypes = new Set(); - for (const switchCase of node.cases) { - if (switchCase.test == null) { - // Switch has 'default' branch - do nothing. - return; - } - - caseTypes.add( - getConstrainedTypeAtLocation(services, switchCase.test), - ); - } - - const missingBranchTypes = unionTypes.filter( - unionType => !caseTypes.has(unionType), - ); - - if (missingBranchTypes.length === 0) { - // All cases matched - do nothing. - return; - } + function checkSwitchDangerousDefaultCase( + node: TSESTree.SwitchStatement, + switchStatementMetadata: SwitchStatementMetadata, + ): void { + const { missingBranchTypes, defaultCase } = switchStatementMetadata; + if (missingBranchTypes.length === 0 && defaultCase !== undefined) { context.report({ - node: node.discriminant, - messageId: 'switchIsNotExhaustive', - data: { - missingBranches: missingBranchTypes - .map(missingType => - tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) - ? `typeof ${missingType.getSymbol()?.escapedName as string}` - : checker.typeToString(missingType), - ) - .join(' | '), - }, - suggest: [ - { - messageId: 'addMissingCases', - fix(fixer): TSESLint.RuleFix | null { - return fixSwitch( - fixer, - node, - missingBranchTypes, - symbolName?.toString(), - ); - }, - }, - ], + node: defaultCase, + messageId: 'dangerousDefaultCase', }); } } return { - SwitchStatement: checkSwitchExhaustive, + SwitchStatement(node): void { + const switchStatementMetadata = getSwitchStatementMetadata(node); + if (switchStatementMetadata === undefined) { + return; + } + + checkSwitchExhaustive(node, switchStatementMetadata); + checkSwitchDangerousDefaultCase(node, switchStatementMetadata); + }, }; }, }); 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..57ec49d18653 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -602,5 +602,29 @@ function test(arg: Enum): string { }, ], }, + { + // has dangerous default case + code: ` +type MyUnion = 'foo' | 'bar' | 'baz'; + +declare const myUnion: MyUnion; + +switch (myUnion) { + case 'foo': + case 'bar': + case 'baz': { + break; + } + default: { + break; + } +} + `, + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + }, ], }); From 12a7635b421f15aa586f7705d1a9788dc8078fa1 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:02:04 -0400 Subject: [PATCH 02/43] fix: spelling --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 42307fae17a4..61895ad04475 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -42,7 +42,7 @@ export default createRule({ switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', dangerousDefaultCase: - 'The switch statement is exhaustive, so the default case is superfluous and will obfucate future additions to the union.', + 'The switch statement is exhaustive, so the default case is superfluous and will obfuscate future additions to the union.', addMissingCases: 'Add branches for missing cases.', }, }, From 729378fa49fa2cd9b4b929896b39239b7a262974 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:03:00 -0400 Subject: [PATCH 03/43] fix: comment --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 61895ad04475..5f84e17bdb60 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -119,7 +119,7 @@ export default createRule({ switchStatementMetadata; // We only trigger the rule if a `default` case does not exist, since that - // would disqualifies the switch statement from having cases that exactly + // would disqualify the switch statement from having cases that exactly // match the members of a union. if (missingBranchTypes.length > 0 && defaultCase === undefined) { context.report({ From 329c99ef16dadffc1f90d1d35bfabc5fc342f79a Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:09:49 -0400 Subject: [PATCH 04/43] fix: docs --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index c951dd12a436..80f76602273d 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -6,11 +6,11 @@ description: 'Require switch-case statements to be exhaustive with union type.' > > See **https://typescript-eslint.io/rules/switch-exhaustiveness-check** for documentation. -When working with union types in TypeScript, it's common to want to write a `switch` statement intended to contain a `case` for each constituent (possible type in the union). However, if the union type changes, it's easy to forget to modify the cases to account for any new types. +When working with union types (or enums) in TypeScript, it is common to write a `switch` statement intended to contain a `case` for each constituent (possible type in the union). However, if the union type (or enum) is added to, it is easy to forget to modify the switch statements throughout the codebase to account for the new addition. 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. -Additionally, this also reports when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. `default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases are actively harmful: if a new value is added to the switch statement union, the `default` statement would prevent this rule from alerting you that you need to handle the new case. +Additionally, this rule also reports when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. `default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases are actively harmful: if a new value is added to the switch statement union, the `default` statement would prevent this rule from alerting you that you need to handle the new case. ## Examples From 0e5afe502cfb8b506d3095a80b70c7b593c2e250 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 12 Oct 2023 08:57:18 -0400 Subject: [PATCH 05/43] feat: allowDefaultCase option --- .../docs/rules/switch-exhaustiveness-check.md | 12 ++++- .../src/rules/no-useless-empty-export.ts | 4 +- .../src/rules/switch-exhaustiveness-check.ts | 45 +++++++++++++------ 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 80f76602273d..265bc55dca4c 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -10,7 +10,17 @@ When working with union types (or enums) in TypeScript, it is common to write 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. -Additionally, this rule also reports when a `switch` statement has a case for everything in a union and _also_ contains a `default` case. `default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases are actively harmful: if a new value is added to the switch statement union, the `default` statement would prevent this rule from alerting you that you need to handle the new case. +## Options + +### `"allowDefaultCase"` + +Defaults to true. 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. + +`default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases can be harmful: if a new value is added to the switch statement union, the `default` statement would prevent the `switch-exhaustiveness-check` rule from alerting you that you need to handle the new case. + +Why is this important? Consider why TypeScript is valuable: when we add a new argument to a widely-used function, we don't have to go on a scavenger hunt through our codebase. We can simply run the TypeScript compiler and it will tell us all the exact places in the code that need to be updated. The `switch-exhaustiveness-check` rule is similar: when we add a new enum member, we don't have to go on a scavenger hunt. We can simply run ESLint and it will tell us all the exact places in the code that need to be updated. So in order to preserve the ability of ESLint to do this, we have to remove the `default` cases. + +Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version and have not refreshed the page yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. ## Examples diff --git a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts index 2fc04b3746af..524e0fc9c082 100644 --- a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts +++ b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts @@ -1,7 +1,7 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule } from '../util'; +import { createRule, isDefinitionFile } from '../util'; function isEmptyExport( node: TSESTree.Node, @@ -43,7 +43,7 @@ export default createRule({ // In a definition file, export {} is necessary to make the module properly // encapsulated, even when there are other exports // https://github.com/typescript-eslint/typescript-eslint/issues/4975 - if (util.isDefinitionFile(context.getFilename())) { + if (isDefinitionFile(context.getFilename())) { return {}; } function checkNode( diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 5f84e17bdb60..3f99ca2df7ac 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -37,7 +37,18 @@ export default createRule({ requiresTypeChecking: true, }, hasSuggestions: true, - schema: [], + schema: [ + { + type: 'object', + properties: { + allowDefaultCase: { + type: 'boolean', + default: true, + }, + }, + additionalProperties: false, + }, + ], messages: { switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', @@ -46,8 +57,8 @@ export default createRule({ addMissingCases: 'Add branches for missing cases.', }, }, - defaultOptions: [], - create(context) { + defaultOptions: [{ allowDefaultCase: true }], + create(context, [{ allowDefaultCase }]) { const sourceCode = context.getSourceCode(); const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -161,20 +172,22 @@ export default createRule({ node.cases.length > 0 ? node.cases[node.cases.length - 1] : null; const caseIndent = lastCase ? ' '.repeat(lastCase.loc.start.column) - : // if there are no cases, use indentation of the switch statement - // and leave it to user to format it correctly + : // If there are no cases, use indentation of the switch statement and + // leave it to the user to format it correctly. ' '.repeat(node.loc.start.column); const missingCases = []; for (const missingBranchType of missingBranchTypes) { - // While running this rule on checker.ts of TypeScript project - // the fix introduced a compiler error due to: + // While running this rule on the "checker.ts" file of TypeScript, the + // fix introduced a compiler error due to: // + // ```ts // type __String = (string & { - // __escapedIdentifier: void; - // }) | (void & { - // __escapedIdentifier: void; - // }) | InternalSymbolName; + // __escapedIdentifier: void; + // }) | (void & { + // __escapedIdentifier: void; + // }) | InternalSymbolName; + // ``` // // The following check fixes it. if (missingBranchType.isIntersection()) { @@ -207,7 +220,7 @@ export default createRule({ return fixer.insertTextAfter(lastCase, `\n${fixString}`); } - // there were no existing cases + // There were no existing cases. const openingBrace = sourceCode.getTokenAfter( node.discriminant, isOpeningBraceToken, @@ -224,9 +237,13 @@ export default createRule({ } function checkSwitchDangerousDefaultCase( - node: TSESTree.SwitchStatement, switchStatementMetadata: SwitchStatementMetadata, ): void { + // This feature of the rule is gated behind an option. + if (allowDefaultCase) { + return; + } + const { missingBranchTypes, defaultCase } = switchStatementMetadata; if (missingBranchTypes.length === 0 && defaultCase !== undefined) { @@ -245,7 +262,7 @@ export default createRule({ } checkSwitchExhaustive(node, switchStatementMetadata); - checkSwitchDangerousDefaultCase(node, switchStatementMetadata); + checkSwitchDangerousDefaultCase(switchStatementMetadata); }, }; }, From 99ef806b55a05ee259539233b8bb4074c61bd392 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:00:39 -0400 Subject: [PATCH 06/43] fix: tests --- .../tests/rules/switch-exhaustiveness-check.test.ts | 1 + 1 file changed, 1 insertion(+) 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 57ec49d18653..c3c1d349ebad 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -604,6 +604,7 @@ function test(arg: Enum): string { }, { // has dangerous default case + options: [{ allowDefaultCase: false }], code: ` type MyUnion = 'foo' | 'bar' | 'baz'; From c7ca23481711b38aa8400ad14737b13a7eb5286b Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:41:06 -0400 Subject: [PATCH 07/43] fix: lint --- .../src/rules/switch-exhaustiveness-check.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 3f99ca2df7ac..6c5daa54b9f3 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -27,7 +27,18 @@ interface SwitchStatementMetadata { defaultCase: TSESTree.SwitchCase | undefined; } -export default createRule({ +type Options = [ + { + allowDefaultCase: boolean; + } +] + +type MessageIds = + | 'switchIsNotExhaustive' + | 'dangerousDefaultCase' + | 'addMissingCases'; + +export default createRule({ name: 'switch-exhaustiveness-check', meta: { type: 'suggestion', From 5709fe344b94ac9be6b664bf4dcbef2d74d606a7 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:14:13 -0400 Subject: [PATCH 08/43] fix: prettier --- .../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 6c5daa54b9f3..ca5b3016ba4e 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -30,8 +30,8 @@ interface SwitchStatementMetadata { type Options = [ { allowDefaultCase: boolean; - } -] + }, +]; type MessageIds = | 'switchIsNotExhaustive' From 77292240efe913e77256656a8c3fd15391ec5a3d Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:40:10 -0500 Subject: [PATCH 09/43] refactor: finish merge --- .../src/rules/switch-exhaustiveness-check.ts | 91 ++++++++----------- .../rules/switch-exhaustiveness-check.test.ts | 26 +++--- 2 files changed, 48 insertions(+), 69 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index c98a0f6af404..eb892d86b710 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -31,7 +31,8 @@ interface SwitchStatementMetadata { type Options = [ { /** - * If `true`, allow superfluous `default` cases that obfucate future type additions. + * If `true`, allow superfluous `default` cases that obfuscate future type + * additions. */ allowDefaultCase: boolean; @@ -62,14 +63,15 @@ export default createRule({ { type: 'object', properties: { - requireDefaultForNonUnion: { - description: `If 'true', require a 'default' clause for switches on non-union types.`, - type: 'boolean', - }, allowDefaultCase: { + description: "If `true`, allow superfluous `default` cases that obfuscate future type additions.", type: 'boolean', default: true, }, + requireDefaultForNonUnion: { + description: `If 'true', require a 'default' clause for switches on non-union types.`, + type: 'boolean', + }, }, additionalProperties: false, }, @@ -147,10 +149,34 @@ export default createRule({ }; } + function checkSwitchNoUnionDefaultCase(node: TSESTree.SwitchStatement) { + 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]); + }, + }, + ], + }); + } + } + function checkSwitchExhaustive( node: TSESTree.SwitchStatement, switchStatementMetadata: SwitchStatementMetadata, - ): void { + ) { const { missingBranchTypes, symbolName, defaultCase } = switchStatementMetadata; @@ -287,55 +313,6 @@ export default createRule({ node: defaultCase, messageId: 'dangerousDefaultCase', }); - - context.report({ - node: node.discriminant, - messageId: 'switchIsNotExhaustive', - data: { - missingBranches: missingBranchTypes - .map(missingType => - tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) - ? `typeof ${missingType.getSymbol()?.escapedName as string}` - : checker.typeToString(missingType), - ) - .join(' | '), - }, - suggest: [ - { - messageId: 'addMissingCases', - fix(fixer): TSESLint.RuleFix { - return fixSwitch( - fixer, - node, - missingBranchTypes, - symbolName?.toString(), - ); - }, - }, - ], - }); - } 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]); - }, - }, - ], - }); - } } } @@ -343,6 +320,10 @@ export default createRule({ SwitchStatement(node): void { const switchStatementMetadata = getSwitchStatementMetadata(node); if (switchStatementMetadata === undefined) { + if (requireDefaultForNonUnion) { + checkSwitchNoUnionDefaultCase(node); + } + return; } 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 8e80123d681f..03bbd8a1d341 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -142,7 +142,7 @@ function test(value: Union): number { } } `, - // Switch contains default clause. + // Switch contains default clause ` type Day = | 'Monday' @@ -182,7 +182,7 @@ switch (day) { } } `, - // ... and enums (at least for now). + // ... and enums (at least for now) ` enum Enum { A, @@ -209,7 +209,7 @@ function test(value: ObjectUnion): number { } } `, - // switch with default clause on non-union type + // Switch with default clause on non-union type { code: ` declare const value: number; @@ -222,12 +222,12 @@ switch (value) { return -1; } `, - options: [{ requireDefaultForNonUnion: true }], + options: [{ allowDefaultCase: false, requireDefaultForNonUnion: true }], }, ], invalid: [ { - // Matched only one branch out of seven. + // Matched only one branch out of seven code: ` type Day = | 'Monday' @@ -627,7 +627,7 @@ switch (value) { return 1; } `, - options: [{ requireDefaultForNonUnion: true }], + options: [{ allowDefaultCase: false, requireDefaultForNonUnion: true }], errors: [ { messageId: 'switchIsNotExhaustive', @@ -718,17 +718,13 @@ switch (value) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } } - `, + `, }, ], }, - }, - { - code: noFormat` - - - // has dangerous default case - options: [{ allowDefaultCase: false }], + ], + }, + { code: ` type MyUnion = 'foo' | 'bar' | 'baz'; @@ -745,9 +741,11 @@ switch (myUnion) { } } `, + options: [{ allowDefaultCase: false, requireDefaultForNonUnion: false }], errors: [ { messageId: 'dangerousDefaultCase', + } ], }, ], From 592de1d7b040e817eeb1b095f755d1aaf92a08c8 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:45:06 -0500 Subject: [PATCH 10/43] fix: format --- .../docs/rules/switch-exhaustiveness-check.md | 8 ++++++-- .../src/rules/switch-exhaustiveness-check.ts | 11 ++++++----- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 3cd7e809e20d..2e085c4cb245 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -11,8 +11,6 @@ However, if the union type or the enum changes, it is easy to forget to modify t 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. - ## Options ### `"allowDefaultCase"` @@ -25,6 +23,12 @@ Why is this important? Consider why TypeScript is valuable: when we add a new ar Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version and have not refreshed the page yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. +## `requireDefaultForNonUnion` + +Defaults to false. It set to true, this rule will also report when a `switch` statement switches over a non-union type (like a `number` or `string`, for example) and that `switch` statement does not have a `default` case. + +This is generally desirable so that `number` and `string` switches will be subject to the same exhaustive checks that your other switches are. + ## 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 eb892d86b710..5a54d1742737 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -64,7 +64,8 @@ export default createRule({ type: 'object', properties: { allowDefaultCase: { - description: "If `true`, allow superfluous `default` cases that obfuscate future type additions.", + description: + 'If `true`, allow superfluous `default` cases that obfuscate future type additions.', type: 'boolean', default: true, }, @@ -84,7 +85,9 @@ export default createRule({ addMissingCases: 'Add branches for missing cases.', }, }, - defaultOptions: [{ allowDefaultCase: true, requireDefaultForNonUnion: false }], + defaultOptions: [ + { allowDefaultCase: true, requireDefaultForNonUnion: false }, + ], create(context, [{ allowDefaultCase, requireDefaultForNonUnion }]) { const sourceCode = getSourceCode(context); const services = getParserServices(context); @@ -150,9 +153,7 @@ export default createRule({ } function checkSwitchNoUnionDefaultCase(node: TSESTree.SwitchStatement) { - const hasDefault = node.cases.some( - switchCase => switchCase.test == null, - ); + const hasDefault = node.cases.some(switchCase => switchCase.test == null); if (!hasDefault) { context.report({ 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 03bbd8a1d341..fb22b48cf402 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -745,7 +745,7 @@ switch (myUnion) { errors: [ { messageId: 'dangerousDefaultCase', - } + }, ], }, ], From 26a9919a7b599bbe129f908c548b7fca3f75bd35 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:00:49 -0500 Subject: [PATCH 11/43] fix: lint --- .../docs/rules/switch-exhaustiveness-check.md | 28 +++++++++++++++++-- .../src/rules/switch-exhaustiveness-check.ts | 6 ++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 2e085c4cb245..6e1ff37c6cdd 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -21,11 +21,35 @@ Defaults to true. If set to false, this rule will also report when a `switch` st Why is this important? Consider why TypeScript is valuable: when we add a new argument to a widely-used function, we don't have to go on a scavenger hunt through our codebase. We can simply run the TypeScript compiler and it will tell us all the exact places in the code that need to be updated. The `switch-exhaustiveness-check` rule is similar: when we add a new enum member, we don't have to go on a scavenger hunt. We can simply run ESLint and it will tell us all the exact places in the code that need to be updated. So in order to preserve the ability of ESLint to do this, we have to remove the `default` cases. -Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version and have not refreshed the page yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. +#### `"allowDefaultCase"` Caveats + +Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version, having not refreshed the page yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. (In Flow, there is a special syntax to define [enums with unknown members](https://flow.org/en/docs/enums/defining-enums/#toc-flow-enums-with-unknown-members), but TypeScript does not have analogous functionality.) + +In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. You can do this by using [the `default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) combined with a `satisfies never` check. For example: + +```ts +type Fruit = 'apple' | 'banana'; + +function useFruit(fruit: Fruit): string { + switch (fruit) { + case 'apple': { + return 'appleJuice'; + } + + case 'banana': { + return 'bananaJuice'; + } + + default: { + return fruit satisfies never; + } + } +} +``` ## `requireDefaultForNonUnion` -Defaults to false. It set to true, this rule will also report when a `switch` statement switches over a non-union type (like a `number` or `string`, for example) and that `switch` statement does not have a `default` case. +Defaults to false. It set to true, this rule will also report when a `switch` statement switches over a non-union type (like a `number` or `string`, for example) and that `switch` statement does not have a `default` case. Thus, by setting this option to true, the rule becomes stricter. This is generally desirable so that `number` and `string` switches will be subject to the same exhaustive checks that your other switches are. diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 5a54d1742737..3bf6510abdd8 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -152,7 +152,9 @@ export default createRule({ }; } - function checkSwitchNoUnionDefaultCase(node: TSESTree.SwitchStatement) { + function checkSwitchNoUnionDefaultCase( + node: TSESTree.SwitchStatement, + ): void { const hasDefault = node.cases.some(switchCase => switchCase.test == null); if (!hasDefault) { @@ -177,7 +179,7 @@ export default createRule({ function checkSwitchExhaustive( node: TSESTree.SwitchStatement, switchStatementMetadata: SwitchStatementMetadata, - ) { + ): void { const { missingBranchTypes, symbolName, defaultCase } = switchStatementMetadata; From e865d6b2682c56f6a655e39da01decb56e905856 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:09:01 -0500 Subject: [PATCH 12/43] chore: update docs --- .../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 6e1ff37c6cdd..e850d51b85bf 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -47,6 +47,8 @@ function useFruit(fruit: Fruit): string { } ``` +Doing this gives you the best of both worlds: explicit out-of-band value handling while still having the ability of TypeScript to alert to you all the places in your code-base where a new type constituent needs to be added. However, the downside is two-fold: there is no way for the `default-case` lint rule to distinguish between a `satisfies never` default case and some other kind of default case, so unsafe switch statements can still sneak into your codebase. Additionally, you have to maintain `default` cases everywhere, which makes things much more verbose. + ## `requireDefaultForNonUnion` Defaults to false. It set to true, this rule will also report when a `switch` statement switches over a non-union type (like a `number` or `string`, for example) and that `switch` statement does not have a `default` case. Thus, by setting this option to true, the rule becomes stricter. From 5be7b48c1673750e358e0ef0c90bc6d6d5b30bb9 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:11:41 -0500 Subject: [PATCH 13/43] chore: update docs --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index e850d51b85bf..bae90ad269d0 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -23,7 +23,7 @@ Why is this important? Consider why TypeScript is valuable: when we add a new ar #### `"allowDefaultCase"` Caveats -Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version, having not refreshed the page yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. (In Flow, there is a special syntax to define [enums with unknown members](https://flow.org/en/docs/enums/defining-enums/#toc-flow-enums-with-unknown-members), but TypeScript does not have analogous functionality.) +Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version, having not refreshed the webpage yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. (In Flow, there is a special syntax to define [enums with unknown members](https://flow.org/en/docs/enums/defining-enums/#toc-flow-enums-with-unknown-members), but TypeScript does not have analogous functionality.) In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. You can do this by using [the `default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) combined with a `satisfies never` check. For example: @@ -41,7 +41,8 @@ function useFruit(fruit: Fruit): string { } default: { - return fruit satisfies never; + fruit satisfies never; + return "unknownJuice"; } } } From d18f0f12728b39a2db517aa5cbd565c95e0ba97a Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:19:11 -0500 Subject: [PATCH 14/43] chore: format --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index bae90ad269d0..ecbba7099b2e 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -42,7 +42,7 @@ function useFruit(fruit: Fruit): string { default: { fruit satisfies never; - return "unknownJuice"; + return 'unknownJuice'; } } } From 4091daf1b9b2a2291c2777a9513567ffc077f81e Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:48:38 -0500 Subject: [PATCH 15/43] fix: test --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 3bf6510abdd8..d760066f26ce 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -67,7 +67,6 @@ export default createRule({ description: 'If `true`, allow superfluous `default` cases that obfuscate future type additions.', type: 'boolean', - default: true, }, requireDefaultForNonUnion: { description: `If 'true', require a 'default' clause for switches on non-union types.`, From d68f7b74c7656d093f4c275d0020092b9b38507c Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:55:06 -0500 Subject: [PATCH 16/43] fix: tests --- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fb22b48cf402..b88d2a2efa7f 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -683,7 +683,7 @@ switch (value) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\n\\n new-line\\'] case') } } - `, + `, }, ], }, From 1fa4494e94e2b754e6ce7d2de1a4dab1a0d661c8 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:56:39 -0500 Subject: [PATCH 17/43] fix: tests --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 3 +-- 1 file changed, 1 insertion(+), 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 d760066f26ce..409e0e744d4f 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -31,8 +31,7 @@ interface SwitchStatementMetadata { type Options = [ { /** - * If `true`, allow superfluous `default` cases that obfuscate future type - * additions. + * If `true`, allow superfluous `default` cases that obfuscate future type additions. */ allowDefaultCase: boolean; From 22c1503896d4a346662c8f96ffc3e659c6fc309a Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:57:11 -0500 Subject: [PATCH 18/43] fix: tests --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 409e0e744d4f..40b164977a44 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -33,7 +33,7 @@ type Options = [ /** * If `true`, allow superfluous `default` cases that obfuscate future type additions. */ - allowDefaultCase: boolean; + allowDefaultCase?: boolean; /** * If `true`, require a `default` clause for switches on non-union types. From c928b1875b5db246dca5b473f597b01707a5d8e0 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:58:18 -0500 Subject: [PATCH 19/43] fix: test --- .../tests/rules/switch-exhaustiveness-check.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b88d2a2efa7f..da74a883277b 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -683,7 +683,7 @@ switch (value) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } case Enum['key-with\\n\\n new-line']: { throw new Error('Not implemented yet: Enum[\\'key-with\\n\\n new-line\\'] case') } } - `, + `, }, ], }, @@ -718,7 +718,7 @@ switch (value) { case Enum.a: { throw new Error('Not implemented yet: Enum.a case') } case Enum['\\'a\\' \`b\` "c"']: { throw new Error('Not implemented yet: Enum[\\'\\\\'a\\\\' \`b\` "c"\\'] case') } } - `, + `, }, ], }, From c32204e81859c3630ab89a0b8ae1c5551930b682 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:59:23 -0500 Subject: [PATCH 20/43] fix: test --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 40b164977a44..6d0c2f2fd52d 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -32,6 +32,8 @@ type Options = [ { /** * If `true`, allow superfluous `default` cases that obfuscate future type additions. + * + * @default true */ allowDefaultCase?: boolean; From 6ea1b329f265e9cc25d84142a10ee33e28fe9352 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:14:03 -0500 Subject: [PATCH 21/43] fix: tests --- .../tests/schema-snapshots/switch-exhaustiveness-check.shot | 6 ++++++ 1 file changed, 6 insertions(+) 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 10996a21371f..c6e4a7b9c553 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -8,6 +8,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos { "additionalProperties": false, "properties": { + "allowDefaultCase": { + "description": "If `true`, allow superfluous `default` cases that obfuscate future type additions.", + "type": "boolean" + }, "requireDefaultForNonUnion": { "description": "If 'true', require a 'default' clause for switches on non-union types.", "type": "boolean" @@ -22,6 +26,8 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { + /** If `true`, allow superfluous `default` cases that obfuscate future type additions. */ + allowDefaultCase?: boolean; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; }, From 090a737725f8cb1655f9a98911a01ee595d665b4 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:19:09 -0500 Subject: [PATCH 22/43] Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 6d0c2f2fd52d..e70f82041580 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -304,7 +304,6 @@ export default createRule({ function checkSwitchDangerousDefaultCase( switchStatementMetadata: SwitchStatementMetadata, ): void { - // This feature of the rule is gated behind an option. if (allowDefaultCase) { return; } From b93f5011af4bee78d56224de6f4eeb5239299935 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:21:45 -0500 Subject: [PATCH 23/43] fix: double options in docs --- .../docs/rules/switch-exhaustiveness-check.md | 40 +++++++++---------- .../switch-exhaustiveness-check.shot | 4 +- 2 files 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 ecbba7099b2e..97e38bdf2b3d 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -50,12 +50,29 @@ function useFruit(fruit: Fruit): string { Doing this gives you the best of both worlds: explicit out-of-band value handling while still having the ability of TypeScript to alert to you all the places in your code-base where a new type constituent needs to be added. However, the downside is two-fold: there is no way for the `default-case` lint rule to distinguish between a `satisfies never` default case and some other kind of default case, so unsafe switch statements can still sneak into your codebase. Additionally, you have to maintain `default` cases everywhere, which makes things much more verbose. -## `requireDefaultForNonUnion` +### `requireDefaultForNonUnion` Defaults to false. It set to true, this rule will also report when a `switch` statement switches over a non-union type (like a `number` or `string`, for example) and that `switch` statement does not have a `default` case. Thus, by setting this option to true, the rule becomes stricter. This is generally desirable so that `number` and `string` switches will be subject to the same exhaustive checks that your other switches are. +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. + + + ## Examples When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint. @@ -224,27 +241,6 @@ 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, this rule may not be for you. 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 c6e4a7b9c553..3f4bb93056c8 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -9,7 +9,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "additionalProperties": false, "properties": { "allowDefaultCase": { - "description": "If `true`, allow superfluous `default` cases that obfuscate future type additions.", + "description": "If \`true\`, allow superfluous \`default\` cases that obfuscate future type additions.", "type": "boolean" }, "requireDefaultForNonUnion": { @@ -26,7 +26,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { - /** If `true`, allow superfluous `default` cases that obfuscate future type additions. */ + /** If \`true\`, allow superfluous \`default\` cases that obfuscate future type additions. */ allowDefaultCase?: boolean; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; From 0fe1cd9f4f4b9a0fac2d5e52745b9a4bccee435a Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:24:28 -0500 Subject: [PATCH 24/43] Update packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md 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 | 5 ++--- 1 file changed, 2 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 97e38bdf2b3d..3a1c4c85fef0 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -17,9 +17,8 @@ This rule reports when a `switch` statement over a value typed as a union of lit Defaults to true. 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. -`default` cases in this situation are obviously superfluous, as they would contain dead code. But beyond being superfluous, these kind of `default` cases can be harmful: if a new value is added to the switch statement union, the `default` statement would prevent the `switch-exhaustiveness-check` rule from alerting you that you need to handle the new case. - -Why is this important? Consider why TypeScript is valuable: when we add a new argument to a widely-used function, we don't have to go on a scavenger hunt through our codebase. We can simply run the TypeScript compiler and it will tell us all the exact places in the code that need to be updated. The `switch-exhaustiveness-check` rule is similar: when we add a new enum member, we don't have to go on a scavenger hunt. We can simply run ESLint and it will tell us all the exact places in the code that need to be updated. So in order to preserve the ability of ESLint to do this, we have to remove the `default` cases. +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. #### `"allowDefaultCase"` Caveats From cab680a68f589da9bf077b8bb3189b9383ca9b58 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:34:15 -0500 Subject: [PATCH 25/43] feat: simplify code flow --- .../src/rules/switch-exhaustiveness-check.ts | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index e70f82041580..fbfa5f3c0d88 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -13,19 +13,10 @@ import { } from '../util'; interface SwitchStatementMetadata { - /** The name of the union that is inside of the switch statement. */ symbolName: string | undefined; - - /** - * If the length of this array is equal to 0, then the switch statement is - * exhaustive. - */ missingBranchTypes: ts.Type[]; - - /** - * The node representing the `default` case on the switch statement, if any. - */ defaultCase: TSESTree.SwitchCase | undefined; + isUnion: boolean; } type Options = [ @@ -100,13 +91,26 @@ export default createRule({ */ function getSwitchStatementMetadata( node: TSESTree.SwitchStatement, - ): SwitchStatementMetadata | undefined { + ): SwitchStatementMetadata { + /** + * The `test` property of a `SwitchCase` node will usually be a `Literal` + * node. However, on a `default` case, it will be equal to `null`. + */ + const defaultCase = node.cases.find( + switchCase => switchCase.test == null, + ); + const discriminantType = getConstrainedTypeAtLocation( services, node.discriminant, ); if (!discriminantType.isUnion()) { - return undefined; + return { + symbolName: undefined, + missingBranchTypes: [], + defaultCase, + isUnion: true, + }; } const caseTypes = new Set(); @@ -137,45 +141,14 @@ export default createRule({ | string | undefined; - /** - * The `test` property of a `SwitchCase` node will usually be a `Literal` - * node. However, on a `default` case, it will be equal to `null`. - */ - const defaultCase = node.cases.find( - switchCase => switchCase.test == null, - ); - return { - missingBranchTypes, symbolName, + missingBranchTypes, defaultCase, + isUnion: false, }; } - function checkSwitchNoUnionDefaultCase( - node: TSESTree.SwitchStatement, - ): void { - 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]); - }, - }, - ], - }); - } - } - function checkSwitchExhaustive( node: TSESTree.SwitchStatement, switchStatementMetadata: SwitchStatementMetadata, @@ -318,19 +291,43 @@ export default createRule({ } } + function checkSwitchNoUnionDefaultCase( + node: TSESTree.SwitchStatement, + switchStatementMetadata: SwitchStatementMetadata, + ): void { + if (!requireDefaultForNonUnion) { + return; + } + + if ( + !switchStatementMetadata.isUnion && + switchStatementMetadata.defaultCase === null + ) { + context.report({ + node: node.discriminant, + messageId: 'switchIsNotExhaustive', + data: { + missingBranches: 'default', + }, + suggest: [ + { + messageId: 'addMissingCases', + fix(fixer): TSESLint.RuleFix { + return fixSwitch(fixer, node, [null]); + }, + }, + ], + }); + } + } + return { SwitchStatement(node): void { const switchStatementMetadata = getSwitchStatementMetadata(node); - if (switchStatementMetadata === undefined) { - if (requireDefaultForNonUnion) { - checkSwitchNoUnionDefaultCase(node); - } - - return; - } checkSwitchExhaustive(node, switchStatementMetadata); checkSwitchDangerousDefaultCase(switchStatementMetadata); + checkSwitchNoUnionDefaultCase(node, switchStatementMetadata); }, }; }, From 8ad5037ae42d1b8a1a0868966a1102205046496a Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:36:00 -0500 Subject: [PATCH 26/43] Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index fbfa5f3c0d88..ca3cf1fcb0ee 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -274,7 +274,7 @@ export default createRule({ ); } - function checkSwitchDangerousDefaultCase( + function checkSwitchUnnecessaryDefaultCase( switchStatementMetadata: SwitchStatementMetadata, ): void { if (allowDefaultCase) { From 4aa247b9f1a2d845e95bd974e2ccc3a4d7667430 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:38:35 -0500 Subject: [PATCH 27/43] fix: grammar --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 3a1c4c85fef0..e107eb8260ee 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -6,8 +6,8 @@ description: 'Require switch-case statements to be exhaustive.' > > See **https://typescript-eslint.io/rules/switch-exhaustiveness-check** for documentation. -When working with union types or enums in TypeScript, it is common to want to write a `switch` statement intended to contain a `case` for each possible type in the union or the enum. -However, if the union type or the enum changes, it is easy to forget to modify the cases to account for any new types. +When working with union types or enums in TypeScript, it's common to want to write a `switch` statement intended to contain a `case` for each possible type in the union or the enum. +However, if the union type or the enum changes, it's easy to forget to modify the cases to account for any new types. 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. From 9a63489ae9d12c5734c05279733a6f4ffabcfc17 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:39:14 -0500 Subject: [PATCH 28/43] Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index ca3cf1fcb0ee..293fd4aca444 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -57,7 +57,7 @@ export default createRule({ properties: { allowDefaultCase: { description: - 'If `true`, allow superfluous `default` cases that obfuscate future type additions.', + 'If `true`, allow `default` cases on `switch` statements with exhaustive cases.', type: 'boolean', }, requireDefaultForNonUnion: { From 30b6695c96f320923cf407360668230339589ef3 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:42:23 -0500 Subject: [PATCH 29/43] fix: wording on option --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 5 ++--- .../tests/schema-snapshots/switch-exhaustiveness-check.shot | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 293fd4aca444..aa48b0edb19b 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -22,7 +22,7 @@ interface SwitchStatementMetadata { type Options = [ { /** - * If `true`, allow superfluous `default` cases that obfuscate future type additions. + * If `true`, allow `default` cases on switch statements with exhaustive cases. * * @default true */ @@ -56,8 +56,7 @@ export default createRule({ type: 'object', properties: { allowDefaultCase: { - description: - 'If `true`, allow `default` cases on `switch` statements with exhaustive cases.', + description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`, type: 'boolean', }, requireDefaultForNonUnion: { 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 3f4bb93056c8..a18c4f2952f7 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -9,7 +9,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "additionalProperties": false, "properties": { "allowDefaultCase": { - "description": "If \`true\`, allow superfluous \`default\` cases that obfuscate future type additions.", + "description": "If 'true', allow 'default' cases on switch statements with exhaustive cases.", "type": "boolean" }, "requireDefaultForNonUnion": { @@ -26,7 +26,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { - /** If \`true\`, allow superfluous \`default\` cases that obfuscate future type additions. */ + /** If 'true', allow 'default' cases on switch statements with exhaustive cases. */ allowDefaultCase?: boolean; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; From 5a3bf3cce683da0b8c0eff62a7e047a33b99a8db Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:43:33 -0500 Subject: [PATCH 30/43] Update packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../docs/rules/switch-exhaustiveness-check.md | 28 +++---------------- 1 file changed, 4 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 e107eb8260ee..f467284ad3cb 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -22,32 +22,12 @@ Additionally, if a new value is added to the union type, a `default` would preve #### `"allowDefaultCase"` Caveats -Note that in some situations, like when switch statements use data from external APIs, `default` cases can be valuable, so you might want to turn the option off. For example, if you update the API of a web application to return a new value, it is possible that users will be using the app on the older version, having not refreshed the webpage yet. Thus, they might query the new API on an older version of the code, which would result in undefined behavior. (In Flow, there is a special syntax to define [enums with unknown members](https://flow.org/en/docs/enums/defining-enums/#toc-flow-enums-with-unknown-members), but TypeScript does not have analogous functionality.) +It can sometimes be useful to include a redundant `default` case on an exhaustive `switch` statement if it's possible for values to have types not represented by the union type. +For example, in applications that can have version mismatches between clients and servers, it's possible for a server running a newer software version to send a value not recognized by the client's older typings. -In these kinds of situations, you might want to enforce an explicit `default` case that throws an error, or allows the user to safely save their work, or something along those lines. You can do this by using [the `default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) combined with a `satisfies never` check. For example: +If your project has a small number of intentionally redundant `default` cases, you might want to use an [inline ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for each of them. -```ts -type Fruit = 'apple' | 'banana'; - -function useFruit(fruit: Fruit): string { - switch (fruit) { - case 'apple': { - return 'appleJuice'; - } - - case 'banana': { - return 'bananaJuice'; - } - - default: { - fruit satisfies never; - return 'unknownJuice'; - } - } -} -``` - -Doing this gives you the best of both worlds: explicit out-of-band value handling while still having the ability of TypeScript to alert to you all the places in your code-base where a new type constituent needs to be added. However, the downside is two-fold: there is no way for the `default-case` lint rule to distinguish between a `satisfies never` default case and some other kind of default case, so unsafe switch statements can still sneak into your codebase. Additionally, you have to maintain `default` cases everywhere, which makes things much more verbose. +If your project has many intentionally redundant `default` cases, you may want to disable `allowDefaultCase` and use the [`default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case). ### `requireDefaultForNonUnion` From e1e8554bebd3e786bae3b4aef1a0312bae8154a2 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:44:46 -0500 Subject: [PATCH 31/43] docs: add playground link --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index f467284ad3cb..6426c9a4680d 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -27,7 +27,7 @@ For example, in applications that can have version mismatches between clients an If your project has a small number of intentionally redundant `default` cases, you might want to use an [inline ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for each of them. -If your project has many intentionally redundant `default` cases, you may want to disable `allowDefaultCase` and use the [`default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case). +If your project has many intentionally redundant `default` cases, you may want to disable `allowDefaultCase` and use the [`default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) along with [a `satisfies never` check](https://www.typescriptlang.org/play?#code/C4TwDgpgBAYgTgVwJbCgXigcgIZjAGwkygB8sAjbAO2u0wG4AoRgMwSoGNgkB7KqBAGcI8ZMAAULRCgBcsacACUcwcDhIqAcygBvRlCiCA7ig4ALKJIWLd+g1A7ZhWXASJy99+3AjAEcfhw8QgApZA4iJi8AX2YvR2dMShoaTA87Lx8-AIpaGjCkCIYMqFiSgBMIFmwEfGB0rwMpMUNsbkEWJAhBKCoIADcIOCjGrP9A9gBrKh4jKgKikYNY5cZYoA). ### `requireDefaultForNonUnion` From 7dbafe5ab2cb277cef20468030b2809216fe95b4 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:45:44 -0500 Subject: [PATCH 32/43] Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index aa48b0edb19b..081e7f3d1a4a 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -71,7 +71,7 @@ export default createRule({ switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', dangerousDefaultCase: - 'The switch statement is exhaustive, so the default case is superfluous and will obfuscate future additions to the union.', + 'The switch statement is exhaustive, so the default case is unnecessary.', addMissingCases: 'Add branches for missing cases.', }, }, From d0611cb1a0369316fd07643c0cd3f985b384c367 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:49:42 -0500 Subject: [PATCH 33/43] chore: add punctuation --- .../tests/rules/switch-exhaustiveness-check.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 da74a883277b..b28ef08a740f 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -142,7 +142,7 @@ function test(value: Union): number { } } `, - // Switch contains default clause + // Switch contains default clause. ` type Day = | 'Monday' @@ -182,7 +182,7 @@ switch (day) { } } `, - // ... and enums (at least for now) + // ... and enums (at least for now). ` enum Enum { A, @@ -209,7 +209,7 @@ function test(value: ObjectUnion): number { } } `, - // Switch with default clause on non-union type + // switch with default clause on non-union type { code: ` declare const value: number; @@ -227,7 +227,7 @@ switch (value) { ], invalid: [ { - // Matched only one branch out of seven + // Matched only one branch out of seven. code: ` type Day = | 'Monday' From 2c6dfb4219589647eb9aa0e0b0a51681c6c12cae Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:53:20 -0500 Subject: [PATCH 34/43] Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 081e7f3d1a4a..321d4b06abf6 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -132,10 +132,6 @@ export default createRule({ unionType => !caseTypes.has(unionType), ); - /** - * Convert `ts.__String | undefined` to `string | undefined` for - * simplicity. - */ const symbolName = discriminantType.getSymbol()?.escapedName as | string | undefined; From 279aa9c8a22a655cafdcddb753cbc0ce6f9ae7b5 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:55:06 -0500 Subject: [PATCH 35/43] chore: remove comment --- .../eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 321d4b06abf6..158a4d88a336 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -91,10 +91,6 @@ export default createRule({ function getSwitchStatementMetadata( node: TSESTree.SwitchStatement, ): SwitchStatementMetadata { - /** - * The `test` property of a `SwitchCase` node will usually be a `Literal` - * node. However, on a `default` case, it will be equal to `null`. - */ const defaultCase = node.cases.find( switchCase => switchCase.test == null, ); @@ -321,7 +317,7 @@ export default createRule({ const switchStatementMetadata = getSwitchStatementMetadata(node); checkSwitchExhaustive(node, switchStatementMetadata); - checkSwitchDangerousDefaultCase(switchStatementMetadata); + checkSwitchUnnecessaryDefaultCase(switchStatementMetadata); checkSwitchNoUnionDefaultCase(node, switchStatementMetadata); }, }; From 29284b2a0847d2c2a0563fafe58135078e2e3894 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:56:05 -0500 Subject: [PATCH 36/43] refactor: rename option --- .../docs/rules/switch-exhaustiveness-check.md | 6 +++--- .../src/rules/switch-exhaustiveness-check.ts | 10 +++++----- .../tests/rules/switch-exhaustiveness-check.test.ts | 6 +++--- .../schema-snapshots/switch-exhaustiveness-check.shot | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md index 6426c9a4680d..b0cbaf1c47d4 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md @@ -13,21 +13,21 @@ This rule reports when a `switch` statement over a value typed as a union of lit ## Options -### `"allowDefaultCase"` +### `"allowDefaultCaseForExhaustiveSwitch"` Defaults to true. 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. -#### `"allowDefaultCase"` Caveats +#### `"allowDefaultCaseForExhaustiveSwitch"` Caveats It can sometimes be useful to include a redundant `default` case on an exhaustive `switch` statement if it's possible for values to have types not represented by the union type. For example, in applications that can have version mismatches between clients and servers, it's possible for a server running a newer software version to send a value not recognized by the client's older typings. If your project has a small number of intentionally redundant `default` cases, you might want to use an [inline ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for each of them. -If your project has many intentionally redundant `default` cases, you may want to disable `allowDefaultCase` and use the [`default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) along with [a `satisfies never` check](https://www.typescriptlang.org/play?#code/C4TwDgpgBAYgTgVwJbCgXigcgIZjAGwkygB8sAjbAO2u0wG4AoRgMwSoGNgkB7KqBAGcI8ZMAAULRCgBcsacACUcwcDhIqAcygBvRlCiCA7ig4ALKJIWLd+g1A7ZhWXASJy99+3AjAEcfhw8QgApZA4iJi8AX2YvR2dMShoaTA87Lx8-AIpaGjCkCIYMqFiSgBMIFmwEfGB0rwMpMUNsbkEWJAhBKCoIADcIOCjGrP9A9gBrKh4jKgKikYNY5cZYoA). +If your project has many intentionally redundant `default` cases, you may want to disable `allowDefaultCaseForExhaustiveSwitch` and use the [`default-case` core ESLint rule](https://eslint.org/docs/latest/rules/default-case) along with [a `satisfies never` check](https://www.typescriptlang.org/play?#code/C4TwDgpgBAYgTgVwJbCgXigcgIZjAGwkygB8sAjbAO2u0wG4AoRgMwSoGNgkB7KqBAGcI8ZMAAULRCgBcsacACUcwcDhIqAcygBvRlCiCA7ig4ALKJIWLd+g1A7ZhWXASJy99+3AjAEcfhw8QgApZA4iJi8AX2YvR2dMShoaTA87Lx8-AIpaGjCkCIYMqFiSgBMIFmwEfGB0rwMpMUNsbkEWJAhBKCoIADcIOCjGrP9A9gBrKh4jKgKikYNY5cZYoA). ### `requireDefaultForNonUnion` diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 158a4d88a336..dc352ea3aae3 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -26,7 +26,7 @@ type Options = [ * * @default true */ - allowDefaultCase?: boolean; + allowDefaultCaseForExhaustiveSwitch?: boolean; /** * If `true`, require a `default` clause for switches on non-union types. @@ -55,7 +55,7 @@ export default createRule({ { type: 'object', properties: { - allowDefaultCase: { + allowDefaultCaseForExhaustiveSwitch: { description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`, type: 'boolean', }, @@ -76,9 +76,9 @@ export default createRule({ }, }, defaultOptions: [ - { allowDefaultCase: true, requireDefaultForNonUnion: false }, + { allowDefaultCaseForExhaustiveSwitch: true, requireDefaultForNonUnion: false }, ], - create(context, [{ allowDefaultCase, requireDefaultForNonUnion }]) { + create(context, [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }]) { const sourceCode = getSourceCode(context); const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -268,7 +268,7 @@ export default createRule({ function checkSwitchUnnecessaryDefaultCase( switchStatementMetadata: SwitchStatementMetadata, ): void { - if (allowDefaultCase) { + if (allowDefaultCaseForExhaustiveSwitch) { return; } 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 b28ef08a740f..7d01de46d654 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -222,7 +222,7 @@ switch (value) { return -1; } `, - options: [{ allowDefaultCase: false, requireDefaultForNonUnion: true }], + options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true }], }, ], invalid: [ @@ -627,7 +627,7 @@ switch (value) { return 1; } `, - options: [{ allowDefaultCase: false, requireDefaultForNonUnion: true }], + options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true }], errors: [ { messageId: 'switchIsNotExhaustive', @@ -741,7 +741,7 @@ switch (myUnion) { } } `, - options: [{ allowDefaultCase: false, requireDefaultForNonUnion: false }], + options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: false }], errors: [ { messageId: 'dangerousDefaultCase', 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 a18c4f2952f7..6146dadc128b 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -8,7 +8,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos { "additionalProperties": false, "properties": { - "allowDefaultCase": { + "allowDefaultCaseForExhaustiveSwitch": { "description": "If 'true', allow 'default' cases on switch statements with exhaustive cases.", "type": "boolean" }, @@ -27,7 +27,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos type Options = [ { /** If 'true', allow 'default' cases on switch statements with exhaustive cases. */ - allowDefaultCase?: boolean; + allowDefaultCaseForExhaustiveSwitch?: boolean; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; }, From 8e24cfb5f3849854aac034fe456ec7ee30d96c69 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:01:36 -0500 Subject: [PATCH 37/43] fix: prettier --- .../src/rules/switch-exhaustiveness-check.ts | 10 +++++++-- .../rules/switch-exhaustiveness-check.test.ts | 21 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index dc352ea3aae3..d51f1d6966df 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -76,9 +76,15 @@ export default createRule({ }, }, defaultOptions: [ - { allowDefaultCaseForExhaustiveSwitch: true, requireDefaultForNonUnion: false }, + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, ], - create(context, [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }]) { + create( + context, + [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }], + ) { const sourceCode = getSourceCode(context); const services = getParserServices(context); const checker = services.program.getTypeChecker(); 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 7d01de46d654..7a9ca5ea81d6 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -222,7 +222,12 @@ switch (value) { return -1; } `, - options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true }], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], }, ], invalid: [ @@ -627,7 +632,12 @@ switch (value) { return 1; } `, - options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: true }], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: true, + }, + ], errors: [ { messageId: 'switchIsNotExhaustive', @@ -741,7 +751,12 @@ switch (myUnion) { } } `, - options: [{ allowDefaultCaseForExhaustiveSwitch: false, requireDefaultForNonUnion: false }], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + requireDefaultForNonUnion: false, + }, + ], errors: [ { messageId: 'dangerousDefaultCase', From 9fda18d10dc96fc4f3b048cb4dbcf6b82cf3b5e4 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:08:32 -0500 Subject: [PATCH 38/43] fix: lint --- packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index d51f1d6966df..325052f6709d 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -298,7 +298,7 @@ export default createRule({ if ( !switchStatementMetadata.isUnion && - switchStatementMetadata.defaultCase === null + switchStatementMetadata.defaultCase === undefined ) { context.report({ node: node.discriminant, From 24327c4c9576fb84df33da45dd7c33ade033dfee Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:33:48 -0500 Subject: [PATCH 39/43] fix: tests --- .../tests/rules/switch-exhaustiveness-check.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7a9ca5ea81d6..9a06ace1e0a9 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -224,7 +224,7 @@ switch (value) { `, options: [ { - allowDefaultCaseForExhaustiveSwitch: false, + allowDefaultCaseForExhaustiveSwitch: true, requireDefaultForNonUnion: true, }, ], @@ -634,7 +634,7 @@ switch (value) { `, options: [ { - allowDefaultCaseForExhaustiveSwitch: false, + allowDefaultCaseForExhaustiveSwitch: true, requireDefaultForNonUnion: true, }, ], From e5f05875ab8b339c722bc16f1547127b30548c34 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 7 Dec 2023 08:38:56 -0500 Subject: [PATCH 40/43] refactor: better metadata --- .../src/rules/switch-exhaustiveness-check.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 325052f6709d..b9998f3e8513 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -105,9 +105,14 @@ export default createRule({ services, node.discriminant, ); + + const symbolName = discriminantType.getSymbol()?.escapedName as + | string + | undefined; + if (!discriminantType.isUnion()) { return { - symbolName: undefined, + symbolName, missingBranchTypes: [], defaultCase, isUnion: true, @@ -134,10 +139,6 @@ export default createRule({ unionType => !caseTypes.has(unionType), ); - const symbolName = discriminantType.getSymbol()?.escapedName as - | string - | undefined; - return { symbolName, missingBranchTypes, From 75e3015f76af4f7dc978a01aa6f8e984a959882e Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 7 Dec 2023 08:53:19 -0500 Subject: [PATCH 41/43] fix: tests --- .../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 b9998f3e8513..a75b52ad035f 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -115,7 +115,7 @@ export default createRule({ symbolName, missingBranchTypes: [], defaultCase, - isUnion: true, + isUnion: false, }; } @@ -143,7 +143,7 @@ export default createRule({ symbolName, missingBranchTypes, defaultCase, - isUnion: false, + isUnion: true, }; } From 6e427b44752a04e1af42f62446a46e4feb1a5efc Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:03:21 -0500 Subject: [PATCH 42/43] refactor: rename interface --- .../src/rules/switch-exhaustiveness-check.ts | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index a75b52ad035f..62dda230d8c0 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -12,7 +12,7 @@ import { requiresQuoting, } from '../util'; -interface SwitchStatementMetadata { +interface SwitchMetadata { symbolName: string | undefined; missingBranchTypes: ts.Type[]; defaultCase: TSESTree.SwitchCase | undefined; @@ -90,13 +90,7 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); - /** - * @returns Metadata about whether the switch is exhaustive (or `undefined` - * if the switch case is not a union). - */ - function getSwitchStatementMetadata( - node: TSESTree.SwitchStatement, - ): SwitchStatementMetadata { + function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata { const defaultCase = node.cases.find( switchCase => switchCase.test == null, ); @@ -149,10 +143,9 @@ export default createRule({ function checkSwitchExhaustive( node: TSESTree.SwitchStatement, - switchStatementMetadata: SwitchStatementMetadata, + switchMetadata: SwitchMetadata, ): void { - const { missingBranchTypes, symbolName, defaultCase } = - switchStatementMetadata; + const { missingBranchTypes, symbolName, defaultCase } = 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 @@ -273,13 +266,13 @@ export default createRule({ } function checkSwitchUnnecessaryDefaultCase( - switchStatementMetadata: SwitchStatementMetadata, + switchMetadata: SwitchMetadata, ): void { if (allowDefaultCaseForExhaustiveSwitch) { return; } - const { missingBranchTypes, defaultCase } = switchStatementMetadata; + const { missingBranchTypes, defaultCase } = switchMetadata; if (missingBranchTypes.length === 0 && defaultCase !== undefined) { context.report({ @@ -291,16 +284,15 @@ export default createRule({ function checkSwitchNoUnionDefaultCase( node: TSESTree.SwitchStatement, - switchStatementMetadata: SwitchStatementMetadata, + switchMetadata: SwitchMetadata, ): void { if (!requireDefaultForNonUnion) { return; } - if ( - !switchStatementMetadata.isUnion && - switchStatementMetadata.defaultCase === undefined - ) { + const { isUnion, defaultCase } = switchMetadata; + + if (!isUnion && defaultCase === undefined) { context.report({ node: node.discriminant, messageId: 'switchIsNotExhaustive', @@ -321,11 +313,11 @@ export default createRule({ return { SwitchStatement(node): void { - const switchStatementMetadata = getSwitchStatementMetadata(node); + const switchMetadata = getSwitchMetadata(node); - checkSwitchExhaustive(node, switchStatementMetadata); - checkSwitchUnnecessaryDefaultCase(switchStatementMetadata); - checkSwitchNoUnionDefaultCase(node, switchStatementMetadata); + checkSwitchExhaustive(node, switchMetadata); + checkSwitchUnnecessaryDefaultCase(switchMetadata); + checkSwitchNoUnionDefaultCase(node, switchMetadata); }, }; }, From 8d8bba653d1faf8127cbd8fa8fbee0e08ce2c19f Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:11:13 -0500 Subject: [PATCH 43/43] refactor: make interface readonly --- .../src/rules/switch-exhaustiveness-check.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 62dda230d8c0..64ab5ef6b2e2 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -13,10 +13,10 @@ import { } from '../util'; interface SwitchMetadata { - symbolName: string | undefined; - missingBranchTypes: ts.Type[]; - defaultCase: TSESTree.SwitchCase | undefined; - isUnion: boolean; + readonly symbolName: string | undefined; + readonly missingBranchTypes: ts.Type[]; + readonly defaultCase: TSESTree.SwitchCase | undefined; + readonly isUnion: boolean; } type Options = [