📄 switch-exhaustiveness-check¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 10 |
| 📦 Imports | 10 |
| 📊 Variables & Constants | 1 |
| 📐 Interfaces | 1 |
| 📑 Type Aliases | 2 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'switch-exhaustiveness-check' |
meta.type |
'suggestion' |
meta.docs.description |
'Require switch-case statements to be exhaustive' |
meta.docs.requiresTypeChecking |
true |
meta.hasSuggestions |
true |
meta.messages.addMissingCases |
'Add branches for missing cases.' |
meta.messages.dangerousDefaultCase |
'The switch statement is exhaustive, so the default case is unnecessary.' |
meta.messages.switchIsNotExhaustive |
'Switch is not exhaustive. Cases not matched: {{missingBranches}}' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { allowDefaultCaseForExhaustiveSwitch: { type: 'boolean'... |
defaultOptions |
[ { allowDefaultCaseForExhaustiveSwitch: true, considerDefaultExhaustiveForUnions: false, requireDefaultForNonUnion: ... |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getParserServices |
../util |
isClosingBraceToken |
../util |
isOpeningBraceToken |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
requiresQuoting |
../util |
Variables & Constants¶
| Name | Type | Kind | Value | Exported |
|---|---|---|---|---|
DEFAULT_COMMENT_PATTERN |
RegExp |
const | /^no default$/iu |
✗ |
Functions¶
create(context: any, [ { allowDefaultCaseForExhausti…: any): { SwitchStatement(node: any): void; }¶
Parameters:
contextany[ { allowDefaultCaseForExhaustiveSwitch, considerDefaultExhaustiveForUnions, defaultCaseCommentPattern, requireDefaultForNonUnion, }, ]any
Returns: { SwitchStatement(node: any): void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckerservices.program.getCompilerOptionsnode.cases.atcontext.sourceCode.getCommentsAftercommentsAfterLastCase.atcommentRegExp.testdefaultCaseComment?.value.trimchecker.typeToStringnode.cases.findgetConstrainedTypeAtLocation (from ../util)discriminantType.getSymboldoesTypeContainNonLiteralTypecaseTypes.addtsutils.unionConstituentstsutils.intersectionConstituentscaseTypes.hasisTypeLiteralLikeType[...caseTypes].sometsutils.isIntrinsicUndefinedTypemissingLiteralBranchTypes.pushgetCommentDefaultCasecontext.reportmissingLiteralBranchTypes .map(missingType => tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) ?typeof ${missingType.getSymbol()?.escapedName as string}: typeToString(missingType), ) .joinfixSwitchsymbolName?.toString' '.repeatmissingCases.pushmissingBranchType.getSymboltsutils.isTypeFlagSettypeToStringrequiresQuoting (from ../util)missingBranchName.toStringmissingBranchName .replaceAll("'", "\\'") .replaceAll('\n', '\\n') .replaceAllcaseTest .replaceAll('\\', '\\\\') .replaceAllmissingCases .map(code =>${caseIndent}${code}) .joinmissingCases .map(code =>${code}\n${caseIndent}) .joinfixer.insertTextBeforefixer.insertTextAfternullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenfixer.replaceTextRange['{', fixString,${caseIndent}}].joingetSwitchMetadatacheckSwitchExhaustivecheckSwitchUnnecessaryDefaultCasecheckSwitchNoUnionDefaultCase
Internal Comments:
// If the `test` property of the switch case is `null`, then we are on a
// `default` case.
// "missing", "optional" and "undefined" types are different runtime objects,
// but all of them have TypeFlags.Undefined type flag
// If considerDefaultExhaustiveForUnions is enabled, the presence of a default case
// always makes the switch exhaustive.
// leave it to the user to format it correctly. (x3)
// There were no existing cases. (x2)
Code
create(
context,
[
{
allowDefaultCaseForExhaustiveSwitch,
considerDefaultExhaustiveForUnions,
defaultCaseCommentPattern,
requireDefaultForNonUnion,
},
],
) {
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 typeToString(type: ts.Type): string {
return checker.typeToString(
type,
undefined,
ts.TypeFormatFlags.AllowUniqueESSymbolType |
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
ts.TypeFormatFlags.UseFullyQualifiedType,
);
}
function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata {
const defaultCase = node.cases.find(
switchCase => switchCase.test == null,
);
const discriminantType = getConstrainedTypeAtLocation(
services,
node.discriminant,
);
const symbolName = discriminantType.getSymbol()?.escapedName as
string | undefined;
const containsNonLiteralType =
doesTypeContainNonLiteralType(discriminantType);
const caseTypes = new Set<ts.Type>();
for (const switchCase of node.cases) {
// If the `test` property of the switch case is `null`, then we are on a
// `default` case.
if (switchCase.test == null) {
continue;
}
const caseType = getConstrainedTypeAtLocation(
services,
switchCase.test,
);
caseTypes.add(caseType);
}
const missingLiteralBranchTypes: ts.Type[] = [];
for (const unionPart of tsutils.unionConstituents(discriminantType)) {
for (const intersectionPart of tsutils.intersectionConstituents(
unionPart,
)) {
if (
caseTypes.has(intersectionPart) ||
!isTypeLiteralLikeType(intersectionPart)
) {
continue;
}
// "missing", "optional" and "undefined" types are different runtime objects,
// but all of them have TypeFlags.Undefined type flag
if (
[...caseTypes].some(tsutils.isIntrinsicUndefinedType) &&
tsutils.isIntrinsicUndefinedType(intersectionPart)
) {
continue;
}
missingLiteralBranchTypes.push(intersectionPart);
}
}
return {
containsNonLiteralType,
defaultCase: defaultCase ?? getCommentDefaultCase(node),
missingLiteralBranchTypes,
symbolName,
};
}
function checkSwitchExhaustive(
node: TSESTree.SwitchStatement,
switchMetadata: SwitchMetadata,
): void {
const { defaultCase, missingLiteralBranchTypes, symbolName } =
switchMetadata;
// If considerDefaultExhaustiveForUnions is enabled, the presence of a default case
// always makes the switch exhaustive.
if (considerDefaultExhaustiveForUnions && defaultCase != null) {
return;
}
if (missingLiteralBranchTypes.length > 0) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: {
missingBranches: missingLiteralBranchTypes
.map(missingType =>
tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike)
? `typeof ${missingType.getSymbol()?.escapedName as string}`
: typeToString(missingType),
)
.join(' | '),
},
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix | null {
return fixSwitch(
fixer,
node,
missingLiteralBranchTypes,
defaultCase,
symbolName?.toString(),
);
},
},
],
});
}
}
function fixSwitch(
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 caseIndent = lastCase
? ' '.repeat(lastCase.loc.start.column)
: // If there are no cases, use indentation of the switch statement and
// leave it to the user to format it correctly.
' '.repeat(node.loc.start.column);
const missingCases = [];
for (const missingBranchType of missingBranchTypes) {
if (missingBranchType == null) {
missingCases.push(`default: { throw new Error('default case') }`);
continue;
}
const missingBranchName = missingBranchType.getSymbol()?.escapedName;
let caseTest = tsutils.isTypeFlagSet(
missingBranchType,
ts.TypeFlags.ESSymbolLike,
)
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
missingBranchName!
: typeToString(missingBranchType);
if (
symbolName &&
(missingBranchName || missingBranchName === '') &&
requiresQuoting(missingBranchName.toString(), compilerOptions.target)
) {
const escapedBranchName = missingBranchName
.replaceAll("'", "\\'")
.replaceAll('\n', '\\n')
.replaceAll('\r', '\\r');
caseTest = `${symbolName}['${escapedBranchName}']`;
}
missingCases.push(
`case ${caseTest}: { throw new Error('Not implemented yet: ${caseTest
.replaceAll('\\', '\\\\')
.replaceAll("'", "\\'")} case') }`,
);
}
const fixString = missingCases
.map(code => `${caseIndent}${code}`)
.join('\n');
if (lastCase) {
if (defaultCase) {
const beforeFixString = missingCases
.map(code => `${code}\n${caseIndent}`)
.join('');
return fixer.insertTextBefore(defaultCase, beforeFixString);
}
return fixer.insertTextAfter(lastCase, `\n${fixString}`);
}
// There were no existing cases.
const openingBrace = nullThrows(
context.sourceCode.getTokenAfter(
node.discriminant,
isOpeningBraceToken,
),
NullThrowsReasons.MissingToken('{', 'discriminant'),
);
const closingBrace = nullThrows(
context.sourceCode.getTokenAfter(
node.discriminant,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', 'discriminant'),
);
return fixer.replaceTextRange(
[openingBrace.range[0], closingBrace.range[1]],
['{', fixString, `${caseIndent}}`].join('\n'),
);
}
function checkSwitchUnnecessaryDefaultCase(
switchMetadata: SwitchMetadata,
): void {
if (allowDefaultCaseForExhaustiveSwitch) {
return;
}
const { containsNonLiteralType, defaultCase, missingLiteralBranchTypes } =
switchMetadata;
if (
missingLiteralBranchTypes.length === 0 &&
defaultCase != null &&
!containsNonLiteralType
) {
context.report({
node: defaultCase,
messageId: 'dangerousDefaultCase',
});
}
}
function checkSwitchNoUnionDefaultCase(
node: TSESTree.SwitchStatement,
switchMetadata: SwitchMetadata,
): void {
if (!requireDefaultForNonUnion) {
return;
}
const { containsNonLiteralType, defaultCase } = switchMetadata;
if (containsNonLiteralType && defaultCase == null) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: { missingBranches: 'default' },
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix {
return fixSwitch(fixer, node, [null], defaultCase);
},
},
],
});
}
}
return {
SwitchStatement(node): void {
const switchMetadata = getSwitchMetadata(node);
checkSwitchExhaustive(node, switchMetadata);
checkSwitchUnnecessaryDefaultCase(switchMetadata);
checkSwitchNoUnionDefaultCase(node, switchMetadata);
},
};
}
isTypeLiteralLikeType(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
tsutils.isTypeFlagSet
Code
doesTypeContainNonLiteralType(type: ts.Type): boolean¶
For example:
"foo" | "bar"is a type with all literal types."foo" | numberis a type that contains non-literal types."foo" & { bar: 1 }is a type that contains non-literal types.
Default cases are never superfluous in switches with non-literal types.
Raw JSDoc
Calls:
tsutils .unionConstituents(type) .sometsutils .intersectionConstituents(type) .everyisTypeLiteralLikeType
Code
Internal helpers¶
Declared inside another function in this file.
getCommentDefaultCase(node: TSESTree.SwitchStatement): TSESTree.Comment | undefined¶
Parameters:
nodeTSESTree.SwitchStatement
Returns: TSESTree.Comment | undefined
Calls:
node.cases.atcontext.sourceCode.getCommentsAftercommentsAfterLastCase.atcommentRegExp.testdefaultCaseComment?.value.trim
Code
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;
}
typeToString(type: ts.Type): string¶
Parameters:
typets.Type
Returns: string
Calls:
checker.typeToString
Code
getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata¶
Parameters:
nodeTSESTree.SwitchStatement
Returns: SwitchMetadata
Calls:
node.cases.findgetConstrainedTypeAtLocation (from ../util)discriminantType.getSymboldoesTypeContainNonLiteralTypecaseTypes.addtsutils.unionConstituentstsutils.intersectionConstituentscaseTypes.hasisTypeLiteralLikeType[...caseTypes].sometsutils.isIntrinsicUndefinedTypemissingLiteralBranchTypes.pushgetCommentDefaultCase
Internal Comments:
// If the `test` property of the switch case is `null`, then we are on a
// `default` case.
// "missing", "optional" and "undefined" types are different runtime objects,
// but all of them have TypeFlags.Undefined type flag
Code
function getSwitchMetadata(node: TSESTree.SwitchStatement): SwitchMetadata {
const defaultCase = node.cases.find(
switchCase => switchCase.test == null,
);
const discriminantType = getConstrainedTypeAtLocation(
services,
node.discriminant,
);
const symbolName = discriminantType.getSymbol()?.escapedName as
string | undefined;
const containsNonLiteralType =
doesTypeContainNonLiteralType(discriminantType);
const caseTypes = new Set<ts.Type>();
for (const switchCase of node.cases) {
// If the `test` property of the switch case is `null`, then we are on a
// `default` case.
if (switchCase.test == null) {
continue;
}
const caseType = getConstrainedTypeAtLocation(
services,
switchCase.test,
);
caseTypes.add(caseType);
}
const missingLiteralBranchTypes: ts.Type[] = [];
for (const unionPart of tsutils.unionConstituents(discriminantType)) {
for (const intersectionPart of tsutils.intersectionConstituents(
unionPart,
)) {
if (
caseTypes.has(intersectionPart) ||
!isTypeLiteralLikeType(intersectionPart)
) {
continue;
}
// "missing", "optional" and "undefined" types are different runtime objects,
// but all of them have TypeFlags.Undefined type flag
if (
[...caseTypes].some(tsutils.isIntrinsicUndefinedType) &&
tsutils.isIntrinsicUndefinedType(intersectionPart)
) {
continue;
}
missingLiteralBranchTypes.push(intersectionPart);
}
}
return {
containsNonLiteralType,
defaultCase: defaultCase ?? getCommentDefaultCase(node),
missingLiteralBranchTypes,
symbolName,
};
}
checkSwitchExhaustive(node: TSESTree.SwitchStatement, switchMetadata: SwitchMetadata): void¶
Parameters:
nodeTSESTree.SwitchStatementswitchMetadataSwitchMetadata
Returns: void
Calls:
context.reportmissingLiteralBranchTypes .map(missingType => tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike) ?typeof ${missingType.getSymbol()?.escapedName as string}: typeToString(missingType), ) .joinfixSwitchsymbolName?.toString
Internal Comments:
// If considerDefaultExhaustiveForUnions is enabled, the presence of a default case
// always makes the switch exhaustive.
Code
function checkSwitchExhaustive(
node: TSESTree.SwitchStatement,
switchMetadata: SwitchMetadata,
): void {
const { defaultCase, missingLiteralBranchTypes, symbolName } =
switchMetadata;
// If considerDefaultExhaustiveForUnions is enabled, the presence of a default case
// always makes the switch exhaustive.
if (considerDefaultExhaustiveForUnions && defaultCase != null) {
return;
}
if (missingLiteralBranchTypes.length > 0) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: {
missingBranches: missingLiteralBranchTypes
.map(missingType =>
tsutils.isTypeFlagSet(missingType, ts.TypeFlags.ESSymbolLike)
? `typeof ${missingType.getSymbol()?.escapedName as string}`
: typeToString(missingType),
)
.join(' | '),
},
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix | null {
return fixSwitch(
fixer,
node,
missingLiteralBranchTypes,
defaultCase,
symbolName?.toString(),
);
},
},
],
});
}
}
fixSwitch(…): TSESLint.RuleFix¶
Parameters:
fixerTSESLint.RuleFixernodeTSESTree.SwitchStatementmissingBranchTypes(ts.Type | null)[]defaultCaseTSESTree.Comment | TSESTree.SwitchCase | undefinedsymbolNamestring
Returns: TSESLint.RuleFix
Calls:
' '.repeatmissingCases.pushmissingBranchType.getSymboltsutils.isTypeFlagSettypeToStringrequiresQuoting (from ../util)missingBranchName.toStringmissingBranchName .replaceAll("'", "\\'") .replaceAll('\n', '\\n') .replaceAllcaseTest .replaceAll('\\', '\\\\') .replaceAllmissingCases .map(code =>${caseIndent}${code}) .joinmissingCases .map(code =>${code}\n${caseIndent}) .joinfixer.insertTextBeforefixer.insertTextAfternullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenfixer.replaceTextRange['{', fixString,${caseIndent}}].join
Internal Comments:
Code
function fixSwitch(
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 caseIndent = lastCase
? ' '.repeat(lastCase.loc.start.column)
: // If there are no cases, use indentation of the switch statement and
// leave it to the user to format it correctly.
' '.repeat(node.loc.start.column);
const missingCases = [];
for (const missingBranchType of missingBranchTypes) {
if (missingBranchType == null) {
missingCases.push(`default: { throw new Error('default case') }`);
continue;
}
const missingBranchName = missingBranchType.getSymbol()?.escapedName;
let caseTest = tsutils.isTypeFlagSet(
missingBranchType,
ts.TypeFlags.ESSymbolLike,
)
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
missingBranchName!
: typeToString(missingBranchType);
if (
symbolName &&
(missingBranchName || missingBranchName === '') &&
requiresQuoting(missingBranchName.toString(), compilerOptions.target)
) {
const escapedBranchName = missingBranchName
.replaceAll("'", "\\'")
.replaceAll('\n', '\\n')
.replaceAll('\r', '\\r');
caseTest = `${symbolName}['${escapedBranchName}']`;
}
missingCases.push(
`case ${caseTest}: { throw new Error('Not implemented yet: ${caseTest
.replaceAll('\\', '\\\\')
.replaceAll("'", "\\'")} case') }`,
);
}
const fixString = missingCases
.map(code => `${caseIndent}${code}`)
.join('\n');
if (lastCase) {
if (defaultCase) {
const beforeFixString = missingCases
.map(code => `${code}\n${caseIndent}`)
.join('');
return fixer.insertTextBefore(defaultCase, beforeFixString);
}
return fixer.insertTextAfter(lastCase, `\n${fixString}`);
}
// There were no existing cases.
const openingBrace = nullThrows(
context.sourceCode.getTokenAfter(
node.discriminant,
isOpeningBraceToken,
),
NullThrowsReasons.MissingToken('{', 'discriminant'),
);
const closingBrace = nullThrows(
context.sourceCode.getTokenAfter(
node.discriminant,
isClosingBraceToken,
),
NullThrowsReasons.MissingToken('}', 'discriminant'),
);
return fixer.replaceTextRange(
[openingBrace.range[0], closingBrace.range[1]],
['{', fixString, `${caseIndent}}`].join('\n'),
);
}
checkSwitchUnnecessaryDefaultCase(switchMetadata: SwitchMetadata): void¶
Parameters:
switchMetadataSwitchMetadata
Returns: void
Calls:
context.report
Code
function checkSwitchUnnecessaryDefaultCase(
switchMetadata: SwitchMetadata,
): void {
if (allowDefaultCaseForExhaustiveSwitch) {
return;
}
const { containsNonLiteralType, defaultCase, missingLiteralBranchTypes } =
switchMetadata;
if (
missingLiteralBranchTypes.length === 0 &&
defaultCase != null &&
!containsNonLiteralType
) {
context.report({
node: defaultCase,
messageId: 'dangerousDefaultCase',
});
}
}
checkSwitchNoUnionDefaultCase(node: TSESTree.SwitchStatement, switchMetadata: SwitchMetadata): void¶
Parameters:
nodeTSESTree.SwitchStatementswitchMetadataSwitchMetadata
Returns: void
Calls:
context.reportfixSwitch
Code
function checkSwitchNoUnionDefaultCase(
node: TSESTree.SwitchStatement,
switchMetadata: SwitchMetadata,
): void {
if (!requireDefaultForNonUnion) {
return;
}
const { containsNonLiteralType, defaultCase } = switchMetadata;
if (containsNonLiteralType && defaultCase == null) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: { missingBranches: 'default' },
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix {
return fixSwitch(fixer, node, [null], defaultCase);
},
},
],
});
}
}
Interfaces¶
SwitchMetadata¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
containsNonLiteralType |
boolean |
✗ | not shown |
defaultCase |
TSESTree.Comment \| TSESTree.SwitchCase \| undefined |
✗ | not shown |
missingLiteralBranchTypes |
ts.Type[] |
✗ | not shown |
symbolName |
string \| undefined |
✗ | not shown |
Type Aliases¶
Options¶
type Options = [
{
/**
* If `true`, allow `default` cases on switch statements with exhaustive
* cases.
*
* @default true
*/
allowDefaultCaseForExhaustiveSwitch?: boolean;
/**
* If `true`, require a `default` clause for switches on non-union types.
*
* @default false
*/
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.
*
* @default false
*/
considerDefaultExhaustiveForUnions?: boolean;
},
];
MessageIds¶
Generated by Syntax Scribe