diff --git a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx index 09ce6014d689..20ee0568d164 100644 --- a/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx +++ b/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.mdx @@ -78,6 +78,27 @@ switch (literal) { } ``` +### `defaultCaseCommentPattern` + +{/* 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:`. + +Examples of additional **correct** code with `{ defaultCaseCommentPattern: "^skip\\sdefault" }`: + +```ts option='{ "defaultCaseCommentPattern": "^skip default" }' 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. diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 4b5b178068cd..0151d9a79746 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -14,9 +14,11 @@ import { requiresQuoting, } from '../util'; +const DEFAULT_COMMENT_PATTERN = /^no default$/iu; + interface SwitchMetadata { readonly containsNonLiteralType: boolean; - readonly defaultCase: TSESTree.SwitchCase | undefined; + readonly defaultCase: TSESTree.Comment | TSESTree.SwitchCase | undefined; readonly missingLiteralBranchTypes: ts.Type[]; readonly symbolName: string | undefined; } @@ -38,6 +40,11 @@ type Options = [ */ requireDefaultForNonUnion?: boolean; + /** + * Regular expression for a comment that can indicate an intentionally omitted default case. + */ + defaultCaseCommentPattern?: string; + /** * If `true`, the `default` clause is used to determine whether the switch statement is exhaustive for union types. * @@ -81,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`, }, + defaultCaseCommentPattern: { + type: 'string', + description: `Regular expression for a comment that can indicate an intentionally omitted default case.`, + }, requireDefaultForNonUnion: { type: 'boolean', description: `If 'true', require a 'default' clause for switches on non-union types.`, @@ -102,6 +113,7 @@ export default createRule({ { allowDefaultCaseForExhaustiveSwitch, considerDefaultExhaustiveForUnions, + defaultCaseCommentPattern, requireDefaultForNonUnion, }, ], @@ -109,6 +121,26 @@ 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, + ): TSESTree.Comment | undefined { + const lastCase = node.cases.at(-1); + const commentsAfterLastCase = lastCase + ? context.sourceCode.getCommentsAfter(lastCase) + : []; + const defaultCaseComment = commentsAfterLastCase.at(-1); + + if (commentRegExp.test(defaultCaseComment?.value.trim() || '')) { + return defaultCaseComment; + } + + return; + } function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata { const defaultCase = node.cases.find( @@ -170,7 +202,7 @@ export default createRule({ return { containsNonLiteralType, - defaultCase, + defaultCase: defaultCase ?? getCommentDefaultCase(node), missingLiteralBranchTypes, symbolName, }; @@ -210,6 +242,7 @@ export default createRule({ fixer, node, missingLiteralBranchTypes, + defaultCase, symbolName?.toString(), ); }, @@ -223,11 +256,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); const caseIndent = lastCase ? ' '.repeat(lastCase.loc.start.column) @@ -349,7 +382,7 @@ export default createRule({ { messageId: 'addMissingCases', fix(fixer): TSESLint.RuleFix { - return fixSwitch(fixer, node, [null]); + return fixSwitch(fixer, node, [null], defaultCase); }, }, ], 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..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 @@ -30,6 +30,20 @@ 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 = @@ -53,7 +67,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 4`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` "Correct type Day = @@ -94,7 +108,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 5`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` "Correct Options: { "considerDefaultExhaustiveForUnions": true } @@ -122,7 +136,7 @@ switch (day) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 6`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` "Incorrect enum Fruit { @@ -142,7 +156,7 @@ switch (fruit) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 7`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = ` "Correct enum Fruit { @@ -169,7 +183,7 @@ switch (fruit) { " `; -exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 8`] = ` +exports[`Validating rule docs switch-exhaustiveness-check.mdx code examples ESLint output 9`] = ` "Correct Options: { "considerDefaultExhaustiveForUnions": true } 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 c7b9b62e9ea4..263c9b81bb5c 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -834,6 +834,7 @@ switch (literal) { options: [ { considerDefaultExhaustiveForUnions: true, + requireDefaultForNonUnion: true, }, ], }, @@ -954,6 +955,54 @@ function foo(x: string[], y: string | undefined) { }, }, }, + { + 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 +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + }, + ], + }, + { + code: ` +declare const value: 'a' | 'b'; +switch (value) { + case 'a': + break; + // skip default +} + `, + options: [ + { + considerDefaultExhaustiveForUnions: true, + defaultCaseCommentPattern: '^skip\\sdefault', + }, + ], + }, ], invalid: [ { @@ -2797,5 +2846,106 @@ function foo(x: string[]) { }, }, }, + { + 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 +} + `, + }, + ], + }, + ], + options: [ + { + considerDefaultExhaustiveForUnions: false, + }, + ], + }, + { + 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: [ + { + considerDefaultExhaustiveForUnions: false, + defaultCaseCommentPattern: '^skip\\sdefault', + }, + ], + }, ], }); 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; },