@blumintinc/eslint-plugin-blumint 1.13.0 → 1.14.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 (186) hide show
  1. package/README.md +170 -139
  2. package/lib/index.js +190 -1
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  5. package/lib/rules/consistent-callback-naming.js +1 -1
  6. package/lib/rules/dynamic-https-errors.js +3 -4
  7. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
  9. package/lib/rules/enforce-callback-memo.js +1 -1
  10. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  11. package/lib/rules/enforce-console-error.js +35 -7
  12. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  13. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  14. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  15. package/lib/rules/enforce-early-destructuring.js +980 -0
  16. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  17. package/lib/rules/enforce-empty-object-check.js +502 -0
  18. package/lib/rules/enforce-exported-function-types.js +1 -1
  19. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  20. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  21. package/lib/rules/enforce-firestore-facade.js +4 -2
  22. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  23. package/lib/rules/enforce-global-constants.js +1 -1
  24. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  25. package/lib/rules/enforce-memoize-async.js +2 -6
  26. package/lib/rules/enforce-memoize-getters.js +39 -15
  27. package/lib/rules/enforce-microdiff.js +1 -1
  28. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  29. package/lib/rules/enforce-positive-naming.js +4 -0
  30. package/lib/rules/enforce-props-argument-name.js +2 -2
  31. package/lib/rules/enforce-safe-stringify.js +1 -1
  32. package/lib/rules/enforce-serializable-params.js +3 -3
  33. package/lib/rules/enforce-singular-type-names.js +1 -1
  34. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  35. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  36. package/lib/rules/enforce-storage-context.d.ts +9 -0
  37. package/lib/rules/enforce-storage-context.js +646 -0
  38. package/lib/rules/enforce-timestamp-now.js +23 -7
  39. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  40. package/lib/rules/enforce-transform-memoization.js +416 -0
  41. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  42. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  43. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  44. package/lib/rules/enforce-verb-noun-naming.js +5 -4
  45. package/lib/rules/ensure-pointer-events-none.js +28 -2
  46. package/lib/rules/export-if-in-doubt.js +27 -4
  47. package/lib/rules/extract-global-constants.js +40 -13
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  49. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  50. package/lib/rules/flatten-push-calls.d.ts +2 -0
  51. package/lib/rules/flatten-push-calls.js +428 -0
  52. package/lib/rules/global-const-style.js +50 -19
  53. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  54. package/lib/rules/jsdoc-above-field.js +208 -0
  55. package/lib/rules/key-only-outermost-element.js +21 -3
  56. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  57. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  58. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  59. package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
  60. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  61. package/lib/rules/memo-nested-react-components.js +410 -0
  62. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  63. package/lib/rules/no-always-true-false-conditions.js +68 -8
  64. package/lib/rules/no-array-length-in-deps.js +9 -3
  65. package/lib/rules/no-async-foreach.js +210 -12
  66. package/lib/rules/no-circular-references.js +18 -17
  67. package/lib/rules/no-class-instance-destructuring.js +55 -12
  68. package/lib/rules/no-complex-cloud-params.js +7 -3
  69. package/lib/rules/no-compositing-layer-props.js +2 -2
  70. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  71. package/lib/rules/no-console-error.d.ts +9 -0
  72. package/lib/rules/no-console-error.js +525 -0
  73. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  74. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  75. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  76. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  77. package/lib/rules/no-entire-object-hook-deps.js +4 -6
  78. package/lib/rules/no-excessive-parent-chain.js +1 -1
  79. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  80. package/lib/rules/no-explicit-return-type.js +215 -26
  81. package/lib/rules/no-filter-without-return.js +5 -1
  82. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  83. package/lib/rules/no-firestore-jest-mock.js +126 -8
  84. package/lib/rules/no-firestore-object-arrays.js +67 -12
  85. package/lib/rules/no-handler-suffix.d.ts +12 -0
  86. package/lib/rules/no-handler-suffix.js +305 -0
  87. package/lib/rules/no-hungarian.js +1 -1
  88. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  89. package/lib/rules/no-inline-component-prop.js +456 -0
  90. package/lib/rules/no-jsx-in-hooks.js +6 -1
  91. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  92. package/lib/rules/no-margin-properties.js +6 -6
  93. package/lib/rules/no-memoize-on-static.js +9 -1
  94. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  95. package/lib/rules/no-misused-switch-case.js +22 -1
  96. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  97. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  98. package/lib/rules/no-mock-firebase-admin.js +5 -2
  99. package/lib/rules/no-object-values-on-strings.js +10 -2
  100. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  101. package/lib/rules/no-passthrough-getters.js +38 -2
  102. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  103. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  104. package/lib/rules/no-redundant-param-types.js +16 -6
  105. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  106. package/lib/rules/no-redundant-this-params.js +459 -0
  107. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  108. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  109. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  110. package/lib/rules/no-restricted-properties-fix.js +11 -10
  111. package/lib/rules/no-separate-loading-state.js +7 -16
  112. package/lib/rules/no-stale-state-across-await.js +1 -1
  113. package/lib/rules/no-type-assertion-returns.js +43 -49
  114. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  115. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  116. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  117. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  118. package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
  119. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  120. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  121. package/lib/rules/no-unpinned-dependencies.js +36 -5
  122. package/lib/rules/no-unused-props.d.ts +2 -2
  123. package/lib/rules/no-unused-props.js +295 -91
  124. package/lib/rules/no-unused-usestate.js +6 -2
  125. package/lib/rules/no-useless-fragment.js +28 -2
  126. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  127. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  128. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  129. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  130. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  131. package/lib/rules/omit-index-html.d.ts +2 -1
  132. package/lib/rules/omit-index-html.js +62 -7
  133. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  134. package/lib/rules/parallelize-async-operations.js +24 -5
  135. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  136. package/lib/rules/prefer-batch-operations.js +32 -5
  137. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  138. package/lib/rules/prefer-clone-deep.js +3 -3
  139. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  140. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  141. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  142. package/lib/rules/prefer-docsetter-setall.js +243 -0
  143. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  144. package/lib/rules/prefer-fragment-component.js +1 -1
  145. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  146. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  147. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  148. package/lib/rules/prefer-global-router-state-key.js +88 -13
  149. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  150. package/lib/rules/prefer-memoized-props.js +445 -0
  151. package/lib/rules/prefer-next-dynamic.js +60 -69
  152. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
  153. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  154. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  155. package/lib/rules/prefer-params-over-parent-id.js +220 -171
  156. package/lib/rules/prefer-settings-object.js +2 -2
  157. package/lib/rules/prefer-type-over-interface.js +7 -2
  158. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  159. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  160. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  161. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  162. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  163. package/lib/rules/prevent-children-clobber.js +536 -0
  164. package/lib/rules/react-memoize-literals.d.ts +4 -0
  165. package/lib/rules/react-memoize-literals.js +495 -0
  166. package/lib/rules/react-usememo-should-be-component.js +2 -2
  167. package/lib/rules/require-hooks-default-params.js +17 -16
  168. package/lib/rules/require-https-error-cause.d.ts +4 -0
  169. package/lib/rules/require-https-error-cause.js +136 -0
  170. package/lib/rules/require-https-error.js +43 -21
  171. package/lib/rules/require-image-optimized.js +1 -1
  172. package/lib/rules/require-memo.js +1 -1
  173. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  174. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  175. package/lib/rules/require-usememo-object-literals.js +2 -3
  176. package/lib/rules/test-file-location-enforcement.js +1 -1
  177. package/lib/rules/use-latest-callback.js +6 -5
  178. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  179. package/lib/rules/vertically-group-related-functions.js +480 -0
  180. package/lib/utils/ASTHelpers.d.ts +13 -1
  181. package/lib/utils/ASTHelpers.js +24 -0
  182. package/lib/utils/getMethodName.d.ts +27 -0
  183. package/lib/utils/getMethodName.js +35 -0
  184. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  185. package/lib/utils/tsTypeClassifier.js +149 -0
  186. 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: [],
