@blumintinc/eslint-plugin-blumint 1.9.1 → 1.11.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 (72) hide show
  1. package/README.md +99 -83
  2. package/lib/index.js +16 -1
  3. package/lib/rules/class-methods-read-top-to-bottom.js +10 -0
  4. package/lib/rules/consistent-callback-naming.js +20 -16
  5. package/lib/rules/enforce-assert-throws.js +10 -5
  6. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +7 -0
  7. package/lib/rules/enforce-boolean-naming-prefixes.js +462 -0
  8. package/lib/rules/enforce-callable-types.d.ts +1 -1
  9. package/lib/rules/enforce-callable-types.js +13 -13
  10. package/lib/rules/enforce-callback-memo.js +6 -3
  11. package/lib/rules/enforce-centralized-mock-firestore.js +8 -6
  12. package/lib/rules/enforce-css-media-queries.js +3 -3
  13. package/lib/rules/enforce-dynamic-file-naming.d.ts +3 -0
  14. package/lib/rules/enforce-dynamic-file-naming.js +82 -0
  15. package/lib/rules/enforce-dynamic-imports.js +1 -1
  16. package/lib/rules/enforce-firestore-doc-ref-generic.js +16 -12
  17. package/lib/rules/enforce-firestore-facade.js +7 -3
  18. package/lib/rules/enforce-firestore-path-utils.js +5 -2
  19. package/lib/rules/enforce-id-capitalization.js +59 -0
  20. package/lib/rules/enforce-identifiable-firestore-type.js +17 -10
  21. package/lib/rules/enforce-memoize-async.js +2 -2
  22. package/lib/rules/enforce-positive-naming.js +71 -27
  23. package/lib/rules/enforce-props-argument-name.js +31 -9
  24. package/lib/rules/enforce-realtimedb-path-utils.js +2 -1
  25. package/lib/rules/enforce-singular-type-names.js +4 -1
  26. package/lib/rules/enforce-timestamp-now.js +3 -4
  27. package/lib/rules/enforce-verb-noun-naming.js +18 -8
  28. package/lib/rules/ensure-pointer-events-none.js +20 -11
  29. package/lib/rules/extract-global-constants.js +25 -10
  30. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +3 -0
  31. package/lib/rules/fast-deep-equal-over-microdiff.js +182 -0
  32. package/lib/rules/key-only-outermost-element.js +13 -7
  33. package/lib/rules/no-always-true-false-conditions.js +189 -5
  34. package/lib/rules/no-async-array-filter.js +1 -1
  35. package/lib/rules/no-circular-references.js +121 -59
  36. package/lib/rules/no-complex-cloud-params.js +4 -2
  37. package/lib/rules/no-entire-object-hook-deps.js +113 -29
  38. package/lib/rules/no-explicit-return-type.js +3 -2
  39. package/lib/rules/no-firestore-jest-mock.js +1 -1
  40. package/lib/rules/no-firestore-object-arrays.js +2 -1
  41. package/lib/rules/no-hungarian.js +18 -1
  42. package/lib/rules/no-jsx-in-hooks.js +4 -3
  43. package/lib/rules/no-margin-properties.d.ts +1 -0
  44. package/lib/rules/no-margin-properties.js +314 -0
  45. package/lib/rules/no-mixed-firestore-transactions.js +8 -4
  46. package/lib/rules/no-mock-firebase-admin.js +6 -1
  47. package/lib/rules/no-object-values-on-strings.js +28 -10
  48. package/lib/rules/no-type-assertion-returns.js +47 -6
  49. package/lib/rules/no-unnecessary-destructuring.js +1 -1
  50. package/lib/rules/no-unnecessary-verb-suffix.js +0 -31
  51. package/lib/rules/no-unused-props.js +10 -7
  52. package/lib/rules/no-unused-usestate.js +19 -6
  53. package/lib/rules/omit-index-html.js +1 -1
  54. package/lib/rules/prefer-batch-operations.js +19 -6
  55. package/lib/rules/prefer-clone-deep.js +16 -11
  56. package/lib/rules/prefer-destructuring-no-class.js +19 -0
  57. package/lib/rules/prefer-fragment-component.js +19 -16
  58. package/lib/rules/prefer-global-router-state-key.js +6 -4
  59. package/lib/rules/prefer-settings-object.js +1 -1
  60. package/lib/rules/prefer-usecallback-over-usememo-for-functions.d.ts +8 -0
  61. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +180 -0
  62. package/lib/rules/react-usememo-should-be-component.js +139 -17
  63. package/lib/rules/require-hooks-default-params.js +25 -15
  64. package/lib/rules/require-usememo-object-literals.js +3 -2
  65. package/lib/rules/semantic-function-prefixes.js +15 -3
  66. package/lib/rules/sync-onwrite-name-func.js +5 -3
  67. package/lib/utils/ASTHelpers.js +6 -2
  68. package/lib/utils/graph/ClassGraphBuilder.js +4 -1
  69. package/lib/utils/graph/ClassGraphSorterReadability.js +3 -1
  70. package/package.json +3 -3
  71. package/lib/rules/require-image-overlayed.d.ts +0 -7
  72. package/lib/rules/require-image-overlayed.js +0 -82
