@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'useFastDeepEqual' | 'addFastDeepEqualImport';
2
- export declare const fastDeepEqualOverMicrodiff: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export declare const fastDeepEqualOverMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
3
4
  export {};
@@ -22,53 +22,268 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
22
22
  create(context) {
23
23
  const sourceCode = context.getSourceCode();
24
24
  let hasFastDeepEqualImport = false;
25
+ let hasMicrodiffImport = false;
25
26
  let microdiffImportName = 'diff';
26
27
  let fastDeepEqualImportName = 'isEqual';
27
28
  const reportedNodes = new Set();
29
+ let plannedFastDeepEqualImport = false;
30
+ const isChainExpression = (node) => node.type === utils_1.AST_NODE_TYPES.ChainExpression;
31
+ function isMicrodiffCallee(callee) {
32
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
33
+ callee.name === microdiffImportName) {
34
+ return true;
35
+ }
36
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
37
+ !callee.computed &&
38
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
39
+ callee.object.name === microdiffImportName &&
40
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
41
+ (callee.property.name === 'diff' || callee.property.name === 'default')) {
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Resolve an identifier to its variable and return the initializer CallExpression if it's a microdiff call.
48
+ */
49
+ function findVariableInScopeChain(identifier) {
50
+ // Use scopeManager to resolve variable from the identifier's reference
51
+ const scopeManager = sourceCode.scopeManager;
52
+ if (!scopeManager)
53
+ return undefined;
54
+ // Find the scope for this identifier
55
+ let scope = null;
56
+ let currentNode = identifier;
57
+ while (currentNode) {
58
+ scope = scopeManager.acquire(currentNode, true) || null;
59
+ if (scope)
60
+ break;
61
+ currentNode = currentNode.parent;
62
+ }
63
+ if (!scope) {
64
+ scope = scopeManager.globalScope;
65
+ }
66
+ if (!scope)
67
+ return undefined;
68
+ // Look up variable in the scope chain
69
+ let currentScope = scope;
70
+ while (currentScope) {
71
+ const variable = currentScope.variables.find((v) => v.name === identifier.name);
72
+ if (variable)
73
+ return variable;
74
+ currentScope = currentScope.upper;
75
+ }
76
+ return undefined;
77
+ }
78
+ function resolveIdentifierToMicrodiffCall(identifier) {
79
+ // Look for a reference in the current scope chain
80
+ const variable = findVariableInScopeChain(identifier);
81
+ if (!variable)
82
+ return undefined;
83
+ const defNode = variable.defs[0]?.node;
84
+ if (!defNode)
85
+ return undefined;
86
+ // Variable declarator with initializer call
87
+ if (defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
88
+ const { init } = defNode;
89
+ if (init &&
90
+ init.type === utils_1.AST_NODE_TYPES.CallExpression &&
91
+ init.callee &&
92
+ init.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
93
+ init.callee.name === microdiffImportName) {
94
+ return init;
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /**
100
+ * Determine if the given variable is used only for `.length` property accesses.
101
+ * If there is any non-length usage (e.g., indexing, iteration, method calls), return false.
102
+ */
103
+ function isVariableOnlyUsedForLength(identifier) {
104
+ // Walk up scope chain to find a reference for this identifier
105
+ const variable = findVariableInScopeChain(identifier);
106
+ if (!variable)
107
+ return false;
108
+ // All references must be strictly of the form <id>.length, except the variable's own declaration
109
+ return variable.references.every((reference) => {
110
+ const idNode = reference.identifier;
111
+ const parent = idNode.parent;
112
+ if (!parent)
113
+ return false;
114
+ // Allow the declaration id (not considered a read)
115
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
116
+ parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
117
+ parent.id === idNode) {
118
+ return true;
119
+ }
120
+ // Accept only the specific pattern: Identifier used as object in MemberExpression with property `length`
121
+ if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
122
+ parent.object === idNode &&
123
+ !parent.computed &&
124
+ parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
125
+ parent.property.name === 'length') {
126
+ return true;
127
+ }
128
+ // Accept optional chaining: identifier?.length represented as ChainExpression
129
+ if (parent.type === utils_1.AST_NODE_TYPES.ChainExpression &&
130
+ parent.expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
131
+ parent.expression.object === idNode &&
132
+ !parent.expression.computed &&
133
+ parent.expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
134
+ parent.expression.property.name === 'length') {
135
+ return true;
136
+ }
137
+ return false;
138
+ });
139
+ }
140
+ /**
141
+ * If a MemberExpression is `<something>.length`, returns the underlying microdiff call if applicable.
142
+ * Supports direct `diff(a,b).length` and `changes.length` where `changes` is initialized to a microdiff call.
143
+ */
144
+ function getMicrodiffCallFromLengthAccess(member) {
145
+ if (!hasMicrodiffImport)
146
+ return { viaIdentifier: false };
147
+ if (member.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
148
+ member.property.name !== 'length') {
149
+ return { viaIdentifier: false };
150
+ }
151
+ const obj = member.object;
152
+ if (obj.type === utils_1.AST_NODE_TYPES.CallExpression &&
153
+ isMicrodiffCallee(obj.callee)) {
154
+ return { diffCall: obj, viaIdentifier: false };
155
+ }
156
+ if (obj.type === utils_1.AST_NODE_TYPES.Identifier) {
157
+ const resolved = resolveIdentifierToMicrodiffCall(obj);
158
+ if (!resolved)
159
+ return { viaIdentifier: true };
160
+ // Ensure the identifier is only used for `.length` accesses (no content-based usage)
161
+ if (!isVariableOnlyUsedForLength(obj)) {
162
+ return { viaIdentifier: true };
163
+ }
164
+ return { diffCall: resolved, viaIdentifier: true };
165
+ }
166
+ return { viaIdentifier: false };
167
+ }
28
168
  /**
29
169
  * Check if a node is a microdiff equality check pattern
30
170
  * Looks for patterns like:
31
171
  * - diff(a, b).length === 0
32
- * - diff(a, b).length !== 0
172
+ * - 0 === diff(a, b).length
173
+ * - changes.length === 0 (where `changes = diff(a,b)` and changes is only used for length checks)
174
+ * - changes.length !== 0
33
175
  * - !diff(a, b).length
176
+ * - !changes.length
34
177
  */
35
178
  function isMicrodiffEqualityCheck(node) {
36
- // Check for binary expressions like diff(a, b).length === 0
37
- if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
38
- (node.operator === '===' ||
39
- node.operator === '==' ||
40
- node.operator === '!==' ||
41
- node.operator === '!=') &&
42
- node.right.type === utils_1.AST_NODE_TYPES.Literal &&
43
- node.right.value === 0 &&
44
- node.left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
45
- node.left.property.type === utils_1.AST_NODE_TYPES.Identifier &&
46
- node.left.property.name === 'length' &&
47
- node.left.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
48
- node.left.object.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
49
- node.left.object.callee.name === microdiffImportName) {
50
- return {
51
- isEquality: node.operator === '===' || node.operator === '==',
52
- diffCall: node.left.object,
53
- };
54
- }
55
- // Check for unary expressions like !diff(a, b).length
179
+ // Check for binary expressions comparing length to 0 (both directions)
180
+ if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
181
+ const operators = [
182
+ '===',
183
+ '==',
184
+ '!==',
185
+ '!=',
186
+ ];
187
+ if (operators.includes(node.operator)) {
188
+ // side A: MemberExpression .length, side B: 0
189
+ if (node.right.type === utils_1.AST_NODE_TYPES.Literal &&
190
+ node.right.value === 0 &&
191
+ node.left.type === utils_1.AST_NODE_TYPES.MemberExpression) {
192
+ const { diffCall } = getMicrodiffCallFromLengthAccess(node.left);
193
+ if (diffCall) {
194
+ return {
195
+ isEquality: node.operator === '===' || node.operator === '==',
196
+ diffCall,
197
+ };
198
+ }
199
+ }
200
+ // side A: 0, side B: MemberExpression .length
201
+ if (node.left.type === utils_1.AST_NODE_TYPES.Literal &&
202
+ node.left.value === 0 &&
203
+ node.right.type === utils_1.AST_NODE_TYPES.MemberExpression) {
204
+ const { diffCall } = getMicrodiffCallFromLengthAccess(node.right);
205
+ if (diffCall) {
206
+ return {
207
+ isEquality: node.operator === '===' || node.operator === '==',
208
+ diffCall,
209
+ };
210
+ }
211
+ }
212
+ }
213
+ }
214
+ // Check for unary expressions like !diff(a, b).length or !changes.length (including optional chaining)
56
215
  if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
57
- node.operator === '!' &&
58
- node.argument.type === utils_1.AST_NODE_TYPES.MemberExpression &&
59
- node.argument.property.type === utils_1.AST_NODE_TYPES.Identifier &&
60
- node.argument.property.name === 'length' &&
61
- node.argument.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
62
- node.argument.object.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
63
- node.argument.object.callee.name === microdiffImportName) {
64
- return {
65
- isEquality: true,
66
- diffCall: node.argument.object,
67
- };
216
+ node.operator === '!') {
217
+ const argumentNode = node.argument;
218
+ const target = isChainExpression(argumentNode)
219
+ ? argumentNode.expression
220
+ : argumentNode;
221
+ if (target.type === utils_1.AST_NODE_TYPES.MemberExpression) {
222
+ const { diffCall } = getMicrodiffCallFromLengthAccess(target);
223
+ if (diffCall) {
224
+ return {
225
+ isEquality: true,
226
+ diffCall,
227
+ };
228
+ }
229
+ }
68
230
  }