@@ -44,12 +44,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
44
44
  },
45
45
  ],
46
46
  messages: {
47
- parallelizeAsyncOperations: 'Multiple sequential awaits detected. Consider using Promise.all() to parallelize independent async operations for better performance.',
47
+ 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
48
  },
49
49
  },
50
50
  defaultOptions,
51
51
  create(context, [options]) {
52
- const sourceCode = context.getSourceCode();
52
+ const sourceCode = context.sourceCode;
53
53
  const sideEffectMatchers = (options?.sideEffectPatterns ?? []).map((pattern) => typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern);
54
54
  const reportedRanges = new Set();
55
55
  /**
@@ -269,8 +269,13 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
269
269
  }
270
270
  /**
271
271
  * Generates a fix for sequential awaits
272
+ *
273
+ * Returns null when the sequential awaits cannot be safely rewritten as a Promise.all.
272
274
  */
273
275
  function generateFix(fixer, awaitNodes) {
276
+ if (awaitNodes.length < 2) {
277
+ return null;
278
+ }
274
279
  // Extract the await expressions
275
280
  const awaitExpressions = awaitNodes
276
281
  .map((node) => getAwaitExpression(node))
@@ -312,11 +317,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
312
317
  promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
313
318
  }
314
319
  // Find the start position, accounting for leading comments
315
- let startPos = awaitNodes[0].range[0];
320
+ const startPos = awaitNodes[0].range[0];
316
321
  // Replace the range from the start of the first await to the end of the last await
317
322
  const endPos = awaitNodes[awaitNodes.length - 1].range[1];
318
323
  return fixer.replaceTextRange([startPos, endPos], promiseAllText);
319
324
  }
