UNPKG

eslint-plugin-switch-statement

Version:

Rules for properly handling switch statements, including ensuring that appropriate exhaustive case handling.

232 lines (231 loc) 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RULE_NAME = exports.DEFAULT_ASSERT_NEVER_FN_NAME = void 0; const utils_1 = require("@typescript-eslint/utils"); const typescript_1 = __importDefault(require("typescript")); exports.DEFAULT_ASSERT_NEVER_FN_NAME = "assertUnreachable"; exports.RULE_NAME = "require-appropriate-default-case"; const messages = { addDefaultCase: "Every switch statement must have a `default` case, which should either " + "have Typescript verify that the switch is exhaustive (by including a " + "function call that will trigger a type error unless the variable's type " + "has been narrowed to never) or should have generic fallback logic.", considerExhaustiveSwitch: "This switch statement is switching over a set of known values, so it " + "could handle them exhaustively, with a `default` case that calls " + "`{{ exhaustiveFunctionName }}`, to have TS verify the exhaustiveness. " + "Consider updating your `default` case to have that exhaustiveness check, " + "or disable the lint rule on this `switch` if you're sure you want to " + "apply general fallback logic (e.g., to all future cases).", }; exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({ defaultOptions: [ { unreachableDefaultCaseAssertionFunctionName: exports.DEFAULT_ASSERT_NEVER_FN_NAME, }, ], meta: { type: "problem", fixable: "code", docs: { description: "require unreachable default case", url: "https://github.com/ethanresnick/eslint-plugin-exhaustive-switch/blob/main/docs/rules/require-unreachable-default-case.md", }, schema: [ { type: "object", properties: { unreachableDefaultCaseAssertionFunctionName: { type: "string", default: exports.DEFAULT_ASSERT_NEVER_FN_NAME, }, }, }, ], messages, }, create(context, options) { const sourceCode = context.sourceCode; const services = utils_1.ESLintUtils.getParserServices(context); if (!services || !services.getTypeAtLocation) { throw new Error("This rule requires type checking to be available"); } const checker = services.program.getTypeChecker(); const { getTypeAtLocation } = services; const exhaustiveFunctionName = options[0].unreachableDefaultCaseAssertionFunctionName; function isCandidateForExhaustiveSwitch(type) { if ((type.flags & typescript_1.default.TypeFlags.Unit) > 0) return true; if (type.isUnion()) { return type.types.every((t) => isCandidateForExhaustiveSwitch(t)); } if (type.isIntersection()) { return type.types.some((t) => isCandidateForExhaustiveSwitch(t)); } // For everything else, try to check their constraint. This handles type // parameters, but also things like conditional types and, probably, index // access types. const constraint = checker.getBaseConstraintOfType(type); return constraint ? isCandidateForExhaustiveSwitch(constraint) : false; } // Keep a stack of switch statements as we traverse down into them. const switchStatements = []; return { SwitchStatement(node) { switchStatements.push({ node, switchesOnLiteralTypeUnion: isCandidateForExhaustiveSwitch(getTypeAtLocation(node.discriminant)), // if we find one, we'll change this below. hasDefaultCase: false, defaultCaseCallsExhaustiveFunction: false, }); }, "SwitchCase[test=null]"(_node) { // If we're in a switch case, we know we're in a switch statement. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion switchStatements.at(-1).hasDefaultCase = true; }, [`SwitchCase[test=null] CallExpression[callee.name=${exhaustiveFunctionName}]`](_node) { // If we're in a switch case, we know we're in a switch statement. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion switchStatements.at(-1).defaultCaseCallsExhaustiveFunction = true; }, "SwitchStatement:exit"(node) { const switchStatement = switchStatements.pop(); if ((switchStatement === null || switchStatement === void 0 ? void 0 : switchStatement.node) !== node) { throw new Error("Unexpected switch statement exit"); } const { switchesOnLiteralTypeUnion, hasDefaultCase, defaultCaseCallsExhaustiveFunction, } = switchStatement; // If the node doesn't switch on a union of literal types, and does have // a default case, it must be ok, because we don't require a call to the // exhaustiveness check function when the switch isn't on a literal union. if (hasDefaultCase && !switchesOnLiteralTypeUnion) { return; } // If the switch is on a union of literal types, we must check for an // assert unreachable call in its default. if (hasDefaultCase && switchesOnLiteralTypeUnion) { if (defaultCaseCallsExhaustiveFunction) { return; } else { context.report({ node, data: { exhaustiveFunctionName }, messageId: "considerExhaustiveSwitch", }); return; } } // If the switch doesn't have a default case, and it switches on a union // of literal types, we report an "add default case error", but this one // is fixable (because we can synthesize a default case that calls the // exhaustiveness check function). if (!hasDefaultCase && switchesOnLiteralTypeUnion) { context.report({ node, messageId: "addDefaultCase", fix(fixer) { const argumentNode = getExhaustivenessCheckFunctionArg(node); if (!argumentNode) { return null; } const switchStatementSourceCode = sourceCode.getText(node); const argSourceCode = sourceCode.getText(argumentNode); // We assume the source code for the switch statement always ends // with a `}`, and that the default case should come last, so we // trim off the trailing `}` and add the default case. This logic // feels more brittle than `insertTextAfter(nodeForLastCase)`, but // that logic wouldn't work for empty switch statements, so we'd // need something like this anyway. // // NB: We add a new line above the default to reduce the cases // where this would produce invalid Javascript (e.g., if the prior // case statement doesn't end with a `;` and isn't in a a block), // but the nature of string-based fixers is that there will // probably always be some. return fixer.replaceTextRange(node.range, switchStatementSourceCode.slice(0, -1) + "\ndefault: " + `${exhaustiveFunctionName}(${argSourceCode}); }`); }, }); } // Finally, if the switch doesn't have a default case and doesn't switch // on a union of literal types, we report an "add default case" error, // but this one is unfixable, because it's unclear what the default case // should do. if (!hasDefaultCase && !switchesOnLiteralTypeUnion) { context.report({ node, messageId: "addDefaultCase", }); } }, }; }, }); function assertUnreachable(it) { throw new Error(`Unreachable case: ${JSON.stringify(it)}`); } function getExhaustivenessCheckFunctionArg(node) { switch (node.discriminant.type) { case utils_1.TSESTree.AST_NODE_TYPES.ArrayPattern: case utils_1.TSESTree.AST_NODE_TYPES.ArrowFunctionExpression: case utils_1.TSESTree.AST_NODE_TYPES.AssignmentExpression: case utils_1.TSESTree.AST_NODE_TYPES.AwaitExpression: case utils_1.TSESTree.AST_NODE_TYPES.BinaryExpression: case utils_1.TSESTree.AST_NODE_TYPES.CallExpression: case utils_1.TSESTree.AST_NODE_TYPES.ChainExpression: case utils_1.TSESTree.AST_NODE_TYPES.ClassExpression: case utils_1.TSESTree.AST_NODE_TYPES.ConditionalExpression: case utils_1.TSESTree.AST_NODE_TYPES.FunctionExpression: case utils_1.TSESTree.AST_NODE_TYPES.ImportExpression: case utils_1.TSESTree.AST_NODE_TYPES.JSXElement: case utils_1.TSESTree.AST_NODE_TYPES.JSXFragment: case utils_1.TSESTree.AST_NODE_TYPES.Literal: case utils_1.TSESTree.AST_NODE_TYPES.TemplateLiteral: case utils_1.TSESTree.AST_NODE_TYPES.LogicalExpression: case utils_1.TSESTree.AST_NODE_TYPES.MetaProperty: case utils_1.TSESTree.AST_NODE_TYPES.NewExpression: case utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression: case utils_1.TSESTree.AST_NODE_TYPES.ObjectPattern: case utils_1.TSESTree.AST_NODE_TYPES.SequenceExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSAsExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSSatisfiesExpression: case utils_1.TSESTree.AST_NODE_TYPES.Super: case utils_1.TSESTree.AST_NODE_TYPES.TaggedTemplateExpression: case utils_1.TSESTree.AST_NODE_TYPES.ThisExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSInstantiationExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSNonNullExpression: case utils_1.TSESTree.AST_NODE_TYPES.TSTypeAssertion: case utils_1.TSESTree.AST_NODE_TYPES.UnaryExpression: case utils_1.TSESTree.AST_NODE_TYPES.UpdateExpression: case utils_1.TSESTree.AST_NODE_TYPES.YieldExpression: case utils_1.TSESTree.AST_NODE_TYPES.ArrayExpression: { return undefined; } case utils_1.TSESTree.AST_NODE_TYPES.Identifier: return node.discriminant; // If we're switching on a member expression, it's _usually_ a discriminant, // in which case the object will be narrowed to never, so we want to return // the object (not the member expression itself) as the argument to the call // to the exhaustiveness check function, but only if it's an identifier; if // it was something dynamic -- like `switch(callSomeFunction().foo)` -- then // we don't want to evaluate the expression again in the default case. // Granted, the default case should be unreachable, but it's still unsafe if // the types are inaccurate, and it's unlikely to be useful because TS // doesn't know that callSomeFunction() returns a constant value. case utils_1.TSESTree.AST_NODE_TYPES.MemberExpression: { const object = node.discriminant.object; if (object.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier) { return object; } return undefined; } default: { assertUnreachable(node.discriminant); } } }