@blumintinc/eslint-plugin-blumint 1.14.0 → 1.16.0

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.
Files changed (68) hide show
  1. package/README.md +36 -14
  2. package/lib/index.js +20 -12
  3. package/lib/rules/avoid-utils-directory.js +0 -4
  4. package/lib/rules/consistent-callback-naming.js +68 -3
  5. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  6. package/lib/rules/dynamic-https-errors.js +119 -49
  7. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
  9. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  10. package/lib/rules/enforce-date-ttime.js +156 -0
  11. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  12. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  13. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  14. package/lib/rules/enforce-dynamic-imports.js +42 -21
  15. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  16. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  17. package/lib/rules/enforce-global-constants.js +3 -3
  18. package/lib/rules/enforce-id-capitalization.js +1 -1
  19. package/lib/rules/enforce-memoize-async.js +66 -15
  20. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  21. package/lib/rules/enforce-props-argument-name.js +42 -16
  22. package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
  23. package/lib/rules/enforce-transform-memoization.js +1 -1
  24. package/lib/rules/enforce-verb-noun-naming.js +3817 -4641
  25. package/lib/rules/global-const-style.js +25 -4
  26. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  27. package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
  28. package/lib/rules/memo-nested-react-components.js +243 -103
  29. package/lib/rules/no-array-length-in-deps.js +74 -3
  30. package/lib/rules/no-async-foreach.js +7 -2
  31. package/lib/rules/no-circular-references.d.ts +2 -1
  32. package/lib/rules/no-circular-references.js +150 -489
  33. package/lib/rules/no-compositing-layer-props.js +31 -0
  34. package/lib/rules/no-console-error.js +12 -10
  35. package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
  36. package/lib/rules/no-entire-object-hook-deps.js +147 -65
  37. package/lib/rules/no-excessive-parent-chain.js +3 -0
  38. package/lib/rules/no-explicit-return-type.js +6 -0
  39. package/lib/rules/no-hungarian.js +119 -24
  40. package/lib/rules/no-inline-component-prop.js +16 -7
  41. package/lib/rules/no-margin-properties.js +7 -38
  42. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  43. package/lib/rules/no-passthrough-getters.js +83 -1
  44. package/lib/rules/no-redundant-this-params.js +50 -1
  45. package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
  46. package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
  47. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  48. package/lib/rules/no-unused-props.js +215 -37
  49. package/lib/rules/no-useless-fragment.js +10 -2
  50. package/lib/rules/parallelize-async-operations.js +117 -54
  51. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
  52. package/lib/rules/prefer-params-over-parent-id.js +1 -1
  53. package/lib/rules/prefer-settings-object.js +27 -10
  54. package/lib/rules/prefer-type-alias-over-typeof-constant.js +75 -4
  55. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  56. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  57. package/lib/rules/prevent-children-clobber.js +9 -5
  58. package/lib/rules/react-memoize-literals.js +218 -13
  59. package/lib/rules/require-https-error-cause.js +30 -11
  60. package/lib/rules/require-memo.js +17 -9
  61. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  62. package/lib/rules/require-migration-script-metadata.js +206 -0
  63. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  64. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  65. package/lib/utils/ASTHelpers.d.ts +49 -1
  66. package/lib/utils/ASTHelpers.js +394 -112
  67. package/package.json +7 -6
  68. package/release-manifest.json +166 -0
@@ -63,6 +63,77 @@ const COMMON_PREPOSITION_SUFFIXES = new Set([
63
63
  'Async',
64
64
  'Sync',
65
65
  ]);