69
231
  // Not a microdiff equality check
70
232
  return { isEquality: false };
71
233
  }
234
+ /**
235
+ * Try to find the identifier used as `<id>.length` for the given equality node
236
+ */
237
+ function getLengthIdentifierFromNode(node) {
238
+ if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
239
+ const left = node.left;
240
+ const right = node.right;
241
+ const isLengthMember = (n) => n.type === utils_1.AST_NODE_TYPES.MemberExpression &&
242
+ !n.computed &&
243
+ n.property.type === utils_1.AST_NODE_TYPES.Identifier &&
244
+ n.property.name === 'length';
245
+ if (isLengthMember(left) &&
246
+ left.object.type === utils_1.AST_NODE_TYPES.Identifier) {
247
+ return left.object;
248
+ }
249
+ if (isLengthMember(right) &&
250
+ right.object.type === utils_1.AST_NODE_TYPES.Identifier) {
251
+ return right.object;
252
+ }
253
+ }
254
+ if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
255
+ const arg = node.argument;
256
+ if (arg.type === utils_1.AST_NODE_TYPES.MemberExpression &&
257
+ !arg.computed &&
258
+ arg.property.type === utils_1.AST_NODE_TYPES.Identifier &&
259
+ arg.property.name === 'length' &&
260
+ arg.object.type === utils_1.AST_NODE_TYPES.Identifier) {
261
+ return arg.object;
262
+ }
263
+ }
264
+ return undefined;
265
+ }
266
+ /**
267
+ * Get the indentation at the start of the current line for a node
268
+ */
269
+ function getLineBaseIndent(node) {
270
+ const text = sourceCode.text;
271
+ const start = node.range?.[0] ?? sourceCode.getIndexFromLoc(node.loc.start);
272
+ const lineStart = text.lastIndexOf('\n', start - 1) + 1;
273
+ let i = lineStart;
274
+ let indent = '';
275
+ while (i < text.length) {
276
+ const ch = text[i];
277
+ if (ch === ' ' || ch === '\t') {
278
+ indent += ch;
279
+ i++;
280
+ }
281
+ else {
282
+ break;
283
+ }
284
+ }
285
+ return indent;
286
+ }
72
287
  /**
73
288
  * Create a fix for replacing microdiff equality check with fast-deep-equal
74
289
  */