@@ -76,7 +76,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
76
76
  reactImportNode = node;
77
77
  }
78
78
  // Keep track of default import for prioritization
79
- const hasDefaultImport = node.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
79
+ const hasDefaultImport = node.specifiers.some((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
80
80
  if (hasDefaultImport) {
81
81
  defaultReactImportNode = node;
82
82
  }
@@ -98,14 +98,14 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
98
98
  const targetImportNode = defaultReactImportNode || reactImportNode;
99
99
  if (targetImportNode) {
100
100
  // Check if it's a namespace import
101
- const hasNamespaceImport = targetImportNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
101
+ const hasNamespaceImport = targetImportNode.specifiers.some((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
102
102
  if (hasNamespaceImport) {
103
103
  // Add separate import for Fragment
104
104
  return fixer.insertTextAfter(targetImportNode, "\nimport { Fragment } from 'react';");
105
105
  }
106
106
  // Add Fragment to existing React import
107
107
  const lastSpecifier = targetImportNode.specifiers[targetImportNode.specifiers.length - 1];
108
- const hasNamedImports = targetImportNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
108
+ const hasNamedImports = targetImportNode.specifiers.some((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
109
109
  if (hasNamedImports) {
110
110
  return fixer.insertTextAfter(lastSpecifier, ', Fragment');
111
111
  }
@@ -131,11 +131,14 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
131
131
  // Special handling for nested fragments
132
132
  const reactFragmentParent = findReactFragmentParent(node);
133
133
  // Check if this fragment contains React.Fragment children
134
- const hasReactFragmentChild = node.children.some(child => child.type === utils_1.AST_NODE_TYPES.JSXElement &&
135
- child.openingElement.name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
136
- child.openingElement.name.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
134
+ const hasReactFragmentChild = node.children.some((child) => child.type === utils_1.AST_NODE_TYPES.JSXElement &&
135
+ child.openingElement.name.type ===
136
+ utils_1.AST_NODE_TYPES.JSXMemberExpression &&
137
+ child.openingElement.name.object.type ===
138
+ utils_1.AST_NODE_TYPES.JSXIdentifier &&
137
139
  child.openingElement.name.object.name === 'React' &&
138
- child.openingElement.name.property.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
140
+ child.openingElement.name.property.type ===
141
+ utils_1.AST_NODE_TYPES.JSXIdentifier &&
139
142
  child.openingElement.name.property.name === 'Fragment');
140
143
  // For nested fragments, we have multiple test cases with different expected behaviors
141
144
  if (reactFragmentParent) {
@@ -170,13 +173,13 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
170
173
  fixes.push(fixer.replaceText(node.openingFragment, newInnerOpeningText));
171
174
  fixes.push(fixer.replaceText(node.closingFragment, newInnerClosingText));
172
175
  return fixes;
173
- }
176
+ },
174
177
  });
175
178
  // Also report on the parent React.Fragment
176
179
  context.report({
177
180
  node: reactFragmentParent.openingElement.name,
178
181
  messageId: 'preferFragment',
179
- data: { type: 'React.Fragment' }
182
+ data: { type: 'React.Fragment' },
180
183
  });
181
184
  // Mark the parent as already handled
182
185
  reportedNodes.add(reactFragmentParent);
@@ -215,7 +218,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
215
218
  fixes.push(fixer.replaceText(node.openingFragment, newOpeningText));
216
219
  fixes.push(fixer.replaceText(node.closingFragment, newClosingText));
217
220
  return fixes;
218
- }
221
+ },
219
222
  });
220
223
  },
