@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. package/package.json +3 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noUuidv4Base62AsKey = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
7
8
  name: 'no-uuidv4-base62-as-key',
8
9
  meta: {
@@ -13,13 +14,13 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
13
14
  },
14
15
  schema: [],
15
16
  messages: {
16
- noUuidv4Base62AsKey: 'Do not use uuidv4Base62() to generate keys for elements in a list or loop. ' +
17
- 'These keys are not stable across renders and can cause performance issues. ' +
18
- 'Use a stable identifier from your data instead.',
17
+ noUuidv4Base62AsKey: 'Key "{{keyExpression}}" comes from uuidv4Base62(), which regenerates on every render so React cannot reconcile list items and may remount components, drop local state, or reorder DOM nodes. ' +
18
+ 'Use a stable identifier from your data (for example a database id, slug, or memoized array of ids) instead of generating a new UUID inside render.',
19
19
  },
20
20
  },
21
21
  defaultOptions: [],
22
22
  create(context) {
23
+ const sourceCode = context.sourceCode;
23
24
  // Track imported uuidv4Base62 identifiers
24
25
  const importedUuidv4Base62 = new Set();
25
26
  // Track elements that we've already reported to avoid duplicate reports
@@ -31,14 +32,18 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
31
32
  // Flag to indicate we've seen the itemKeys pattern
32
33
  let hasPreGeneratedKeys = false;
33
34
  // Helper function to report a rule violation while avoiding duplicates
34
- function reportViolation(node, messageId = 'noUuidv4Base62AsKey') {
35
+ function reportViolation(node, keyExpression, messageId = 'noUuidv4Base62AsKey') {
35
36
  // Skip if we've already reported this element
36
37
  if (reportedElements.has(node))
37
38
  return;
38
39
  reportedElements.add(node);
40
+ const keyExpressionText = keyExpression
41
+ ? sourceCode.getText(keyExpression).trim()
42
+ : 'uuidv4Base62()';
39
43
  context.report({
40
44
  node,
41
45
  messageId,
46
+ data: { keyExpression: keyExpressionText },
42
47
  });
43
48
  }
44
49
  // Helper to check if a node is a call to uuidv4Base62()
@@ -117,7 +122,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
117
122
  const { expression } = attr.value;
118
123
  // Direct uuidv4Base62() call in key
119
124
  if (containsUuidV4Base62Call(expression)) {
120
- reportViolation(jsxElement);
125
+ reportViolation(jsxElement, expression);
121
126
  return;
122
127
  }
123
128
  // Check for member expressions (item.key pattern)
@@ -128,12 +133,12 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
128
133
  const objName = expression.object.name;
129
134
  // Check if this variable has been marked as containing keys with uuidv4Base62
130
135
  if (variablesWithUuidv4Base62Keys.has(objName)) {
131
- reportViolation(jsxElement);
136
+ reportViolation(jsxElement, expression);
132
137
  return;
133
138
  }
134
139
  // Special case for the failing 'itemKeys' test
135
140
  if (hasPreGeneratedKeys) {
136
- const ancestors = context.getAncestors();
141
+ const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, jsxElement);
137
142
  // Check if we're inside a map callback
138
143
  for (let i = ancestors.length - 1; i >= 0; i--) {
139
144
  const ancestor = ancestors[i];
@@ -144,7 +149,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
144
149
  ancestor.callee.object.type === utils_1.AST_NODE_TYPES.Identifier) {
145
150
  // If we're mapping over a variable with pre-generated keys
146
151
  if (variablesWithUuidv4Base62Keys.has(ancestor.callee.object.name)) {
147
- reportViolation(jsxElement);
152
+ reportViolation(jsxElement, expression);
148
153
  return;
149
154
  }
150
155
  }
@@ -155,8 +160,8 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
155
160
  }
156
161
  }
157
162
  // Helper to check for react mapping functions