@@ -79,21 +294,71 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
79
294
  }
80
295
  const arg1 = sourceCode.getText(args[0]);
81
296
  const arg2 = sourceCode.getText(args[1]);
82
- // If fast-deep-equal is not imported, we need to add the import after the microdiff import
83
- if (!hasFastDeepEqualImport) {
84
- // Find the end of the microdiff import statement
297
+ const fixes = [];
298
+ // Add import if needed (only once across all fixes in this file)
299
+ if (!hasFastDeepEqualImport && !plannedFastDeepEqualImport) {
85
300
  const importDeclarations = sourceCode.ast.body.filter((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
86
301
  const microdiffImport = importDeclarations.find((node) => node.source.value === 'microdiff');
87
- const importFix = fixer.insertTextAfter(microdiffImport, `\nimport isEqual from 'fast-deep-equal';`);
88
- const replaceFix = fixer.replaceText(node, isEquality
89
- ? `isEqual(${arg1}, ${arg2})`
90
- : `!isEqual(${arg1}, ${arg2})`);
91
- return [importFix, replaceFix];
92
- }
93
- // Otherwise just replace the expression
94
- return fixer.replaceText(node, isEquality
95
- ? `${fastDeepEqualImportName}(${arg1}, ${arg2})`
96
- : `!${fastDeepEqualImportName}(${arg1}, ${arg2})`);
302
+ if (microdiffImport) {
303
+ fixes.push(fixer.insertTextAfter(microdiffImport, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`));
304
+ plannedFastDeepEqualImport = true;
305
+ }
306
+ else {
307
+ const lastImport = importDeclarations[importDeclarations.length - 1];
308
+ if (lastImport) {
309
+ fixes.push(fixer.insertTextAfter(lastImport, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`));
310
+ }
311
+ else {
312
+ fixes.push(fixer.insertTextBeforeRange([0, 0], `import ${fastDeepEqualImportName} from 'fast-deep-equal';\n`));
313
+ }
314
+ plannedFastDeepEqualImport = true;
315
+ }
316
+ }
317
+ // If the equality was via an identifier like `changes.length`, and that identifier
318
+ // is declared as `const changes = diff(...);` and used ONLY for `.length` checks,
319
+ // remove the redundant variable declaration.
320
+ const maybeIdentifier = getLengthIdentifierFromNode(node);
321
+ if (maybeIdentifier && isVariableOnlyUsedForLength(maybeIdentifier)) {
322
+ const variable = findVariableInScopeChain(maybeIdentifier);
323
+ const defNode = variable?.defs?.[0]?.node;
324
+ if (defNode &&
325
+ defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
326
+ defNode.parent &&
327
+ defNode.parent.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
328
+ const declaration = defNode.parent;
329
+ // Only remove if it's the only declaration in the const/let statement
330
+ if (declaration.declarations.length === 1) {
331
+ const text = sourceCode.text;
332
+ const startOfLine = text.lastIndexOf('\n', declaration.range[0] - 1) + 1;
333
+ const nextNewline = text.indexOf('\n', declaration.range[1]);
334
+ const endOfLine = nextNewline === -1 ? declaration.range[1] : nextNewline + 1;
335
+ fixes.push(fixer.removeRange([startOfLine, endOfLine]));
336
+ }
337
+ }
338
+ }
339
+ // Replace the equality expression with isEqual call
340
+ const isMultilineCall = diffCall.loc &&
341
+ args[0].loc &&
342
+ (diffCall.loc.start.line !== args[0].loc.start.line ||
343
+ (args[1] && args[0].loc.start.line !== args[1].loc.start.line));
344
+ let replacement;
345
+ if (isMultilineCall) {
346
+ const baseIndent = getLineBaseIndent(node);
347
+ const innerIndentPlus = baseIndent + ' ';
348
+ replacement =
349
+ `${fastDeepEqualImportName}(` +
350
+ `\n${innerIndentPlus}${arg1},` +
351
+ `\n${innerIndentPlus}${arg2},` +
352
+ `\n${baseIndent})`;
353
+ }
354
+ else {
355
+ replacement = `${fastDeepEqualImportName}(${arg1}, ${arg2})`;
356
+ }
357
+ if (!isEquality) {
358
+ replacement = `!${replacement}`;
359
+ }
360
+ fixes.push(fixer.replaceText(node, replacement));
361
+ return fixes;
97
362
  }
