Skip to content

Commit

Permalink
feat(eslint-plugin): [switch-exhaustiveness-check] add requireDefault…
Browse files Browse the repository at this point in the history
…ForNonUnion option (#7880)

* rule(switch-exhaustiveness-check): add requireDefaultForNonUnion option

* Apply suggestions from code review

Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>

* chore: apply suggestions to more locations

* fix lint

* Fix linting and ordering of md

---------

Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
  • Loading branch information
ST-DDT and JoshuaKGoldberg committed Nov 19, 2023
1 parent 9034d17 commit 4cfcd45
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 13 deletions.
25 changes: 24 additions & 1 deletion packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md
@@ -1,5 +1,5 @@
---
description: 'Require switch-case statements to be exhaustive with union types and enums.'
description: 'Require switch-case statements to be exhaustive.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
Expand All @@ -11,6 +11,8 @@ However, if the union type or the enum changes, it's easy to forget to modify th

This rule reports when a `switch` statement over a value typed as a union of literals or as an enum is missing a case for any of those literal types and does not have a `default` clause.

There is also an option to check the exhaustiveness of switches on non-union types by requiring a default clause.

## Examples

When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint.
Expand Down Expand Up @@ -179,6 +181,27 @@ switch (fruit) {

<!--/tabs-->

## 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.

<!--/tabs-->

## 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.
66 changes: 57 additions & 9 deletions packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
Expand Up @@ -12,25 +12,47 @@ import {
requiresQuoting,
} from '../util';

export default createRule({
type MessageIds = 'switchIsNotExhaustive' | 'addMissingCases';
type Options = [
{
/**
* If `true`, require a `default` clause for switches on non-union types.
*
* @default false
*/
requireDefaultForNonUnion?: boolean;
},
];

export default createRule<Options, MessageIds>({
name: 'switch-exhaustiveness-check',
meta: {
type: 'suggestion',
docs: {
description:
'Require switch-case statements to be exhaustive with union types and enums',
description: 'Require switch-case statements to be exhaustive',
requiresTypeChecking: true,
},
hasSuggestions: true,
schema: [],
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
requireDefaultForNonUnion: {
description: `If 'true', require a 'default' clause for switches on non-union types.`,
type: 'boolean',
},
},
},
],
messages: {
switchIsNotExhaustive:
'Switch is not exhaustive. Cases not matched: {{missingBranches}}',
addMissingCases: 'Add branches for missing cases.',
},
},
defaultOptions: [],
create(context) {
defaultOptions: [{ requireDefaultForNonUnion: false }],
create(context, [{ requireDefaultForNonUnion }]) {
const sourceCode = getSourceCode(context);
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
Expand All @@ -39,9 +61,9 @@ export default createRule({
function fixSwitch(
fixer: TSESLint.RuleFixer,
node: TSESTree.SwitchStatement,
missingBranchTypes: ts.Type[],
missingBranchTypes: (ts.Type | null)[], // null means default branch
symbolName?: string,
): TSESLint.RuleFix | null {
): TSESLint.RuleFix {
const lastCase =
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
const caseIndent = lastCase
Expand All @@ -52,6 +74,10 @@ export default createRule({

const missingCases = [];
for (const missingBranchType of missingBranchTypes) {
if (missingBranchType == null) {
missingCases.push(`default: { throw new Error('default case') }`);
continue;
}
// While running this rule on checker.ts of TypeScript project
// the fix introduced a compiler error due to:
//
Expand Down Expand Up @@ -159,7 +185,7 @@ export default createRule({
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix | null {
fix(fixer): TSESLint.RuleFix {
return fixSwitch(
fixer,
node,
Expand All @@ -170,6 +196,28 @@ export default createRule({
},
],
});
} else if (requireDefaultForNonUnion) {
const hasDefault = node.cases.some(
switchCase => switchCase.test == null,
);

if (!hasDefault) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: {
missingBranches: 'default',
},
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix {
return fixSwitch(fixer, node, [null]);
},
},
],
});
}
}
}

Expand Down
Expand Up @@ -209,6 +209,21 @@ function test(value: ObjectUnion): number {
}
}
`,
// switch with default clause on non-union type
{
code: `
declare const value: number;
switch (value) {
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
`,
options: [{ requireDefaultForNonUnion: true }],
},
],
invalid: [
{
Expand Down Expand Up @@ -595,6 +610,38 @@ function test(arg: Enum): string {
case Enum['9test']: { throw new Error('Not implemented yet: Enum[\\'9test\\'] case') }
case Enum.test: { throw new Error('Not implemented yet: Enum.test case') }
}
}
`,
},
],
},
],
},
{
code: `
const value: number = Math.floor(Math.random() * 3);
switch (value) {
case 0:
return 0;
case 1:
return 1;
}
`,
options: [{ requireDefaultForNonUnion: true }],
errors: [
{
messageId: 'switchIsNotExhaustive',
suggestions: [
{
messageId: 'addMissingCases',
output: `
const value: number = Math.floor(Math.random() * 3);
switch (value) {
case 0:
return 0;
case 1:
return 1;
default: { throw new Error('default case') }
}
`,
},
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4cfcd45

Please sign in to comment.