66
+ /**
67
+ * Phrasal-verb particles that fuse with a preceding past participle to form an
68
+ * inseparable state adjective (e.g. "signed in", "logged in", "opted in",
69
+ * "logged out", "zoomed in"). When such a particle is the trailing suffix AND
70
+ * the token before it is a past participle, the ending is NOT a redundant
71
+ * verb-preposition action suffix — it is a single adjective describing state.
72
+ */
73
+ const PHRASAL_PARTICLES = new Set(['In', 'On', 'Out', 'Up', 'Off', 'Down']);
74
+ /**
75
+ * Verb stems that fuse with a phrasal particle into an established phrasal verb
76
+ * where the particle is inseparable (e.g. "signIn", "logOut", "optIn",
77
+ * "checkIn"). Matched in base form ("signIn", "useGuardSignIn") and
78
+ * past-participle form ("signedIn", "loggedOut", "droppedIn"). A particle
79
+ * preceded by a NOUN object instead — "searchItemsIn", "processEventOn",
80
+ * "loadEmbedIn", "isWidgetIn" — is a genuine redundant verb-preposition suffix
81
+ * and stays flagged. Extend this set when a new phrasal verb appears.
82
+ */
83
+ const PHRASAL_VERB_STEMS = new Set([
84
+ 'sign',
85
+ 'log',
86
+ 'opt',
87
+ 'check',
88
+ 'zoom',
89
+ 'drop',
90
+ 'shut',
91
+ 'turn',
92
+ 'switch',
93
+ 'scroll',
94
+ ]);
95
+ /**
96
+ * Resolves the lowercased phrasal-verb stem of a final camelCase word, or null
97
+ * when it is not a known phrasal verb. Handles the base form ("sign"), the
98
+ * regular past participle ("signed" → "sign"), and the doubled-consonant
99
+ * participle ("dropped" → "drop"). A noun that merely ends in "ed" (e.g.
100
+ * "embed" → "emb", "shed" → "sh") resolves to null, so it stays flagged — this
101
+ * is what keeps a leaky "ends in ed" heuristic from exempting genuine targets.
102
+ */
103
+ function phrasalVerbStem(lastWord) {
104
+ const word = lastWord.toLowerCase();
105
+ if (PHRASAL_VERB_STEMS.has(word)) {
106
+ return word;
107
+ }
108
+ if (word.endsWith('ed')) {
109
+ let stem = word.slice(0, -2);
110
+ if (stem.length >= 2 && stem[stem.length - 1] === stem[stem.length - 2]) {
111
+ stem = stem.slice(0, -1);
112
+ }
113
+ if (PHRASAL_VERB_STEMS.has(stem)) {
114
+ return stem;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Returns true when the trailing `suffix` of `name` is a phrasal-verb particle
121
+ * fused to its verb — a past-participle adjective ("signedIn", "loggedOut") or a
122
+ * base-form phrasal verb ("signIn", "logOut"). The word immediately before the
123
+ * particle must be a KNOWN phrasal verb (via phrasalVerbStem), not merely any
124
+ * word ending in "ed"; that keeps redundant suffixes where a noun object
125
+ * precedes the particle ("loadEmbedIn", "isWidgetIn", "searchItemsIn") flagged.
126
+ * This single check also covers boolean predicates: "isSignedIn" resolves the
127
+ * pre-particle "signed" → "sign", while "isWidgetIn" resolves "widget" → null.
128
+ */
129
+ function isPhrasalVerbEnding(name, suffix) {
130
+ if (!PHRASAL_PARTICLES.has(suffix)) {
131
+ return false;
132
+ }
133
+ const beforeSuffix = name.substring(0, name.length - suffix.length);
134
+ const lastWord = beforeSuffix.match(/[A-Z]?[a-z]+$/)?.[0] ?? '';
135
+ return phrasalVerbStem(lastWord) !== null;
136
+ }
66
137
  exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
67
138
  name: 'no-unnecessary-verb-suffix',
68
139
  meta: {
@@ -94,6 +165,14 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
94
165
  // Skip if the suggestion would be empty or just a single character
95
166
  if (suggestion.length <= 1)
96
167
  continue;
168
+ // Skip phrasal-verb endings (e.g. past-participle adjectives
169
+ // "signedIn"/"loggedOut" or compound phrasal verbs "signIn"/
170
+ // "logOut"): the trailing particle fuses with its verb, so stripping
171
+ // it ("signed", "sign") destroys the meaning. The pre-particle word
172
+ // must be a known phrasal verb, so noun-object endings like
173
+ // "loadEmbedIn"/"isWidgetIn" remain flagged.
174
+ if (isPhrasalVerbEnding(name, suffix))
175
+ continue;
97
176
  context.report({
98
177
  node,
99
178
  messageId: 'unnecessaryVerbSuffix',
@@ -118,6 +118,45 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
118
118
  }
119
119
  return null;
120
120
  };
121
+ /**
122
+ * Resolves the `*Props` type name from a parameter's type-annotation node.
123
+ * Unwraps parenthesized types and descends through generic wrapper type
124
+ * arguments (e.g. `Readonly<Foo>`, `Partial<...>`, `Readonly<Partial<Foo>>`)
125
+ * to find the underlying `*Props` reference. Returns the first `*Props`
126
+ * identifier found, or `null`. Uses the same `endsWith('Props')` heuristic
127
+ * as the direct path so forward-declared Props types still resolve.
128
+ */
129
+ const resolvePropsTypeName = (typeNode, visited = new Set()) => {
130
+ if (!typeNode || visited.has(typeNode)) {
131
+ return null;
132
+ }
133
+ visited.add(typeNode);
134
+ if (isTSParenthesizedType(typeNode)) {
135
+ return resolvePropsTypeName(typeNode.typeAnnotation, visited);
136
+ }
137
+ if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
138
+ return null;
139
+ }
140
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
141
+ typeNode.typeName.name.endsWith('Props')) {
142
+ return typeNode.typeName.name;
143
+ }
144
+ // Generic wrapper (Readonly/Partial/etc.): descend into its type arguments
145
+ // to find the nested *Props reference. typeParameters is the v5 AST shape;
146
+ // typeArguments is the v6+ shape — handle both for forward compatibility.
147
+ const typeArgs = typeNode.typeParameters ??
148
+ typeNode
149
+ .typeArguments;
150
+ if (typeArgs) {
151
+ for (const arg of typeArgs.params) {
152
+ const resolved = resolvePropsTypeName(arg, visited);
153
+ if (resolved) {
154
+ return resolved;
155
+ }
156
+ }
157
+ }
158
+ return null;
159
+ };
121
160
  const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used, knownProps) => {
122
161
  const spreadTypeProps = spreadTypeToPropNames.get(spreadTypeName);
123
162
  if (!spreadTypeProps || spreadTypeProps.size === 0) {
@@ -170,6 +209,131 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
170
209
  }
171
210
  return false;
172
211
  };
212
+ /**
213
+ * Records which props an ObjectPattern consumes into `used`. A rest element
214
+ * (`...rest`) consumes all remaining props, so it flips `restUsed` and also
215
+ * marks spread-type keys used. Renamed bindings (`{ a: localA }`) still mark
216
+ * the original prop name `a`. Returns whether a rest element was present.
217
+ */
218
+ const collectUsedFromObjectPattern = (pattern, typeName, used) => {
219
+ let restUsed = false;
220
+ pattern.properties.forEach((prop) => {
221
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
222
+ const propName = getStaticPropName(prop.key);
223
+ if (propName) {
224
+ used.add(propName);
225
+ }
226
+ }
227
+ else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
228
+ prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
229
+ // Rest spread (`{...rest}`): all remaining props are forwarded ⇒ used.
230
+ // Spread-type markers are also flagged so the spread-forwarding skip
231
+ // applies; remaining plain props are handled at report time via
232
+ // `restUsed` (the props map may be incomplete here on forward refs).
233
+ restUsed = true;
234
+ const propsType = propsTypes.get(typeName);
235
+ if (propsType) {
236
+ Object.keys(propsType).forEach((key) => {
237
+ if (key.startsWith('...')) {
238
+ used.add(key);
239
+ }
240
+ });
241
+ }
242
+ }
243
+ });
244
+ return restUsed;
245
+ };
246
+ /**
247
+ * Whether an Identifier node is a value-position *reference* to a variable
248
+ * (vs. a declaration binding, an object-property key, or the property of a
249
+ * member expression like `foo.paramName`). Used to detect opaque param use.
250
+ */
251
+ const isParamReference = (parent, parentKey) => {
252
+ if (!parent) {
253
+ return false;
254
+ }
255
+ // `foo.paramName` — the identifier is the member's property, not the param.
256
+ if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
257
+ parentKey === 'property' &&
258
+ !parent.computed) {
259
+ return false;
260
+ }
261
+ // Non-shorthand object property key: `{ paramName: x }`.
262
+ if (parent.type === utils_1.AST_NODE_TYPES.Property &&
263
+ parentKey === 'key' &&
264
+ !parent.computed) {
265
+ return false;
266
+ }
267
+ // A binding position (declaration id, function param, etc.).
268
+ if ((parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
269
+ parentKey === 'id') ||
270
+ parentKey === 'params') {
271
+ return false;
272
+ }
273
+ return true;
274
+ };
275
+ const collectBodyDestructuring = (body, paramName, typeName, used) => {
276
+ const result = {
277
+ destructureCount: 0,
278
+ restUsed: false,
279
+ hasOpaqueUsage: false,
280
+ };
281
+ const declaresParamName = (declarators) => declarators.some((d) => d.id.type === utils_1.AST_NODE_TYPES.Identifier && d.id.name === paramName);
282
+ /** Identifier nodes that are the `init` of a `const {} = props` declarator. */
283
+ const destructureInits = new Set();
284
+ const visit = (node, parent, parentKey) => {
285
+ if (!node || typeof node.type !== 'string') {
286
+ return;
287
+ }
288
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
289
+ if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
290
+ node.init?.type === utils_1.AST_NODE_TYPES.Identifier &&
291
+ node.init.name === paramName) {
292
+ destructureInits.add(node.init);
293
+ result.destructureCount += 1;
294
+ if (collectUsedFromObjectPattern(node.id, typeName, used)) {
295
+ result.restUsed = true;
296
+ }
297
+ }
298
+ }
299
+ // A bare reference to the param that is NOT a destructuring init is an
300
+ // opaque consumption (member access, spread, argument, return, etc.).
301
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
302
+ node.name === paramName &&
303
+ !destructureInits.has(node) &&
304
+ isParamReference(parent, parentKey)) {
305
+ result.hasOpaqueUsage = true;
306
+ }
307
+ // Stop descending into nested functions that re-bind `paramName`, since
308
+ // that shadowing binding refers to a different variable.
309
+ if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
310
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
311
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) &&
312
+ node.params.some((p) => p.type === utils_1.AST_NODE_TYPES.Identifier && p.name === paramName)) {
313
+ return;
314
+ }
315
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
316
+ declaresParamName(node.declarations)) {
317
+ // A `const <paramName> = ...` rebind shadows the param from here on.
318
+ return;
319
+ }
320
+ for (const key of Object.keys(node)) {
321
+ if (key === 'parent') {
322
+ continue;
323
+ }
324
+ const value = node[key];
325
+ if (Array.isArray(value)) {
326
+ value.forEach((child) => visit(child, node, key));
327
+ }
328
+ else if (value &&
329
+ typeof value.type === 'string') {
330
+ visit(value, node, key);
331
+ }
332
+ }
333
+ };
334
+ visit(body, null, null);
335
+ return result;
336
+ };
173
337
  const reportUnusedProps = (typeName, used, restUsed) => {
174
338
  const propsType = propsTypes.get(typeName);
175
339
  if (!propsType) {
@@ -190,6 +354,11 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
190
354
  if (isGenericTypeSpread(prop)) {
191
355
  shouldReport = false;
192
356
  }
357
+ else if (restUsed) {
358
+ // An explicit rest element (`{ a, ...rest }`) captures every prop not
359
+ // named before it, so the remaining props are forwarded ⇒ used.
360
+ shouldReport = false;
361
+ }
193
362
  else if (prop.startsWith('...')) {
194
363
  shouldReport = !shouldSkipSpreadType(prop, hasRestSpread, used, knownProps);
195
364
  }
@@ -590,45 +759,54 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
590
759
  }
591
760
  },