98
363
  return {
99
364
  // Track imports of microdiff and fast-deep-equal
@@ -101,12 +366,16 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
101
366
  const importSource = node.source.value;
102
367
  // Check for microdiff import
103
368
  if (importSource === 'microdiff') {
369
+ hasMicrodiffImport = true;
104
370
  // Get the local name of the imported diff function
105
371
  node.specifiers.forEach((specifier) => {
106
372
  if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
107
373
  specifier.imported.name === 'diff') {
108
374
  microdiffImportName = specifier.local.name;
109
375
  }
376
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
377
+ microdiffImportName = specifier.local.name;
378
+ }
110
379
  });
111
380
  }
112
381
  // Check for fast-deep-equal import
@@ -123,58 +392,67 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
123
392
  },
124
393
  // Check expressions for microdiff equality patterns
125
394
  ['BinaryExpression, UnaryExpression'](node) {
395
+ if (!hasMicrodiffImport)
396
+ return;
126
397
  // Skip if we've already reported this node
127
398
  if (reportedNodes.has(node)) {
128
399
  return;
129
400
  }
130
401
  const result = isMicrodiffEqualityCheck(node);
131
- if (result.isEquality !== undefined && result.diffCall) {
132
- reportedNodes.add(node);
133
- context.report({
134
- node,
135
- messageId: 'useFastDeepEqual',
136
- fix(fixer) {
137
- return createFix(fixer, node, result.diffCall, result.isEquality);
138
- },
139
- });
140
- }
402
+ const { diffCall, isEquality } = result;
403
+ if (!diffCall)
404
+ return;
405
+ reportedNodes.add(node);
406
+ context.report({
407
+ node,
408
+ messageId: 'useFastDeepEqual',
409
+ fix(fixer) {
410
+ return createFix(fixer, node, diffCall, isEquality);
411
+ },
412
+ });
141
413
  },
