1 | "use strict";
|
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3 | if (k2 === undefined) k2 = k;
|
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5 | }) : (function(o, m, k, k2) {
|
6 | if (k2 === undefined) k2 = k;
|
7 | o[k2] = m[k];
|
8 | }));
|
9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
10 | Object.defineProperty(o, "default", { enumerable: true, value: v });
|
11 | }) : function(o, v) {
|
12 | o["default"] = v;
|
13 | });
|
14 | var __importStar = (this && this.__importStar) || function (mod) {
|
15 | if (mod && mod.__esModule) return mod;
|
16 | var result = {};
|
17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
18 | __setModuleDefault(result, mod);
|
19 | return result;
|
20 | };
|
21 | Object.defineProperty(exports, "__esModule", { value: true });
|
22 | const ts = __importStar(require("typescript"));
|
23 | const util_1 = require("../util");
|
24 | const tsutils_1 = require("tsutils");
|
25 | exports.default = (0, util_1.createRule)({
|
26 | name: 'switch-exhaustiveness-check',
|
27 | meta: {
|
28 | type: 'suggestion',
|
29 | docs: {
|
30 | description: 'Exhaustiveness checking in switch with union type',
|
31 | recommended: false,
|
32 | suggestion: true,
|
33 | requiresTypeChecking: true,
|
34 | },
|
35 | hasSuggestions: true,
|
36 | schema: [],
|
37 | messages: {
|
38 | switchIsNotExhaustive: 'Switch is not exhaustive. Cases not matched: {{missingBranches}}',
|
39 | addMissingCases: 'Add branches for missing cases.',
|
40 | },
|
41 | },
|
42 | defaultOptions: [],
|
43 | create(context) {
|
44 | const sourceCode = context.getSourceCode();
|
45 | const service = (0, util_1.getParserServices)(context);
|
46 | const checker = service.program.getTypeChecker();
|
47 | const compilerOptions = service.program.getCompilerOptions();
|
48 | function getNodeType(node) {
|
49 | const tsNode = service.esTreeNodeToTSNodeMap.get(node);
|
50 | return (0, util_1.getConstrainedTypeAtLocation)(checker, tsNode);
|
51 | }
|
52 | function fixSwitch(fixer, node, missingBranchTypes, symbolName) {
|
53 | var _a;
|
54 | const lastCase = node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
|
55 | const caseIndent = lastCase
|
56 | ? ' '.repeat(lastCase.loc.start.column)
|
57 | :
|
58 |
|
59 | ' '.repeat(node.loc.start.column);
|
60 | const missingCases = [];
|
61 | for (const missingBranchType of missingBranchTypes) {
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | if (missingBranchType.isIntersection()) {
|
73 | continue;
|
74 | }
|
75 | const missingBranchName = (_a = missingBranchType.getSymbol()) === null || _a === void 0 ? void 0 : _a.escapedName;
|
76 | let caseTest = checker.typeToString(missingBranchType);
|
77 | if (symbolName &&
|
78 | (missingBranchName || missingBranchName === '') &&
|
79 | (0, util_1.requiresQuoting)(missingBranchName.toString(), compilerOptions.target)) {
|
80 | caseTest = `${symbolName}['${missingBranchName}']`;
|
81 | }
|
82 | const errorMessage = `Not implemented yet: ${caseTest} case`;
|
83 | missingCases.push(`case ${caseTest}: { throw new Error('${errorMessage}') }`);
|
84 | }
|
85 | const fixString = missingCases
|
86 | .map(code => `${caseIndent}${code}`)
|
87 | .join('\n');
|
88 | if (lastCase) {
|
89 | return fixer.insertTextAfter(lastCase, `\n${fixString}`);
|
90 | }
|
91 |
|
92 | const openingBrace = sourceCode.getTokenAfter(node.discriminant, util_1.isOpeningBraceToken);
|
93 | const closingBrace = sourceCode.getTokenAfter(node.discriminant, util_1.isClosingBraceToken);
|
94 | return fixer.replaceTextRange([openingBrace.range[0], closingBrace.range[1]], ['{', fixString, `${caseIndent}}`].join('\n'));
|
95 | }
|
96 | function checkSwitchExhaustive(node) {
|
97 | var _a;
|
98 | const discriminantType = getNodeType(node.discriminant);
|
99 | const symbolName = (_a = discriminantType.getSymbol()) === null || _a === void 0 ? void 0 : _a.escapedName;
|
100 | if (discriminantType.isUnion()) {
|
101 | const unionTypes = (0, tsutils_1.unionTypeParts)(discriminantType);
|
102 | const caseTypes = new Set();
|
103 | for (const switchCase of node.cases) {
|
104 | if (switchCase.test === null) {
|
105 |
|
106 | return;
|
107 | }
|
108 | caseTypes.add(getNodeType(switchCase.test));
|
109 | }
|
110 | const missingBranchTypes = unionTypes.filter(unionType => !caseTypes.has(unionType));
|
111 | if (missingBranchTypes.length === 0) {
|
112 |
|
113 | return;
|
114 | }
|
115 | context.report({
|
116 | node: node.discriminant,
|
117 | messageId: 'switchIsNotExhaustive',
|
118 | data: {
|
119 | missingBranches: missingBranchTypes
|
120 | .map(missingType => {
|
121 | var _a;
|
122 | return (0, tsutils_1.isTypeFlagSet)(missingType, ts.TypeFlags.ESSymbolLike)
|
123 | ? `typeof ${(_a = missingType.getSymbol()) === null || _a === void 0 ? void 0 : _a.escapedName}`
|
124 | : checker.typeToString(missingType);
|
125 | })
|
126 | .join(' | '),
|
127 | },
|
128 | suggest: [
|
129 | {
|
130 | messageId: 'addMissingCases',
|
131 | fix(fixer) {
|
132 | return fixSwitch(fixer, node, missingBranchTypes, symbolName === null || symbolName === void 0 ? void 0 : symbolName.toString());
|
133 | },
|
134 | },
|
135 | ],
|
136 | });
|
137 | }
|
138 | }
|
139 | return {
|
140 | SwitchStatement: checkSwitchExhaustive,
|
141 | };
|
142 | },
|
143 | });
|
144 |
|
\ | No newline at end of file |