158
- function isInsideMapCallback() {
159
- const ancestors = context.getAncestors();
163
+ function isInsideMapCallback(node) {
164
+ const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
160
165
  for (let i = ancestors.length - 1; i >= 0; i--) {
161
166
  const ancestor = ancestors[i];
162
167
  if (ancestor.type === utils_1.AST_NODE_TYPES.CallExpression &&
@@ -276,7 +281,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
276
281
  },
277
282
  // Check JSX elements for uuidv4Base62 keys
278
283
  JSXElement(node) {
279
- if (!isInsideMapCallback())
284
+ if (!isInsideMapCallback(node))
280
285
  return;
281
286
  if (node.openingElement.attributes) {
282
287
  checkJSXAttributesForUuidv4Base62(node.openingElement.attributes, node);
@@ -284,7 +289,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
284
289
  },
285
290
  // Check JSX fragments for children with uuidv4Base62 keys
286
291
  JSXFragment(node) {
287
- if (!isInsideMapCallback())
292
+ if (!isInsideMapCallback(node))
288
293
  return;
289
294
  for (const child of node.children) {
290
295
  if (child.type === utils_1.AST_NODE_TYPES.JSXElement &&
@@ -336,7 +341,7 @@ exports.noUuidv4Base62AsKey = (0, createRule_1.createRule)({
336
341
  utils_1.AST_NODE_TYPES.Identifier &&
337
342
  attr.value.expression.property.name === 'key') {
338
343
  // The test case - directly report this element
339
- reportViolation(returnExpr);
344
+ reportViolation(returnExpr, attr.value.expression);
340
345
  break;
341
346
  }
342
347
  }
@@ -1,7 +1,8 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type Options = [
2
3
  {
3
4
  allowWithQueryOrHash?: boolean;
4
5
  }
5
6
  ];
6
- export declare const omitIndexHtml: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"omitIndexHtml", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
7
+ export declare const omitIndexHtml: TSESLint.RuleModule<"omitIndexHtml", Options, TSESLint.RuleListener>;
7
8
  export {};
@@ -25,8 +25,26 @@ function hasQueryOrHash(url) {
25
25
  * Removes 'index.html' from a URL and ensures proper trailing slash
26
26
  */
27
27
  function fixUrl(url) {
28
- // Replace /index.html with /
29
- return url.replace(/\/index\.html($|[?#])/, '/$1');
28
+ // Replace /index.html/ with / and /index.html with / at the end or before ?/#/${
29
+ return url
30
+ .replace(/\/index\.html\//g, '/')
31
+ .replace(/\/index\.html(?=(?:$|[?#]|\$\{))/g, '/');
32
+ }
33
+ /**
34
+ * Reconstructs a template literal using cooked values so static checks and
35
+ * reporting align with what runtime sees (escape sequences are resolved).
36
+ */
37
+ function describeTemplateLiteral(node, sourceCode) {
38
+ const parts = [];
39
+ node.quasis.forEach((quasi, index) => {
40
+ const text = quasi.value.cooked ?? quasi.value.raw;
41
+ parts.push(text);
42
+ const expression = node.expressions[index];
43
+ if (expression) {
44
+ parts.push(`\${${sourceCode.getText(expression)}}`);
45
+ }
46
+ });
47
+ return parts.join('');
30
48
  }
31
49
  exports.omitIndexHtml = (0, createRule_1.createRule)({
32
50
  name: 'omit-index-html',
@@ -49,11 +67,12 @@ exports.omitIndexHtml = (0, createRule_1.createRule)({
49
67
  },
50
68
  ],
51
69
  messages: {
52
- omitIndexHtml: 'Avoid using "index.html" in URLs. Many web servers automatically resolve directory requests to index.html.',
70
+ omitIndexHtml: 'URL "{{url}}" includes "index.html", which servers already serve implicitly for directory paths. Keeping the file name creates duplicate URLs, breaks canonical links, and can make caches treat the same page as different assets. {{fixHint}}',
53
71
  },
54
72
  },
55
73
  defaultOptions: [{ allowWithQueryOrHash: true }],
56
74
  create(context, [options]) {
75
+ const sourceCode = context.getSourceCode();
57
76
  const allowWithQueryOrHash = options.allowWithQueryOrHash !== false;
58
77
  return {
59
78
  Literal(node) {
@@ -65,22 +84,58 @@ exports.omitIndexHtml = (0, createRule_1.createRule)({
65
84
  // Skip if it has query parameters or hash fragments and we're allowing those
66
85
  if (allowWithQueryOrHash && hasQueryOrHash(value))
67
86
  return;
87
+ const suggestedUrl = fixUrl(value);
88
+ const fixHint = `Replace it with the directory path (e.g., "${suggestedUrl}").`;
68
89
  context.report({
69
90
  node,
70
91
  messageId: 'omitIndexHtml',
92
+ data: {
93
+ url: value,
94
+ suggestedUrl,
95
+ fixHint,
96
+ },
71
97
  fix: (fixer) => {
72
- return fixer.replaceText(node, `"${fixUrl(value)}"`);
98
+ const quote = sourceCode.getText(node).startsWith("'")
99
+ ? "'"
100
+ : '"';
101
+ const escapedUrl = suggestedUrl.split(quote).join(`\\${quote}`);
102
+ return fixer.replaceText(node, `${quote}${escapedUrl}${quote}`);
73
103
  },
74
104
  });
75
105
  }
76
106
  },
77
107
  TemplateLiteral(node) {
78
- // For template literals, we can only check if the static parts contain index.html
79
- const value = node.quasis.map((q) => q.value.raw).join('');
80
- if (value.includes('/index.html')) {
108
+ const hasIndexHtmlInStatic = node.quasis.some((quasi) => {
109
+ const text = quasi.value.cooked ?? quasi.value.raw;
110
+ return text.includes('/index.html');
111
+ });
112
+ if (!hasIndexHtmlInStatic) {
113
+ return;
114
+ }
115
+ const value = describeTemplateLiteral(node, sourceCode);
116
+ // Check if it's a URL and contains index.html
117
+ if (isLikelyUrl(value) && value.includes('/index.html')) {
118
+ // Skip if it has query parameters or hash fragments and we're allowing those
119
+ if (allowWithQueryOrHash && hasQueryOrHash(value))
120
+ return;
121
+ const hasDynamicParts = node.expressions.length > 0;
122
+ const suggestedUrlRaw = fixUrl(value);
123
+ const suggestedUrl = hasDynamicParts
124
+ ? `\`${suggestedUrlRaw
125
+ .replace(/\\/g, '\\\\')
126
+ .replace(/`/g, '\\`')}\``
127
+ : suggestedUrlRaw;
128
+ const fixHint = hasDynamicParts
129
+ ? `Remove "index.html" from the static portion of the template so it resolves to the directory path (e.g., ${suggestedUrl}).`
130
+ : `Replace it with the directory path (e.g., "${suggestedUrl}").`;
81
131
  context.report({
82
132
  node,
83
133
  messageId: 'omitIndexHtml',
134
+ data: {
135
+ url: value,
136
+ suggestedUrl,
137
+ fixHint,
138
+ },
84
139
  // No automatic fix for template literals as they may contain dynamic parts
85
140
  });
86
141
  }
@@ -6,7 +6,8 @@ const createRule_1 = require("../utils/createRule");
6
6
  const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
7
7
  function isHookCall(node) {
8
8
  const callee = node.callee;
9
- return ((callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name)) ||
9
+ return ((callee.type === utils_1.AST_NODE_TYPES.Identifier &&
10
+ HOOK_NAMES.has(callee.name)) ||
10
11
  (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
11
12
  !callee.computed &&
12
13
  callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -32,7 +33,8 @@ function isObjectExistenceCheck(node) {
32
33
  // Check for patterns like !obj
33
34
  if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
34
35
  // !obj
35
- if (node.operator === '!' && node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
36
+ if (node.operator === '!' &&
37
+ node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
36
38
  return true;
37
39
  }
38
40
  // !!obj
@@ -111,7 +113,7 @@ function extractObjectName(node) {
111
113
  return null;
112
114
  }
113
115
  function analyzeBooleanCondition(node, context) {
114
- const sourceCode = context.getSourceCode();
116
+ const sourceCode = context.sourceCode;
115
117
  const expression = sourceCode.getText(node);
116
118
  if (isObjectExistenceCheck(node)) {
117
119
  const objectName = extractObjectName(node);
@@ -195,7 +197,7 @@ exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
195
197
  },
196
198
  schema: [],
197
199
  messages: {
198
- extractBooleanCondition: 'Extract boolean condition "{{expression}}" into a separate variable "{{suggestedName}}" to optimize hook re-runs. The condition depends on object "{{objectName}}" which may change frequently, but the boolean result changes less often.',
200
+ extractBooleanCondition: 'Dependency array includes boolean condition "{{expression}}" derived from object "{{objectName}}". Hook re-runs every time the object reference changes even when the boolean outcome stays the same, leading to wasted renders and unstable memoization. Extract the condition into a stable boolean like "{{suggestedName}}" and depend on that variable instead.',
199
201
  },
200
202
  },
201
203
  defaultOptions: [],
@@ -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
  ];
@@ -44,12 +47,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
44
47
  },
45
48
  ],
46
49
  messages: {
47
- parallelizeAsyncOperations: 'Multiple sequential awaits detected. Consider using Promise.all() to parallelize independent async operations for better performance.',
50
+ parallelizeAsyncOperations: 'Awaiting {{awaitCount}} independent async operations sequentially makes their network and I/O latency add up, which slows responses and wastes compute. These awaits have no data dependency or per-call error handling, so run them together with Promise.all([...]) and destructure the results when you need individual values.',
48
51
  },
49
52
  },
50
53
  defaultOptions,
51
54
  create(context, [options]) {
52
- const sourceCode = context.getSourceCode();
55
+ const sourceCode = context.sourceCode;
53
56
  const sideEffectMatchers = (options?.sideEffectPatterns ?? []).map((pattern) => typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern);
54
57
  const reportedRanges = new Set();
55
58
  /**
@@ -95,81 +98,149 @@ 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
+ }
107
113
  }
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);
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 &&
117
+ !parent.computed &&
118
+ !parent.shorthand) {
119
+ return false;
120
+ }
121
+ }
122
+ if (callback(node.name) === true)
123
+ return true;
124
+ }
125
+ /**
126
+ * Recursively traverses child nodes while skipping 'parent' to avoid
127
+ * circular back-references, and 'range'/'loc' which are metadata.
128
+ */
129
+ for (const key in node) {
130
+ if (key === 'parent' || key === 'range' || key === 'loc')
131
+ continue;
132
+ const child = node[key];
133
+ if (child && typeof child === 'object') {
134
+ if (Array.isArray(child)) {
135
+ for (const item of child) {
136
+ if (item && typeof item === 'object' && 'type' in item) {
137
+ if (visitIdentifiers(item, callback, options)) {
138
+ return true;
118
139
  }
119
140
  }
120
141
  }
121
- else if ('type' in child) {
122
- visit(child);
142
+ }
143
+ else if ('type' in child) {
144
+ if (visitIdentifiers(child, callback, options)) {
145
+ return true;
123
146
  }
124
147
  }
125
148
  }
126
149
  }
127
- visit(node);
128
- return isUsed;
150
+ return false;
129
151
  }
152
+ /**
153
+ * Checks if an identifier is used in a node
154
+ */
155
+ function isIdentifierUsedInNode(identifier, node) {
156
+ return visitIdentifiers(node, (name) => name === identifier);
157
+ }
158
+ /**
159
+ * Extracts all identifiers used in a node
160
+ */
161
+ function getAllIdentifiers(node, options) {
162
+ const identifiers = new Set();
163
+ visitIdentifiers(node, (name) => {
164
+ identifiers.add(name);
165
+ }, options);
166
+ return identifiers;
167
+ }
168
+ /**
169
+ * Matches coordinator-like identifiers. Intentionally uses substring
170
+ * matching (no word boundaries) to catch patterns like "batchManager",
171
+ * "transactionCollector", etc. This may produce false positives for
172
+ * unrelated managers, but errs on the side of safety.
173
+ */
174
+ const COORDINATOR_PATTERN = /batch|manager|collector|transaction|tx|unitofwork|accumulator/i;
130
175
  /**
131
176
  * Checks if there are dependencies between await expressions
132
177
  */
133
178
  function hasDependencies(awaitNodes, variableNames, sideEffectPatterns) {
134
- // If we have fewer than 2 nodes, there are no dependencies to check
135
179
  if (awaitNodes.length < 2) {
136
180
  return false;
137
181
  }
138
- // For each await node (except the first), check if it depends on previous variables
139
- for (let i = 1; i < awaitNodes.length; i++) {
182
+ const allIdentifiers = awaitNodes.map((node) => {
183
+ const awaitExpr = getAwaitExpression(node);
184
+ return awaitExpr
185
+ ? getAllIdentifiers(awaitExpr.argument, {
186
+ includeMemberProperties: true,
187
+ })
188
+ : new Set();
189
+ });
190
+ // Check all nodes for side effects and shared coordinators
191
+ for (let i = 0; i < awaitNodes.length; i++) {
140
192
  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
193
  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))) {
194
+ if (!awaitExpr)
195
+ continue;
196
+ // 1. Check if current node depends on variables DECLARED in previous awaits
197
+ if (i > 0) {
198
+ for (const varName of variableNames) {
199
+ if (isIdentifierUsedInNode(varName, awaitExpr.argument)) {
160
200
  return true;
161
201
  }
162
202
  }
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;
203
+ }
204
+ // 2. Check for shared coordinators between this node and ANY previous node
205
+ if (i > 0) {
206
+ const currentIds = allIdentifiers[i];
207
+ for (const id of currentIds) {
208
+ if (COORDINATOR_PATTERN.test(id)) {
209
+ for (let j = 0; j < i; j++) {
210
+ if (allIdentifiers[j].has(id)) {
211
+ return true;
212
+ }
213
+ }
167
214
  }
168
215
  }
169
216
  }
217
+ // 3. Check for operations that might have side effects
218
+ // If any node has a side effect, we should not parallelize the sequence
219
+ let callExpr = null;
220
+ if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
221
+ callExpr = awaitExpr.argument;
222
+ }
223
+ else if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
224
+ awaitExpr.argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
225
+ callExpr = awaitExpr.argument.expression;
226
+ }
227
+ if (callExpr) {
228
+ const callee = callExpr.callee;
229
+ let methodName = null;
230
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
231
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
232
+ methodName = callee.property.name;
233
+ }
234
+ else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
235
+ methodName = callee.name;
236
+ }
237
+ if (methodName &&
238
+ sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
239
+ return true;
240
+ }
241
+ }
170
242
  }
171
243
  // 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
244
  for (const node of awaitNodes) {
174
245
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
175
246
  for (const declaration of node.declarations) {
@@ -234,7 +305,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
234
305
  function areInTryCatchBlocks(nodes) {
235
306
  for (const node of nodes) {
236
307
  let current = node;
237
- // Traverse up to find if the node is in a try block
238
308
  while (current && current.parent) {
239
309
  if (current.parent.type === utils_1.AST_NODE_TYPES.TryStatement &&
240
310
  current.parent.block === current) {
@@ -253,7 +323,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
253
323
  function areInLoop(nodes) {
254
324
  for (const node of nodes) {
255
325
  let current = node;
256
- // Traverse up to find if the node is in a loop
257
326
  while (current && current.parent) {
258
327
  if (current.parent.type === utils_1.AST_NODE_TYPES.ForStatement ||
259
328
  current.parent.type === utils_1.AST_NODE_TYPES.ForInStatement ||
@@ -269,16 +338,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
269
338
  }
270
339
  /**
271
340
  * Generates a fix for sequential awaits
341
+ *
342
+ * Returns null when the sequential awaits cannot be safely rewritten as a Promise.all.
272
343
  */
273
344
  function generateFix(fixer, awaitNodes) {
274
- // Extract the await expressions
345
+ if (awaitNodes.length < 2) {
346
+ return null;
347
+ }
275
348
  const awaitExpressions = awaitNodes
276
349
  .map((node) => getAwaitExpression(node))
277
350
  .filter((node) => node !== null);
278
351
  if (awaitExpressions.length < 2) {
279
352
  return null;
280
353
  }
281
- // Get the text of each await argument
282
354
  const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
283
355
  const idsText = [];
284
356
  const declKinds = new Set();
@@ -311,12 +383,18 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
311
383
  // Simple Promise.all without variable assignments
312
384
  promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
313
385
  }
314
- // Find the start position, accounting for leading comments
315
- let startPos = awaitNodes[0].range[0];
316
- // Replace the range from the start of the first await to the end of the last await
386
+ const startPos = awaitNodes[0].range[0];
317
387
  const endPos = awaitNodes[awaitNodes.length - 1].range[1];
318
388
  return fixer.replaceTextRange([startPos, endPos], promiseAllText);
319
389
  }
390
+ /**
391
+ * Generates a deduplication key from await nodes' range metadata.
392
+ */
393
+ function getDeduplicationKey(awaitNodes) {
394
+ const rangeStart = awaitNodes[0].range[0];
395
+ const rangeEnd = awaitNodes[awaitNodes.length - 1].range[1];
396
+ return `${rangeStart}-${rangeEnd}`;
397
+ }
320
398
  const processStatementList = (statements) => {
321
399
  const awaitNodes = [];
322
400
  for (const statement of statements) {
@@ -329,12 +407,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
329
407
  if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
330
408
  !areInTryCatchBlocks(awaitNodes) &&
331
409
  !areInLoop(awaitNodes)) {
332
- const key = `${awaitNodes[0].range?.[0]}-${awaitNodes[awaitNodes.length - 1].range?.[1]}`;
410
+ const key = getDeduplicationKey(awaitNodes);
333
411
  if (!reportedRanges.has(key)) {
334
412
  reportedRanges.add(key);
335
413
  context.report({
336
414
  node: awaitNodes[0],
337
415
  messageId: 'parallelizeAsyncOperations',
416
+ data: {
417
+ awaitCount: awaitNodes.length.toString(),
418
+ },
338
419
  fix: (fixer) => generateFix(fixer, awaitNodes),
339
420
  });
340
421
  }
@@ -350,12 +431,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
350
431
  if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
351
432
  !areInTryCatchBlocks(awaitNodes) &&
352
433
  !areInLoop(awaitNodes)) {
353
- const key = `${awaitNodes[0].range?.[0]}-${awaitNodes[awaitNodes.length - 1].range?.[1]}`;
434
+ const key = getDeduplicationKey(awaitNodes);
354
435
  if (!reportedRanges.has(key)) {
355
436
  reportedRanges.add(key);
356
437
  context.report({
357
438
  node: awaitNodes[0],
358
439
  messageId: 'parallelizeAsyncOperations',
440
+ data: {
441
+ awaitCount: awaitNodes.length.toString(),
442
+ },
359
443
  fix: (fixer) => generateFix(fixer, awaitNodes),
360
444
  });
361
445
  }
@@ -1,3 +1 @@
1
- type MessageIds = 'preferSetAll' | 'preferOverwriteAll';
2
- export declare const preferBatchOperations: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
- export {};
1
+ export declare const preferBatchOperations: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferBatch", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;