From b465b39e8ea1f5c3051ebd98bea585ef006e1316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 27 Oct 2024 21:49:22 +0900 Subject: [PATCH 01/13] feat: no default comment --- .../rules/switch-exhaustiveness-check.mdx | 2 + .../src/rules/switch-exhaustiveness-check.ts | 27 ++++++++- .../rules/switch-exhaustiveness-check.test.ts | 58 ++++++++++++++++++- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 538b911049c3..ff9fdf7128ac 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -14,6 +14,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. +If you have enabled an option that requires you to enter a default case, or if you did not enter it but want it to act as if you had entered it, specify `// no default` below. + ## Options ### `allowDefaultCaseForExhaustiveSwitch` diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index c5ebc2970175..49dfe4a39e64 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -1,5 +1,4 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; - import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -16,7 +15,7 @@ import { interface SwitchMetadata { readonly containsNonLiteralType: boolean; - readonly defaultCase: TSESTree.SwitchCase | undefined; + readonly defaultCase: TSESTree.SwitchCase | TSESTree.Comment | undefined; readonly missingLiteralBranchTypes: ts.Type[]; readonly symbolName: string | undefined; } @@ -92,6 +91,28 @@ export default createRule({ const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); + function getCommentDefaultCase( + node: TSESTree.SwitchStatement, + ): TSESTree.Comment | undefined { + const defaultCaseCommentConstants = ['no default', 'No Default']; + + const lastCase = node.cases.at(-1); + const commentsAfterLastCase = lastCase + ? context.sourceCode.getCommentsAfter(lastCase) + : []; + const defaultCaseComment = commentsAfterLastCase.at(-1); + + if ( + defaultCaseCommentConstants.includes( + defaultCaseComment?.value.trim() || '', + ) + ) { + return defaultCaseComment; + } + + return; + } + function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata { const defaultCase = node.cases.find( switchCase => switchCase.test == null, @@ -143,7 +164,7 @@ export default createRule({ return { containsNonLiteralType, - defaultCase, + defaultCase: defaultCase ? defaultCase : getCommentDefaultCase(node), missingLiteralBranchTypes, symbolName, }; 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 13869b62c859..abfc8320bff3 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1,6 +1,7 @@ -import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import path from 'node:path'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + import switchExhaustivenessCheck from '../../src/rules/switch-exhaustiveness-check'; const rootPath = path.join(process.cwd(), 'tests/fixtures/'); @@ -810,6 +811,37 @@ switch (value) { }, ], }, + { + code: ` +declare const value: number; + +switch (value) { + case 0: + break; + case 1: + break; + + // no default +} + `, + options: [ + { + requireDefaultForNonUnion: true, + }, + ], + }, + { + code: ` +declare const value: 'a' | 'b'; + +switch (value) { + case 'a': + break; + + // no default +} + `, + }, ], invalid: [ { @@ -2373,5 +2405,29 @@ switch (myValue) { }, ], }, + { + code: ` +declare const myValue: 'a' | 'b'; + +switch (myValue) { + case 'a': + return 'a'; + case 'b': + return 'b'; + + // no default +} + `, + errors: [ + { + messageId: 'dangerousDefaultCase', + }, + ], + options: [ + { + allowDefaultCaseForExhaustiveSwitch: false, + }, + ], + }, ], }); From d394fd3ad85fb488551bd8159f9297d022c6a54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Mon, 28 Oct 2024 21:04:22 +0900 Subject: [PATCH 02/13] feat: considerDefaultExhaustiveForUnions option apply --- .../src/rules/switch-exhaustiveness-check.ts | 4 +- .../rules/switch-exhaustiveness-check.test.ts | 45 +++++++++++++++++-- 2 files changed, 44 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 7689966fb653..09972f05e681 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -197,7 +197,7 @@ export default createRule({ // Unless considerDefaultExhaustiveForUnions is enabled, the presence of a default case // always makes the switch exhaustive. - if (!considerDefaultExhaustiveForUnions && defaultCase != null) { + if (!considerDefaultExhaustiveForUnions && defaultCase) { return; } @@ -239,7 +239,7 @@ export default createRule({ ): TSESLint.RuleFix { const lastCase = node.cases.length > 0 ? node.cases[node.cases.length - 1] : null; - const defaultCase = node.cases.find(caseEl => caseEl.test == null); + const defaultCase = node.cases.find(caseEl => caseEl.test == null) ?? getCommentDefaultCase(node) const caseIndent = lastCase ? ' '.repeat(lastCase.loc.start.column) 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 58a0af92871a..5cb00fd73a75 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -828,8 +828,8 @@ switch (literal) { }, ], }, - { - code:` + { + code: ` declare const literal: 'a' | 'b'; switch (literal) { case 'a': @@ -2513,7 +2513,7 @@ switch (literal) { `, errors: [ { - messageId: 'dangerousDefaultCase', + messageId: 'switchIsNotExhaustive', column: 9, line: 4, suggestions: [ @@ -2770,5 +2770,44 @@ switch (myValue) { }, ], }, + { + code: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + // no default +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + case "b": { throw new Error('Not implemented yet: "b" case') } + case "c": { throw new Error('Not implemented yet: "c" case') } + // no default +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, ], }); From 57614566ad4345acfe7c7d4a5f1ac4cabe0ddfd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 2 Nov 2024 14:05:54 +0900 Subject: [PATCH 03/13] feat: apply regex in no default --- .../src/rules/switch-exhaustiveness-check.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index de64d2f60bec..cd43c2274b4b 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -13,9 +14,11 @@ import { requiresQuoting, } from '../util'; +const DEFAULT_COMMENT_PATTERN = /^no default$/iu; + interface SwitchMetadata { readonly containsNonLiteralType: boolean; - readonly defaultCase: TSESTree.SwitchCase | TSESTree.Comment | undefined; + readonly defaultCase: TSESTree.Comment | TSESTree.SwitchCase | undefined; readonly missingLiteralBranchTypes: ts.Type[]; readonly symbolName: string | undefined; } @@ -112,8 +115,6 @@ export default createRule({ function getCommentDefaultCase( node: TSESTree.SwitchStatement, ): TSESTree.Comment | undefined { - const defaultCaseCommentConstants = ['no default', 'No Default']; - const lastCase = node.cases.at(-1); const commentsAfterLastCase = lastCase ? context.sourceCode.getCommentsAfter(lastCase) @@ -121,9 +122,7 @@ export default createRule({ const defaultCaseComment = commentsAfterLastCase.at(-1); if ( - defaultCaseCommentConstants.includes( - defaultCaseComment?.value.trim() || '', - ) + DEFAULT_COMMENT_PATTERN.test(defaultCaseComment?.value.trim() || '') ) { return defaultCaseComment; } From 1e9aa9552bc61a48f7d841fcf6e18f3b6aa31c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sat, 2 Nov 2024 14:06:03 +0900 Subject: [PATCH 04/13] fix: test case fix --- .../rules/switch-exhaustiveness-check.test.ts | 73 ++----------------- 1 file changed, 8 insertions(+), 65 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 adb10c459e67..b558de9b01f9 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1,6 +1,5 @@ -import path from 'node:path'; - import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; +import path from 'node:path'; import switchExhaustivenessCheck from '../../src/rules/switch-exhaustiveness-check'; @@ -834,8 +833,8 @@ switch (literal) { `, options: [ { - requireDefaultForNonUnion: true, considerDefaultExhaustiveForUnions: true, + requireDefaultForNonUnion: true, }, ], }, @@ -940,6 +939,11 @@ switch (value) { // no default } `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], }, ], invalid: [ @@ -2517,9 +2521,9 @@ switch (literal) { `, errors: [ { - messageId: 'switchIsNotExhaustive', column: 9, line: 4, + messageId: 'switchIsNotExhaustive', suggestions: [ { messageId: 'addMissingCases', @@ -2734,67 +2738,6 @@ switch (value) { default: { break; } -} - `, - }, - ], - }, - ], - options: [ - { - considerDefaultExhaustiveForUnions: true, - }, - ], - }, - { - code: ` -declare const myValue: 'a' | 'b'; -switch (myValue) { - case 'a': - return 'a'; - case 'b': - return 'b'; - // no default -} - `, - errors: [ - { - messageId: 'dangerousDefaultCase', - }, - ], - options: [ - { - allowDefaultCaseForExhaustiveSwitch: false, - }, - ], - }, - { - code: ` -declare const literal: 'a' | 'b' | 'c'; - -switch (literal) { - case 'a': - break; - // no default -} - `, - errors: [ - { - column: 9, - line: 4, - messageId: 'switchIsNotExhaustive', - suggestions: [ - { - messageId: 'addMissingCases', - output: ` -declare const literal: 'a' | 'b' | 'c'; - -switch (literal) { - case 'a': - break; - case "b": { throw new Error('Not implemented yet: "b" case') } - case "c": { throw new Error('Not implemented yet: "c" case') } - // no default } `, }, From 6844c0fe99d6e40bef30611055a4dcc17c0b0b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Thu, 7 Nov 2024 23:40:33 +0900 Subject: [PATCH 05/13] fix: add defalutCaseCommentPattern option --- .../src/rules/switch-exhaustiveness-check.ts | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index cd43c2274b4b..bfac98661e85 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -40,6 +40,11 @@ type Options = [ */ requireDefaultForNonUnion?: boolean; + /** + * Regular expression to evaluate comments to mean default case + */ + defalutCaseCommentPattern?: string; + /** * If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types. * @@ -83,6 +88,10 @@ export default createRule({ type: 'boolean', description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`, }, + defalutCaseCommentPattern: { + type: 'string', + description: `Regular expression to evaluate comments to mean default case`, + }, requireDefaultForNonUnion: { type: 'boolean', description: `If 'true', require a 'default' clause for switches on non-union types.`, @@ -104,6 +113,7 @@ export default createRule({ { allowDefaultCaseForExhaustiveSwitch, considerDefaultExhaustiveForUnions, + defalutCaseCommentPattern, requireDefaultForNonUnion, }, ], @@ -120,10 +130,12 @@ export default createRule({ ? context.sourceCode.getCommentsAfter(lastCase) : []; const defaultCaseComment = commentsAfterLastCase.at(-1); + const commentRegExp = + defalutCaseCommentPattern != null + ? new RegExp(defalutCaseCommentPattern, 'u') + : DEFAULT_COMMENT_PATTERN; - if ( - DEFAULT_COMMENT_PATTERN.test(defaultCaseComment?.value.trim() || '') - ) { + if (commentRegExp.test(defaultCaseComment?.value.trim() || '')) { return defaultCaseComment; } @@ -181,7 +193,7 @@ export default createRule({ return { containsNonLiteralType, - defaultCase: defaultCase ? defaultCase : getCommentDefaultCase(node), + defaultCase: defaultCase ?? getCommentDefaultCase(node), missingLiteralBranchTypes, symbolName, }; @@ -196,7 +208,7 @@ export default createRule({ // If considerDefaultExhaustiveForUnions is enabled, the presence of a default case // always makes the switch exhaustive. - if (considerDefaultExhaustiveForUnions && defaultCase) { + if (considerDefaultExhaustiveForUnions && defaultCase != null) { return; } @@ -221,6 +233,7 @@ export default createRule({ fixer, node, missingLiteralBranchTypes, + defaultCase, symbolName?.toString(), ); }, @@ -234,13 +247,11 @@ export default createRule({ fixer: TSESLint.RuleFixer, node: TSESTree.SwitchStatement, missingBranchTypes: (ts.Type | null)[], // null means default branch + defaultCase: TSESTree.Comment | TSESTree.SwitchCase | undefined, symbolName?: string, ): TSESLint.RuleFix { const lastCase = node.cases.length > 0 ? node.cases[node.cases.length - 1] : null; - const defaultCase = - node.cases.find(caseEl => caseEl.test == null) ?? - getCommentDefaultCase(node); const caseIndent = lastCase ? ' '.repeat(lastCase.loc.start.column) @@ -362,7 +373,7 @@ export default createRule({ { messageId: 'addMissingCases', fix(fixer): TSESLint.RuleFix { - return fixSwitch(fixer, node, [null]); + return fixSwitch(fixer, node, [null], defaultCase); }, }, ], From 88fad9588a0961edf68e71f5ddcbe3b76b791bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Wed, 13 Nov 2024 20:07:25 +0900 Subject: [PATCH 06/13] feat: add testcase and docs --- .../rules/switch-exhaustiveness-check.mdx | 50 +++++++++++++++++ .../src/rules/switch-exhaustiveness-check.ts | 14 ++--- .../rules/switch-exhaustiveness-check.test.ts | 54 +++++++++++++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 8b826c98fcbe..da90d8f3ae79 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -80,6 +80,25 @@ switch (literal) { } ``` +### defaultCaseCommentPattern + +{/* insert option description */} + +Basically, there is logic to treat annotations matching /^no default$/iu as default case. +If you want to change this pattern, just enter the pattern in this option. +ㄴ +Examples of additional **correct** code with `{ defaultCaseCommentPattern: "^skip\\sdefault" }`: + +```ts option='{ "defaultCaseCommentPattern": "^skip\sdefault" }' showPlaygroundButton +declare const value: 'a' | 'b'; + +switch (value) { + case 'a': + break; + // skip default +} +``` + ## Examples 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. @@ -256,6 +275,37 @@ switch (fruit) { +And you can mark the default case as a comment. + + + + +```ts option='{ "considerDefaultExhaustiveForUnions": true } +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + // no default +} +``` + + + + +```ts +declare const value: 'a' | 'b'; + +switch (value) { + case 'a': + break; + // no default +} +``` + + + + ## 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/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index bfac98661e85..6f260c1dcc42 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -41,9 +41,9 @@ type Options = [ requireDefaultForNonUnion?: boolean; /** - * Regular expression to evaluate comments to mean default case + * Regular expression for a comment that can indicate an intentionally omitted default case. */ - defalutCaseCommentPattern?: string; + defaultCaseCommentPattern?: string; /** * If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types. @@ -88,9 +88,9 @@ export default createRule({ type: 'boolean', description: `If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type`, }, - defalutCaseCommentPattern: { + defaultCaseCommentPattern: { type: 'string', - description: `Regular expression to evaluate comments to mean default case`, + description: `Regular expression for a comment that can indicate an intentionally omitted default case.`, }, requireDefaultForNonUnion: { type: 'boolean', @@ -113,7 +113,7 @@ export default createRule({ { allowDefaultCaseForExhaustiveSwitch, considerDefaultExhaustiveForUnions, - defalutCaseCommentPattern, + defaultCaseCommentPattern, requireDefaultForNonUnion, }, ], @@ -131,8 +131,8 @@ export default createRule({ : []; const defaultCaseComment = commentsAfterLastCase.at(-1); const commentRegExp = - defalutCaseCommentPattern != null - ? new RegExp(defalutCaseCommentPattern, 'u') + defaultCaseCommentPattern != null + ? new RegExp(defaultCaseCommentPattern, 'u') : DEFAULT_COMMENT_PATTERN; if (commentRegExp.test(defaultCaseComment?.value.trim() || '')) { 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 b558de9b01f9..a80018ecf8ad 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -945,6 +945,21 @@ switch (value) { }, ], }, + { + code: ` +declare const value: 'a' | 'b'; +switch (value) { + case 'a': + break; + // skip default +} + `, + options: [ + { + defaultCaseCommentPattern: '^skip\\sdefault', + }, + ], + }, ], invalid: [ { @@ -2811,5 +2826,44 @@ switch (literal) { }, ], }, + { + code: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + // skip default +} + `, + errors: [ + { + column: 9, + line: 4, + messageId: 'switchIsNotExhaustive', + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +declare const literal: 'a' | 'b' | 'c'; + +switch (literal) { + case 'a': + break; + case "b": { throw new Error('Not implemented yet: "b" case') } + case "c": { throw new Error('Not implemented yet: "c" case') } + // skip default +} + `, + }, + ], + }, + ], + options: [ + { + defaultCaseCommentPattern: '^skip\\sdefault', + }, + ], + }, ], }); From 966aebe8878387a80746ab39517dc872c771d23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Wed, 13 Nov 2024 20:16:22 +0900 Subject: [PATCH 07/13] fix:test case --- .../tests/rules/switch-exhaustiveness-check.test.ts | 2 ++ 1 file changed, 2 insertions(+) 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 a80018ecf8ad..c2ef10f40d6c 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -956,6 +956,7 @@ switch (value) { `, options: [ { + considerDefaultExhaustiveForUnions: true, defaultCaseCommentPattern: '^skip\\sdefault', }, ], @@ -2861,6 +2862,7 @@ switch (literal) { ], options: [ { + considerDefaultExhaustiveForUnions: false, defaultCaseCommentPattern: '^skip\\sdefault', }, ], From 764fd6b848cd44a521608157d70d5f1e7cd6f329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Mon, 2 Dec 2024 23:26:22 +0900 Subject: [PATCH 08/13] fix: code review apply --- .../rules/switch-exhaustiveness-check.mdx | 39 ++----------------- .../src/rules/switch-exhaustiveness-check.ts | 8 ++-- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index da90d8f3ae79..b0e49070b5bb 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -14,8 +14,6 @@ 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. -If you have enabled an option that requires you to enter a default case, or if you did not enter it but want it to act as if you had entered it, specify `// no default` below. - ## Options ### `allowDefaultCaseForExhaustiveSwitch` @@ -84,9 +82,9 @@ switch (literal) { {/* insert option description */} -Basically, there is logic to treat annotations matching /^no default$/iu as default case. -If you want to change this pattern, just enter the pattern in this option. -ㄴ +It can sometimes be preferable to omit the default case for only some switch statements. +For those situations, this rule can be given a pattern for a comment that's allowed to take the place of a `default:`. + Examples of additional **correct** code with `{ defaultCaseCommentPattern: "^skip\\sdefault" }`: ```ts option='{ "defaultCaseCommentPattern": "^skip\sdefault" }' showPlaygroundButton @@ -275,37 +273,6 @@ switch (fruit) { -And you can mark the default case as a comment. - - - - -```ts option='{ "considerDefaultExhaustiveForUnions": true } -declare const literal: 'a' | 'b' | 'c'; - -switch (literal) { - case 'a': - break; - // no default -} -``` - - - - -```ts -declare const value: 'a' | 'b'; - -switch (value) { - case 'a': - break; - // no default -} -``` - - - - ## 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/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 6f260c1dcc42..46d0a0d3a40f 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -121,6 +121,10 @@ export default createRule({ const services = getParserServices(context); const checker = services.program.getTypeChecker(); const compilerOptions = services.program.getCompilerOptions(); + const commentRegExp = + defaultCaseCommentPattern != null + ? new RegExp(defaultCaseCommentPattern, 'u') + : DEFAULT_COMMENT_PATTERN; function getCommentDefaultCase( node: TSESTree.SwitchStatement, @@ -130,10 +134,6 @@ export default createRule({ ? context.sourceCode.getCommentsAfter(lastCase) : []; const defaultCaseComment = commentsAfterLastCase.at(-1); - const commentRegExp = - defaultCaseCommentPattern != null - ? new RegExp(defaultCaseCommentPattern, 'u') - : DEFAULT_COMMENT_PATTERN; if (commentRegExp.test(defaultCaseComment?.value.trim() || '')) { return defaultCaseComment; From c7f743b4f13a1d076b39c7b96f6e3e38ae72b234 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 7 Dec 2024 09:50:13 -0500 Subject: [PATCH 09/13] Update test snapshots --- .../switch-exhaustiveness-check.shot | 166 ------------------ .../switch-exhaustiveness-check.shot | 6 + 2 files changed, 6 insertions(+), 166 deletions(-) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot index 95c3ee829974..15041bafac82 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot @@ -28,169 +28,3 @@ switch (literal) { } " `; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = ` -"Incorrect - -type Day = - | 'Monday' - | 'Tuesday' - | 'Wednesday' - | 'Thursday' - | 'Friday' - | 'Saturday' - | 'Sunday'; - -declare const day: Day; -let result = 0; - -switch (day) { - ~~~ Switch is not exhaustive. Cases not matched: "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday" - case 'Monday': - result = 1; - break; -} -" -`; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = ` -"Correct - -type Day = - | 'Monday' - | 'Tuesday' - | 'Wednesday' - | 'Thursday' - | 'Friday' - | 'Saturday' - | 'Sunday'; - -declare const day: Day; -let result = 0; - -switch (day) { - case 'Monday': - result = 1; - break; - case 'Tuesday': - result = 2; - break; - case 'Wednesday': - result = 3; - break; - case 'Thursday': - result = 4; - break; - case 'Friday': - result = 5; - break; - case 'Saturday': - result = 6; - break; - case 'Sunday': - result = 7; - break; -} -" -`; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` -"Correct -Options: { "considerDefaultExhaustiveForUnions": true } - -// requires \`considerDefaultExhaustiveForUnions\` to be set to true - -type Day = - | 'Monday' - | 'Tuesday' - | 'Wednesday' - | 'Thursday' - | 'Friday' - | 'Saturday' - | 'Sunday'; - -declare const day: Day; -let result = 0; - -switch (day) { - case 'Monday': - result = 1; - break; - default: - result = 42; -} -" -`; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` -"Incorrect - -enum Fruit { - Apple, - Banana, - Cherry, -} - -declare const fruit: Fruit; - -switch (fruit) { - ~~~~~ Switch is not exhaustive. Cases not matched: Fruit.Banana | Fruit.Cherry - case Fruit.Apple: - console.log('an apple'); - break; -} -" -`; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` -"Correct - -enum Fruit { - Apple, - Banana, - Cherry, -} - -declare const fruit: Fruit; - -switch (fruit) { - case Fruit.Apple: - console.log('an apple'); - break; - - case Fruit.Banana: - console.log('a banana'); - break; - - case Fruit.Cherry: - console.log('a cherry'); - break; -} -" -`; - -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = ` -"Correct -Options: { "considerDefaultExhaustiveForUnions": true } - -// requires \`considerDefaultExhaustiveForUnions\` to be set to true - -enum Fruit { - Apple, - Banana, - Cherry, -} - -declare const fruit: Fruit; - -switch (fruit) { - case Fruit.Apple: - console.log('an apple'); - break; - - default: - console.log('a fruit'); - break; -} -" -`; 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 05cbeedd6162..4a8d9d4ddfc2 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot @@ -16,6 +16,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "description": "If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type", "type": "boolean" }, + "defaultCaseCommentPattern": { + "description": "Regular expression for a comment that can indicate an intentionally omitted default case.", + "type": "string" + }, "requireDefaultForNonUnion": { "description": "If 'true', require a 'default' clause for switches on non-union types.", "type": "boolean" @@ -34,6 +38,8 @@ type Options = [ allowDefaultCaseForExhaustiveSwitch?: boolean; /** If 'true', the 'default' clause is used to determine whether the switch statement is exhaustive for union type */ considerDefaultExhaustiveForUnions?: boolean; + /** Regular expression for a comment that can indicate an intentionally omitted default case. */ + defaultCaseCommentPattern?: string; /** If 'true', require a 'default' clause for switches on non-union types. */ requireDefaultForNonUnion?: boolean; }, From 175681c51def414f0cfd5e32c194c5697852911b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sat, 7 Dec 2024 09:59:54 -0500 Subject: [PATCH 10/13] Update packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index b0e49070b5bb..0b1fc2f5df9c 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -78,7 +78,7 @@ switch (literal) { } ``` -### defaultCaseCommentPattern +### `defaultCaseCommentPattern` {/* insert option description */} From cbb3f81cdb586df0c54a7e47685d4306c5edc6da Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 7 Dec 2024 10:13:48 -0500 Subject: [PATCH 11/13] Fix regex complaint in code block --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 0b1fc2f5df9c..89384a75c554 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -87,7 +87,7 @@ For those situations, this rule can be given a pattern for a comment that's allo Examples of additional **correct** code with `{ defaultCaseCommentPattern: "^skip\\sdefault" }`: -```ts option='{ "defaultCaseCommentPattern": "^skip\sdefault" }' showPlaygroundButton +```ts option='{ "defaultCaseCommentPattern": "^skip default" }' showPlaygroundButton declare const value: 'a' | 'b'; switch (value) { From 844a96ca9bd9834381db8d04b302bbc556f80909 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 7 Dec 2024 10:33:29 -0500 Subject: [PATCH 12/13] update test snapshots in general --- .../switch-exhaustiveness-check.shot | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot index 15041bafac82..e215ae5a9436 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/switch-exhaustiveness-check.shot @@ -28,3 +28,183 @@ switch (literal) { } " `; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 3`] = ` +"Options: { "defaultCaseCommentPattern": "^skip default" } + +declare const value: 'a' | 'b'; + +switch (value) { + ~~~~~ Switch is not exhaustive. Cases not matched: "b" + case 'a': + break; + // skip default +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = ` +"Incorrect + +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +declare const day: Day; +let result = 0; + +switch (day) { + ~~~ Switch is not exhaustive. Cases not matched: "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday" + case 'Monday': + result = 1; + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` +"Correct + +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +declare const day: Day; +let result = 0; + +switch (day) { + case 'Monday': + result = 1; + break; + case 'Tuesday': + result = 2; + break; + case 'Wednesday': + result = 3; + break; + case 'Thursday': + result = 4; + break; + case 'Friday': + result = 5; + break; + case 'Saturday': + result = 6; + break; + case 'Sunday': + result = 7; + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` +"Correct +Options: { "considerDefaultExhaustiveForUnions": true } + +// requires \`considerDefaultExhaustiveForUnions\` to be set to true + +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +declare const day: Day; +let result = 0; + +switch (day) { + case 'Monday': + result = 1; + break; + default: + result = 42; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` +"Incorrect + +enum Fruit { + Apple, + Banana, + Cherry, +} + +declare const fruit: Fruit; + +switch (fruit) { + ~~~~~ Switch is not exhaustive. Cases not matched: Fruit.Banana | Fruit.Cherry + case Fruit.Apple: + console.log('an apple'); + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = ` +"Correct + +enum Fruit { + Apple, + Banana, + Cherry, +} + +declare const fruit: Fruit; + +switch (fruit) { + case Fruit.Apple: + console.log('an apple'); + break; + + case Fruit.Banana: + console.log('a banana'); + break; + + case Fruit.Cherry: + console.log('a cherry'); + break; +} +" +`; + +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 9`] = ` +"Correct +Options: { "considerDefaultExhaustiveForUnions": true } + +// requires \`considerDefaultExhaustiveForUnions\` to be set to true + +enum Fruit { + Apple, + Banana, + Cherry, +} + +declare const fruit: Fruit; + +switch (fruit) { + case Fruit.Apple: + console.log('an apple'); + break; + + default: + console.log('a fruit'); + break; +} +" +`; From f4709a5e35fede9cd8b078096757a1dcabb34517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sat, 7 Dec 2024 10:41:28 -0500 Subject: [PATCH 13/13] Apply suggestions from code review --- .../eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 89384a75c554..20ee0568d164 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -82,6 +82,8 @@ switch (literal) { {/* insert option description */} +Default: `/^no default$/iu`. + It can sometimes be preferable to omit the default case for only some switch statements. For those situations, this rule can be given a pattern for a comment that's allowed to take the place of a `default:`.