142
414
  // Check if statements for microdiff equality patterns
143
415
  IfStatement(node) {
416
+ if (!hasMicrodiffImport)
417
+ return;
144
418
  // Skip if we've already reported this node
145
419
  if (reportedNodes.has(node.test)) {
146
420
  return;
147
421
  }
148
422
  const result = isMicrodiffEqualityCheck(node.test);
149
- if (result.isEquality !== undefined && result.diffCall) {
150
- reportedNodes.add(node.test);
151
- context.report({
152
- node: node.test,
153
- messageId: 'useFastDeepEqual',
154
- fix(fixer) {
155
- return createFix(fixer, node.test, result.diffCall, result.isEquality);
156
- },
157
- });
158
- }
423
+ const { diffCall, isEquality } = result;
424
+ if (!diffCall)
425
+ return;
426
+ reportedNodes.add(node.test);
427
+ context.report({
428
+ node: node.test,
429
+ messageId: 'useFastDeepEqual',
430
+ fix(fixer) {
431
+ return createFix(fixer, node.test, diffCall, isEquality);
432
+ },
433
+ });
159
434
  },
160
435
  // Check return statements for microdiff equality patterns
161
436
  ReturnStatement(node) {
437
+ if (!hasMicrodiffImport)
438
+ return;
162
439
  // Skip if we've already reported this node or if there's no argument
163
- if (!node.argument || reportedNodes.has(node.argument)) {
440
+ const argument = node.argument;
441
+ if (!argument || reportedNodes.has(argument)) {
164
442
  return;
165
443
  }
166
- const result = isMicrodiffEqualityCheck(node.argument);
167
- if (result.isEquality !== undefined && result.diffCall) {
168
- reportedNodes.add(node.argument);
169
- context.report({
170
- node: node.argument,
171
- messageId: 'useFastDeepEqual',
172
- fix(fixer) {
173
- // We already checked that node.argument is not null above
174
- return createFix(fixer, node.argument, result.diffCall, result.isEquality);
175
- },
176
- });
177
- }
444
+ const result = isMicrodiffEqualityCheck(argument);
445
+ const { diffCall, isEquality } = result;
446
+ if (!diffCall)
447
+ return;
448
+ reportedNodes.add(argument);
449
+ context.report({
450
+ node: argument,
451
+ messageId: 'useFastDeepEqual',
452
+ fix(fixer) {
453
+ return createFix(fixer, argument, diffCall, isEquality);
454
+ },
455
+ });
178
456
  },
179
457
  };
180
458
  },
@@ -0,0 +1 @@
1
+ export declare const firestoreTransactionReadsBeforeWrites: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"readsAfterWrites", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;