592
761
  VariableDeclaration(node) {
593
- if (node.declarations.length === 1) {
594
- const declaration = node.declarations[0];
595
- if (declaration.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
596
- const param = declaration.init.params[0];
597
- if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
598
- param.typeAnnotation?.typeAnnotation.type ===
599
- utils_1.AST_NODE_TYPES.TSTypeReference &&
600
- param.typeAnnotation.typeAnnotation.typeName.type ===
601
- utils_1.AST_NODE_TYPES.Identifier) {
602
- const typeName = param.typeAnnotation.typeAnnotation.typeName.name;
603
- if (typeName.endsWith('Props')) {
604
- const used = new Set();
605
- let restUsed = false;
606
- param.properties.forEach((prop) => {
607
- if (prop.type === utils_1.AST_NODE_TYPES.Property) {
608
- const propName = getStaticPropName(prop.key);
609
- if (propName) {
610
- used.add(propName);
611
- }
612
- }
613
- else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
614
- prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
615
- // Handle rest spread operator {...rest}
616
- // When a rest operator is used, all remaining props are considered used
617
- restUsed = true;
618
- const propsType = propsTypes.get(typeName);
619
- if (propsType) {
620
- Object.keys(propsType).forEach((key) => {
621
- if (key.startsWith('...')) {
622
- used.add(key);
623
- }
624
- });
625
- }
626
- }
627
- });
628
- currentComponent = { node, typeName, used, restUsed };
629
- }
762
+ if (node.declarations.length !== 1) {
763
+ return;
764
+ }
765
+ const declaration = node.declarations[0];
766
+ if (declaration.init?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
767
+ return;
768
+ }
769
+ const fn = declaration.init;
770
+ const param = fn.params[0];
771
+ if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
772
+ // Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
773
+ // as the plain `FooProps` annotation.
774
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
775
+ if (typeName) {
776
+ const used = new Set();
777
+ const restUsed = collectUsedFromObjectPattern(param, typeName, used);
778
+ currentComponent = { node, typeName, used, restUsed };
779
+ }
780
+ return;
781
+ }
782
+ if (param?.type === utils_1.AST_NODE_TYPES.Identifier) {
783
+ // Identifier param (`props: FooProps`): the destructuring happens in
784
+ // the body. Resolve the Props type (unwrapping generic wrappers) and
785
+ // scan the body for `const { ... } = props`.
786
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
787
+ if (!typeName) {
788
+ return;
789
+ }
790
+ const used = new Set();
791
+ const { destructureCount, restUsed, hasOpaqueUsage } = collectBodyDestructuring(fn.body, param.name, typeName, used);
792
+ // Only check when the body destructures the param at least once.
793
+ // Member access (`props.x`), spread (`{...props}`), passing `props`
794
+ // whole, or never referencing it (e.g. `_props`) is left to prior
795
+ // behavior (no report) — those usages can't be enumerated reliably.
796
+ if (destructureCount === 0) {
797
+ return;
798
+ }
799
+ // Mixed usage (some destructure + opaque consumption) forwards the
800
+ // remaining props, so treat every prop as used to avoid false reports.
801
+ if (hasOpaqueUsage) {
802
+ const propsType = propsTypes.get(typeName);
803
+ if (propsType) {
804
+ Object.keys(propsType).forEach((key) => used.add(key));
630
805
  }
806
+ currentComponent = { node, typeName, used, restUsed: true };
807
+ return;
631
808
  }
809
+ currentComponent = { node, typeName, used, restUsed };
632
810
  }
633
811
  },
