Skip to content

Commit 3c8978d

Browse files
developer-bandikirkwaiblingerJoshuaKGoldberg
authored
feat(eslint-plugin): [switch-exhaustiveness-check] add allowDefaultCaseMatchUnionMember option (#9954)
* feat: add option * fix: test error * fix: lint error * fix: apply code reivew * fix: reflect code review * docs: Remove unnecessary phrases * docs: change option name * docs: considerDefaultExhaustiveForUnions option description edit * docs: apply code reivew * fix: test and lint error * fix:lint error * fix heading issue * refactor to tabs * Update packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx * fix text snapshots --------- Co-authored-by: Kirk Waiblinger <[email protected]> Co-authored-by: Josh Goldberg ✨ <[email protected]>
1 parent 74ace4d commit 3c8978d

File tree

5 files changed

+483
-11
lines changed

5 files changed

+483
-11
lines changed

packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ This rule reports when a `switch` statement over a value typed as a union of lit
2323
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.
2424

2525
When a `switch` statement over a union type is exhaustive, a final `default` case would be a form of dead code.
26-
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.
26+
Additionally, if a new value is added to the union type and you're using [`considerDefaultExhaustiveForUnions`](#considerDefaultExhaustiveForUnions), a `default` would prevent the `switch-exhaustiveness-check` rule from reporting on the new case not being handled in the `switch` statement.
2727

2828
#### `allowDefaultCaseForExhaustiveSwitch` Caveats
2929

@@ -57,6 +57,57 @@ switch (value) {
5757

5858
Since `value` is a non-union type it requires the switch case to have a default clause only with `requireDefaultForNonUnion` enabled.
5959

60+
### `considerDefaultExhaustiveForUnions`
61+
62+
{/* insert option description */}
63+
64+
If set to true, a `switch` statement over a union type that includes a `default` case is considered exhaustive.
65+
Otherwise, the rule enforces explicitly handling every constituent of the union type with their own explicit `case`.
66+
Keeping this option disabled can be useful if you want to make sure every value added to the union receives explicit handling, with the `default` case reserved for reporting an error.
67+
68+
Examples of code with `{ considerDefaultExhaustiveForUnions: true }`:
69+
70+
<Tabs>
71+
<TabItem value="❌ Incorrect">
72+
73+
```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton
74+
declare const literal: 'a' | 'b';
75+
76+
switch (literal) {
77+
case 'a':
78+
break;
79+
default:
80+
break;
81+
}
82+
```
83+
84+
</TabItem>
85+
86+
<TabItem value="✅ Correct">
87+
88+
```ts option='{ "considerDefaultExhaustiveForUnions": true }' showPlaygroundButton
89+
declare const literal: 'a' | 'b';
90+
91+
switch (literal) {
92+
case 'a':
93+
break;
94+
case 'b':
95+
break;
96+
default:
97+
break;
98+
}
99+
100+
switch (literal) {
101+
case 'a':
102+
break;
103+
case 'b':
104+
break;
105+
}
106+
```
107+
108+
</TabItem>
109+
</Tabs>
110+
60111
## Examples
61112

62113
When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint.

packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ type Options = [
3737
* @default false
3838
*/
3939
requireDefaultForNonUnion?: boolean;
40+
41+
/**
42+
* If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types.
43+
*
44+
* @default false
45+
*/
46+
considerDefaultExhaustiveForUnions?: boolean;
4047
},
4148
];
4249

@@ -70,6 +77,10 @@ export default createRule<Options, MessageIds>({
7077
type: 'boolean',
7178
description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`,
7279
},
80+
considerDefaultExhaustiveForUnions: {
81+
type: 'boolean',
82+
description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`,
83+
},
7384
requireDefaultForNonUnion: {
7485
type: 'boolean',
7586
description: `If 'true', require a 'default' clause for switches on non-union types.`,
@@ -81,12 +92,19 @@ export default createRule<Options, MessageIds>({
8192
defaultOptions: [
8293
{
8394
allowDefaultCaseForExhaustiveSwitch: true,
95+
considerDefaultExhaustiveForUnions: false,
8496
requireDefaultForNonUnion: false,
8597
},
8698
],
8799
create(
88100
context,
89-
[{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }],
101+
[
102+
{
103+
allowDefaultCaseForExhaustiveSwitch,
104+
considerDefaultExhaustiveForUnions,
105+
requireDefaultForNonUnion,
106+
},
107+
],
90108
) {
91109
const services = getParserServices(context);
92110
const checker = services.program.getTypeChecker();
@@ -156,10 +174,13 @@ export default createRule<Options, MessageIds>({
156174
const { defaultCase, missingLiteralBranchTypes, symbolName } =
157175
switchMetadata;
158176

159-
// We only trigger the rule if a `default` case does not exist, since that
160-
// would disqualify the switch statement from having cases that exactly
161-
// match the members of a union.
162-
if (missingLiteralBranchTypes.length > 0 && defaultCase === undefined) {
177+
// Unless considerDefaultExhaustiveForUnions is enabled, the presence of a default case
178+
// always makes the switch exhaustive.
179+
if (!considerDefaultExhaustiveForUnions && defaultCase != null) {
180+
return;
181+
}
182+
183+
if (missingLiteralBranchTypes.length > 0) {
163184
context.report({
164185
node: node.discriminant,
165186
messageId: 'switchIsNotExhaustive',
@@ -197,6 +218,8 @@ export default createRule<Options, MessageIds>({
197218
): TSESLint.RuleFix {
198219
const lastCase =
199220
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
221+
const defaultCase = node.cases.find(caseEl => caseEl.test == null);
222+
200223
const caseIndent = lastCase
201224
? ' '.repeat(lastCase.loc.start.column)
202225
: // If there are no cases, use indentation of the switch statement and
@@ -244,6 +267,13 @@ export default createRule<Options, MessageIds>({
244267
.join('\n');
245268

246269
if (lastCase) {
270+
if (defaultCase) {
271+
const beforeFixString = missingCases
272+
.map(code => `${code}\n${caseIndent}`)
273+
.join('');
274+
275+
return fixer.insertTextBefore(defaultCase, beforeFixString);
276+
}
247277
return fixer.insertTextAfter(lastCase, `\n${fixString}`);
248278
}
249279

packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot

Lines changed: 45 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)