Skip to content

Commit 47f1ab3

Browse files
feat(eslint-plugin): [switch-exhaustiveness-check] add support for "no default" comment (#10218)
* feat: no default comment * feat: considerDefaultExhaustiveForUnions option apply * feat: apply regex in no default * fix: test case fix * fix: add defalutCaseCommentPattern option * feat: add testcase and docs * fix:test case * fix: code review apply * Update test snapshots * Update packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx * Fix regex complaint in code block * update test snapshots in general * Apply suggestions from code review --------- Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com>
1 parent 772bd43 commit 47f1ab3

File tree

5 files changed

+233
-9
lines changed

5 files changed

+233
-9
lines changed

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

+21
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@ switch (literal) {
7878
}
7979
```
8080

81+
### `defaultCaseCommentPattern`
82+
83+
{/* insert option description */}
84+
85+
Default: `/^no default$/iu`.
86+
87+
It can sometimes be preferable to omit the default case for only some switch statements.
88+
For those situations, this rule can be given a pattern for a comment that's allowed to take the place of a `default:`.
89+
90+
Examples of additional **correct** code with `{ defaultCaseCommentPattern: "^skip\\sdefault" }`:
91+
92+
```ts option='{ "defaultCaseCommentPattern": "^skip default" }' showPlaygroundButton
93+
declare const value: 'a' | 'b';
94+
95+
switch (value) {
96+
case 'a':
97+
break;
98+
// skip default
99+
}
100+
```
101+
81102
## Examples
82103

83104
When the switch doesn't have exhaustive cases, either filling them all out or adding a default (if you have `considerDefaultExhaustiveForUnions` enabled) will address the rule's complaint.

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

+37-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
requiresQuoting,
1515
} from '../util';
1616

17+
const DEFAULT_COMMENT_PATTERN = /^no default$/iu;
18+
1719
interface SwitchMetadata {
1820
readonly containsNonLiteralType: boolean;
19-
readonly defaultCase: TSESTree.SwitchCase | undefined;
21+
readonly defaultCase: TSESTree.Comment | TSESTree.SwitchCase | undefined;
2022
readonly missingLiteralBranchTypes: ts.Type[];
2123
readonly symbolName: string | undefined;
2224
}
@@ -38,6 +40,11 @@ type Options = [
3840
*/
3941
requireDefaultForNonUnion?: boolean;
4042

43+
/**
44+
* Regular expression for a comment that can indicate an intentionally omitted default case.
45+
*/
46+
defaultCaseCommentPattern?: string;
47+
4148
/**
4249
* If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types.
4350
*
@@ -81,6 +88,10 @@ export default createRule<Options, MessageIds>({
8188
type: 'boolean',
8289
description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`,
8390
},
91+
defaultCaseCommentPattern: {
92+
type: 'string',
93+
description: `Regular expression for a comment that can indicate an intentionally omitted default case.`,
94+
},
8495
requireDefaultForNonUnion: {
8596
type: 'boolean',
8697
description: `If 'true', require a 'default' clause for switches on non-union types.`,
@@ -102,13 +113,34 @@ export default createRule<Options, MessageIds>({
102113
{
103114
allowDefaultCaseForExhaustiveSwitch,
104115
considerDefaultExhaustiveForUnions,
116+
defaultCaseCommentPattern,
105117
requireDefaultForNonUnion,
106118
},
107119
],
108120
) {
109121
const services = getParserServices(context);
110122
const checker = services.program.getTypeChecker();
111123
const compilerOptions = services.program.getCompilerOptions();
124+
const commentRegExp =
125+
defaultCaseCommentPattern != null
126+
? new RegExp(defaultCaseCommentPattern, 'u')
127+
: DEFAULT_COMMENT_PATTERN;
128+
129+
function getCommentDefaultCase(
130+
node: TSESTree.SwitchStatement,
131+
): TSESTree.Comment | undefined {
132+
const lastCase = node.cases.at(-1);
133+
const commentsAfterLastCase = lastCase
134+
? context.sourceCode.getCommentsAfter(lastCase)
135+
: [];
136+
const defaultCaseComment = commentsAfterLastCase.at(-1);
137+
138+
if (commentRegExp.test(defaultCaseComment?.value.trim() || '')) {
139+
return defaultCaseComment;
140+
}
141+
142+
return;
143+
}
112144

113145
function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata {
114146
const defaultCase = node.cases.find(
@@ -170,7 +202,7 @@ export default createRule<Options, MessageIds>({
170202

171203
return {
172204
containsNonLiteralType,
173-
defaultCase,
205+
defaultCase: defaultCase ?? getCommentDefaultCase(node),
174206
missingLiteralBranchTypes,
175207
symbolName,
176208
};
@@ -210,6 +242,7 @@ export default createRule<Options, MessageIds>({
210242
fixer,
211243
node,
212244
missingLiteralBranchTypes,
245+
defaultCase,
213246
symbolName?.toString(),
214247
);
215248
},
@@ -223,11 +256,11 @@ export default createRule<Options, MessageIds>({
223256
fixer: TSESLint.RuleFixer,
224257
node: TSESTree.SwitchStatement,
225258
missingBranchTypes: (ts.Type | null)[], // null means default branch
259+
defaultCase: TSESTree.Comment | TSESTree.SwitchCase | undefined,
226260
symbolName?: string,
227261
): TSESLint.RuleFix {
228262
const lastCase =
229263
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
230-
const defaultCase = node.cases.find(caseEl => caseEl.test == null);
231264

232265
const caseIndent = lastCase
233266
? ' '.repeat(lastCase.loc.start.column)
@@ -349,7 +382,7 @@ export default createRule<Options, MessageIds>({
349382
{
350383
messageId: 'addMissingCases',
351384
fix(fixer): TSESLint.RuleFix {
352-
return fixSwitch(fixer, node, [null]);
385+
return fixSwitch(fixer, node, [null], defaultCase);
353386
},
354387
},
355388
],

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

+19-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts

+150
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ switch (literal) {
834834
options: [
835835
{
836836
considerDefaultExhaustiveForUnions: true,
837+
requireDefaultForNonUnion: true,
837838
},
838839
],
839840
},
@@ -954,6 +955,54 @@ function foo(x: string[], y: string | undefined) {
954955
},
955956
},
956957
},
958+
{
959+
code: `
960+
declare const value: number;
961+
switch (value) {
962+
case 0:
963+
break;
964+
case 1:
965+
break;
966+
// no default
967+
}
968+
`,
969+
options: [
970+
{
971+
requireDefaultForNonUnion: true,
972+
},
973+
],
974+
},
975+
{
976+
code: `
977+
declare const value: 'a' | 'b';
978+
switch (value) {
979+
case 'a':
980+
break;
981+
// no default
982+
}
983+
`,
984+
options: [
985+
{
986+
considerDefaultExhaustiveForUnions: true,
987+
},
988+
],
989+
},
990+
{
991+
code: `
992+
declare const value: 'a' | 'b';
993+
switch (value) {
994+
case 'a':
995+
break;
996+
// skip default
997+
}
998+
`,
999+
options: [
1000+
{
1001+
considerDefaultExhaustiveForUnions: true,
1002+
defaultCaseCommentPattern: '^skip\\sdefault',
1003+
},
1004+
],
1005+
},
9571006
],
9581007
invalid: [
9591008
{
@@ -2797,5 +2846,106 @@ function foo(x: string[]) {
27972846
},
27982847
},
27992848
},
2849+
{
2850+
code: `
2851+
declare const myValue: 'a' | 'b';
2852+
switch (myValue) {
2853+
case 'a':
2854+
return 'a';
2855+
case 'b':
2856+
return 'b';
2857+
// no default
2858+
}
2859+
`,
2860+
errors: [
2861+
{
2862+
messageId: 'dangerousDefaultCase',
2863+
},
2864+
],
2865+
options: [
2866+
{
2867+
allowDefaultCaseForExhaustiveSwitch: false,
2868+
},
2869+
],
2870+
},
2871+
{
2872+
code: `
2873+
declare const literal: 'a' | 'b' | 'c';
2874+
2875+
switch (literal) {
2876+
case 'a':
2877+
break;
2878+
// no default
2879+
}
2880+
`,
2881+
errors: [
2882+
{
2883+
column: 9,
2884+
line: 4,
2885+
messageId: 'switchIsNotExhaustive',
2886+
suggestions: [
2887+
{
2888+
messageId: 'addMissingCases',
2889+
output: `
2890+
declare const literal: 'a' | 'b' | 'c';
2891+
2892+
switch (literal) {
2893+
case 'a':
2894+
break;
2895+
case "b": { throw new Error('Not implemented yet: "b" case') }
2896+
case "c": { throw new Error('Not implemented yet: "c" case') }
2897+
// no default
2898+
}
2899+
`,
2900+
},
2901+
],
2902+
},
2903+
],
2904+
options: [
2905+
{
2906+
considerDefaultExhaustiveForUnions: false,
2907+
},
2908+
],
2909+
},
2910+
{
2911+
code: `
2912+
declare const literal: 'a' | 'b' | 'c';
2913+
2914+
switch (literal) {
2915+
case 'a':
2916+
break;
2917+
// skip default
2918+
}
2919+
`,
2920+
errors: [
2921+
{
2922+
column: 9,
2923+
line: 4,
2924+
messageId: 'switchIsNotExhaustive',
2925+
suggestions: [
2926+
{
2927+
messageId: 'addMissingCases',
2928+
output: `
2929+
declare const literal: 'a' | 'b' | 'c';
2930+
2931+
switch (literal) {
2932+
case 'a':
2933+
break;
2934+
case "b": { throw new Error('Not implemented yet: "b" case') }
2935+
case "c": { throw new Error('Not implemented yet: "c" case') }
2936+
// skip default
2937+
}
2938+
`,
2939+
},
2940+
],
2941+
},
2942+
],
2943+
options: [
2944+
{
2945+
considerDefaultExhaustiveForUnions: false,
2946+
defaultCaseCommentPattern: '^skip\\sdefault',
2947+
},
2948+
],
2949+
},
28002950
],
28012951
});

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

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)