221
224
  // Find React.Fragment usage
@@ -236,7 +239,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
236
239
  reportedNodes.add(node);
237
240
  reportedNodes.add(node.name);
238
241
  // Check if this React.Fragment has a JSXFragment child
239
- const hasJSXFragmentChild = jsxElement.children.some(child => child.type === utils_1.AST_NODE_TYPES.JSXFragment);
242
+ const hasJSXFragmentChild = jsxElement.children.some((child) => child.type === utils_1.AST_NODE_TYPES.JSXFragment);
240
243
  // Check if this React.Fragment is inside a JSXFragment
241
244
  const fragmentParent = isInsideJSXFragment(node);
242
245
  // Special case: React.Fragment inside a JSX Fragment
@@ -264,7 +267,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
264
267
  fixes.push(fixer.replaceText(jsxElement.closingElement, newClosingText));
265
268
  }
266
269
  return fixes;
267
- }
270
+ },
268
271
  });
269
272
  return;
270
273
  }
@@ -295,7 +298,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
295
298
  fixes.push(fixer.replaceText(jsxElement.closingElement, newClosingText));
296
299
  }
297
300
  return fixes;
298
- }
301
+ },
299
302
  });
300
303
  },
301
304
  'Program:exit'() {
@@ -312,7 +315,7 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
312
315
  // Prioritize default import for adding Fragment
313
316
  if (reactImports.length > 0) {
314
317
  for (const importNode of reactImports) {
315
- const hasDefaultImport = importNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
318
+ const hasDefaultImport = importNode.specifiers.some((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
316
319
  if (hasDefaultImport) {
317
320
  defaultReactImportNode = importNode;
318
321
  reactImportNode = importNode;
@@ -325,8 +328,8 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
325
328
  }
326
329
  }
327
330
  }
328
- }
331
+ },
329
332
  };
330
- }
333
+ },
331
334
  });
332
335
  //# sourceMappingURL=prefer-fragment-component.js.map
@@ -27,7 +27,8 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
27
27
  */
28
28
  function containsStringLiteral(node) {
29
29
  // Direct string literal
30
- if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
30
+ if (node.type === utils_1.AST_NODE_TYPES.Literal &&
31
+ typeof node.value === 'string') {
31
32
  return true;
32
33
  }
33
34
  // String concatenation with + operator
@@ -38,17 +39,18 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
38
39
  }
39
40
  // Conditional (ternary) expression with string literals
40
41
  if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
41
- (containsStringLiteral(node.consequent) || containsStringLiteral(node.alternate))) {
42
+ (containsStringLiteral(node.consequent) ||
43
+ containsStringLiteral(node.alternate))) {
42
44
  return true;
43
45
  }
44
46
  // Template literal with static parts
45
47
  if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
46
48
  // Only report if there's a meaningful static part in the template
47
49
  // This allows dynamic templates like `${prefix}-${id}` but catches `static-${id}`
