@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.
- package/README.md +99 -83
- package/lib/index.js +16 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +10 -0
- package/lib/rules/consistent-callback-naming.js +20 -16
- package/lib/rules/enforce-assert-throws.js +10 -5
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +7 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +462 -0
- package/lib/rules/enforce-callable-types.d.ts +1 -1
- package/lib/rules/enforce-callable-types.js +13 -13
- package/lib/rules/enforce-callback-memo.js +6 -3
- package/lib/rules/enforce-centralized-mock-firestore.js +8 -6
- package/lib/rules/enforce-css-media-queries.js +3 -3
- package/lib/rules/enforce-dynamic-file-naming.d.ts +3 -0
- package/lib/rules/enforce-dynamic-file-naming.js +82 -0
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-firestore-doc-ref-generic.js +16 -12
- package/lib/rules/enforce-firestore-facade.js +7 -3
- package/lib/rules/enforce-firestore-path-utils.js +5 -2
- package/lib/rules/enforce-id-capitalization.js +59 -0
- package/lib/rules/enforce-identifiable-firestore-type.js +17 -10
- package/lib/rules/enforce-memoize-async.js +2 -2
- package/lib/rules/enforce-positive-naming.js +71 -27
- package/lib/rules/enforce-props-argument-name.js +31 -9
- package/lib/rules/enforce-realtimedb-path-utils.js +2 -1
- package/lib/rules/enforce-singular-type-names.js +4 -1
- package/lib/rules/enforce-timestamp-now.js +3 -4
- package/lib/rules/enforce-verb-noun-naming.js +18 -8
- package/lib/rules/ensure-pointer-events-none.js +20 -11
- package/lib/rules/extract-global-constants.js +25 -10
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +3 -0
- package/lib/rules/fast-deep-equal-over-microdiff.js +182 -0
- package/lib/rules/key-only-outermost-element.js +13 -7
- package/lib/rules/no-always-true-false-conditions.js +189 -5
- package/lib/rules/no-async-array-filter.js +1 -1
- package/lib/rules/no-circular-references.js +121 -59
- package/lib/rules/no-complex-cloud-params.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.js +113 -29
- package/lib/rules/no-explicit-return-type.js +3 -2
- package/lib/rules/no-firestore-jest-mock.js +1 -1
- package/lib/rules/no-firestore-object-arrays.js +2 -1
- package/lib/rules/no-hungarian.js +18 -1
- package/lib/rules/no-jsx-in-hooks.js +4 -3
- package/lib/rules/no-margin-properties.d.ts +1 -0
- package/lib/rules/no-margin-properties.js +314 -0
- package/lib/rules/no-mixed-firestore-transactions.js +8 -4
- package/lib/rules/no-mock-firebase-admin.js +6 -1
- package/lib/rules/no-object-values-on-strings.js +28 -10
- package/lib/rules/no-type-assertion-returns.js +47 -6
- package/lib/rules/no-unnecessary-destructuring.js +1 -1
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -31
- package/lib/rules/no-unused-props.js +10 -7
- package/lib/rules/no-unused-usestate.js +19 -6
- package/lib/rules/omit-index-html.js +1 -1
- package/lib/rules/prefer-batch-operations.js +19 -6
- package/lib/rules/prefer-clone-deep.js +16 -11
- package/lib/rules/prefer-destructuring-no-class.js +19 -0
- package/lib/rules/prefer-fragment-component.js +19 -16
- package/lib/rules/prefer-global-router-state-key.js +6 -4
- package/lib/rules/prefer-settings-object.js +1 -1
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.d.ts +8 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +180 -0
- package/lib/rules/react-usememo-should-be-component.js +139 -17
- package/lib/rules/require-hooks-default-params.js +25 -15
- package/lib/rules/require-usememo-object-literals.js +3 -2
- package/lib/rules/semantic-function-prefixes.js +15 -3
- package/lib/rules/sync-onwrite-name-func.js +5 -3
- package/lib/utils/ASTHelpers.js +6 -2
- package/lib/utils/graph/ClassGraphBuilder.js +4 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +3 -1
- package/package.json +3 -3
- package/lib/rules/require-image-overlayed.d.ts +0 -7
- 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 ===
|
|
136
|
-
|
|
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 ===
|
|
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 &&
|
|
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) ||
|
|
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 &&
|
|
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 &&
|
|
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 = [
|
|
83
|
-
|
|
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) ||
|
|
205
|
+
return (containsJsxInExpression(node.consequent) ||
|
|
206
|
+
containsJsxInExpression(node.alternate));
|
|
122
207
|
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
123
|
-
|
|
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 (
|
|
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 = [
|
|
191
|
-
|
|
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 (
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 ===
|
|
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 ===
|
|
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',
|