634
812
  'VariableDeclaration:exit'(node) {
@@ -13,8 +13,6 @@ const describeChild = (child) => {
13
13
  return 'JSX element';
14
14
  case 'JSXFragment':
15
15
  return 'fragment';
16
- case 'JSXExpressionContainer':
17
- return 'expression result';
18
16
  case 'JSXText':
19
17
  return 'text node';
20
18
  case 'JSXSpreadChild':
@@ -29,6 +27,16 @@ exports.noUselessFragment = {
29
27
  JSXFragment(node) {
30
28
  if (node.children.length === 1) {
31
29
  const [child] = node.children;
30
+ /**
31
+ * A fragment whose only child is an expression container — e.g.
32
+ * `<>{portal}</>` — is NOT useless. Unwrapping it to a bare
33
+ * `{portal}` is invalid in statement/return position, and wrapping a
34
+ * single ReactNode expression in a fragment is the idiomatic way to
35
+ * render it. (Mirrors the upstream rule's `allowExpressions`.)
36
+ */
37
+ if (child.type === 'JSXExpressionContainer') {
38
+ return;
39
+ }
32
40
  context.report({
33
41
  node,
34
42
  messageId: 'noUselessFragment',
@@ -13,6 +13,9 @@ const defaultOptions = [
13
13
  'updatethreshold',
14
14
  'setthreshold',
15
15
  'checkthreshold',
16
+ 'commit',
17
+ 'flush',
18
+ 'saveall',
16
19
  ],
17
20
  },
18
21
  ];
@@ -95,81 +98,147 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
95
98
  return null;
96
99
  }
97
100
  /**
98
- * Checks if an identifier is used in a node
101
+ * Generic AST visitor for identifier collection
99
102
  */
100
- function isIdentifierUsedInNode(identifier, node) {
101
- let isUsed = false;
102
- function visit(node) {
103
- if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
104
- node.name === identifier) {
105
- isUsed = true;
106
- return;
103
+ function visitIdentifiers(node, callback, options = {}) {
104
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
105
+ const parent = node.parent;
106
+ // Skip non-computed properties in MemberExpressions as they are not value uses
107
+ if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
108
+ if (parent.property === node &&
109
+ !parent.computed &&
110
+ !options.includeMemberProperties) {
111
+ return false;
112
+ }
113
+ }
114
+ // Skip non-shorthand keys in object literals as they are not value uses
115
+ if (parent && parent.type === utils_1.AST_NODE_TYPES.Property) {
116
+ if (parent.key === node && !parent.computed && !parent.shorthand) {
117
+ return false;
118
+ }
107
119
  }
108
- // Recursively visit all child nodes
109
- for (const key in node) {
110
- if (key === 'parent' || key === 'range' || key === 'loc')
111
- continue;
112
- const child = node[key];
113
- if (child && typeof child === 'object') {
114
- if (Array.isArray(child)) {
115
- for (const item of child) {
116
- if (item && typeof item === 'object' && 'type' in item) {
117
- visit(item);
120
+ if (callback(node.name) === true)
121
+ return true;
122
+ }
123
+ /**
124
+ * Recursively traverses child nodes while skipping 'parent' to avoid
125
+ * circular back-references, and 'range'/'loc' which are metadata.
126
+ */
127
+ for (const key in node) {
128
+ if (key === 'parent' || key === 'range' || key === 'loc')
129
+ continue;
130
+ const child = node[key];
131
+ if (child && typeof child === 'object') {
132
+ if (Array.isArray(child)) {
133
+ for (const item of child) {
134
+ if (item && typeof item === 'object' && 'type' in item) {
135
+ if (visitIdentifiers(item, callback, options)) {
136
+ return true;
118
137
  }
119
138
  }
120
139
  }
121
- else if ('type' in child) {
122
- visit(child);
140
+ }
141
+ else if ('type' in child) {
142
+ if (visitIdentifiers(child, callback, options)) {
143
+ return true;
123
144
  }
124
145
  }
125
146
  }
126
147
  }
127
- visit(node);
128
- return isUsed;
148
+ return false;
149
+ }
150
+ /**
151
+ * Checks if an identifier is used in a node
152
+ */
153
+ function isIdentifierUsedInNode(identifier, node) {
154
+ return visitIdentifiers(node, (name) => name === identifier);
129
155
  }
156
+ /**
157
+ * Extracts all identifiers used in a node
158
+ */
159
+ function getAllIdentifiers(node, options) {
160
+ const identifiers = new Set();
161
+ visitIdentifiers(node, (name) => {
162
+ identifiers.add(name);
163
+ }, options);
164
+ return identifiers;
165
+ }
166
+ /**
167
+ * Matches coordinator-like identifiers. Intentionally uses substring
168
+ * matching (no word boundaries) to catch patterns like "batchManager",
169
+ * "transactionCollector", etc. This may produce false positives for
170
+ * unrelated managers, but errs on the side of safety.
171
+ */
172
+ const COORDINATOR_PATTERN = /batch|manager|collector|transaction|tx|unitofwork|accumulator/i;
130
173
  /**
131
174
  * Checks if there are dependencies between await expressions
132
175
  */
133
176
  function hasDependencies(awaitNodes, variableNames, sideEffectPatterns) {
134
- // If we have fewer than 2 nodes, there are no dependencies to check
135
177
  if (awaitNodes.length < 2) {
136
178
  return false;
137
179
  }
138
- // For each await node (except the first), check if it depends on previous variables
139
- for (let i = 1; i < awaitNodes.length; i++) {
180
+ const allIdentifiers = awaitNodes.map((node) => {
181
+ const awaitExpr = getAwaitExpression(node);
182
+ return awaitExpr
183
+ ? getAllIdentifiers(awaitExpr.argument, {
184
+ includeMemberProperties: true,
185
+ })
186
+ : new Set();
187
+ });
188
+ // Check all nodes for side effects and shared coordinators
189
+ for (let i = 0; i < awaitNodes.length; i++) {
140
190
  const currentNode = awaitNodes[i];
141
- // Check if any previous variable is used in the current await expression
142
- for (const varName of variableNames) {
143
- const awaitExpr = getAwaitExpression(currentNode);
144
- if (awaitExpr &&
145
- isIdentifierUsedInNode(varName, awaitExpr.argument)) {
146
- return true;
147
- }
148
- }
149
- // Check for operations that might have side effects that affect subsequent operations
150
- // This is a conservative heuristic - we only flag very specific patterns
151
191
  const awaitExpr = getAwaitExpression(currentNode);
152
- if (awaitExpr &&
153
- awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
154
- const callee = awaitExpr.argument.callee;
155
- // Check for method calls that might indicate side effects
156
- if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
157
- callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
158
- const methodName = callee.property.name;
159
- if (sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
192
+ if (!awaitExpr)
193
+ continue;
194
+ // 1. Check if current node depends on variables DECLARED in previous awaits
195
+ if (i > 0) {
196
+ for (const varName of variableNames) {
197
+ if (isIdentifierUsedInNode(varName, awaitExpr.argument)) {
160
198
  return true;
161
199
  }
162
200
  }
163
- if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
164
- const functionName = callee.name;
165
- if (sideEffectPatterns.some((pattern) => pattern.test(functionName))) {
166
- return true;
201
+ }
202
+ // 2. Check for shared coordinators between this node and ANY previous node
203
+ if (i > 0) {
204
+ const currentIds = allIdentifiers[i];
205
+ for (const id of currentIds) {
206
+ if (COORDINATOR_PATTERN.test(id)) {
207
+ for (let j = 0; j < i; j++) {
208
+ if (allIdentifiers[j].has(id)) {
209
+ return true;
210
+ }
211
+ }
167
212
  }
168
213
  }
169
214
  }
215
+ // 3. Check for operations that might have side effects
216
+ // If any node has a side effect, we should not parallelize the sequence
217
+ let callExpr = null;
218
+ if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
219
+ callExpr = awaitExpr.argument;
220
+ }
221
+ else if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
222
+ awaitExpr.argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
223
+ callExpr = awaitExpr.argument.expression;
224
+ }
225
+ if (callExpr) {
226
+ const callee = callExpr.callee;
227
+ let methodName = null;
228
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
229
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
230
+ methodName = callee.property.name;
231
+ }
232
+ else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
233
+ methodName = callee.name;
234
+ }
235
+ if (methodName &&
236
+ sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
237
+ return true;
238
+ }
239
+ }
170
240
  }
171
241
  // If any node is a variable declaration with destructuring, consider it as having dependencies
172
- // This is because destructuring often creates variables that are used later
173
242
  for (const node of awaitNodes) {
174
243
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
175
244
  for (const declaration of node.declarations) {
@@ -234,7 +303,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
234
303
  function areInTryCatchBlocks(nodes) {
235
304
  for (const node of nodes) {
236
305
  let current = node;
237
- // Traverse up to find if the node is in a try block
238
306
  while (current && current.parent) {
239
307
  if (current.parent.type === utils_1.AST_NODE_TYPES.TryStatement &&
240
308
  current.parent.block === current) {
@@ -253,7 +321,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
253
321
  function areInLoop(nodes) {
254
322
  for (const node of nodes) {
255
323
  let current = node;
256
- // Traverse up to find if the node is in a loop
257
324
  while (current && current.parent) {
258
325
  if (current.parent.type === utils_1.AST_NODE_TYPES.ForStatement ||
259
326
  current.parent.type === utils_1.AST_NODE_TYPES.ForInStatement ||
@@ -276,14 +343,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
276
343
  if (awaitNodes.length < 2) {
277
344
  return null;
278
345
  }
279
- // Extract the await expressions
280
346
  const awaitExpressions = awaitNodes
281
347
  .map((node) => getAwaitExpression(node))
282
348
  .filter((node) => node !== null);
283
349
  if (awaitExpressions.length < 2) {
284
350
  return null;
285
351
  }
286
- // Get the text of each await argument
287
352
  const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
288
353
  const idsText = [];
289
354
  const declKinds = new Set();
@@ -316,9 +381,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
316
381
  // Simple Promise.all without variable assignments
317
382
  promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
318
383
  }
319
- // Find the start position, accounting for leading comments
320
384
  const startPos = awaitNodes[0].range[0];
321
- // Replace the range from the start of the first await to the end of the last await
322
385
  const endPos = awaitNodes[awaitNodes.length - 1].range[1];
323
386
  return fixer.replaceTextRange([startPos, endPos], promiseAllText);
324
387
  }