48
- const hasSignificantStaticPart = node.quasis.some(quasi => {
50
+ const hasSignificantStaticPart = node.quasis.some((quasi) => {
49
51
  const content = quasi.value.raw.trim();
50
52
  // Allow common separators like '-', '_', ':', '/' as they're just joining dynamic parts
51
- return content.length > 0 && !(/^[-_:/.]+$/.test(content));
53
+ return content.length > 0 && !/^[-_:/.]+$/.test(content);
52
54
  });
53
55
  return hasSignificantStaticPart;
54
56
  }
@@ -211,7 +211,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
211
211
  function hasABPattern(params) {
212
212
  if (params.length !== 2)
213
213
  return false;
214
- const paramNames = params.map(param => {
214
+ const paramNames = params.map((param) => {
215
215
  if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
216
216
  return param.name;
217
217
  }
@@ -0,0 +1,8 @@
1
+ type Options = [
2
+ {
3
+ allowComplexBodies?: boolean;
4
+ allowFunctionFactories?: boolean;
5
+ }
6
+ ];
7
+ export declare const preferUseCallbackOverUseMemoForFunctions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferUseCallback", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export default preferUseCallbackOverUseMemoForFunctions;
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferUseCallbackOverUseMemoForFunctions = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.preferUseCallbackOverUseMemoForFunctions = (0, createRule_1.createRule)({
7
+ name: 'prefer-usecallback-over-usememo-for-functions',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce using useCallback instead of useMemo for memoizing functions',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ allowComplexBodies: {
20
+ type: 'boolean',
21
+ default: true,
22
+ },
23
+ allowFunctionFactories: {
24
+ type: 'boolean',
25
+ default: true,
26
+ },
27
+ },
28
+ additionalProperties: false,
29
+ },
30
+ ],
31
+ messages: {
32
+ preferUseCallback: 'Use useCallback instead of useMemo for memoizing functions',
33
+ },
34
+ },
35
+ defaultOptions: [{ allowComplexBodies: false, allowFunctionFactories: true }],
36
+ create(context) {
37
+ const options = context.options[0] || {
38
+ allowComplexBodies: false,
39
+ allowFunctionFactories: true,
40
+ };
41
+ /**
42
+ * Checks if a node is a function factory (returns an object with functions or a function that generates functions)
43
+ */
44
+ function isFunctionFactory(node) {
45
+ // If we're not checking for function factories, return false
46
+ if (!options.allowFunctionFactories) {
47
+ return false;
48
+ }
49
+ // For arrow functions with implicit return
50
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
51
+ // Check if it's returning an object literal
52
+ if (node.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
53
+ // Check if any property in the object is a function
54
+ return node.body.properties.some((prop) => {
55
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
56
+ const value = prop.value;
57
+ return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
58
+ value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
59
+ }
60
+ return false;
61
+ });
62
+ }
63
+ return false;
64
+ }
65
+ // For arrow functions with block body
66
+ if (node.body.body.length === 1 &&
67
+ node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
68
+ node.body.body[0].argument) {
69
+ const returnValue = node.body.body[0].argument;
70
+ // Check if returning an object literal with functions
71
+ if (returnValue.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
72
+ return returnValue.properties.some((prop) => {
73
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
74
+ const value = prop.value;
75
+ return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
76
+ value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
77
+ }
78
+ return false;
79
+ });
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ /**
85
+ * Checks if a function body is complex (more than one statement before returning)
86
+ */
87
+ function hasComplexBody(node) {
88
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
89
+ return false;
90
+ }
91
+ // If there's more than one statement, or the single statement isn't a return
92
+ if (node.body.body.length > 1 ||
93
+ (node.body.body.length === 1 &&
94
+ node.body.body[0].type !== utils_1.AST_NODE_TYPES.ReturnStatement)) {
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ /**
100
+ * Checks if a node returns a function
101
+ */
102
+ function returnsFunction(node) {
103
+ // For arrow functions with implicit return
104
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
105
+ return (node.body.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
106
+ node.body.type === utils_1.AST_NODE_TYPES.FunctionExpression);
107
+ }
108
+ // For arrow functions with block body
109
+ if (node.body.body.length === 1 &&
110
+ node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
111
+ node.body.body[0].argument) {
112
+ const returnValue = node.body.body[0].argument;
113
+ return (returnValue.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
114
+ returnValue.type === utils_1.AST_NODE_TYPES.FunctionExpression);
115
+ }
116
+ return false;
117
+ }
118
+ return {
119
+ CallExpression(node) {
120
+ // Check if the call is to useMemo
121
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
122
+ node.callee.name === 'useMemo' &&
123
+ node.arguments.length > 0) {
124
+ const callback = node.arguments[0];
125
+ // Check if the callback is an arrow function or function expression
126
+ if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
127
+ callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
128
+ callback.body) {
129
+ // Skip if it's a function factory and we're allowing those
130
+ if (isFunctionFactory(callback)) {
131
+ return;
132
+ }
133
+ // Skip if it has a complex body and we're allowing those
134
+ if (hasComplexBody(callback) && options.allowComplexBodies) {
135
+ return;
136
+ }
137
+ // Check if it returns a function
138
+ if (returnsFunction(callback)) {
139
+ reportAndFix(node, context);
140
+ }
141
+ }
142
+ }
143
+ },
144
+ };
145
+ },
146
+ });
147
+ function reportAndFix(node, context) {
148
+ const sourceCode = context.getSourceCode();
149
+ const useMemoCallback = node.arguments[0];
150
+ const dependencyArray = node.arguments[1]
151
+ ? sourceCode.getText(node.arguments[1])
152
+ : '[]';
153
+ // Get the returned function from useMemo
154
+ let returnedFunction;
155
+ if (useMemoCallback.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
156
+ // For block body arrow functions or function expressions
157
+ const returnStatement = useMemoCallback.body.body[0];
158
+ returnedFunction = returnStatement.argument;
159
+ }
160
+ else {
161
+ // For implicit return arrow functions
162
+ returnedFunction = useMemoCallback.body;
163
+ }
164
+ // Create the useCallback replacement
165
+ const returnedFunctionText = sourceCode.getText(returnedFunction);
166
+ // Check if useMemo has TypeScript generic type parameters
167
+ const hasTypeParameters = node.typeParameters !== undefined;
168
+ const typeParametersText = hasTypeParameters
169
+ ? sourceCode.getText(node.typeParameters)
170
+ : '';
171
+ context.report({
172
+ node,
173
+ messageId: 'preferUseCallback',
174
+ fix: (fixer) => {
175
+ return fixer.replaceText(node, `useCallback${typeParametersText}(${returnedFunctionText}, ${dependencyArray})`);
176
+ },
177
+ });
178
+ }
179
+ exports.default = exports.preferUseCallbackOverUseMemoForFunctions;
180
+ //# sourceMappingURL=prefer-usecallback-over-usememo-for-functions.js.map
@@ -12,9 +12,83 @@ const isJsxElement = (node) => {
12
12
  if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
13
13
  return isJsxElement(node.consequent) || isJsxElement(node.alternate);
14
14
  }