325
+ /**
326
+ * Generates a deduplication key from await nodes' range metadata.
327
+ */
328
+ function getDeduplicationKey(awaitNodes) {
329
+ const rangeStart = awaitNodes[0].range[0];
330
+ const rangeEnd = awaitNodes[awaitNodes.length - 1].range[1];
331
+ return `${rangeStart}-${rangeEnd}`;
332
+ }
320
333
  const processStatementList = (statements) => {
321
334
  const awaitNodes = [];
322
335
  for (const statement of statements) {
@@ -329,12 +342,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
329
342
  if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
330
343
  !areInTryCatchBlocks(awaitNodes) &&
331
344
  !areInLoop(awaitNodes)) {
332
- const key = `${awaitNodes[0].range?.[0]}-${awaitNodes[awaitNodes.length - 1].range?.[1]}`;
345
+ const key = getDeduplicationKey(awaitNodes);
333
346
  if (!reportedRanges.has(key)) {
334
347
  reportedRanges.add(key);
335
348
  context.report({
336
349
  node: awaitNodes[0],
337
350
  messageId: 'parallelizeAsyncOperations',
351
+ data: {
352
+ awaitCount: awaitNodes.length.toString(),
353
+ },
338
354
  fix: (fixer) => generateFix(fixer, awaitNodes),
339
355
  });
340
356
  }
@@ -350,12 +366,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
350
366
  if (!hasDependencies(awaitNodes, variableNames, sideEffectMatchers) &&
351
367
  !areInTryCatchBlocks(awaitNodes) &&
352
368
  !areInLoop(awaitNodes)) {
353
- const key = `${awaitNodes[0].range?.[0]}-${awaitNodes[awaitNodes.length - 1].range?.[1]}`;
369
+ const key = getDeduplicationKey(awaitNodes);
354
370
  if (!reportedRanges.has(key)) {
355
371
  reportedRanges.add(key);
356
372
  context.report({
357
373
  node: awaitNodes[0],
358
374
  messageId: 'parallelizeAsyncOperations',
375
+ data: {
376
+ awaitCount: awaitNodes.length.toString(),
377
+ },
359
378
  fix: (fixer) => generateFix(fixer, awaitNodes),
360
379
  });
361
380
  }
@@ -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>;
@@ -224,6 +224,27 @@ function findLoopNode(node) {
224
224
  }
225
225
  return undefined;
226
226
  }
227
+ function describeLoopContext(loopInfo) {
228
+ if (loopInfo.isPromiseAll)
229
+ return 'Promise.all()';
230
+ if (loopInfo.isArrayMethod) {
231
+ return `${loopInfo.isArrayMethod}() callback`;
232
+ }
233
+ switch (loopInfo.node.type) {
234
+ case utils_1.AST_NODE_TYPES.ForStatement:
235
+ return 'for loop';
236
+ case utils_1.AST_NODE_TYPES.ForInStatement:
237
+ return 'for...in loop';
238
+ case utils_1.AST_NODE_TYPES.ForOfStatement:
239
+ return 'for...of loop';
240
+ case utils_1.AST_NODE_TYPES.WhileStatement:
241
+ return 'while loop';
242
+ case utils_1.AST_NODE_TYPES.DoWhileStatement:
243
+ return 'do...while loop';
244
+ default:
245
+ return 'this control flow';
246
+ }
247
+ }
227
248
  function isFirestoreSetterInstance(node) {
228
249
  // Check if it's a DocSetter instance
229
250
  if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
@@ -301,8 +322,7 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
301
322
  fixable: 'code',
302
323
  schema: [],
303
324
  messages: {
304
- preferSetAll: 'Use setAll() instead of multiple set() calls for better performance',
305
- preferOverwriteAll: 'Use overwriteAll() instead of multiple overwrite() calls for better performance',
325
+ preferBatch: 'DocSetter.{{setterMethod}} is invoked repeatedly inside {{contextDescription}}, which issues separate Firestore writes per document and can leave partial updates when later calls fail. Batch the documents with DocSetter.{{batchMethod}} so the writes stay grouped and latency stays predictable.',
306
326
  },
307
327
  },
308
328
  defaultOptions: [],
@@ -321,6 +341,13 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
321
341
  const loopInfo = findLoopNode(node);
322
342
  if (!loopInfo)
323
343
  return;
344
+ const batchMethod = methodName === 'set' ? 'setAll()' : 'overwriteAll()';
345
+ const messageData = {
346
+ setterMethod: `${methodName}()`,
347
+ batchMethod,
348
+ contextDescription: describeLoopContext(loopInfo),
349
+ };
350
+ const messageId = 'preferBatch';
324
351
  // Get or create the setter calls map for this loop
325
352
  let setterCalls = loopSetterCalls.get(loopInfo.node);
326
353
  if (!setterCalls) {
@@ -348,10 +375,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
348
375
  existing.count === 2 &&
349
376
  !reportedLoops.has(loopInfo.node)) {
350
377
  reportedLoops.add(loopInfo.node);
351
- const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
352
378
  context.report({
353
379
  node: existing.firstNode,
354
380
  messageId,
381
+ data: messageData,
355
382
  fix: () => null, // We can't provide a fix because we don't know the array structure
356
383
  });
357
384
  }
@@ -371,10 +398,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
371
398
  // For traditional loops, report on the first occurrence since we know it's in a loop
372
399
  if (!reportedLoops.has(loopInfo.node)) {
373
400
  reportedLoops.add(loopInfo.node);
374
- const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
375
401
  context.report({
376
402
  node,
377
403
  messageId,
404
+ data: messageData,
378
405
  fix: () => null, // We can't provide a fix because we don't know the array structure
379
406
  });
380
407
  }
@@ -383,10 +410,10 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
383
410
  else if (loopInfo.isArrayMethod) {
384
411
  if (!reportedLoops.has(loopInfo.node)) {
385
412
  reportedLoops.add(loopInfo.node);
386
- const messageId = methodName === 'set' ? 'preferSetAll' : 'preferOverwriteAll';
387
413
  context.report({
388
414
  node,
389
415
  messageId,
416
+ data: messageData,
390
417
  fix: () => null, // We can't provide a fix because we don't know the array structure
391
418
  });
392
419
  }
@@ -65,17 +65,22 @@ exports.preferBlockCommentsForDeclarations = (0, createRule_1.createRule)({
65
65
  if (isInsideFunctionBody(node)) {
66
66
  return;
67
67
  }
68
- const sourceCode = context.getSourceCode();
68
+ const sourceCode = context.sourceCode;
69
69
  const comments = sourceCode.getCommentsBefore(node);
70
70
  // Find the closest comment to the node
71
71
  const lastComment = comments[comments.length - 1];
72
72
  if (lastComment && isLineCommentBeforeDeclaration(lastComment, node)) {
73
+ const commentText = lastComment.value.trim();
74
+ const commentLabel = commentText || 'declaration comment';
73
75
  context.report({
74
76
  loc: lastComment.loc,
75
77
  messageId: 'preferBlockComment',
78
+ data: { commentText: commentLabel },
76
79
  fix: (fixer) => {
77
- const commentText = lastComment.value.trim();
78
- return fixer.replaceText(lastComment, `/** ${commentText} */`);
80
+ if (commentLabel.includes('*/')) {
81
+ return null;
82
+ }
83
+ return fixer.replaceText(lastComment, `/** ${commentLabel} */`);
79
84
  },
80
85
  });
81
86
  }
@@ -129,7 +134,7 @@ exports.preferBlockCommentsForDeclarations = (0, createRule_1.createRule)({
129
134
  fixable: 'code',
130
135
  schema: [],
131
136
  messages: {
132
- preferBlockComment: 'Use block comments (/** */) instead of line comments (//) for declarations to improve IDE support.',
137
+ preferBlockComment: 'Line comment "{{commentText}}" sits on a declaration, and IDE hovers and TypeScript docs ignore it as documentation. Rewrite it as a block comment (/** ... */) so the text stays attached to the declaration and tooling surfaces it.',
133
138
  },
134
139
  },
135
140
  defaultOptions: [],
@@ -14,7 +14,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
14
14
  fixable: 'code',
15
15
  schema: [],
16
16
  messages: {
17
- preferCloneDeep: 'Use cloneDeep from functions/src/util/cloneDeep.ts instead of nested spread operators for deep object copying',
17
+ preferCloneDeep: 'Nested spread copies only clone one level, so inner objects still point at the original and later mutations leak back. Use cloneDeep from functions/src/util/cloneDeep.ts and pass overrides as the second argument so the base object is deeply cloned before applying updates.',
18
18
  },
19
19
  },
20
20
  defaultOptions: [],
@@ -74,7 +74,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
74
74
  !hasSymbol);
75
75
  }
76
76
  function generateCloneDeepFix(node) {
77
- const sourceCode = context.getSourceCode();
77
+ const sourceCode = context.sourceCode;
78
78
  // Find the base object (first spread element)
79
79
  let baseObj = null;
80
80
  // Extract the base object from the first spread element
@@ -248,7 +248,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
248
248
  }
249
249
  // Helper function to extract nested overrides without spread elements
250
250
  function extractNestedOverrides(node) {
251
- const sourceCode = context.getSourceCode();
251
+ const sourceCode = context.sourceCode;
252
252
  const overrides = [];
253
253
  for (const prop of node.properties) {
254
254
  if (prop.type === utils_1.AST_NODE_TYPES.Property) {
@@ -1,8 +1,9 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type Options = [
2
3
  {
3
4
  object?: boolean;
4
5
  enforceForRenamedProperties?: boolean;
5
6
  }
6
7
  ];
7
- export declare const preferDestructuringNoClass: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferDestructuring", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export declare const preferDestructuringNoClass: TSESLint.RuleModule<"preferDestructuring", Options, TSESLint.RuleListener>;
8
9
  export {};