@blumintinc/eslint-plugin-blumint 1.20.117 → 1.20.119
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-callback-memo.js +7 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +9 -30
- package/lib/rules/enforce-firestore-set-merge.js +48 -46
- package/lib/rules/no-direct-function-state.js +9 -30
- package/lib/rules/no-explicit-return-type.js +10 -37
- package/lib/rules/no-firestore-object-arrays.js +14 -65
- package/lib/rules/prefer-batch-operations.js +24 -45
- package/lib/rules/prefer-block-comments-for-declarations.js +127 -17
- package/lib/rules/prefer-spread-over-reassembly.js +3 -34
- package/lib/rules/prevent-children-clobber.js +9 -38
- package/lib/rules/require-hooks-default-params.js +16 -20
- package/lib/rules/require-memo.js +135 -19
- package/lib/rules/require-props-composition.js +138 -93
- package/lib/utils/ASTHelpers.d.ts +30 -3
- package/lib/utils/ASTHelpers.js +61 -5
- package/lib/utils/lexicalScope.d.ts +84 -0
- package/lib/utils/lexicalScope.js +131 -0
- package/package.json +1 -1
- package/release-manifest.json +69 -0
package/lib/index.js
CHANGED
|
@@ -200,6 +200,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
200
200
|
// that happens to build JSX, and JSX rendered from a test body are all
|
|
201
201
|
// outside any render path, so wrapping there would throw
|
|
202
202
|
// "Invalid hook call" while saving no re-render.
|
|
203
|
+
//
|
|
204
|
+
// The question the gate asks is RELATIVE — is a render function interposed
|
|
205
|
+
// between this attribute and whatever encloses it further out — so a
|
|
206
|
+
// component built by a plain factory (`function makeCard() { return
|
|
207
|
+
// memo(() => <X onClick={...} />); }`) reports: the arrow handed to `memo`
|
|
208
|
+
// IS the component, and `useCallback` is legal inside it no matter what
|
|
209
|
+
// the factory is called.
|
|
203
210
|
if (!ASTHelpers_1.ASTHelpers.isInsideComponentOrHook(node, context)) {
|
|
204
211
|
return;
|
|
205
212
|
}
|
|
@@ -11,6 +11,7 @@ exports.enforceFirestoreDocRefGeneric = void 0;
|
|
|
11
11
|
const utils_1 = require("@typescript-eslint/utils");
|
|
12
12
|
const createRule_1 = require("../utils/createRule");
|
|
13
13
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
14
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
14
15
|
/** The Firestore reference types that carry a document-shape generic. */
|
|
15
16
|
const REFERENCE_TYPE_NAMES = new Set([
|
|
16
17
|
'DocumentReference',
|
|
@@ -38,20 +39,6 @@ const referenceTypeNameOf = (typeName) => {
|
|
|
38
39
|
}
|
|
39
40
|
return undefined;
|
|
40
41
|
};
|
|
41
|
-
/** Statement containers a type declaration can be a direct child of. */
|
|
42
|
-
function statementsOf(node) {
|
|
43
|
-
switch (node.type) {
|
|
44
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
45
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
46
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
47
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
48
|
-
return node.body;
|
|
49
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
50
|
-
return node.consequent;
|
|
51
|
-
default:
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
42
|
/**
|
|
56
43
|
* The type declaration a statement makes, looking through `export`.
|
|
57
44
|
*
|
|
@@ -61,10 +48,7 @@ function statementsOf(node) {
|
|
|
61
48
|
* distinction the document shape knows anything about.
|
|
62
49
|
*/
|
|
63
50
|
function typeDeclarationNamed(statement, name) {
|
|
64
|
-
const declared =
|
|
65
|
-
statement.declaration
|
|
66
|
-
? statement.declaration
|
|
67
|
-
: statement;
|
|
51
|
+
const declared = (0, lexicalScope_1.declarationOf)(statement);
|
|
68
52
|
if ((declared.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
69
53
|
declared.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) &&
|
|
70
54
|
declared.id.name === name) {
|
|
@@ -83,20 +67,15 @@ function typeDeclarationNamed(statement, name) {
|
|
|
83
67
|
* dropped the nested-`any` check rather than reporting anything.
|
|
84
68
|
*/
|
|
85
69
|
function declarationOfType(from, name) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const declaration = typeDeclarationNamed(statement, name);
|
|
92
|
-
if (declaration) {
|
|
93
|
-
return declaration;
|
|
94
|
-
}
|
|
70
|
+
return (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => {
|
|
71
|
+
for (const statement of statements) {
|
|
72
|
+
const declaration = typeDeclarationNamed(statement, name);
|
|
73
|
+
if (declaration) {
|
|
74
|
+
return declaration;
|
|
95
75
|
}
|
|
96
76
|
}
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
77
|
+
return undefined;
|
|
78
|
+
});
|
|
100
79
|
}
|
|
101
80
|
/**
|
|
102
81
|
* @type {import('eslint').Rule.RuleModule}
|
|
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
8
9
|
const FIRESTORE_MODULES = new Set(['firebase/firestore', 'firebase-admin']);
|
|
9
10
|
const UPDATE_DOC = 'updateDoc';
|
|
10
11
|
const SET_DOC = 'setDoc';
|
|
@@ -192,20 +193,6 @@ function isPrimitiveLiteral(node) {
|
|
|
192
193
|
typeof value === 'boolean' ||
|
|
193
194
|
typeof value === 'bigint');
|
|
194
195
|
}
|
|
195
|
-
/** Statement containers a declaration can be a direct child of. */
|
|
196
|
-
function statementsOf(node) {
|
|
197
|
-
switch (node.type) {
|
|
198
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
199
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
200
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
201
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
202
|
-
return node.body;
|
|
203
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
204
|
-
return node.consequent;
|
|
205
|
-
default:
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
196
|
/** Whether a declarator is initialized from a `<x>.firestore()` call. */
|
|
210
197
|
function initializesFirestore(declarator) {
|
|
211
198
|
const { init } = declarator;
|
|
@@ -221,14 +208,11 @@ function initializesFirestore(declarator) {
|
|
|
221
208
|
* deeper, inside an `ExportNamedDeclaration`. Reading the statement without
|
|
222
209
|
* unwrapping makes the `export` keyword alone decide whether the file's
|
|
223
210
|
* Firestore evidence is visible, which is not a distinction a `db` handle knows
|
|
224
|
-
* anything about — `
|
|
211
|
+
* anything about — `resolveClassBody()` unwraps it for the same
|
|
225
212
|
* "find the in-file declaration that carries the evidence" purpose.
|
|
226
213
|
*/
|
|
227
214
|
function declaresFirestoreInstance(statement) {
|
|
228
|
-
const declaration =
|
|
229
|
-
statement.declaration
|
|
230
|
-
? statement.declaration
|
|
231
|
-
: statement;
|
|
215
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
232
216
|
return (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
233
217
|
declaration.declarations.some(initializesFirestore));
|
|
234
218
|
}
|
|
@@ -244,15 +228,9 @@ function declaresFirestoreInstance(statement) {
|
|
|
244
228
|
* falls through to the next one out instead of answering for the whole chain.
|
|
245
229
|
*/
|
|
246
230
|
function hasFirestoreInstanceInScope(node) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (statements?.some(declaresFirestoreInstance)) {
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
current = current.parent;
|
|
254
|
-
}
|
|
255
|
-
return false;
|
|
231
|
+
return ((0, lexicalScope_1.resolveInEnclosingScopes)(node, (statements) => statements.some((statement) => declaresFirestoreInstance(statement))
|
|
232
|
+
? true
|
|
233
|
+
: undefined) === true);
|
|
256
234
|
}
|
|
257
235
|
exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
258
236
|
name: 'enforce-firestore-set-merge',
|
|
@@ -285,35 +263,59 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
285
263
|
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
286
264
|
let plannedSetDocBinding = false;
|
|
287
265
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
266
|
+
* Classes declared directly in one statement container, by name — both the
|
|
267
|
+
* `class X {}` spelling and the `const X = class {}` one, each looked
|
|
268
|
+
* through its optional `export` wrapper.
|
|
269
|
+
*
|
|
270
|
+
* The memo hangs off the container rather than off the file. What a single
|
|
271
|
+
* container declares is the same answer for every call site that asks, so
|
|
272
|
+
* caching it is safe, whereas a file-wide name map is not: lexical
|
|
273
|
+
* resolution is position dependent, and one map computed for whichever site
|
|
274
|
+
* asked first would hand that site's answer to every other one.
|
|
290
275
|
*/
|
|
291
|
-
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
276
|
+
const classesByContainer = new WeakMap();
|
|
277
|
+
function containerClasses(container, statements) {
|
|
278
|
+
const cached = classesByContainer.get(container);
|
|
279
|
+
if (cached) {
|
|
280
|
+
return cached;
|
|
295
281
|
}
|
|
296
|
-
|
|
297
|
-
for (const statement of
|
|
298
|
-
const declaration =
|
|
299
|
-
|
|
300
|
-
? statement.declaration
|
|
301
|
-
: statement;
|
|
302
|
-
if (declaration?.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
282
|
+
const classes = new Map();
|
|
283
|
+
for (const statement of statements) {
|
|
284
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
285
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
303
286
|
declaration.id) {
|
|
304
|
-
|
|
287
|
+
classes.set(declaration.id.name, declaration.body);
|
|
305
288
|
continue;
|
|
306
289
|
}
|
|
307
|
-
if (declaration
|
|
290
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
308
291
|
for (const declarator of declaration.declarations) {
|
|
309
292
|
if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
310
293
|
declarator.init?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
311
|
-
|
|
294
|
+
classes.set(declarator.id.name, declarator.init.body);
|
|
312
295
|
}
|
|
313
296
|
}
|
|
314
297
|
}
|
|
315
298
|
}
|
|
316
|
-
|
|
299
|
+
classesByContainer.set(container, classes);
|
|
300
|
+
return classes;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The class a superclass reference names, searched from the reference
|
|
304
|
+
* outward through every enclosing statement container so the declaration
|
|
305
|
+
* that carries the field's evidence is found wherever it sits.
|
|
306
|
+
*
|
|
307
|
+
* Scanning `Program.body` alone made the `export` keyword and the depth of a
|
|
308
|
+
* declaration decide whether a base class is visible, which is not a
|
|
309
|
+
* distinction an inherited field knows anything about. This map feeds the
|
|
310
|
+
* Realtime Database carve-out, so a miss switches the exemption off and
|
|
311
|
+
* turns a call the rule cannot legally rewrite into a report — the same
|
|
312
|
+
* hole `hasFirestoreInstanceInScope` closes for the detection direction,
|
|
313
|
+
* and the two must agree. The innermost container wins, so a nested class
|
|
314
|
+
* shadowing an outer one of the same name answers for the code that sees
|
|
315
|
+
* the shadow.
|
|
316
|
+
*/
|
|
317
|
+
function resolveClassBody(name, reference) {
|
|
318
|
+
return ((0, lexicalScope_1.resolveInEnclosingScopes)(reference, (statements, container) => containerClasses(container, statements).get(name)) ?? null);
|
|
317
319
|
}
|
|
318
320
|
function enclosingClassBody(node) {
|
|
319
321
|
let current = node.parent;
|
|
@@ -342,7 +344,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
342
344
|
if (superClass?.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
343
345
|
return false;
|
|
344
346
|
}
|
|
345
|
-
const superBody =
|
|
347
|
+
const superBody = resolveClassBody(superClass.name, superClass);
|
|
346
348
|
return superBody ? classBindsRealtime(superBody, name, seen) : false;
|
|
347
349
|
}
|
|
348
350
|
function identifierBindsRealtime(identifier) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noDirectFunctionState = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
6
7
|
const DEFAULT_FUNCTION_PATTERNS = [
|
|
7
8
|
'callback',
|
|
8
9
|
'handler',
|
|
@@ -10,20 +11,6 @@ const DEFAULT_FUNCTION_PATTERNS = [
|
|
|
10
11
|
'func',
|
|
11
12
|
'on[A-Z].*',
|
|
12
13
|
];
|
|
13
|
-
/** Statement containers a type alias can be a direct child of. */
|
|
14
|
-
function statementsOf(node) {
|
|
15
|
-
switch (node.type) {
|
|
16
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
17
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
18
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
19
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
20
|
-
return node.body;
|
|
21
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
22
|
-
return node.consequent;
|
|
23
|
-
default:
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
14
|
/**
|
|
28
15
|
* The type alias a statement declares under `name`, looking through `export`.
|
|
29
16
|
*
|
|
@@ -33,10 +20,7 @@ function statementsOf(node) {
|
|
|
33
20
|
* readable, which says nothing about what the state holds.
|
|
34
21
|
*/
|
|
35
22
|
function typeAliasNamed(statement, name) {
|
|
36
|
-
const declaration =
|
|
37
|
-
statement.declaration
|
|
38
|
-
? statement.declaration
|
|
39
|
-
: statement;
|
|
23
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
40
24
|
return declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
41
25
|
declaration.id.name === name
|
|
42
26
|
? declaration
|
|
@@ -56,20 +40,15 @@ function typeAliasNamed(statement, name) {
|
|
|
56
40
|
* hoisting of type declarations.
|
|
57
41
|
*/
|
|
58
42
|
function resolveTypeAlias(from, name) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const alias = typeAliasNamed(statement, name);
|
|
65
|
-
if (alias) {
|
|
66
|
-
return alias;
|
|
67
|
-
}
|
|
43
|
+
return (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => {
|
|
44
|
+
for (const statement of statements) {
|
|
45
|
+
const alias = typeAliasNamed(statement, name);
|
|
46
|
+
if (alias) {
|
|
47
|
+
return alias;
|
|
68
48
|
}
|
|
69
49
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
50
|
+
return undefined;
|
|
51
|
+
});
|
|
73
52
|
}
|
|
74
53
|
/**
|
|
75
54
|
* Returns true if the TSTypeAnnotation node (from useState's type parameter)
|
|
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
7
7
|
const importRemoval_1 = require("../utils/importRemoval");
|
|
8
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
8
9
|
const defaultOptions = {
|
|
9
10
|
allowRecursiveFunctions: true,
|
|
10
11
|
allowOverloadedFunctions: true,
|
|
@@ -401,24 +402,6 @@ function collectReturnIdentifierNames(fn, visitorKeys) {
|
|
|
401
402
|
}
|
|
402
403
|
return names;
|
|
403
404
|
}
|
|
404
|
-
/**
|
|
405
|
-
* The statement list a container holds, or undefined for a node that holds
|
|
406
|
-
* none. These are every place a function can be declared as a direct child, so
|
|
407
|
-
* walking them outward reproduces TypeScript's own scope chain.
|
|
408
|
-
*/
|
|
409
|
-
function statementsOf(node) {
|
|
410
|
-
switch (node.type) {
|
|
411
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
412
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
413
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
414
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
415
|
-
return node.body;
|
|
416
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
417
|
-
return node.consequent;
|
|
418
|
-
default:
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
405
|
/**
|
|
423
406
|
* What a statement list binds each of its names to: the function when the name
|
|
424
407
|
* denotes one, `null` when it denotes anything else.
|
|
@@ -437,11 +420,7 @@ function scopeBindings(statements) {
|
|
|
437
420
|
for (const rawStatement of statements) {
|
|
438
421
|
// `export function f() {}` is the same declaration one AST node deeper, and
|
|
439
422
|
// the `export` keyword alone cannot decide whether a name is resolvable.
|
|
440
|
-
const statement = (
|
|
441
|
-
rawStatement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) &&
|
|
442
|
-
rawStatement.declaration
|
|
443
|
-
? rawStatement.declaration
|
|
444
|
-
: rawStatement;
|
|
423
|
+
const statement = (0, lexicalScope_1.declarationOf)(rawStatement);
|
|
445
424
|
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && statement.id) {
|
|
446
425
|
bind(statement.id.name, statement.body ? statement : null);
|
|
447
426
|
continue;
|
|
@@ -510,20 +489,14 @@ function createReturnCycleResolver(visitorKeys) {
|
|
|
510
489
|
bindings.set(container, computed);
|
|
511
490
|
return computed;
|
|
512
491
|
};
|
|
513
|
-
const resolveFunctionInScope = (from, name) => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
current = current.parent;
|
|
524
|
-
}
|
|
525
|
-
return undefined;
|
|
526
|
-
};
|
|
492
|
+
const resolveFunctionInScope = (from, name) => (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements, container) => {
|
|
493
|
+
const scope = bindingsOf(container, statements);
|
|
494
|
+
if (!scope.has(name))
|
|
495
|
+
return undefined;
|
|
496
|
+
// A non-function binding still shadows a same-named outer function, so
|
|
497
|
+
// it ends the search without yielding one.
|
|
498
|
+
return scope.get(name) ?? lexicalScope_1.BOUND_UNPROVABLE;
|
|
499
|
+
});
|
|
527
500
|
const edgesOf = (fn) => {
|
|
528
501
|
const cached = edges.get(fn);
|
|
529
502
|
if (cached)
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noFirestoreObjectArrays = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
6
7
|
const PRIMITIVE_TYPES = new Set([
|
|
7
8
|
'string',
|
|
8
9
|
'number',
|
|
@@ -14,36 +15,6 @@ const PRIMITIVE_TYPES = new Set([
|
|
|
14
15
|
'GeoPoint',
|
|
15
16
|
]);
|
|
16
17
|
const UNKNOWN_FIELD_LABEL = '<unknown field>';
|
|
17
|
-
/** Statements a declaration can be a direct child of, innermost outward. */
|
|
18
|
-
const statementsOf = (node) => {
|
|
19
|
-
switch (node.type) {
|
|
20
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
21
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
22
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
23
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
24
|
-
return node.body;
|
|
25
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
26
|
-
return node.consequent;
|
|
27
|
-
default:
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* The declaration a statement carries, looking through `export`.
|
|
33
|
-
*
|
|
34
|
-
* `export type Comment = ...` is the same declaration one AST node deeper,
|
|
35
|
-
* inside an `ExportNamedDeclaration`. Reading the statement without unwrapping
|
|
36
|
-
* would let the `export` keyword alone decide whether an element type is
|
|
37
|
-
* resolvable, which says nothing about the shape it denotes.
|
|
38
|
-
*/
|
|
39
|
-
const declarationOf = (statement) => {
|
|
40
|
-
if ((statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
41
|
-
statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) &&
|
|
42
|
-
statement.declaration) {
|
|
43
|
-
return statement.declaration;
|
|
44
|
-
}
|
|
45
|
-
return statement;
|
|
46
|
-
};
|
|
47
18
|
/**
|
|
48
19
|
* Interface beats enum beats alias, matching the order the file-wide tables are
|
|
49
20
|
* consulted in. The orderings can only differ for a name declared twice in one
|
|
@@ -55,7 +26,7 @@ const typeDeclarationIn = (statements, name) => {
|
|
|
55
26
|
let enumMatch;
|
|
56
27
|
let aliasMatch;
|
|
57
28
|
for (const statement of statements) {
|
|
58
|
-
const declaration = declarationOf(statement);
|
|
29
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
59
30
|
switch (declaration.type) {
|
|
60
31
|
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
61
32
|
if (declaration.id.name === name)
|
|
@@ -146,13 +117,13 @@ const unwrapExpressionAssertions = (node) => {
|
|
|
146
117
|
};
|
|
147
118
|
/**
|
|
148
119
|
* A same-named `const` binding shadows any outer one whether or not it holds an
|
|
149
|
-
* array literal, so the lexical search must stop at it
|
|
150
|
-
* it to a binding the reference
|
|
120
|
+
* array literal, so the lexical search must stop at it — reported as
|
|
121
|
+
* `BOUND_UNPROVABLE` — rather than reaching past it to a binding the reference
|
|
122
|
+
* cannot denote, or falling back to the file-wide table.
|
|
151
123
|
*/
|
|
152
|
-
const CONST_ARRAY_SHADOWED = 'shadowed';
|
|
153
124
|
const constArrayIn = (statements, name) => {
|
|
154
125
|
for (const statement of statements) {
|
|
155
|
-
const declaration = declarationOf(statement);
|
|
126
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
156
127
|
if (declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
157
128
|
declaration.kind !== 'const') {
|
|
158
129
|
continue;
|
|
@@ -163,11 +134,11 @@ const constArrayIn = (statements, name) => {
|
|
|
163
134
|
continue;
|
|
164
135
|
}
|
|
165
136
|
if (!declarator.init)
|
|
166
|
-
return
|
|
137
|
+
return lexicalScope_1.BOUND_UNPROVABLE;
|
|
167
138
|
const init = unwrapExpressionAssertions(declarator.init);
|
|
168
139
|
return init.type === utils_1.AST_NODE_TYPES.ArrayExpression
|
|
169
140
|
? init
|
|
170
|
-
:
|
|
141
|
+
: lexicalScope_1.BOUND_UNPROVABLE;
|
|
171
142
|
}
|
|
172
143
|
}
|
|
173
144
|
return undefined;
|
|
@@ -395,39 +366,17 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
395
366
|
* declaration written below its own reference still resolves, matching
|
|
396
367
|
* TypeScript's hoisting of type declarations.
|
|
397
368
|
*/
|
|
398
|
-
const resolveTypeName = (from, name) => {
|
|
399
|
-
let current = from;
|
|
400
|
-
while (current) {
|
|
401
|
-
const statements = statementsOf(current);
|
|
402
|
-
if (statements) {
|
|
403
|
-
const declared = typeDeclarationIn(statements, name);
|
|
404
|
-
if (declared)
|
|
405
|
-
return declared;
|
|
406
|
-
}
|
|
407
|
-
current = current.parent;
|
|
408
|
-
}
|
|
369
|
+
const resolveTypeName = (from, name) => (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => typeDeclarationIn(statements, name), () => {
|
|
409
370
|
if (interfaceNames.has(name))
|
|
410
371
|
return { kind: 'interface' };
|
|
411
372
|
if (enumNames.has(name))
|
|
412
373
|
return { kind: 'enum' };
|
|
413
374
|
const aliased = aliasNameToType.get(name);
|
|
414
|
-
return aliased
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const statements = statementsOf(current);
|
|
420
|
-
if (statements) {
|
|
421
|
-
const declared = constArrayIn(statements, name);
|
|
422
|
-
if (declared === CONST_ARRAY_SHADOWED)
|
|
423
|
-
return undefined;
|
|
424
|
-
if (declared)
|
|
425
|
-
return declared;
|
|
426
|
-
}
|
|
427
|
-
current = current.parent;
|
|
428
|
-
}
|
|
429
|
-
return constArrayNameToLiteral.get(name);
|
|
430
|
-
};
|
|
375
|
+
return aliased
|
|
376
|
+
? { kind: 'alias', typeAnnotation: aliased }
|
|
377
|
+
: undefined;
|
|
378
|
+
});
|
|
379
|
+
const resolveConstArray = (from, name) => (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => constArrayIn(statements, name), () => constArrayNameToLiteral.get(name));
|
|
431
380
|
// Keyed by resolved declaration rather than by name: one name can denote
|
|
432
381
|
// different declarations in different scopes, so a name-keyed memo would
|
|
433
382
|
// answer for whichever scope asked first.
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferBatchOperations = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
6
7
|
const SETTER_METHODS = new Set(['set', 'overwrite']);
|
|
7
8
|
const ARRAY_METHODS = new Set([
|
|
8
9
|
'map',
|
|
@@ -261,20 +262,6 @@ function isMapInstance(node) {
|
|
|
261
262
|
}
|
|
262
263
|
return false;
|
|
263
264
|
}
|
|
264
|
-
/** Statements a declaration can be a direct child of, innermost outward. */
|
|
265
|
-
function statementsOf(node) {
|
|
266
|
-
switch (node.type) {
|
|
267
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
268
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
269
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
270
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
271
|
-
return node.body;
|
|
272
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
273
|
-
return node.consequent;
|
|
274
|
-
default:
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
265
|
/**
|
|
279
266
|
* Lexical lookup of a local declaration, innermost scope outward.
|
|
280
267
|
*
|
|
@@ -285,28 +272,20 @@ function statementsOf(node) {
|
|
|
285
272
|
* searched instead, and the first match wins, which is what shadowing requires.
|
|
286
273
|
*/
|
|
287
274
|
function findVariableDeclaration(node, varName) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
298
|
-
continue;
|
|
299
|
-
for (const decl of declaration.declarations) {
|
|
300
|
-
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
301
|
-
decl.id.name === varName) {
|
|
302
|
-
return decl;
|
|
303
|
-
}
|
|
275
|
+
return (0, lexicalScope_1.resolveInEnclosingScopes)(node, (statements) => {
|
|
276
|
+
for (const statement of statements) {
|
|
277
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
278
|
+
if (declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
279
|
+
continue;
|
|
280
|
+
for (const decl of declaration.declarations) {
|
|
281
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
282
|
+
decl.id.name === varName) {
|
|
283
|
+
return decl;
|
|
304
284
|
}
|
|
305
285
|
}
|
|
306
286
|
}
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
return undefined;
|
|
287
|
+
return undefined;
|
|
288
|
+
});
|
|
310
289
|
}
|
|
311
290
|
function isSetterMethodCall(node) {
|
|
312
291
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
@@ -431,19 +410,19 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
431
410
|
});
|
|
432
411
|
}
|
|
433
412
|
}
|
|
434
|
-
/**
|
|
435
|
-
* For array methods, report on the first occurrence — one syntactic
|
|
436
|
-
* call inside a `map` callback still runs once per element.
|
|
437
|
-
*
|
|
438
|
-
* A direct `Promise.all([...])` is not that: its elements are written
|
|
439
|
-
* out, so a lone `set()` is a single write and the docs call it
|
|
440
|
-
* valid. `findLoopNode` stamps `isArrayMethod: 'map'` onto the
|
|
441
|
-
* Promise.all result, which used to route it here and report on the
|
|
442
|
-
* first call — and, by adding the node to `reportedLoops`, made the
|
|
443
|
-
* deferred second-occurrence branch below unreachable for every
|
|
444
|
-
* input. Excluding it hands the decision back to that branch.
|
|
445
|
-
*/
|
|
446
413
|
else if (loopInfo.isArrayMethod && !loopInfo.isPromiseAll) {
|
|
414
|
+
/**
|
|
415
|
+
* For array methods, report on the first occurrence — one syntactic
|
|
416
|
+
* call inside a `map` callback still runs once per element.
|
|
417
|
+
*
|
|
418
|
+
* A direct `Promise.all([...])` is not that: its elements are written
|
|
419
|
+
* out, so a lone `set()` is a single write and the docs call it
|
|
420
|
+
* valid. `findLoopNode` stamps `isArrayMethod: 'map'` onto the
|
|
421
|
+
* Promise.all result, which used to route it here and report on the
|
|
422
|
+
* first call — and, by adding the node to `reportedLoops`, made the
|
|
423
|
+
* deferred second-occurrence branch below unreachable for every
|
|
424
|
+
* input. Excluding it hands the decision back to that branch.
|
|
425
|
+
*/
|
|
447
426
|
if (!reportedLoops.has(loopInfo.node)) {
|
|
448
427
|
reportedLoops.add(loopInfo.node);
|
|
449
428
|
context.report({
|