@blumintinc/eslint-plugin-blumint 1.18.16 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/index.js +13 -1
- package/lib/rules/consistent-callback-naming.js +24 -23
- package/lib/rules/enforce-single-exported-unit-per-file.d.ts +3 -0
- package/lib/rules/enforce-single-exported-unit-per-file.js +571 -0
- package/lib/rules/no-harness-coupled-disables.d.ts +1 -0
- package/lib/rules/no-harness-coupled-disables.js +134 -0
- package/lib/rules/prefer-clone-deep.js +8 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.d.ts +4 -0
- package/lib/rules/prefer-map-over-conditional-dispatch.js +907 -0
- package/lib/rules/prefer-union-from-const-array.d.ts +3 -0
- package/lib/rules/prefer-union-from-const-array.js +137 -0
- package/package.json +1 -1
- package/release-manifest.json +61 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.preferMapOverConditionalDispatch = void 0;
|
|
27
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
28
|
+
const ts = __importStar(require("typescript"));
|
|
29
|
+
const createRule_1 = require("../utils/createRule");
|
|
30
|
+
/**
|
|
31
|
+
* Node types whose presence anywhere in a branch value means the value is NOT
|
|
32
|
+
* safe to eager-evaluate inside a `Record` (all entries construct at once).
|
|
33
|
+
*/
|
|
34
|
+
const EAGER_UNSAFE_NODES = new Set([
|
|
35
|
+
utils_1.AST_NODE_TYPES.CallExpression,
|
|
36
|
+
utils_1.AST_NODE_TYPES.NewExpression,
|
|
37
|
+
utils_1.AST_NODE_TYPES.AwaitExpression,
|
|
38
|
+
utils_1.AST_NODE_TYPES.YieldExpression,
|
|
39
|
+
utils_1.AST_NODE_TYPES.TaggedTemplateExpression,
|
|
40
|
+
utils_1.AST_NODE_TYPES.UpdateExpression,
|
|
41
|
+
utils_1.AST_NODE_TYPES.AssignmentExpression,
|
|
42
|
+
]);
|
|
43
|
+
const CONTAINER_TYPES = new Set([
|
|
44
|
+
utils_1.AST_NODE_TYPES.BlockStatement,
|
|
45
|
+
utils_1.AST_NODE_TYPES.Program,
|
|
46
|
+
utils_1.AST_NODE_TYPES.SwitchCase,
|
|
47
|
+
]);
|
|
48
|
+
const FUNCTION_TYPES = new Set([
|
|
49
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
50
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
51
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
52
|
+
]);
|
|
53
|
+
exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
54
|
+
name: 'prefer-map-over-conditional-dispatch',
|
|
55
|
+
meta: {
|
|
56
|
+
type: 'suggestion',
|
|
57
|
+
docs: {
|
|
58
|
+
description: 'Prefer a Record<Discriminant, Value> lookup over switch/ternary/if-else dispatch on a literal-union discriminant where every branch returns or assigns a single value.',
|
|
59
|
+
recommended: 'error',
|
|
60
|
+
requiresTypeChecking: true,
|
|
61
|
+
},
|
|
62
|
+
fixable: 'code',
|
|
63
|
+
schema: [],
|
|
64
|
+
messages: {
|
|
65
|
+
preferMap: 'This dispatch on a literal-union discriminant maps each case to a single value; replace it with a Record<Discriminant, Value> lookup so exhaustiveness is a compile-time guarantee and adding a case is a one-line data edit.',
|
|
66
|
+
preferMapManual: 'This dispatch on a literal-union discriminant is a lookup table in disguise; prefer a Record<Discriminant, Value>. Autofix skipped: {{reason}}.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
defaultOptions: [],
|
|
70
|
+
create(context) {
|
|
71
|
+
const sourceCode = context.getSourceCode();
|
|
72
|
+
const parserServices = sourceCode.parserServices;
|
|
73
|
+
// Type-aware rule: without the TypeScript program we cannot verify the
|
|
74
|
+
// discriminant is a finite literal union, so we silently skip (the plugin
|
|
75
|
+
// prefers false negatives over false positives on trust-boundary switches).
|
|
76
|
+
if (!parserServices?.program ||
|
|
77
|
+
!parserServices.esTreeNodeToTSNodeMap ||
|
|
78
|
+
typeof parserServices.program.getTypeChecker !== 'function') {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
const checker = parserServices.program.getTypeChecker();
|
|
82
|
+
const esTreeNodeToTSNodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
83
|
+
// Ternary/if forms stash their resolved discriminant here so the shared
|
|
84
|
+
// `report`/fixer can recover it (switch reads `node.discriminant` directly).
|
|
85
|
+
const discriminantMap = new WeakMap();
|
|
86
|
+
// ---- Type helpers -------------------------------------------------------
|
|
87
|
+
function tsTypeOf(node) {
|
|
88
|
+
try {
|
|
89
|
+
const tsNode = esTreeNodeToTSNodeMap.get(node);
|
|
90
|
+
if (!tsNode) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return checker.getTypeAtLocation(tsNode);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function unionPartsOf(type) {
|
|
100
|
+
return type.isUnion() ? type.types : [type];
|
|
101
|
+
}
|
|
102
|
+
function literalValueOf(t) {
|
|
103
|
+
if (t.flags & ts.TypeFlags.StringLiteral) {
|
|
104
|
+
return { value: t.value, kind: 'string' };
|
|
105
|
+
}
|
|
106
|
+
if (t.flags & ts.TypeFlags.NumberLiteral) {
|
|
107
|
+
return { value: t.value, kind: 'number' };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Classify the discriminant's static type. The rule fires only when every
|
|
113
|
+
* non-nullish constituent is a string/number literal. `undefined`/`null`
|
|
114
|
+
* do not block firing but force the report-only path (they are not
|
|
115
|
+
* expressible as Record keys).
|
|
116
|
+
*/
|
|
117
|
+
function classifyDiscriminant(type) {
|
|
118
|
+
const literalKeys = [];
|
|
119
|
+
let hasNullish = false;
|
|
120
|
+
let hasOther = false;
|
|
121
|
+
for (const part of unionPartsOf(type)) {
|
|
122
|
+
const f = part.flags;
|
|
123
|
+
if (f & ts.TypeFlags.Undefined || f & ts.TypeFlags.Null) {
|
|
124
|
+
hasNullish = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// `boolean` is `true | false`; boolean literals are NOT literal keys.
|
|
128
|
+
if (f & ts.TypeFlags.BooleanLiteral) {
|
|
129
|
+
hasOther = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const lit = literalValueOf(part);
|
|
133
|
+
if (lit) {
|
|
134
|
+
literalKeys.push(lit);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
hasOther = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { literalKeys, hasNullish, hasOther };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a case-test / equality-test node to a literal key. Handles inline
|
|
144
|
+
* literals directly and constant references (e.g. `THIS_DEVICE_STATUS.active`)
|
|
145
|
+
* via the checker.
|
|
146
|
+
*/
|
|
147
|
+
function resolveLiteralKey(node) {
|
|
148
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
149
|
+
if (typeof node.value === 'string') {
|
|
150
|
+
return { value: node.value, kind: 'string' };
|
|
151
|
+
}
|
|
152
|
+
if (typeof node.value === 'number') {
|
|
153
|
+
return { value: node.value, kind: 'number' };
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const type = tsTypeOf(node);
|
|
158
|
+
if (!type) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return literalValueOf(type);
|
|
162
|
+
}
|
|
163
|
+
function computeValueTypeText(exprs) {
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const parts = [];
|
|
166
|
+
for (const expr of exprs) {
|
|
167
|
+
const type = tsTypeOf(expr);
|
|
168
|
+
if (!type) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
let widened;
|
|
172
|
+
try {
|
|
173
|
+
widened = checker.getBaseTypeOfLiteralType(type);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
widened = type;
|
|
177
|
+
}
|
|
178
|
+
let text;
|
|
179
|
+
try {
|
|
180
|
+
text = checker.typeToString(widened);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (!text || text === 'error') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!seen.has(text)) {
|
|
189
|
+
seen.add(text);
|
|
190
|
+
parts.push(text);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return parts.length > 0 ? parts.join(' | ') : null;
|
|
194
|
+
}
|
|
195
|
+
function discriminantTypeText(type) {
|
|
196
|
+
try {
|
|
197
|
+
const text = checker.typeToString(type);
|
|
198
|
+
return text && text !== 'error' ? text : null;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ---- AST helpers --------------------------------------------------------
|
|
205
|
+
/** Whether a discriminant expression is an identifier or a call-free,
|
|
206
|
+
* non-optional member chain (safe to collapse repeated evaluations). */
|
|
207
|
+
function isValidDiscriminant(node) {
|
|
208
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
let cur = node;
|
|
212
|
+
while (cur.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
213
|
+
if (cur.optional || cur.computed) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
cur = cur.object;
|
|
217
|
+
}
|
|
218
|
+
return cur.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
219
|
+
}
|
|
220
|
+
/** Root identifier of a member chain (`a.b.c` -> `a`). */
|
|
221
|
+
function rootIdentifierName(node) {
|
|
222
|
+
let cur = node;
|
|
223
|
+
while (cur.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
224
|
+
cur = cur.object;
|
|
225
|
+
}
|
|
226
|
+
return cur.type === utils_1.AST_NODE_TYPES.Identifier ? cur.name : null;
|
|
227
|
+
}
|
|
228
|
+
function isInlineLiteralKey(node) {
|
|
229
|
+
return (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
230
|
+
(typeof node.value === 'string' || typeof node.value === 'number'));
|
|
231
|
+
}
|
|
232
|
+
/** Extract the single value produced by a run of statements, tolerating a
|
|
233
|
+
* wrapping block and a trailing `break`. Returns null for any other shape. */
|
|
234
|
+
function extractBranchValue(statements) {
|
|
235
|
+
let stmts = statements;
|
|
236
|
+
if (stmts.length === 1 &&
|
|
237
|
+
stmts[0].type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
238
|
+
stmts = stmts[0].body;
|
|
239
|
+
}
|
|
240
|
+
if (stmts.length > 0 &&
|
|
241
|
+
stmts[stmts.length - 1].type === utils_1.AST_NODE_TYPES.BreakStatement) {
|
|
242
|
+
stmts = stmts.slice(0, -1);
|
|
243
|
+
}
|
|
244
|
+
if (stmts.length !== 1) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const stmt = stmts[0];
|
|
248
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement && stmt.argument) {
|
|
249
|
+
return { kind: 'return', expr: stmt.argument };
|
|
250
|
+
}
|
|
251
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
|
|
252
|
+
stmt.expression.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
253
|
+
stmt.expression.operator === '=') {
|
|
254
|
+
return {
|
|
255
|
+
kind: 'assign',
|
|
256
|
+
target: stmt.expression.left,
|
|
257
|
+
expr: stmt.expression.right,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
/** True when any node in the subtree is unsafe to eager-evaluate. */
|
|
263
|
+
function containsEagerUnsafe(node) {
|
|
264
|
+
let found = false;
|
|
265
|
+
const visit = (n) => {
|
|
266
|
+
if (found || !n || typeof n !== 'object') {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const anyNode = n;
|
|
270
|
+
if (typeof anyNode.type !== 'string') {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (EAGER_UNSAFE_NODES.has(anyNode.type)) {
|
|
274
|
+
found = true;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
for (const key of Object.keys(anyNode)) {
|
|
278
|
+
if (key === 'parent') {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const val = anyNode[key];
|
|
282
|
+
if (Array.isArray(val)) {
|
|
283
|
+
for (const child of val) {
|
|
284
|
+
visit(child);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
visit(val);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
visit(node);
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
/** Whether the subtree references an identifier with the given name in a
|
|
296
|
+
* non-property-key position (i.e., an actual read of that binding). */
|
|
297
|
+
function referencesIdentifier(node, name) {
|
|
298
|
+
let found = false;
|
|
299
|
+
const visit = (n, parent) => {
|
|
300
|
+
if (found || !n || typeof n !== 'object') {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const anyNode = n;
|
|
304
|
+
if (typeof anyNode.type !== 'string') {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (anyNode.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
308
|
+
anyNode.name === name) {
|
|
309
|
+
// Ignore non-computed property names / object-literal keys — those
|
|
310
|
+
// are not references to the outer binding.
|
|
311
|
+
const isMemberProperty = parent &&
|
|
312
|
+
parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
313
|
+
parent.property === anyNode &&
|
|
314
|
+
parent.computed === false;
|
|
315
|
+
const isPropertyKey = parent &&
|
|
316
|
+
parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
317
|
+
parent.key === anyNode &&
|
|
318
|
+
parent.computed === false;
|
|
319
|
+
if (!isMemberProperty && !isPropertyKey) {
|
|
320
|
+
found = true;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const key of Object.keys(anyNode)) {
|
|
325
|
+
if (key === 'parent') {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const val = anyNode[key];
|
|
329
|
+
if (Array.isArray(val)) {
|
|
330
|
+
for (const child of val) {
|
|
331
|
+
visit(child, anyNode);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
visit(val, anyNode);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
visit(node);
|
|
340
|
+
return found;
|
|
341
|
+
}
|
|
342
|
+
// ---- Name derivation ----------------------------------------------------
|
|
343
|
+
function toUpperSnake(name) {
|
|
344
|
+
return name
|
|
345
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
346
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
347
|
+
.replace(/^_+|_+$/g, '')
|
|
348
|
+
.toUpperCase();
|
|
349
|
+
}
|
|
350
|
+
function deriveLookupName(discriminant) {
|
|
351
|
+
let key = null;
|
|
352
|
+
if (discriminant.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
353
|
+
key = discriminant.name;
|
|
354
|
+
}
|
|
355
|
+
else if (discriminant.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
356
|
+
!discriminant.computed &&
|
|
357
|
+
discriminant.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
358
|
+
key = discriminant.property.name;
|
|
359
|
+
}
|
|
360
|
+
if (!key) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const snake = toUpperSnake(key);
|
|
364
|
+
if (!snake) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const name = `RESULT_BY_${snake}`;
|
|
368
|
+
// Conservative collision check: any textual occurrence of the name blocks
|
|
369
|
+
// the autofix (a false collision only downgrades to report-only).
|
|
370
|
+
if (new RegExp(`\\b${name}\\b`).test(sourceCode.getText())) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return name;
|
|
374
|
+
}
|
|
375
|
+
// ---- Fix construction ---------------------------------------------------
|
|
376
|
+
function formatKey(key) {
|
|
377
|
+
if (key.kind === 'number') {
|
|
378
|
+
return String(key.value);
|
|
379
|
+
}
|
|
380
|
+
const str = String(key.value);
|
|
381
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str)) {
|
|
382
|
+
return str;
|
|
383
|
+
}
|
|
384
|
+
return `'${str.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
385
|
+
}
|
|
386
|
+
function indentOf(node) {
|
|
387
|
+
const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
388
|
+
const before = line.slice(0, node.loc.start.column);
|
|
389
|
+
const match = /[ \t]*$/.exec(before);
|
|
390
|
+
return match ? match[0] : '';
|
|
391
|
+
}
|
|
392
|
+
function buildRecordText(name, dText, vText, entries, baseIndent) {
|
|
393
|
+
const lines = entries.map((e) => `${baseIndent} ${formatKey(e.key)}: ${e.valueText},`);
|
|
394
|
+
return [
|
|
395
|
+
`const ${name}: Record<${dText}, ${vText}> = {`,
|
|
396
|
+
...lines,
|
|
397
|
+
`${baseIndent}};`,
|
|
398
|
+
].join('\n');
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Given ordered explicit branches + optional tail, resolve coverage against
|
|
402
|
+
* the union's literal keys and (when full) the ordered Record entries.
|
|
403
|
+
* Returns null when the construct is not a qualifying dispatch at all.
|
|
404
|
+
*/
|
|
405
|
+
function resolveCoverage(explicit, unionKeys, tail) {
|
|
406
|
+
const unionValues = new Set(unionKeys.map((k) => String(k.value)));
|
|
407
|
+
const explicitValues = new Set();
|
|
408
|
+
for (const branch of explicit) {
|
|
409
|
+
for (const k of branch.keys) {
|
|
410
|
+
const s = String(k.value);
|
|
411
|
+
if (!unionValues.has(s) || explicitValues.has(s)) {
|
|
412
|
+
// Key outside the union, or duplicated — bail entirely.
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
explicitValues.add(s);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const remaining = unionKeys.filter((k) => !explicitValues.has(String(k.value)));
|
|
419
|
+
const entries = [];
|
|
420
|
+
for (const branch of explicit) {
|
|
421
|
+
for (const k of branch.keys) {
|
|
422
|
+
entries.push({ key: k, valueText: branch.valueText });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (remaining.length === 0) {
|
|
426
|
+
// Full coverage; any tail is unreachable for typed values and dropped.
|
|
427
|
+
return { entries, fullCoverage: true, remainingCount: 0 };
|
|
428
|
+
}
|
|
429
|
+
if (remaining.length === 1 && tail) {
|
|
430
|
+
entries.push({ key: remaining[0], valueText: tail.valueText });
|
|
431
|
+
return { entries, fullCoverage: true, remainingCount: 1 };
|
|
432
|
+
}
|
|
433
|
+
if (!tail) {
|
|
434
|
+
// Not exhaustive and no default/else — genuine control flow, skip.
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
// Partial coverage relying on a shared default — report-only.
|
|
438
|
+
return { entries, fullCoverage: false, remainingCount: remaining.length };
|
|
439
|
+
}
|
|
440
|
+
function manualReason(flags) {
|
|
441
|
+
if (flags.hasNullish) {
|
|
442
|
+
return 'the union includes undefined/null, which cannot be a Record key — use Partial<Record<D, V>> with a ?? fallback';
|
|
443
|
+
}
|
|
444
|
+
if (!flags.fullCoverage) {
|
|
445
|
+
return 'the default/else covers multiple union members — use Partial<Record<D, V>> with a ?? fallback';
|
|
446
|
+
}
|
|
447
|
+
if (!flags.eagerSafe) {
|
|
448
|
+
return 'a branch value invokes a call/await and would run eagerly for every entry — use a thunk Record<D, () => V> invoked after lookup';
|
|
449
|
+
}
|
|
450
|
+
if (!flags.canPlaceFix) {
|
|
451
|
+
return 'the dispatch sits inside an expression-bodied function; extract the Record manually so it stays in scope';
|
|
452
|
+
}
|
|
453
|
+
return 'a collision-free lookup name could not be derived from the discriminant';
|
|
454
|
+
}
|
|
455
|
+
function report(node, analysis) {
|
|
456
|
+
const { entries, contributingValues, dText, form, assignTargetText, fullCoverage, hasNullish, canPlaceFix, } = analysis;
|
|
457
|
+
const eagerSafe = contributingValues.every((expr) => !containsEagerUnsafe(expr));
|
|
458
|
+
let name = null;
|
|
459
|
+
// Name derivation is only needed for the autofix path.
|
|
460
|
+
if (fullCoverage && !hasNullish && eagerSafe && canPlaceFix) {
|
|
461
|
+
name = deriveLookupName(discriminantOf(node));
|
|
462
|
+
}
|
|
463
|
+
const autofixable = fullCoverage &&
|
|
464
|
+
!hasNullish &&
|
|
465
|
+
eagerSafe &&
|
|
466
|
+
canPlaceFix &&
|
|
467
|
+
name !== null;
|
|
468
|
+
if (!autofixable) {
|
|
469
|
+
context.report({
|
|
470
|
+
node,
|
|
471
|
+
messageId: 'preferMapManual',
|
|
472
|
+
data: {
|
|
473
|
+
reason: manualReason({
|
|
474
|
+
fullCoverage,
|
|
475
|
+
hasNullish,
|
|
476
|
+
eagerSafe,
|
|
477
|
+
canPlaceFix,
|
|
478
|
+
}),
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const lookupName = name;
|
|
484
|
+
const vText = computeValueTypeText(contributingValues);
|
|
485
|
+
if (!vText) {
|
|
486
|
+
context.report({
|
|
487
|
+
node,
|
|
488
|
+
messageId: 'preferMapManual',
|
|
489
|
+
data: {
|
|
490
|
+
reason: 'the branch value type could not be resolved for the annotation',
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
context.report({
|
|
496
|
+
node,
|
|
497
|
+
messageId: 'preferMap',
|
|
498
|
+
fix(fixer) {
|
|
499
|
+
const discText = sourceCode.getText(discriminantOf(node));
|
|
500
|
+
if (form === 'expr') {
|
|
501
|
+
// Ternary: insert the Record before the enclosing statement and
|
|
502
|
+
// replace the conditional expression with the lookup.
|
|
503
|
+
let stmt = node;
|
|
504
|
+
while (stmt.parent && !CONTAINER_TYPES.has(stmt.parent.type)) {
|
|
505
|
+
stmt = stmt.parent;
|
|
506
|
+
}
|
|
507
|
+
const stmtIndent = indentOf(stmt);
|
|
508
|
+
const recordText = buildRecordText(lookupName, dText, vText, entries, stmtIndent);
|
|
509
|
+
return [
|
|
510
|
+
fixer.insertTextBefore(stmt, `${recordText}\n${stmtIndent}`),
|
|
511
|
+
fixer.replaceText(node, `${lookupName}[${discText}]`),
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
const baseIndent = indentOf(node);
|
|
515
|
+
const recordText = buildRecordText(lookupName, dText, vText, entries, baseIndent);
|
|
516
|
+
const lookup = form === 'assign'
|
|
517
|
+
? `${assignTargetText} = ${lookupName}[${discText}];`
|
|
518
|
+
: `return ${lookupName}[${discText}];`;
|
|
519
|
+
return fixer.replaceText(node, `${recordText}\n${baseIndent}${lookup}`);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function discriminantOf(node) {
|
|
524
|
+
if (node.type === utils_1.AST_NODE_TYPES.SwitchStatement) {
|
|
525
|
+
return node.discriminant;
|
|
526
|
+
}
|
|
527
|
+
// For ternary/if we stash the discriminant on a WeakMap.
|
|
528
|
+
return discriminantMap.get(node) ?? node;
|
|
529
|
+
}
|
|
530
|
+
// ---- Shared type gate + narrowing exemption -----------------------------
|
|
531
|
+
/**
|
|
532
|
+
* Type gate (Edge Case 4): fire only when every non-nullish union member is
|
|
533
|
+
* a string/number literal. Returns the union keys + nullish flag, or null to
|
|
534
|
+
* skip entirely (boolean/open string/number/object/function discriminants).
|
|
535
|
+
*/
|
|
536
|
+
function typeGate(discriminant) {
|
|
537
|
+
const type = tsTypeOf(discriminant);
|
|
538
|
+
if (!type) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const { literalKeys, hasNullish, hasOther } = classifyDiscriminant(type);
|
|
542
|
+
if (literalKeys.length === 0 || hasOther) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const dText = discriminantTypeText(type);
|
|
546
|
+
if (!dText) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
return { unionKeys: literalKeys, hasNullish, dText };
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Narrowing exemption (Edge Case 1): when the discriminant is `obj.tag`, a
|
|
553
|
+
* flat Record cannot express variant narrowing. If any KEPT branch value
|
|
554
|
+
* references the base object beyond the tag access itself, do not fire.
|
|
555
|
+
*/
|
|
556
|
+
function isNarrowingExempt(discriminant, keptValues) {
|
|
557
|
+
if (discriminant.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const root = rootIdentifierName(discriminant);
|
|
561
|
+
if (!root) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
return keptValues.some((value) => referencesIdentifier(value, root));
|
|
565
|
+
}
|
|
566
|
+
// ---- Switch form --------------------------------------------------------
|
|
567
|
+
function handleSwitch(node) {
|
|
568
|
+
const parsed = [];
|
|
569
|
+
let pending = [];
|
|
570
|
+
for (const c of node.cases) {
|
|
571
|
+
if (c.consequent.length === 0) {
|
|
572
|
+
pending.push(c.test);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const value = extractBranchValue(c.consequent);
|
|
576
|
+
parsed.push({ tests: [...pending, c.test], value });
|
|
577
|
+
pending = [];
|
|
578
|
+
}
|
|
579
|
+
if (pending.length > 0) {
|
|
580
|
+
// Trailing empty cases falling through to nothing — malformed.
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (parsed.length === 0) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// Separate explicit branches from the default; disallow mixed groups.
|
|
587
|
+
const explicit = [];
|
|
588
|
+
let defaultValue;
|
|
589
|
+
let hasDefault = false;
|
|
590
|
+
for (const branch of parsed) {
|
|
591
|
+
const literalTests = branch.tests.filter((t) => t !== null);
|
|
592
|
+
const isDefaultGroup = literalTests.length !== branch.tests.length;
|
|
593
|
+
if (isDefaultGroup) {
|
|
594
|
+
if (literalTests.length > 0) {
|
|
595
|
+
// `default: case 'x':` mixed group — bail (rare).
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
hasDefault = true;
|
|
599
|
+
defaultValue = branch.value;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!branch.value) {
|
|
603
|
+
// A non-default branch that is not a single value — control flow.
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const keys = [];
|
|
607
|
+
for (const test of literalTests) {
|
|
608
|
+
const key = resolveLiteralKey(test);
|
|
609
|
+
if (!key) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
keys.push(key);
|
|
613
|
+
}
|
|
614
|
+
explicit.push({ keys, value: branch.value });
|
|
615
|
+
}
|
|
616
|
+
if (explicit.length === 0) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// Consistent kind/target across contributing branches.
|
|
620
|
+
const kind = explicit[0].value.kind;
|
|
621
|
+
const assignTargetText = kind === 'assign'
|
|
622
|
+
? sourceCode.getText(explicit[0].value.target)
|
|
623
|
+
: undefined;
|
|
624
|
+
for (const branch of explicit) {
|
|
625
|
+
if (branch.value.kind !== kind) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (kind === 'assign' &&
|
|
629
|
+
sourceCode.getText(branch.value.target) !== assignTargetText) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Determine tail value from the default (only usable if it is a value).
|
|
634
|
+
const defaultVal = hasDefault && defaultValue != null ? defaultValue : null;
|
|
635
|
+
if (defaultVal && defaultVal.kind !== kind) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (defaultVal &&
|
|
639
|
+
kind === 'assign' &&
|
|
640
|
+
sourceCode.getText(defaultVal.target) !==
|
|
641
|
+
assignTargetText) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const gated = typeGate(node.discriminant);
|
|
645
|
+
if (!gated) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const { unionKeys, hasNullish, dText } = gated;
|
|
649
|
+
// Compute coverage.
|
|
650
|
+
const explicitForCoverage = explicit.map((b) => ({
|
|
651
|
+
keys: b.keys,
|
|
652
|
+
valueText: sourceCode.getText(b.value.expr),
|
|
653
|
+
}));
|
|
654
|
+
const tail = defaultVal
|
|
655
|
+
? { valueText: sourceCode.getText(defaultVal.expr) }
|
|
656
|
+
: null;
|
|
657
|
+
// A throwing/omitted default cannot satisfy a needed tail: when the
|
|
658
|
+
// remaining members are guarded (not value-mapped), `resolveCoverage`
|
|
659
|
+
// returns null because there is no usable tail, so the construct is not a
|
|
660
|
+
// pure lookup and is skipped below.
|
|
661
|
+
const coverage = resolveCoverage(explicitForCoverage, unionKeys, tail);
|
|
662
|
+
if (!coverage) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// Kept values (materialize as Record entries / shared fallback): explicit
|
|
666
|
+
// branches always, plus the default only when the tail is used.
|
|
667
|
+
const contributingValues = explicit.map((b) => b.value.expr);
|
|
668
|
+
if (coverage.remainingCount >= 1 && defaultVal) {
|
|
669
|
+
contributingValues.push(defaultVal.expr);
|
|
670
|
+
}
|
|
671
|
+
if (isNarrowingExempt(node.discriminant, contributingValues)) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
report(node, {
|
|
675
|
+
entries: coverage.entries,
|
|
676
|
+
contributingValues,
|
|
677
|
+
dText,
|
|
678
|
+
form: kind,
|
|
679
|
+
assignTargetText,
|
|
680
|
+
fullCoverage: coverage.fullCoverage,
|
|
681
|
+
hasNullish,
|
|
682
|
+
canPlaceFix: true,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
// ---- Ternary form -------------------------------------------------------
|
|
686
|
+
function equalityDiscriminant(test) {
|
|
687
|
+
if (test.type !== utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
688
|
+
test.operator !== '===') {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const { left, right } = test;
|
|
692
|
+
let discNode = null;
|
|
693
|
+
let keyNode = null;
|
|
694
|
+
if (isInlineLiteralKey(left) && !isInlineLiteralKey(right)) {
|
|
695
|
+
keyNode = left;
|
|
696
|
+
discNode = right;
|
|
697
|
+
}
|
|
698
|
+
else if (isInlineLiteralKey(right) && !isInlineLiteralKey(left)) {
|
|
699
|
+
keyNode = right;
|
|
700
|
+
discNode = left;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
if (!isValidDiscriminant(discNode)) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return { discNode, keyNode };
|
|
709
|
+
}
|
|
710
|
+
function handleConditional(node) {
|
|
711
|
+
const head = equalityDiscriminant(node.test);
|
|
712
|
+
if (!head) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const discText = sourceCode.getText(head.discNode);
|
|
716
|
+
// Skip chain continuations — only the outermost link reports.
|
|
717
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
718
|
+
node.parent.alternate === node) {
|
|
719
|
+
const parentHead = equalityDiscriminant(node.parent.test);
|
|
720
|
+
if (parentHead &&
|
|
721
|
+
sourceCode.getText(parentHead.discNode) === discText) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// Walk the chain.
|
|
726
|
+
const links = [];
|
|
727
|
+
let cur = node;
|
|
728
|
+
while (cur.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
729
|
+
const link = equalityDiscriminant(cur.test);
|
|
730
|
+
if (!link || sourceCode.getText(link.discNode) !== discText) {
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
links.push({ keyNode: link.keyNode, expr: cur.consequent });
|
|
734
|
+
cur = cur.alternate;
|
|
735
|
+
}
|
|
736
|
+
const tailExpr = cur;
|
|
737
|
+
if (links.length === 0) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const explicitKeys = [];
|
|
741
|
+
for (const link of links) {
|
|
742
|
+
const key = resolveLiteralKey(link.keyNode);
|
|
743
|
+
if (!key) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
explicitKeys.push([key]);
|
|
747
|
+
}
|
|
748
|
+
const gated = typeGate(head.discNode);
|
|
749
|
+
if (!gated) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const { unionKeys, hasNullish, dText } = gated;
|
|
753
|
+
const explicitForCoverage = links.map((l, i) => ({
|
|
754
|
+
keys: explicitKeys[i],
|
|
755
|
+
valueText: sourceCode.getText(l.expr),
|
|
756
|
+
}));
|
|
757
|
+
const coverage = resolveCoverage(explicitForCoverage, unionKeys, {
|
|
758
|
+
valueText: sourceCode.getText(tailExpr),
|
|
759
|
+
});
|
|
760
|
+
if (!coverage) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
// Contributing values: links, plus tail only when it is kept.
|
|
764
|
+
const contributingValues = links.map((l) => l.expr);
|
|
765
|
+
if (coverage.remainingCount >= 1) {
|
|
766
|
+
contributingValues.push(tailExpr);
|
|
767
|
+
}
|
|
768
|
+
if (isNarrowingExempt(head.discNode, contributingValues)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// A ternary hoists its Record to the enclosing statement; if that crosses
|
|
772
|
+
// a function boundary the values/discriminant may fall out of scope, so
|
|
773
|
+
// the fix cannot be placed safely there — downgrade to report-only.
|
|
774
|
+
let stmt = node;
|
|
775
|
+
let crossesFunction = false;
|
|
776
|
+
while (stmt.parent && !CONTAINER_TYPES.has(stmt.parent.type)) {
|
|
777
|
+
stmt = stmt.parent;
|
|
778
|
+
if (FUNCTION_TYPES.has(stmt.type)) {
|
|
779
|
+
crossesFunction = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
discriminantMap.set(node, head.discNode);
|
|
783
|
+
report(node, {
|
|
784
|
+
entries: coverage.entries,
|
|
785
|
+
contributingValues,
|
|
786
|
+
dText,
|
|
787
|
+
form: 'expr',
|
|
788
|
+
fullCoverage: coverage.fullCoverage,
|
|
789
|
+
hasNullish,
|
|
790
|
+
canPlaceFix: !crossesFunction,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
// ---- if / else-if form --------------------------------------------------
|
|
794
|
+
function handleIf(node) {
|
|
795
|
+
const head = equalityDiscriminant(node.test);
|
|
796
|
+
if (!head) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const discText = sourceCode.getText(head.discNode);
|
|
800
|
+
// Skip continuations (this if is the else-if of a same-discriminant chain).
|
|
801
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.IfStatement &&
|
|
802
|
+
node.parent.alternate === node) {
|
|
803
|
+
const parentHead = equalityDiscriminant(node.parent.test);
|
|
804
|
+
if (parentHead &&
|
|
805
|
+
sourceCode.getText(parentHead.discNode) === discText) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const links = [];
|
|
810
|
+
let tail = null;
|
|
811
|
+
let cur = node;
|
|
812
|
+
while (cur) {
|
|
813
|
+
const link = equalityDiscriminant(cur.test);
|
|
814
|
+
if (!link || sourceCode.getText(link.discNode) !== discText) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const value = extractBranchValue([cur.consequent]);
|
|
818
|
+
if (!value) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
links.push({ keyNode: link.keyNode, value });
|
|
822
|
+
const alt = cur.alternate;
|
|
823
|
+
if (!alt) {
|
|
824
|
+
cur = null;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
if (alt.type === utils_1.AST_NODE_TYPES.IfStatement) {
|
|
828
|
+
cur = alt;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const tailValue = extractBranchValue([alt]);
|
|
832
|
+
if (!tailValue) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
tail = tailValue;
|
|
836
|
+
cur = null;
|
|
837
|
+
}
|
|
838
|
+
if (links.length === 0) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
// Consistent kind/target.
|
|
842
|
+
const kind = links[0].value.kind;
|
|
843
|
+
const assignTargetText = kind === 'assign'
|
|
844
|
+
? sourceCode.getText(links[0].value.target)
|
|
845
|
+
: undefined;
|
|
846
|
+
const allValues = [...links.map((l) => l.value)];
|
|
847
|
+
if (tail) {
|
|
848
|
+
allValues.push(tail);
|
|
849
|
+
}
|
|
850
|
+
for (const v of allValues) {
|
|
851
|
+
if (v.kind !== kind) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (kind === 'assign' &&
|
|
855
|
+
sourceCode.getText(v.target) !==
|
|
856
|
+
assignTargetText) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const explicitKeys = [];
|
|
861
|
+
for (const link of links) {
|
|
862
|
+
const key = resolveLiteralKey(link.keyNode);
|
|
863
|
+
if (!key) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
explicitKeys.push([key]);
|
|
867
|
+
}
|
|
868
|
+
const gated = typeGate(head.discNode);
|
|
869
|
+
if (!gated) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const { unionKeys, hasNullish, dText } = gated;
|
|
873
|
+
const explicitForCoverage = links.map((l, i) => ({
|
|
874
|
+
keys: explicitKeys[i],
|
|
875
|
+
valueText: sourceCode.getText(l.value.expr),
|
|
876
|
+
}));
|
|
877
|
+
const coverage = resolveCoverage(explicitForCoverage, unionKeys, tail ? { valueText: sourceCode.getText(tail.expr) } : null);
|
|
878
|
+
if (!coverage) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const contributingValues = links.map((l) => l.value.expr);
|
|
882
|
+
if (coverage.remainingCount >= 1 && tail) {
|
|
883
|
+
contributingValues.push(tail.expr);
|
|
884
|
+
}
|
|
885
|
+
if (isNarrowingExempt(head.discNode, contributingValues)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
discriminantMap.set(node, head.discNode);
|
|
889
|
+
report(node, {
|
|
890
|
+
entries: coverage.entries,
|
|
891
|
+
contributingValues,
|
|
892
|
+
dText,
|
|
893
|
+
form: kind,
|
|
894
|
+
assignTargetText,
|
|
895
|
+
fullCoverage: coverage.fullCoverage,
|
|
896
|
+
hasNullish,
|
|
897
|
+
canPlaceFix: true,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
SwitchStatement: handleSwitch,
|
|
902
|
+
ConditionalExpression: handleConditional,
|
|
903
|
+
IfStatement: handleIf,
|
|
904
|
+
};
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
//# sourceMappingURL=prefer-map-over-conditional-dispatch.js.map
|