15
+ // For logical expressions like '&&', the result can be a non-JSX value
16
+ if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
17
+ // If it's a logical AND (&&), it can return the left operand which might be non-JSX
18
+ if (node.operator === '&&') {
19
+ return false;
20
+ }
21
+ // For other logical operators, check both sides
22
+ return isJsxElement(node.left) || isJsxElement(node.right);
23
+ }
15
24
  return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
16
25
  node.type === utils_1.AST_NODE_TYPES.JSXFragment);
17
26
  };
27
+ /**
28
+ * Checks if a variable is used multiple times in a component
29
+ */
30
+ const isUsedMultipleTimes = (variableName, node) => {
31
+ // Find the function component that contains this node
32
+ let currentNode = node;
33
+ let functionNode;
34
+ // Walk up the AST to find the function component
35
+ while (currentNode.parent) {
36
+ currentNode = currentNode.parent;
37
+ if (currentNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
38
+ currentNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
39
+ currentNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
40
+ functionNode = currentNode;
41
+ break;
42
+ }
43
+ }
44
+ if (!functionNode) {
45
+ return false;
46
+ }
47
+ // Count occurrences of the variable in the function body
48
+ let count = 0;
49
+ // Function to recursively search for references to the variable
50
+ const findReferences = (searchNode) => {
51
+ if (!searchNode)
52
+ return;
53
+ // Check if this node is a reference to our variable
54
+ if (searchNode.type === utils_1.AST_NODE_TYPES.Identifier &&
55
+ searchNode.name === variableName &&
56
+ // Exclude the declaration itself
57
+ !(searchNode.parent &&
58
+ searchNode.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
59
+ searchNode.parent.id === searchNode)) {
60
+ count++;
61
+ }
62
+ // Recursively check all properties of the node
63
+ for (const key in searchNode) {
64
+ if (key === 'parent')
65
+ continue; // Skip parent to avoid circular references
66
+ const child = searchNode[key];
67
+ if (child && typeof child === 'object') {
68
+ if (Array.isArray(child)) {
69
+ child.forEach((item) => {
70
+ if (item && typeof item === 'object') {
71
+ findReferences(item);
72
+ }
73
+ });
74
+ }
75
+ else {
76
+ findReferences(child);
77
+ }
78
+ }
79
+ }
80
+ };
81
+ // Start the search from the function body
82
+ if (functionNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
83
+ findReferences(functionNode.body);
84
+ }
85
+ else if (functionNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
86
+ functionNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
87
+ findReferences(functionNode.body);
88
+ }
89
+ // Return true if the variable is referenced more than once
90
+ return count > 1;
91
+ };
18
92
  /**
19
93
  * Checks if an object contains JSX elements in its properties
20
94
  */
