@blumintinc/eslint-plugin-blumint 1.20.8 → 1.20.10
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/lib/index.js +1 -1
- package/lib/rules/enforce-early-destructuring.js +14 -0
- package/lib/rules/enforce-m3-sentence-case.js +211 -45
- package/lib/rules/enforce-props-naming-consistency.js +127 -8
- package/lib/rules/enforce-querykey-ts.js +170 -16
- package/lib/rules/enforce-react-type-naming.d.ts +2 -1
- package/lib/rules/enforce-react-type-naming.js +46 -17
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +3 -1
- package/lib/rules/enforce-snapshot-state-narrowing.js +213 -53
- package/lib/rules/no-class-instance-destructuring.js +7 -0
- package/lib/rules/no-excessive-parent-chain.js +43 -10
- package/lib/rules/no-unnecessary-destructuring.js +8 -1
- package/lib/rules/prefer-clone-deep.d.ts +2 -1
- package/lib/rules/prefer-clone-deep.js +250 -192
- package/lib/rules/prefer-destructuring-no-class.js +8 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +2 -1
- package/lib/rules/prefer-spread-over-reassembly.js +30 -2
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.d.ts +2 -1
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +192 -5
- package/lib/rules/use-custom-memo.d.ts +2 -1
- package/lib/rules/use-custom-memo.js +51 -36
- package/lib/rules/use-latest-callback.js +142 -92
- package/lib/utils/renameFixes.d.ts +69 -0
- package/lib/utils/renameFixes.js +202 -0
- package/package.json +1 -1
- package/release-manifest.json +132 -0
|
@@ -9,7 +9,37 @@ const DEFAULT_SNAPSHOT_HOOKS = [
|
|
|
9
9
|
'useCachedDocSnapshot',
|
|
10
10
|
'useFirestore',
|
|
11
11
|
];
|
|
12
|
-
const
|
|
12
|
+
const GUARD_NAME = 'isSnapshotReady';
|
|
13
|
+
const DEFAULT_GUARD_FUNCTIONS = [GUARD_NAME];
|
|
14
|
+
/**
|
|
15
|
+
* Where the canonical guard lives. The rule's own default `excludeFiles` entry
|
|
16
|
+
* names the guard's implementation file, so the fixer writes an import for that
|
|
17
|
+
* very module rather than guessing a path.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_GUARD_IMPORT_SOURCE = 'src/types/FirestoreSnapshotState';
|
|
20
|
+
/**
|
|
21
|
+
* Parents that accept a bare conditional expression without parentheses. Any
|
|
22
|
+
* other parent binds tighter than `?:` (or reads ambiguously next to it), so the
|
|
23
|
+
* rewritten expression is wrapped.
|
|
24
|
+
*/
|
|
25
|
+
const PARENTHESES_FREE_PARENTS = new Set([
|
|
26
|
+
utils_1.AST_NODE_TYPES.ArrayExpression,
|
|
27
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
28
|
+
utils_1.AST_NODE_TYPES.AssignmentExpression,
|
|
29
|
+
utils_1.AST_NODE_TYPES.AssignmentPattern,
|
|
30
|
+
utils_1.AST_NODE_TYPES.DoWhileStatement,
|
|
31
|
+
utils_1.AST_NODE_TYPES.ExpressionStatement,
|
|
32
|
+
utils_1.AST_NODE_TYPES.ForStatement,
|
|
33
|
+
utils_1.AST_NODE_TYPES.IfStatement,
|
|
34
|
+
utils_1.AST_NODE_TYPES.JSXExpressionContainer,
|
|
35
|
+
utils_1.AST_NODE_TYPES.Property,
|
|
36
|
+
utils_1.AST_NODE_TYPES.ReturnStatement,
|
|
37
|
+
utils_1.AST_NODE_TYPES.SwitchCase,
|
|
38
|
+
utils_1.AST_NODE_TYPES.SwitchStatement,
|
|
39
|
+
utils_1.AST_NODE_TYPES.ThrowStatement,
|
|
40
|
+
utils_1.AST_NODE_TYPES.VariableDeclarator,
|
|
41
|
+
utils_1.AST_NODE_TYPES.WhileStatement,
|
|
42
|
+
]);
|
|
13
43
|
exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
14
44
|
name: 'enforce-snapshot-state-narrowing',
|
|
15
45
|
meta: {
|
|
@@ -38,6 +68,10 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
38
68
|
items: { type: 'string' },
|
|
39
69
|
description: 'File patterns to exclude from this rule',
|
|
40
70
|
},
|
|
71
|
+
guardImportSource: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Module specifier the suggestion imports isSnapshotReady from',
|
|
74
|
+
},
|
|
41
75
|
},
|
|
42
76
|
additionalProperties: false,
|
|
43
77
|
},
|
|
@@ -56,6 +90,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
56
90
|
const excludeFiles = options?.excludeFiles ?? [
|
|
57
91
|
'src/types/FirestoreSnapshotState.ts',
|
|
58
92
|
];
|
|
93
|
+
const guardImportSource = options?.guardImportSource ?? DEFAULT_GUARD_IMPORT_SOURCE;
|
|
59
94
|
// Check if the current file should be excluded
|
|
60
95
|
const filename = context.getFilename();
|
|
61
96
|
if (excludeFiles.some((pattern) => filename.endsWith(pattern))) {
|
|
@@ -98,6 +133,149 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
98
133
|
return '<expression>';
|
|
99
134
|
}
|
|
100
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Strips a module specifier down to its final segment so an import written
|
|
138
|
+
* as a relative path, an alias, or with an extension is still recognized as
|
|
139
|
+
* the guard's module.
|
|
140
|
+
*/
|
|
141
|
+
function moduleBasename(source) {
|
|
142
|
+
const withoutExtension = source.replace(/\.[cm]?[jt]sx?$/, '');
|
|
143
|
+
const segments = withoutExtension.split('/');
|
|
144
|
+
return segments[segments.length - 1];
|
|
145
|
+
}
|
|
146
|
+
const guardModuleName = moduleBasename(guardImportSource);
|
|
147
|
+
function isGuardModule(source) {
|
|
148
|
+
return (source === guardImportSource ||
|
|
149
|
+
moduleBasename(source) === guardModuleName);
|
|
150
|
+
}
|
|
151
|
+
function importDeclarationsOf() {
|
|
152
|
+
return context
|
|
153
|
+
.getSourceCode()
|
|
154
|
+
.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
155
|
+
typeof statement.source.value === 'string');
|
|
156
|
+
}
|
|
157
|
+
function isValueImportSpecifier(specifier) {
|
|
158
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
159
|
+
specifier.importKind !== 'type');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Decides whether the emitted guard call resolves as written, needs an
|
|
163
|
+
* import, or would collide with an unrelated binding of the same name.
|
|
164
|
+
*/
|
|
165
|
+
function resolveGuardBinding(scope) {
|
|
166
|
+
const variable = utils_1.ASTUtils.findVariable(scope, GUARD_NAME);
|
|
167
|
+
if (!variable) {
|
|
168
|
+
return 'missing';
|
|
169
|
+
}
|
|
170
|
+
const [definition] = variable.defs;
|
|
171
|
+
if (!definition) {
|
|
172
|
+
return 'conflict';
|
|
173
|
+
}
|
|
174
|
+
const definitionNode = definition.node;
|
|
175
|
+
if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
176
|
+
definitionNode.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
177
|
+
// A type-only binding is erased at runtime, so calling it is not an
|
|
178
|
+
// option — importing the value would then clash with the type binding.
|
|
179
|
+
if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
180
|
+
definitionNode.importKind === 'type') {
|
|
181
|
+
return 'conflict';
|
|
182
|
+
}
|
|
183
|
+
const declaration = definitionNode.parent;
|
|
184
|
+
if (!declaration ||
|
|
185
|
+
declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
186
|
+
declaration.importKind === 'type') {
|
|
187
|
+
return 'conflict';
|
|
188
|
+
}
|
|
189
|
+
// Any value import under this name reaches the guard (a barrel or
|
|
190
|
+
// re-export is still the same function), so no second import is needed.
|
|
191
|
+
return 'bound';
|
|
192
|
+
}
|
|
193
|
+
// A file that declares the guard itself already has a callable binding.
|
|
194
|
+
if (definitionNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
195
|
+
return 'bound';
|
|
196
|
+
}
|
|
197
|
+
if (definitionNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
198
|
+
(definitionNode.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
199
|
+
definitionNode.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
200
|
+
return 'bound';
|
|
201
|
+
}
|
|
202
|
+
return 'conflict';
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Makes the emitted guard call resolve: extend an existing import of the
|
|
206
|
+
* guard's module when there is one to extend (reusing that file's own path),
|
|
207
|
+
* otherwise add the canonical import statement.
|
|
208
|
+
*/
|
|
209
|
+
function buildImportFix(fixer) {
|
|
210
|
+
const declarations = importDeclarationsOf();
|
|
211
|
+
const guardDeclarations = declarations.filter((declaration) => isGuardModule(String(declaration.source.value)));
|
|
212
|
+
const reusable = guardDeclarations.find((declaration) => declaration.importKind !== 'type' &&
|
|
213
|
+
declaration.specifiers.some(isValueImportSpecifier));
|
|
214
|
+
if (reusable) {
|
|
215
|
+
const namedSpecifiers = reusable.specifiers.filter(isValueImportSpecifier);
|
|
216
|
+
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
217
|
+
return fixer.insertTextAfter(lastSpecifier, `, ${GUARD_NAME}`);
|
|
218
|
+
}
|
|
219
|
+
// A namespace or type-only import of the module cannot take a named value
|
|
220
|
+
// specifier, but its path is proof of how this file reaches the module.
|
|
221
|
+
const source = guardDeclarations.length
|
|
222
|
+
? String(guardDeclarations[0].source.value)
|
|
223
|
+
: guardImportSource;
|
|
224
|
+
const importText = `import { ${GUARD_NAME} } from '${source}';\n`;
|
|
225
|
+
const [firstImport] = declarations;
|
|
226
|
+
if (firstImport) {
|
|
227
|
+
return fixer.insertTextBefore(firstImport, importText);
|
|
228
|
+
}
|
|
229
|
+
// Keep the import visually separated from the code it precedes unless the
|
|
230
|
+
// file already opens with a blank line.
|
|
231
|
+
const separator = /^\r?\n/.test(context.getSourceCode().text) ? '' : '\n';
|
|
232
|
+
return fixer.insertTextBeforeRange([0, 0], `${importText}${separator}`);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Builds the single suggestion shared by every report: swap the flagged
|
|
236
|
+
* expression for its guard-based equivalent and bring `isSnapshotReady` into
|
|
237
|
+
* scope. Declines (no suggestion) when the name is already taken by
|
|
238
|
+
* something that is not the guard.
|
|
239
|
+
*/
|
|
240
|
+
function guardSuggestion(messageId, fixNode, replacement, scope) {
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
messageId,
|
|
244
|
+
data: { expression: replacement },
|
|
245
|
+
fix(fixer) {
|
|
246
|
+
const binding = resolveGuardBinding(scope);
|
|
247
|
+
if (binding === 'conflict') {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const fixes = [fixer.replaceText(fixNode, replacement)];
|
|
251
|
+
if (binding === 'missing') {
|
|
252
|
+
fixes.unshift(buildImportFix(fixer));
|
|
253
|
+
}
|
|
254
|
+
return fixes;
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* True when a conditional expression substituted for `node` would need
|
|
261
|
+
* parentheses to preserve the surrounding expression's grouping.
|
|
262
|
+
*/
|
|
263
|
+
function needsParentheses(node) {
|
|
264
|
+
// Source parentheses live outside the replaced range, so they already do
|
|
265
|
+
// the grouping and a second pair would only add noise.
|
|
266
|
+
if (utils_1.ASTUtils.isParenthesized(node, context.getSourceCode())) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const parent = node.parent;
|
|
270
|
+
if (!parent) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
274
|
+
parent.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
275
|
+
return parent.callee === node;
|
|
276
|
+
}
|
|
277
|
+
return !PARENTHESES_FREE_PARENTS.has(parent.type);
|
|
278
|
+
}
|
|
101
279
|
/**
|
|
102
280
|
* Checks a BinaryExpression for raw typeof narrowing patterns that attempt
|
|
103
281
|
* to narrow the state to data:
|
|
@@ -120,42 +298,16 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
120
298
|
const literal = right.value;
|
|
121
299
|
if (operand.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
122
300
|
isSnapshotVar(operand)) {
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
data: { expression: getText(node) },
|
|
129
|
-
suggest: [
|
|
130
|
-
{
|
|
131
|
-
messageId: 'noRawTypeof',
|
|
132
|
-
data: {
|
|
133
|
-
expression: `isSnapshotReady(${operand.name})`,
|
|
134
|
-
},
|
|
135
|
-
fix(fixer) {
|
|
136
|
-
return fixer.replaceText(node, `isSnapshotReady(${operand.name})`);
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
// typeof state !== 'string' — bad: equivalent to isSnapshotReady
|
|
143
|
-
else if (operator === '!==' && literal === 'string') {
|
|
301
|
+
// Both flagged forms are true exactly when the state holds data, so
|
|
302
|
+
// the guard replaces them with matching polarity.
|
|
303
|
+
const isToDataCheck = (operator === '===' && literal === 'object') ||
|
|
304
|
+
(operator === '!==' && literal === 'string');
|
|
305
|
+
if (isToDataCheck) {
|
|
144
306
|
context.report({
|
|
145
307
|
node,
|
|
146
308
|
messageId: 'noRawTypeof',
|
|
147
309
|
data: { expression: getText(node) },
|
|
148
|
-
suggest:
|
|
149
|
-
{
|
|
150
|
-
messageId: 'noRawTypeof',
|
|
151
|
-
data: {
|
|
152
|
-
expression: `isSnapshotReady(${operand.name})`,
|
|
153
|
-
},
|
|
154
|
-
fix(fixer) {
|
|
155
|
-
return fixer.replaceText(node, `isSnapshotReady(${operand.name})`);
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
],
|
|
310
|
+
suggest: guardSuggestion('noRawTypeof', node, `${GUARD_NAME}(${operand.name})`, context.getScope()),
|
|
159
311
|
});
|
|
160
312
|
}
|
|
161
313
|
// typeof state === 'string' — allowed (narrows to non-data states)
|
|
@@ -165,22 +317,18 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
165
317
|
}
|
|
166
318
|
/**
|
|
167
319
|
* Reports a falsy/truthy check on a snapshot-state identifier.
|
|
320
|
+
*
|
|
321
|
+
* `replacement` must match the polarity of the flagged expression: a falsy
|
|
322
|
+
* check reads `!isSnapshotReady(state)`, a truthy one `isSnapshotReady(state)`.
|
|
323
|
+
* `fixNode` defaults to the reported node but can be widened when the whole
|
|
324
|
+
* surrounding expression has to be rewritten (e.g. `state || fallback`).
|
|
168
325
|
*/
|
|
169
|
-
function reportFalsyCheck(node, expression,
|
|
326
|
+
function reportFalsyCheck(node, expression, replacement, fixNode = node) {
|
|
170
327
|
context.report({
|
|
171
328
|
node,
|
|
172
329
|
messageId: 'noFalsyCheck',
|
|
173
330
|
data: { expression },
|
|
174
|
-
suggest:
|
|
175
|
-
{
|
|
176
|
-
messageId: 'noFalsyCheck',
|
|
177
|
-
data: { expression: `isSnapshotReady(${varName})` },
|
|
178
|
-
fix(fixer) {
|
|
179
|
-
// Replace the entire flagged expression with the canonical guard
|
|
180
|
-
return fixer.replaceText(node, `isSnapshotReady(${varName})`);
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
],
|
|
331
|
+
suggest: guardSuggestion('noFalsyCheck', fixNode, replacement, context.getScope()),
|
|
184
332
|
});
|
|
185
333
|
}
|
|
186
334
|
return {
|
|
@@ -219,19 +367,21 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
219
367
|
return;
|
|
220
368
|
}
|
|
221
369
|
const argument = node.argument;
|
|
222
|
-
// !state — the argument is directly the snapshot var
|
|
370
|
+
// !state — the argument is directly the snapshot var.
|
|
371
|
+
// The check is true when the state is NOT usable, so the guard has to
|
|
372
|
+
// stay negated or the suggestion would reverse the control flow.
|
|
223
373
|
if (argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
224
374
|
isSnapshotVar(argument)) {
|
|
225
|
-
|
|
226
|
-
reportFalsyCheck(node, expression, argument.name);
|
|
375
|
+
reportFalsyCheck(node, `!${argument.name}`, `!${GUARD_NAME}(${argument.name})`);
|
|
227
376
|
}
|
|
228
|
-
// !!state — the argument is another `!` whose argument is the snapshot var
|
|
377
|
+
// !!state — the argument is another `!` whose argument is the snapshot var.
|
|
378
|
+
// The double negation is a truthiness coercion, so the guard is positive.
|
|
229
379
|
else if (argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
230
380
|
argument.operator === '!' &&
|
|
231
381
|
argument.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
232
382
|
isSnapshotVar(argument.argument)) {
|
|
233
383
|
const varName = argument.argument.name;
|
|
234
|
-
reportFalsyCheck(node, `!!${varName}`, varName);
|
|
384
|
+
reportFalsyCheck(node, `!!${varName}`, `${GUARD_NAME}(${varName})`);
|
|
235
385
|
}
|
|
236
386
|
},
|
|
237
387
|
// IfStatement: if (state) { ... } or if (!state) { ... }
|
|
@@ -239,14 +389,14 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
239
389
|
IfStatement(node) {
|
|
240
390
|
const test = node.test;
|
|
241
391
|
if (test.type === utils_1.AST_NODE_TYPES.Identifier && isSnapshotVar(test)) {
|
|
242
|
-
reportFalsyCheck(test, test.name, test.name);
|
|
392
|
+
reportFalsyCheck(test, test.name, `${GUARD_NAME}(${test.name})`);
|
|
243
393
|
}
|
|
244
394
|
},
|
|
245
395
|
// ConditionalExpression: state ? a : b
|
|
246
396
|
ConditionalExpression(node) {
|
|
247
397
|
const test = node.test;
|
|
248
398
|
if (test.type === utils_1.AST_NODE_TYPES.Identifier && isSnapshotVar(test)) {
|
|
249
|
-
reportFalsyCheck(test, test.name, test.name);
|
|
399
|
+
reportFalsyCheck(test, test.name, `${GUARD_NAME}(${test.name})`);
|
|
250
400
|
}
|
|
251
401
|
},
|
|
252
402
|
// LogicalExpression: state && expr, state || expr
|
|
@@ -255,7 +405,17 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
255
405
|
if ((node.operator === '&&' || node.operator === '||') &&
|
|
256
406
|
left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
257
407
|
isSnapshotVar(left)) {
|
|
258
|
-
|
|
408
|
+
if (node.operator === '&&') {
|
|
409
|
+
// `state && expr` guards expr, so swapping the operand for the
|
|
410
|
+
// guard keeps both the polarity and the narrowing of `state`.
|
|
411
|
+
reportFalsyCheck(left, left.name, `${GUARD_NAME}(${left.name})`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// `state || fallback` evaluates to the state itself when it is
|
|
415
|
+
// usable, so a bare operand swap would yield `true` instead of the
|
|
416
|
+
// data. Only the conditional form preserves that value.
|
|
417
|
+
const guarded = `${GUARD_NAME}(${left.name}) ? ${left.name} : ${getText(node.right)}`;
|
|
418
|
+
reportFalsyCheck(left, left.name, needsParentheses(node) ? `(${guarded})` : guarded, node);
|
|
259
419
|
}
|
|
260
420
|
},
|
|
261
421
|
// BinaryExpression: typeof state === 'object', typeof state !== 'string'
|
|
@@ -272,7 +432,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
272
432
|
node.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
273
433
|
isSnapshotVar(node.arguments[0])) {
|
|
274
434
|
const varName = node.arguments[0].name;
|
|
275
|
-
reportFalsyCheck(node, `Boolean(${varName})`, varName);
|
|
435
|
+
reportFalsyCheck(node, `Boolean(${varName})`, `${GUARD_NAME}(${varName})`);
|
|
276
436
|
}
|
|
277
437
|
},
|
|
278
438
|
};
|
|
@@ -101,6 +101,13 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
101
101
|
// Skip if there's no init expression
|
|
102
102
|
if (!node.init)
|
|
103
103
|
return null;
|
|
104
|
+
// An annotation on the pattern types the object as a whole, not
|
|
105
|
+
// each property, so it cannot be split across the per-property
|
|
106
|
+
// declarations this fixer emits. Deriving per-property types needs
|
|
107
|
+
// the type checker, so report without fixing rather than rewrite
|
|
108
|
+
// the code to something more weakly typed than the author wrote.
|
|
109
|
+
if (objectPattern.typeAnnotation)
|
|
110
|
+
return null;
|
|
104
111
|
// For single property, use simple replacement
|
|
105
112
|
if (properties.length === 1) {
|
|
106
113
|
const prop = properties[0];
|
|
@@ -54,6 +54,36 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
54
54
|
}
|
|
55
55
|
return current.type === utils_1.AST_NODE_TYPES.Identifier ? current.name : null;
|
|
56
56
|
};
|
|
57
|
+
// Suggestions rewrite the chain as `<event>.params`, so the event binding has
|
|
58
|
+
// to be resolved back to a real identifier. Handler parameters destructured
|
|
59
|
+
// in place (`async ({ data: change }) => ...`) have no such identifier and
|
|
60
|
+
// resolve to null so the report can decline to suggest rather than guess.
|
|
61
|
+
const resolveEventParamName = (rootIdentifier) => {
|
|
62
|
+
if (eventIdentifiers.has(rootIdentifier) &&
|
|
63
|
+
rootIdentifier !== HANDLER_PARAM_SOURCE) {
|
|
64
|
+
return rootIdentifier;
|
|
65
|
+
}
|
|
66
|
+
const source = eventDataVariables.get(rootIdentifier);
|
|
67
|
+
if (!source || source === HANDLER_PARAM_SOURCE) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return source;
|
|
71
|
+
};
|
|
72
|
+
// Every hop of a chain is reported separately, so an inner report's fix must
|
|
73
|
+
// still span the outermost hop. Replacing only the reported slice leaves a
|
|
74
|
+
// dangling hop behind (for example `event.params.parent.id`).
|
|
75
|
+
const getOutermostParentHop = (node) => {
|
|
76
|
+
let outermost = node;
|
|
77
|
+
let ancestor = outermost.parent;
|
|
78
|
+
while (ancestor?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
79
|
+
ancestor.object === outermost &&
|
|
80
|
+
ancestor.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
|
+
ancestor.property.name === 'parent') {
|
|
82
|
+
outermost = ancestor;
|
|
83
|
+
ancestor = outermost.parent;
|
|
84
|
+
}
|
|
85
|
+
return outermost;
|
|
86
|
+
};
|
|
57
87
|
const hasRefProperty = (node) => {
|
|
58
88
|
let current = node;
|
|
59
89
|
while (current) {
|
|
@@ -251,23 +281,26 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
251
281
|
!eventIdentifiers.has(rootIdentifier))) {
|
|
252
282
|
return;
|
|
253
283
|
}
|
|
284
|
+
const eventParamName = resolveEventParamName(rootIdentifier);
|
|
254
285
|
context.report({
|
|
255
286
|
node,
|
|
256
287
|
messageId: 'excessiveParentChain',
|
|
257
288
|
data: {
|
|
258
289
|
count: parentCount,
|
|
259
290
|
},
|
|
260
|
-
suggest:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
291
|
+
suggest: eventParamName
|
|
292
|
+
? [
|
|
293
|
+
{
|
|
294
|
+
messageId: 'excessiveParentChain',
|
|
295
|
+
data: {
|
|
296
|
+
count: parentCount,
|
|
297
|
+
},
|
|
298
|
+
fix(fixer) {
|
|
299
|
+
return fixer.replaceText(getOutermostParentHop(node), `${eventParamName}.params`);
|
|
300
|
+
},
|
|
268
301
|
},
|
|
269
|
-
|
|
270
|
-
|
|
302
|
+
]
|
|
303
|
+
: [],
|
|
271
304
|
});
|
|
272
305
|
},
|
|
273
306
|
};
|
|
@@ -43,8 +43,15 @@ exports.noUnnecessaryDestructuring = (0, createRule_1.createRule)({
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
const initText = sourceCode.getText(node.init);
|
|
46
|
+
// The declarator's range covers `node.id.typeAnnotation`, so
|
|
47
|
+
// replacing the whole declarator would silently drop it. A lone
|
|
48
|
+
// rest element binds the entire object, meaning the annotation
|
|
49
|
+
// describes the new binding exactly and can be re-emitted as-is.
|
|
50
|
+
const annotationText = node.id.typeAnnotation
|
|
51
|
+
? sourceCode.getText(node.id.typeAnnotation)
|
|
52
|
+
: '';
|
|
46
53
|
// Replace the destructuring with direct assignment
|
|
47
|
-
return fixer.replaceText(node, `${restName} = ${initText}`);
|
|
54
|
+
return fixer.replaceText(node, `${restName}${annotationText} = ${initText}`);
|
|
48
55
|
},
|
|
49
56
|
});
|
|
50
57
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const preferCloneDeep: TSESLint.RuleModule<"preferCloneDeep", [], TSESLint.RuleListener>;
|