@blumintinc/eslint-plugin-blumint 1.20.118 → 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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.118',
226
+ version: '1.20.119',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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 = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
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
- let current = from;
87
- while (current) {
88
- const statements = statementsOf(current);
89
- if (statements) {
90
- for (const statement of statements) {
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
- current = current.parent;
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;
@@ -225,10 +212,7 @@ function initializesFirestore(declarator) {
225
212
  * "find the in-file declaration that carries the evidence" purpose.
226
213
  */
227
214
  function declaresFirestoreInstance(statement) {
228
- const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
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
- let current = node;
248
- while (current) {
249
- const statements = statementsOf(current);
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',
@@ -303,16 +281,13 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
303
281
  }
304
282
  const classes = new Map();
305
283
  for (const statement of statements) {
306
- const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
307
- statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
308
- ? statement.declaration
309
- : statement;
310
- if (declaration?.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
284
+ const declaration = (0, lexicalScope_1.declarationOf)(statement);
285
+ if (declaration.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
311
286
  declaration.id) {
312
287
  classes.set(declaration.id.name, declaration.body);
313
288
  continue;
314
289
  }
315
- if (declaration?.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
290
+ if (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
316
291
  for (const declarator of declaration.declarations) {
317
292
  if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
318
293
  declarator.init?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
@@ -340,18 +315,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
340
315
  * the shadow.
341
316
  */
342
317
  function resolveClassBody(name, reference) {
343
- let current = reference;
344
- while (current) {
345
- const statements = statementsOf(current);
346
- if (statements) {
347
- const found = containerClasses(current, statements).get(name);
348
- if (found) {
349
- return found;
350
- }
351
- }
352
- current = current.parent;
353
- }
354
- return null;
318
+ return ((0, lexicalScope_1.resolveInEnclosingScopes)(reference, (statements, container) => containerClasses(container, statements).get(name)) ?? null);
355
319
  }
356
320
  function enclosingClassBody(node) {
357
321
  let current = node.parent;
@@ -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 = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
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
- let current = from;
60
- while (current) {
61
- const statements = statementsOf(current);
62
- if (statements) {
63
- for (const statement of statements) {
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
- current = current.parent;
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 = (rawStatement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
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
- let current = from;
515
- while (current) {
516
- const statements = statementsOf(current);
517
- if (statements) {
518
- const scope = bindingsOf(current, statements);
519
- if (scope.has(name)) {
520
- return scope.get(name) ?? undefined;
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 rather than reaching past
150
- * it to a binding the reference cannot denote.
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 CONST_ARRAY_SHADOWED;
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
- : CONST_ARRAY_SHADOWED;
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 ? { kind: 'alias', typeAnnotation: aliased } : undefined;
415
- };
416
- const resolveConstArray = (from, name) => {
417
- let current = from;
418
- while (current) {
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
- let current = node;
289
- while (current) {
290
- const statements = statementsOf(current);
291
- if (statements) {
292
- for (const statement of statements) {
293
- const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
294
- statement.declaration
295
- ? statement.declaration
296
- : statement;
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
- current = current.parent;
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({