@@ -40,7 +114,8 @@ const containsJsxInObject = (node) => {
40
114
  const containsJsxInSwitchStatement = (node) => {
41
115
  for (const switchCase of node.cases) {
42
116
  for (const statement of switchCase.consequent) {
43
- if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
117
+ if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
118
+ statement.argument) {
44
119
  if (isJsxElement(statement.argument)) {
45
120
  return true;
46
121
  }
@@ -79,8 +154,17 @@ const containsJsxInFunction = (node) => {
79
154
  if (body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
80
155
  body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
81
156
  // Check array methods like map, filter, find, etc.
82
- const arrayMethods = ['map', 'filter', 'find', 'findIndex', 'some', 'every', 'reduce'];
83
- if (arrayMethods.includes(body.callee.property.name) && body.arguments.length > 0) {
157
+ const arrayMethods = [
158
+ 'map',
159
+ 'filter',
160
+ 'find',
161
+ 'findIndex',
162
+ 'some',
163
+ 'every',
164
+ 'reduce',
165
+ ];
166
+ if (arrayMethods.includes(body.callee.property.name) &&
167
+ body.arguments.length > 0) {
84
168
  const callback = body.arguments[0];
85
169
  if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
86
170
  callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
@@ -118,9 +202,17 @@ const containsJsxInExpression = (node) => {
118
202
  }
119
203
  switch (node.type) {
120
204
  case utils_1.AST_NODE_TYPES.ConditionalExpression:
121
- return containsJsxInExpression(node.consequent) || containsJsxInExpression(node.alternate);
205
+ return (containsJsxInExpression(node.consequent) ||
206
+ containsJsxInExpression(node.alternate));
122
207
  case utils_1.AST_NODE_TYPES.LogicalExpression:
123
- return containsJsxInExpression(node.left) || containsJsxInExpression(node.right);
208
+ // For logical AND (&&) expressions, if the left side can be falsy,
209
+ // then the expression can return a non-JSX value, so we should not flag it
210
+ if (node.operator === '&&') {
211
+ // If the left side is a boolean expression or can be falsy, this can return a non-JSX value
212
+ return false;
213
+ }
214
+ return (containsJsxInExpression(node.left) ||
215
+ containsJsxInExpression(node.right));
124
216
  case utils_1.AST_NODE_TYPES.ObjectExpression:
125
217
  // Special case: If this is an object with both JSX and non-JSX properties,
126
218
  // it's likely a data structure that happens to contain JSX, not a component
@@ -153,8 +245,8 @@ const containsJsxInExpression = (node) => {
153
245
  node.callee.property.name === 'map' &&
154
246
  node.arguments.length > 0) {
155
247
  const callback = node.arguments[0];
156
- if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
157
- callback.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
248
+ if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
249
+ callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
158
250
  // Check if the callback returns an object with both JSX and non-JSX properties
159
251
  if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
160
252
  callback.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
@@ -187,18 +279,27 @@ const containsJsxInExpression = (node) => {
187
279
  // Check array methods
188
280
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
189
281
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
190
- const arrayMethods = ['filter', 'find', 'findIndex', 'some', 'every', 'reduce'];
191
- if (arrayMethods.includes(node.callee.property.name) && node.arguments.length > 0) {
282
+ const arrayMethods = [
283
+ 'filter',
284
+ 'find',
285
+ 'findIndex',
286
+ 'some',
287
+ 'every',
288
+ 'reduce',
289
+ ];
290
+ if (arrayMethods.includes(node.callee.property.name) &&
291
+ node.arguments.length > 0) {
192
292
  const callback = node.arguments[0];
193
- if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
194
- callback.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
293
+ if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
294
+ callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
195
295
  return containsJsxInFunction(callback);
196
296
  }
197
297
  }
198
298
  }
199
299
  // Check arguments for JSX
200
300
  for (const arg of node.arguments) {
201
- if (arg.type !== utils_1.AST_NODE_TYPES.SpreadElement && containsJsxInExpression(arg)) {
301
+ if (arg.type !== utils_1.AST_NODE_TYPES.SpreadElement &&
302
+ containsJsxInExpression(arg)) {
202
303
  return true;
203
304
  }
204
305
  }
@@ -216,7 +317,8 @@ const containsJsxInExpression = (node) => {
216
317
  const containsJsxInBlockStatement = (node) => {
217
318
  for (const statement of node.body) {
218
319
  // Check return statements
219
- if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
320
+ if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
321
+ statement.argument) {
220
322
  if (containsJsxInExpression(statement.argument)) {
221
323
  return true;
222
324
  }
@@ -224,7 +326,8 @@ const containsJsxInBlockStatement = (node) => {
224
326
  // Check if statements
225
327
  if (statement.type === utils_1.AST_NODE_TYPES.IfStatement) {
226
328
  if (statement.consequent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
227
- statement.consequent.argument && containsJsxInExpression(statement.consequent.argument)) {
329
+ statement.consequent.argument &&
330
+ containsJsxInExpression(statement.consequent.argument)) {
228
331
  return true;
229
332
  }
230
333
  if (statement.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
@@ -233,7 +336,8 @@ const containsJsxInBlockStatement = (node) => {
233
336
  }
234
337
  if (statement.alternate) {
235
338
  if (statement.alternate.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
236
- statement.alternate.argument && containsJsxInExpression(statement.alternate.argument)) {
339
+ statement.alternate.argument &&
340
+ containsJsxInExpression(statement.alternate.argument)) {
237
341
  return true;
238
342
  }
239
343
  if (statement.alternate.type === utils_1.AST_NODE_TYPES.BlockStatement &&
@@ -246,10 +350,12 @@ const containsJsxInBlockStatement = (node) => {
246
350
  return true;
247
351
  }
248
352
  if (statement.alternate.consequent &&
249
- ((statement.alternate.consequent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
353
+ ((statement.alternate.consequent.type ===
354
+ utils_1.AST_NODE_TYPES.ReturnStatement &&
250
355
  statement.alternate.consequent.argument &&
251
356
  containsJsxInExpression(statement.alternate.consequent.argument)) ||
252
- (statement.alternate.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
357
+ (statement.alternate.consequent.type ===
358
+ utils_1.AST_NODE_TYPES.BlockStatement &&
253
359
  containsJsxInBlockStatement(statement.alternate.consequent)))) {
254
360
  return true;
255
361
  }
@@ -313,6 +419,11 @@ const containsJsxInUseMemo = (node) => {
313
419
  if (isJsxElement(callback.body)) {
314
420
  return true;
315
421
  }
422
+ // Special case for logical expressions that can return non-JSX values
423
+ if (callback.body.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
424
+ callback.body.operator === '&&') {
425
+ return false;
426
+ }
316
427
  // For non-JSX expressions, we need to check if they contain JSX
317
428
  return containsJsxInExpression(callback.body);
318
429
  }
@@ -338,6 +449,17 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
338
449
  return {
339
450
  CallExpression(node) {
340
451
  if (containsJsxInUseMemo(node)) {
452
+ // Check if this is a variable declaration
453
+ if (node.parent &&
454
+ node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
455
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
456
+ const variableName = node.parent.id.name;
457
+ // Check if the variable is used multiple times in the component
458
+ if (isUsedMultipleTimes(variableName, node)) {
459
+ // If the variable is used multiple times, allow it
460
+ return;
461
+ }
462
+ }
341
463
  context.report({
342
464
  node,
343
465
  messageId: 'useMemoShouldBeComponent',