@blumintinc/eslint-plugin-blumint 1.9.0 → 1.10.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 +1 -0
- package/lib/index.js +10 -1
- package/lib/rules/enforce-callable-types.d.ts +1 -1
- package/lib/rules/enforce-callable-types.js +13 -13
- package/lib/rules/enforce-dynamic-file-naming.d.ts +3 -0
- package/lib/rules/enforce-dynamic-file-naming.js +80 -0
- package/lib/rules/enforce-id-capitalization.js +41 -0
- package/lib/rules/no-always-true-false-conditions.js +150 -0
- package/lib/rules/no-complex-cloud-params.js +4 -2
- package/lib/rules/no-hungarian.js +17 -0
- package/lib/rules/no-margin-properties.d.ts +1 -0
- package/lib/rules/no-margin-properties.js +307 -0
- package/lib/rules/no-type-assertion-returns.js +47 -6
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.d.ts +8 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +172 -0
- package/lib/rules/react-usememo-should-be-component.js +76 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Or use the recommended config:
|
|
|
70
70
|
| [enforce-callback-memo](docs/rules/enforce-callback-memo.md) | Enforce useCallback for inline functions and useMemo for objects/arrays containing functions in JSX props to prevent unnecessary re-renders. This improves React component performance by ensuring stable function references across renders and memoizing complex objects. | ✅ | | | |
|
|
71
71
|
| [enforce-centralized-mock-firestore](docs/rules/enforce-centralized-mock-firestore.md) | Enforce usage of centralized mockFirestore from predefined location | ✅ | | 🔧 | |
|
|
72
72
|
| [enforce-dynamic-firebase-imports](docs/rules/enforce-dynamic-firebase-imports.md) | Enforce dynamic importing for modules within the firebaseCloud directory to optimize initial bundle size. This ensures Firebase-related code is only loaded when needed, improving application startup time and reducing the main bundle size. | ✅ | | | |
|
|
73
|
+
| [enforce-dynamic-file-naming](docs/rules/enforce-dynamic-file-naming.md) | Enforce .dynamic.ts(x) file naming when @blumintinc/blumint/enforce-dynamic-imports rule is disabled. This improves code clarity and helps enforce consistent naming conventions across the codebase. | ✅ | | | |
|
|
73
74
|
| [enforce-exported-function-types](docs/rules/enforce-exported-function-types.md) | Enforce exporting types for function props and return values | ✅ | | | |
|
|
74
75
|
| [enforce-firestore-doc-ref-generic](docs/rules/enforce-firestore-doc-ref-generic.md) | Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup | ✅ | | | 💭 |
|
|
75
76
|
| [enforce-firestore-facade](docs/rules/enforce-firestore-facade.md) | Enforce usage of Firestore facades instead of direct Firestore methods | ✅ | | | |
|
package/lib/index.js
CHANGED
|
@@ -97,10 +97,13 @@ const omit_index_html_1 = require("./rules/omit-index-html");
|
|
|
97
97
|
const enforce_id_capitalization_1 = require("./rules/enforce-id-capitalization");
|
|
98
98
|
const no_unused_usestate_1 = require("./rules/no-unused-usestate");
|
|
99
99
|
const no_uuidv4_base62_as_key_1 = require("./rules/no-uuidv4-base62-as-key");
|
|
100
|
+
const enforce_dynamic_file_naming_1 = __importDefault(require("./rules/enforce-dynamic-file-naming"));
|
|
101
|
+
const prefer_usecallback_over_usememo_for_functions_1 = __importDefault(require("./rules/prefer-usecallback-over-usememo-for-functions"));
|
|
102
|
+
const no_margin_properties_1 = require("./rules/no-margin-properties");
|
|
100
103
|
module.exports = {
|
|
101
104
|
meta: {
|
|
102
105
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
103
|
-
version: '1.
|
|
106
|
+
version: '1.10.0',
|
|
104
107
|
},
|
|
105
108
|
parseOptions: {
|
|
106
109
|
ecmaVersion: 2020,
|
|
@@ -209,6 +212,9 @@ module.exports = {
|
|
|
209
212
|
'@blumintinc/blumint/omit-index-html': 'error',
|
|
210
213
|
'@blumintinc/blumint/enforce-id-capitalization': 'error',
|
|
211
214
|
'@blumintinc/blumint/no-unused-usestate': 'error',
|
|
215
|
+
'@blumintinc/blumint/enforce-dynamic-file-naming': 'error',
|
|
216
|
+
'@blumintinc/blumint/prefer-usecallback-over-usememo-for-functions': 'error',
|
|
217
|
+
'@blumintinc/blumint/no-margin-properties': 'error',
|
|
212
218
|
},
|
|
213
219
|
},
|
|
214
220
|
},
|
|
@@ -238,6 +244,7 @@ module.exports = {
|
|
|
238
244
|
'no-unused-props': no_unused_props_1.noUnusedProps,
|
|
239
245
|
'no-useless-fragment': no_useless_fragment_1.noUselessFragment,
|
|
240
246
|
'no-uuidv4-base62-as-key': no_uuidv4_base62_as_key_1.noUuidv4Base62AsKey,
|
|
247
|
+
'enforce-dynamic-file-naming': enforce_dynamic_file_naming_1.default,
|
|
241
248
|
'prefer-fragment-shorthand': prefer_fragment_shorthand_1.preferFragmentShorthand,
|
|
242
249
|
'prefer-type-over-interface': prefer_type_over_interface_1.preferTypeOverInterface,
|
|
243
250
|
'require-memo': require_memo_1.requireMemo,
|
|
@@ -307,6 +314,8 @@ module.exports = {
|
|
|
307
314
|
'omit-index-html': omit_index_html_1.omitIndexHtml,
|
|
308
315
|
'enforce-id-capitalization': enforce_id_capitalization_1.enforceIdCapitalization,
|
|
309
316
|
'no-unused-usestate': no_unused_usestate_1.noUnusedUseState,
|
|
317
|
+
'prefer-usecallback-over-usememo-for-functions': prefer_usecallback_over_usememo_for_functions_1.default,
|
|
318
|
+
'no-margin-properties': no_margin_properties_1.noMarginProperties,
|
|
310
319
|
},
|
|
311
320
|
};
|
|
312
321
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
type MessageIds = '
|
|
1
|
+
type MessageIds = 'missingParamsPropsType' | 'missingResponseType' | 'unusedParamsPropsType' | 'unusedResponseType';
|
|
2
2
|
export declare const enforceCallableTypes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
3
|
export {};
|
|
@@ -8,14 +8,14 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'problem',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce Params and Response type exports in callable functions',
|
|
11
|
+
description: 'Enforce Props/Params and Response type exports in callable functions',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
|
|
16
|
+
missingParamsPropsType: 'Missing Props or Params type export in callable function file',
|
|
17
17
|
missingResponseType: 'Missing Response type export in callable function file',
|
|
18
|
-
|
|
18
|
+
unusedParamsPropsType: 'Props or Params type is exported but not used in the callable function',
|
|
19
19
|
unusedResponseType: 'Response type is exported but not used in the callable function',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
@@ -28,18 +28,18 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
|
|
|
28
28
|
filename.includes('/callable/scripts/')) {
|
|
29
29
|
return {};
|
|
30
30
|
}
|
|
31
|
-
let
|
|
31
|
+
let hasParamsOrPropsExport = false;
|
|
32
32
|
let hasResponseExport = false;
|
|
33
33
|
let hasCallableFunction = false;
|
|
34
|
-
let
|
|
34
|
+
let paramsOrPropsTypeUsed = false;
|
|
35
35
|
let responseTypeUsed = false;
|
|
36
36
|
return {
|
|
37
37
|
// Check for type exports
|
|
38
38
|
ExportNamedDeclaration(node) {
|
|
39
39
|
if (node.declaration?.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
40
40
|
const typeName = node.declaration.id.name;
|
|
41
|
-
if (typeName === 'Params') {
|
|
42
|
-
|
|
41
|
+
if (typeName === 'Params' || typeName === 'Props') {
|
|
42
|
+
hasParamsOrPropsExport = true;
|
|
43
43
|
}
|
|
44
44
|
else if (typeName === 'Response') {
|
|
45
45
|
hasResponseExport = true;
|
|
@@ -56,8 +56,8 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
|
|
|
56
56
|
// Check for type usage in function parameters and return types
|
|
57
57
|
TSTypeReference(node) {
|
|
58
58
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
59
|
-
if (node.typeName.name === 'Params') {
|
|
60
|
-
|
|
59
|
+
if (node.typeName.name === 'Params' || node.typeName.name === 'Props') {
|
|
60
|
+
paramsOrPropsTypeUsed = true;
|
|
61
61
|
}
|
|
62
62
|
else if (node.typeName.name === 'Response') {
|
|
63
63
|
responseTypeUsed = true;
|
|
@@ -68,16 +68,16 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
|
|
|
68
68
|
if (!hasCallableFunction) {
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
if (!
|
|
71
|
+
if (!hasParamsOrPropsExport) {
|
|
72
72
|
context.report({
|
|
73
73
|
loc: { line: 1, column: 0 },
|
|
74
|
-
messageId: '
|
|
74
|
+
messageId: 'missingParamsPropsType',
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
-
else if (!
|
|
77
|
+
else if (!paramsOrPropsTypeUsed) {
|
|
78
78
|
context.report({
|
|
79
79
|
loc: { line: 1, column: 0 },
|
|
80
|
-
messageId: '
|
|
80
|
+
messageId: 'unusedParamsPropsType',
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
if (!hasResponseExport) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const RULE_NAME = "enforce-dynamic-file-naming";
|
|
2
|
+
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireDynamicExtension" | "requireDisableDirective", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RULE_NAME = void 0;
|
|
7
|
+
const createRule_1 = require("../utils/createRule");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
exports.RULE_NAME = 'enforce-dynamic-file-naming';
|
|
10
|
+
exports.default = (0, createRule_1.createRule)({
|
|
11
|
+
name: exports.RULE_NAME,
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce .dynamic.ts(x) file naming when @blumintinc/blumint/enforce-dynamic-imports rule is disabled',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
requireDynamicExtension: 'Files with disabled @blumintinc/blumint/enforce-dynamic-imports rule must use .dynamic.ts(x) extension',
|
|
21
|
+
requireDisableDirective: 'Files with .dynamic.ts(x) extension must have at least one @blumintinc/blumint/enforce-dynamic-imports disable directive',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultOptions: [],
|
|
25
|
+
create(context) {
|
|
26
|
+
// Get the file path and name
|
|
27
|
+
const filePath = context.getFilename();
|
|
28
|
+
const fileName = path_1.default.basename(filePath);
|
|
29
|
+
// Check if the file is a TypeScript file (ends with .ts or .tsx)
|
|
30
|
+
// Ignore files with other extensions like .test.ts, .deprecated.ts, etc.
|
|
31
|
+
const isTypeScriptFile = /^[^.]+\.tsx?$/.test(fileName);
|
|
32
|
+
// Check if the file has .dynamic.ts or .dynamic.tsx extension
|
|
33
|
+
const hasDynamicExtension = /\.dynamic\.tsx?$/.test(fileName);
|
|
34
|
+
// Skip if not a TypeScript file or has other extensions
|
|
35
|
+
if (!isTypeScriptFile && !hasDynamicExtension) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
// Track if we found a disable directive for enforce-dynamic-imports
|
|
39
|
+
let foundDisableDirective = false;
|
|
40
|
+
return {
|
|
41
|
+
Program() {
|
|
42
|
+
// Get the source code
|
|
43
|
+
const sourceCode = context.getSourceCode();
|
|
44
|
+
const comments = sourceCode.getAllComments();
|
|
45
|
+
// Check all comments for disable directives
|
|
46
|
+
for (const comment of comments) {
|
|
47
|
+
const commentText = comment.value.trim();
|
|
48
|
+
// Check for inline disable directive
|
|
49
|
+
if ((commentText.includes('eslint-disable-next-line') ||
|
|
50
|
+
commentText.includes('ednl')) &&
|
|
51
|
+
commentText.includes('@blumintinc/blumint/enforce-dynamic-imports')) {
|
|
52
|
+
foundDisableDirective = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
// Check for block disable directive
|
|
56
|
+
if (commentText.includes('eslint-disable ') &&
|
|
57
|
+
commentText.includes('@blumintinc/blumint/enforce-dynamic-imports')) {
|
|
58
|
+
foundDisableDirective = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// If we found a disable directive but the file doesn't have .dynamic extension
|
|
63
|
+
if (foundDisableDirective && !hasDynamicExtension) {
|
|
64
|
+
context.report({
|
|
65
|
+
loc: { line: 1, column: 0 },
|
|
66
|
+
messageId: 'requireDynamicExtension',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// If the file has .dynamic extension but no disable directive
|
|
70
|
+
if (hasDynamicExtension && !foundDisableDirective) {
|
|
71
|
+
context.report({
|
|
72
|
+
loc: { line: 1, column: 0 },
|
|
73
|
+
messageId: 'requireDisableDirective',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=enforce-dynamic-file-naming.js.map
|
|
@@ -61,6 +61,47 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
61
61
|
node.parent.id === node) {
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
|
+
// Check if the node is in an object property context
|
|
65
|
+
if (node.parent &&
|
|
66
|
+
node.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
67
|
+
node.parent.value === node) {
|
|
68
|
+
// Check if this is a property in an object pattern (destructuring)
|
|
69
|
+
let currentNode = node.parent;
|
|
70
|
+
while (currentNode.parent) {
|
|
71
|
+
if (currentNode.parent.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
currentNode = currentNode.parent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Check if the node is in a property assignment context
|
|
78
|
+
if (node.parent &&
|
|
79
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Check if the node is in a property access context
|
|
83
|
+
if (node.parent &&
|
|
84
|
+
node.parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// Check if the node is a string literal in a type definition context
|
|
88
|
+
// This handles cases like Pick<Type, 'id' | 'name'>
|
|
89
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
90
|
+
let currentNode = node;
|
|
91
|
+
while (currentNode.parent) {
|
|
92
|
+
// Check for TypeScript type contexts
|
|
93
|
+
if (currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
94
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeParameterInstantiation ||
|
|
95
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSUnionType ||
|
|
96
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSIntersectionType ||
|
|
97
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
98
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
99
|
+
currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
currentNode = currentNode.parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
64
105
|
return false;
|
|
65
106
|
}
|
|
66
107
|
/**
|
|
@@ -276,6 +276,92 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
276
276
|
node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
277
277
|
node.typeAnnotation.typeName.name === 'const');
|
|
278
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Check if a logical expression is being used for default value assignment
|
|
281
|
+
* This includes patterns like:
|
|
282
|
+
* - variable || defaultValue
|
|
283
|
+
* - variable ?? defaultValue
|
|
284
|
+
* - variable && expression
|
|
285
|
+
*/
|
|
286
|
+
function isDefaultValueAssignment(node) {
|
|
287
|
+
// Check if this is a nullish coalescing operator (always used for defaults)
|
|
288
|
+
if (node.operator === '??') {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
// Check if this is in a JSX attribute
|
|
292
|
+
if (node.parent &&
|
|
293
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
|
|
294
|
+
node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
// Check if this is a variable assignment with a default value
|
|
298
|
+
if (node.parent &&
|
|
299
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
300
|
+
node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
// Check if this is a function parameter default value
|
|
304
|
+
if (node.parent &&
|
|
305
|
+
node.parent.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
// Check if this is a return statement with a default value
|
|
309
|
+
if (node.parent &&
|
|
310
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
// Check if this is a function argument (common for default values)
|
|
314
|
+
if (node.parent &&
|
|
315
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
316
|
+
node.parent.type === utils_1.AST_NODE_TYPES.NewExpression)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
// Check if this is part of an object or array expression (common for default values)
|
|
320
|
+
if (node.parent &&
|
|
321
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
322
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
// Check if this is inside a property assignment in an object
|
|
326
|
+
if (node.parent &&
|
|
327
|
+
node.parent.type === utils_1.AST_NODE_TYPES.Property) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
// Check if this is inside a template literal expression
|
|
331
|
+
if (node.parent &&
|
|
332
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
// Check if this is inside a template element
|
|
336
|
+
if (node.parent &&
|
|
337
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TemplateElement) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
// Check if this is inside a spread element
|
|
341
|
+
if (node.parent &&
|
|
342
|
+
node.parent.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
// Check if this is inside an array method callback (map, filter, etc.)
|
|
346
|
+
if (node.parent &&
|
|
347
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
348
|
+
node.parent.parent &&
|
|
349
|
+
node.parent.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
350
|
+
node.parent.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
351
|
+
const methodName = node.parent.parent.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
352
|
+
? node.parent.parent.callee.property.name
|
|
353
|
+
: '';
|
|
354
|
+
if (['map', 'filter', 'find', 'findIndex', 'some', 'every', 'forEach'].includes(methodName)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Check if this is inside an arrow function body
|
|
359
|
+
if (node.parent &&
|
|
360
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
279
365
|
/**
|
|
280
366
|
* Check logical expressions (&&, ||)
|
|
281
367
|
*/
|
|
@@ -285,6 +371,12 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
285
371
|
// Add children to evaluated list to prevent duplicate evaluations
|
|
286
372
|
evaluatedParentNodes.add(node.left);
|
|
287
373
|
evaluatedParentNodes.add(node.right);
|
|
374
|
+
// Check if this is a default value assignment pattern
|
|
375
|
+
const isDefaultValuePattern = isDefaultValueAssignment(node);
|
|
376
|
+
if (isDefaultValuePattern) {
|
|
377
|
+
// Don't flag default value assignments as always true/false conditions
|
|
378
|
+
return {};
|
|
379
|
+
}
|
|
288
380
|
// For &&: if either side is always falsy, the whole expression is falsy
|
|
289
381
|
if (node.operator === '&&') {
|
|
290
382
|
if (leftResult.isFalsy) {
|
|
@@ -724,6 +816,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
724
816
|
}
|
|
725
817
|
// Template literals
|
|
726
818
|
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
819
|
+
// Skip template literals that are likely used for string interpolation
|
|
820
|
+
// rather than conditions
|
|
821
|
+
if (node.parent &&
|
|
822
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
823
|
+
node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
824
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
825
|
+
node.parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
826
|
+
node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
|
|
827
|
+
return {};
|
|
828
|
+
}
|
|
727
829
|
return checkTemplateLiteral(node);
|
|
728
830
|
}
|
|
729
831
|
// Type checking (typeof x === 'string')
|
|
@@ -1092,11 +1194,59 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1092
1194
|
/**
|
|
1093
1195
|
* Check if a condition is always truthy or falsy and report it
|
|
1094
1196
|
*/
|
|
1197
|
+
/**
|
|
1198
|
+
* Check if a ternary expression is being used for default value assignment
|
|
1199
|
+
* This includes patterns like:
|
|
1200
|
+
* - variable ? variable : defaultValue
|
|
1201
|
+
*/
|
|
1202
|
+
function isDefaultValueTernary(node) {
|
|
1203
|
+
// Check if the condition and the consequent are the same variable
|
|
1204
|
+
// This is a common pattern for default values: status ? status : 'offline'
|
|
1205
|
+
if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1206
|
+
node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1207
|
+
node.test.name === node.consequent.name) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
// Check if this is in a JSX attribute
|
|
1211
|
+
if (node.parent &&
|
|
1212
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
|
|
1213
|
+
node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
// Check if this is a variable assignment with a default value
|
|
1217
|
+
if (node.parent &&
|
|
1218
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
1219
|
+
node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression)) {
|
|
1220
|
+
// Only if the condition and consequent are the same variable
|
|
1221
|
+
if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1222
|
+
node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1223
|
+
node.test.name === node.consequent.name) {
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// Check if this is a return statement with a default value
|
|
1228
|
+
if (node.parent &&
|
|
1229
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
1230
|
+
// Only if the condition and consequent are the same variable
|
|
1231
|
+
if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1232
|
+
node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1233
|
+
node.test.name === node.consequent.name) {
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1095
1239
|
function checkCondition(node) {
|
|
1096
1240
|
// Skip if already reported or if it's a part of a larger expression that's been reported
|
|
1097
1241
|
if (reportedNodes.has(node) || evaluatedParentNodes.has(node)) {
|
|
1098
1242
|
return;
|
|
1099
1243
|
}
|
|
1244
|
+
// Check if this is a ternary expression used for default values
|
|
1245
|
+
if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
1246
|
+
isDefaultValueTernary(node)) {
|
|
1247
|
+
// Skip checking ternaries used for default values
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1100
1250
|
// We should NOT clear evaluatedParentNodes here as that can lead to duplicate evaluations
|
|
1101
1251
|
// and miss detection of conditions in nested expressions
|
|
1102
1252
|
// Check for nested expressions to avoid multiple errors
|
|
@@ -244,8 +244,10 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
|
|
|
244
244
|
['WeakMap', 'WeakSet', 'Promise', 'Error'].includes(node.object.name)) {
|
|
245
245
|
return true;
|
|
246
246
|
}
|
|
247
|
-
//
|
|
248
|
-
|
|
247
|
+
// For property access expressions, we only need to check if the object itself is complex
|
|
248
|
+
// We don't need to check the property, as accessing a property will result in a primitive value
|
|
249
|
+
// or another object that we'll check separately
|
|
250
|
+
return isComplexValue(node.object);
|
|
249
251
|
}
|
|
250
252
|
// Check for Symbols
|
|
251
253
|
if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === 'Symbol') {
|
|
@@ -34,6 +34,17 @@ const TYPE_MARKERS = [
|
|
|
34
34
|
'Type',
|
|
35
35
|
'Enum',
|
|
36
36
|
];
|
|
37
|
+
// Allowed descriptive suffixes that should not be flagged as Hungarian notation
|
|
38
|
+
const ALLOWED_SUFFIXES = [
|
|
39
|
+
'Formatted',
|
|
40
|
+
'Parsed',
|
|
41
|
+
'Processed',
|
|
42
|
+
'Transformed',
|
|
43
|
+
'Converted',
|
|
44
|
+
'Rendered',
|
|
45
|
+
'Display',
|
|
46
|
+
'Displayed',
|
|
47
|
+
];
|
|
37
48
|
// Common built-in JavaScript prototype methods
|
|
38
49
|
const BUILT_IN_METHODS = new Set([
|
|
39
50
|
// String methods
|
|
@@ -197,6 +208,12 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
197
208
|
const checkedIdentifiers = new Set();
|
|
198
209
|
// Check if a variable name contains a type marker with proper word boundaries
|
|
199
210
|
function hasTypeMarker(variableName) {
|
|
211
|
+
// Check if the variable name ends with one of the allowed descriptive suffixes
|
|
212
|
+
if (ALLOWED_SUFFIXES.some(suffix => variableName.endsWith(suffix) &&
|
|
213
|
+
variableName.length > suffix.length &&
|
|
214
|
+
/[a-z]/.test(variableName[variableName.length - suffix.length - 1]))) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
200
217
|
const normalizedVarName = variableName.toLowerCase();
|
|
201
218
|
// Handle SCREAMING_SNAKE_CASE separately
|
|
202
219
|
if (variableName === variableName.toUpperCase() &&
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noMarginProperties: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noMarginProperties", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noMarginProperties = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// Convert camelCase to kebab-case
|
|
7
|
+
function toKebabCase(str) {
|
|
8
|
+
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
9
|
+
}
|
|
10
|
+
// Normalize property name to kebab-case for consistent lookup
|
|
11
|
+
function normalizePropertyName(name) {
|
|
12
|
+
// If already contains hyphens, assume it's kebab-case
|
|
13
|
+
if (name.includes('-'))
|
|
14
|
+
return name.toLowerCase();
|
|
15
|
+
// Convert camelCase to kebab-case
|
|
16
|
+
return toKebabCase(name).toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
// List of margin properties to flag
|
|
19
|
+
const MARGIN_PROPERTIES = new Set([
|
|
20
|
+
'margin',
|
|
21
|
+
'margin-left',
|
|
22
|
+
'margin-right',
|
|
23
|
+
'margin-top',
|
|
24
|
+
'margin-bottom',
|
|
25
|
+
'marginleft',
|
|
26
|
+
'marginright',
|
|
27
|
+
'margintop',
|
|
28
|
+
'marginbottom',
|
|
29
|
+
'mx',
|
|
30
|
+
'my',
|
|
31
|
+
'mt',
|
|
32
|
+
'mr',
|
|
33
|
+
'mb',
|
|
34
|
+
'ml',
|
|
35
|
+
'm',
|
|
36
|
+
]);
|
|
37
|
+
exports.noMarginProperties = (0, createRule_1.createRule)({
|
|
38
|
+
name: 'no-margin-properties',
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'suggestion',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Discourage using margin properties (margin, marginLeft, marginRight, marginTop, marginBottom, mx, my, etc.) for spacing in MUI components. Instead, prefer defining spacing with padding, gap, or the spacing prop for more predictable layouts.',
|
|
43
|
+
recommended: 'error',
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
messages: {
|
|
47
|
+
noMarginProperties: 'Avoid using {{property}} for spacing in MUI components. Use padding, gap, or the spacing prop instead for more predictable layouts. See https://www.youtube.com/watch?v=KVQMoEFUee8 for more details.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [],
|
|
51
|
+
create(context) {
|
|
52
|
+
const seenNodes = new WeakSet();
|
|
53
|
+
function checkProperty(propertyName) {
|
|
54
|
+
const normalizedName = normalizePropertyName(propertyName);
|
|
55
|
+
return MARGIN_PROPERTIES.has(normalizedName);
|
|
56
|
+
}
|
|
57
|
+
// Check if a node is within an sx prop context or theme override context
|
|
58
|
+
function isMuiStylingContext(node) {
|
|
59
|
+
let current = node;
|
|
60
|
+
while (current?.parent) {
|
|
61
|
+
// Check for JSX sx attribute (MUI specific)
|
|
62
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
63
|
+
current.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
64
|
+
current.parent.name.name === 'sx') {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
// Check for sx prop in object spread
|
|
68
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
69
|
+
current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
current.parent.key.name === 'sx') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Check for theme overrides (MUI's createTheme)
|
|
74
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
75
|
+
current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
76
|
+
(current.parent.key.name === 'styleOverrides' ||
|
|
77
|
+
current.parent.key.name === 'components')) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
// Check for MUI's css function
|
|
81
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
82
|
+
current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
83
|
+
current.parent.callee.name === 'css') {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
// Skip if we're in a TypeScript type definition
|
|
87
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
88
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
89
|
+
current.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
current = current.parent;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function checkNode(node) {
|
|
97
|
+
// Skip if we've already processed this node
|
|
98
|
+
if (seenNodes.has(node))
|
|
99
|
+
return;
|
|
100
|
+
seenNodes.add(node);
|
|
101
|
+
let propertyName = '';
|
|
102
|
+
// Get property name
|
|
103
|
+
if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
104
|
+
propertyName = node.key.name;
|
|
105
|
+
}
|
|
106
|
+
else if (node.key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
107
|
+
propertyName = String(node.key.value);
|
|
108
|
+
}
|
|
109
|
+
else if (node.computed && node.key.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
110
|
+
// Handle template literals like [`${prop}Top`]
|
|
111
|
+
const quasis = node.key.quasis.map(q => q.value.raw).join('');
|
|
112
|
+
const expressions = node.key.expressions.map(exp => {
|
|
113
|
+
if (exp.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
114
|
+
return exp.name;
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}).join('');
|
|
118
|
+
propertyName = quasis + expressions;
|
|
119
|
+
}
|
|
120
|
+
if (propertyName && checkProperty(propertyName)) {
|
|
121
|
+
// Check if in MUI styling context
|
|
122
|
+
if (isMuiStylingContext(node)) {
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
messageId: 'noMarginProperties',
|
|
126
|
+
data: {
|
|
127
|
+
property: propertyName,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Check object expression for margin properties
|
|
134
|
+
function checkObjectExpression(objExp) {
|
|
135
|
+
objExp.properties.forEach(prop => {
|
|
136
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
137
|
+
checkNode(prop);
|
|
138
|
+
}
|
|
139
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement &&
|
|
140
|
+
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
141
|
+
// Handle spread elements by looking up the variable
|
|
142
|
+
const variableName = prop.argument.name;
|
|
143
|
+
const scope = context.getScope();
|
|
144
|
+
const variable = scope.variables.find(v => v.name === variableName);
|
|
145
|
+
if (variable && variable.defs.length > 0) {
|
|
146
|
+
const def = variable.defs[0];
|
|
147
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
148
|
+
def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
149
|
+
checkObjectExpression(def.node.init);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
// Handle object literal properties (inline styles)
|
|
157
|
+
Property(node) {
|
|
158
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
159
|
+
return;
|
|
160
|
+
checkNode(node);
|
|
161
|
+
},
|
|
162
|
+
// Handle JSX sx attributes with object expressions
|
|
163
|
+
JSXAttribute(node) {
|
|
164
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier ||
|
|
165
|
+
node.name.name !== 'sx')
|
|
166
|
+
return;
|
|
167
|
+
if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
168
|
+
node.value.expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
169
|
+
checkObjectExpression(node.value.expression);
|
|
170
|
+
}
|
|
171
|
+
else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
172
|
+
node.value.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
173
|
+
// Handle variable reference in sx prop
|
|
174
|
+
const variableName = node.value.expression.name;
|
|
175
|
+
const scope = context.getScope();
|
|
176
|
+
const variable = scope.variables.find(v => v.name === variableName);
|
|
177
|
+
if (variable && variable.defs.length > 0) {
|
|
178
|
+
const def = variable.defs[0];
|
|
179
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
180
|
+
def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
181
|
+
checkObjectExpression(def.node.init);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
186
|
+
node.value.expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
187
|
+
// Handle function-based sx props
|
|
188
|
+
if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
189
|
+
// Arrow function with object expression body
|
|
190
|
+
checkObjectExpression(node.value.expression.body);
|
|
191
|
+
}
|
|
192
|
+
else if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
193
|
+
// Arrow function with block body
|
|
194
|
+
const returnStatements = node.value.expression.body.body.filter(stmt => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
|
|
195
|
+
returnStatements.forEach(returnStmt => {
|
|
196
|
+
if (returnStmt.argument?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
197
|
+
checkObjectExpression(returnStmt.argument);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
203
|
+
node.value.expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
204
|
+
// Handle conditional expressions in sx props
|
|
205
|
+
if (node.value.expression.consequent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
206
|
+
checkObjectExpression(node.value.expression.consequent);
|
|
207
|
+
}
|
|
208
|
+
if (node.value.expression.alternate.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
209
|
+
checkObjectExpression(node.value.expression.alternate);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
// Handle variable declarations that might be used in sx props
|
|
214
|
+
VariableDeclarator(node) {
|
|
215
|
+
if (node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
216
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
217
|
+
const variableName = node.id.name;
|
|
218
|
+
const sourceCode = context.getSourceCode().getText();
|
|
219
|
+
// Check for margin properties in the object
|
|
220
|
+
node.init.properties.forEach(prop => {
|
|
221
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
222
|
+
let propertyName = '';
|
|
223
|
+
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
224
|
+
propertyName = prop.key.name;
|
|
225
|
+
}
|
|
226
|
+
else if (prop.key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
227
|
+
propertyName = String(prop.key.value);
|
|
228
|
+
}
|
|
229
|
+
if (propertyName && checkProperty(propertyName)) {
|
|
230
|
+
// Check if this variable is used in an sx prop
|
|
231
|
+
if (sourceCode.includes(`sx={${variableName}}`) ||
|
|
232
|
+
sourceCode.includes(`sx={{ ...${variableName}`) ||
|
|
233
|
+
sourceCode.includes(`sx={Object.assign({}, ${variableName}`)) {
|
|
234
|
+
context.report({
|
|
235
|
+
node: prop,
|
|
236
|
+
messageId: 'noMarginProperties',
|
|
237
|
+
data: {
|
|
238
|
+
property: propertyName,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
// Handle direct margin props on MUI components
|
|
248
|
+
JSXOpeningElement(node) {
|
|
249
|
+
node.attributes.forEach(attr => {
|
|
250
|
+
if (attr.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
251
|
+
attr.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
252
|
+
const attrName = attr.name.name;
|
|
253
|
+
if (checkProperty(attrName)) {
|
|
254
|
+
context.report({
|
|
255
|
+
node: attr,
|
|
256
|
+
messageId: 'noMarginProperties',
|
|
257
|
+
data: {
|
|
258
|
+
property: attrName,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
// Handle MUI's css function
|
|
266
|
+
CallExpression(node) {
|
|
267
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
268
|
+
node.callee.name === 'css' &&
|
|
269
|
+
node.arguments.length > 0) {
|
|
270
|
+
const arg = node.arguments[0];
|
|
271
|
+
if (arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
272
|
+
checkObjectExpression(arg);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Handle createTheme for MUI theme overrides
|
|
276
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
277
|
+
node.callee.name === 'createTheme' &&
|
|
278
|
+
node.arguments.length > 0 &&
|
|
279
|
+
node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
280
|
+
const themeObj = node.arguments[0];
|
|
281
|
+
// Find components property in theme object
|
|
282
|
+
const componentsProperty = themeObj.properties.find(prop => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
283
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
284
|
+
prop.key.name === 'components' &&
|
|
285
|
+
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
286
|
+
if (componentsProperty && componentsProperty.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
287
|
+
// Check each component override
|
|
288
|
+
componentsProperty.value.properties.forEach(componentProp => {
|
|
289
|
+
if (componentProp.type === utils_1.AST_NODE_TYPES.Property &&
|
|
290
|
+
componentProp.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
291
|
+
// Find styleOverrides property
|
|
292
|
+
const styleOverrides = componentProp.value.properties.find(prop => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
293
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
294
|
+
prop.key.name === 'styleOverrides' &&
|
|
295
|
+
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
296
|
+
if (styleOverrides && styleOverrides.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
297
|
+
checkObjectExpression(styleOverrides.value);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
//# sourceMappingURL=no-margin-properties.js.map
|
|
@@ -98,6 +98,24 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
98
98
|
defaultOptions: [defaultOptions],
|
|
99
99
|
create(context, [options]) {
|
|
100
100
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
101
|
+
/**
|
|
102
|
+
* Checks if a node is inside a JSX attribute or object property (which could be JSX props)
|
|
103
|
+
*/
|
|
104
|
+
function isInsideJSXAttributeOrObjectProperty(node) {
|
|
105
|
+
let current = node;
|
|
106
|
+
while (current?.parent) {
|
|
107
|
+
// Direct JSX attribute
|
|
108
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Object property (which could be JSX props)
|
|
112
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.Property) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
current = current.parent;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
101
119
|
/**
|
|
102
120
|
* Common function to check if a type assertion should be allowed
|
|
103
121
|
*/
|
|
@@ -136,13 +154,31 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
136
154
|
return true;
|
|
137
155
|
}
|
|
138
156
|
// Allow type assertions within conditional expressions, but only if they're not part of a return statement
|
|
139
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
157
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
158
|
+
!isInsideReturnStatement(node)) {
|
|
140
159
|
return true;
|
|
141
160
|
}
|
|
142
161
|
// Allow type assertions used to access properties in conditional contexts
|
|
143
162
|
if (isPropertyAccess(node) && isInsideConditionalStatement(node)) {
|
|
144
163
|
return true;
|
|
145
164
|
}
|
|
165
|
+
// Allow type assertions in object instantiations (as constructor arguments)
|
|
166
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
167
|
+
node.parent.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Allow type assertions as arguments to constructors
|
|
171
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
// Allow type assertions in JSX attributes/props or object properties
|
|
175
|
+
if (isInsideJSXAttributeOrObjectProperty(node)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
// Only flag type assertions in return statements
|
|
179
|
+
if (!isInsideReturnStatement(node)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
146
182
|
return false;
|
|
147
183
|
}
|
|
148
184
|
/**
|
|
@@ -152,11 +188,13 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
152
188
|
if (!node.returnType)
|
|
153
189
|
return;
|
|
154
190
|
// Allow type predicates if configured
|
|
155
|
-
if (mergedOptions.allowTypePredicates &&
|
|
191
|
+
if (mergedOptions.allowTypePredicates &&
|
|
192
|
+
isTypePredicate(node.returnType)) {
|
|
156
193
|
return;
|
|
157
194
|
}
|
|
158
195
|
// If type predicates are not allowed, report them
|
|
159
|
-
if (!mergedOptions.allowTypePredicates &&
|
|
196
|
+
if (!mergedOptions.allowTypePredicates &&
|
|
197
|
+
isTypePredicate(node.returnType)) {
|
|
160
198
|
context.report({
|
|
161
199
|
node: node.returnType,
|
|
162
200
|
messageId: 'useExplicitVariable',
|
|
@@ -166,7 +204,8 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
166
204
|
// Check if the function has a return statement with a direct value (not a variable)
|
|
167
205
|
if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
168
206
|
for (const statement of node.body.body) {
|
|
169
|
-
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
207
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
208
|
+
statement.argument) {
|
|
170
209
|
// If returning a variable reference, that's fine
|
|
171
210
|
if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
172
211
|
continue;
|
|
@@ -233,11 +272,13 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
|
|
|
233
272
|
// Check for explicit return type
|
|
234
273
|
if (node.returnType) {
|
|
235
274
|
// Allow type predicates if configured
|
|
236
|
-
if (mergedOptions.allowTypePredicates &&
|
|
275
|
+
if (mergedOptions.allowTypePredicates &&
|
|
276
|
+
isTypePredicate(node.returnType)) {
|
|
237
277
|
return;
|
|
238
278
|
}
|
|
239
279
|
// If type predicates are not allowed, report them
|
|
240
|
-
if (!mergedOptions.allowTypePredicates &&
|
|
280
|
+
if (!mergedOptions.allowTypePredicates &&
|
|
281
|
+
isTypePredicate(node.returnType)) {
|
|
241
282
|
context.report({
|
|
242
283
|
node: node.returnType,
|
|
243
284
|
messageId: 'useExplicitVariable',
|
|
@@ -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,172 @@
|
|
|
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] || { allowComplexBodies: false, allowFunctionFactories: true };
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a node is a function factory (returns an object with functions or a function that generates functions)
|
|
40
|
+
*/
|
|
41
|
+
function isFunctionFactory(node) {
|
|
42
|
+
// If we're not checking for function factories, return false
|
|
43
|
+
if (!options.allowFunctionFactories) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// For arrow functions with implicit return
|
|
47
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
48
|
+
// Check if it's returning an object literal
|
|
49
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
50
|
+
// Check if any property in the object is a function
|
|
51
|
+
return node.body.properties.some((prop) => {
|
|
52
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
53
|
+
const value = prop.value;
|
|
54
|
+
return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
55
|
+
value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// For arrow functions with block body
|
|
63
|
+
if (node.body.body.length === 1 &&
|
|
64
|
+
node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
65
|
+
node.body.body[0].argument) {
|
|
66
|
+
const returnValue = node.body.body[0].argument;
|
|
67
|
+
// Check if returning an object literal with functions
|
|
68
|
+
if (returnValue.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
69
|
+
return returnValue.properties.some((prop) => {
|
|
70
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
71
|
+
const value = prop.value;
|
|
72
|
+
return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
73
|
+
value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a function body is complex (more than one statement before returning)
|
|
83
|
+
*/
|
|
84
|
+
function hasComplexBody(node) {
|
|
85
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// If there's more than one statement, or the single statement isn't a return
|
|
89
|
+
if (node.body.body.length > 1 ||
|
|
90
|
+
(node.body.body.length === 1 && node.body.body[0].type !== utils_1.AST_NODE_TYPES.ReturnStatement)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checks if a node returns a function
|
|
97
|
+
*/
|
|
98
|
+
function returnsFunction(node) {
|
|
99
|
+
// For arrow functions with implicit return
|
|
100
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
101
|
+
return (node.body.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
102
|
+
node.body.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
103
|
+
}
|
|
104
|
+
// For arrow functions with block body
|
|
105
|
+
if (node.body.body.length === 1 &&
|
|
106
|
+
node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
107
|
+
node.body.body[0].argument) {
|
|
108
|
+
const returnValue = node.body.body[0].argument;
|
|
109
|
+
return (returnValue.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
110
|
+
returnValue.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
CallExpression(node) {
|
|
116
|
+
// Check if the call is to useMemo
|
|
117
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
118
|
+
node.callee.name === 'useMemo' &&
|
|
119
|
+
node.arguments.length > 0) {
|
|
120
|
+
const callback = node.arguments[0];
|
|
121
|
+
// Check if the callback is an arrow function or function expression
|
|
122
|
+
if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
123
|
+
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
124
|
+
callback.body) {
|
|
125
|
+
// Skip if it's a function factory and we're allowing those
|
|
126
|
+
if (isFunctionFactory(callback)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Skip if it has a complex body and we're allowing those
|
|
130
|
+
if (hasComplexBody(callback) && options.allowComplexBodies) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Check if it returns a function
|
|
134
|
+
if (returnsFunction(callback)) {
|
|
135
|
+
reportAndFix(node, context);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
function reportAndFix(node, context) {
|
|
144
|
+
const sourceCode = context.getSourceCode();
|
|
145
|
+
const useMemoCallback = node.arguments[0];
|
|
146
|
+
const dependencyArray = node.arguments[1] ? sourceCode.getText(node.arguments[1]) : '[]';
|
|
147
|
+
// Get the returned function from useMemo
|
|
148
|
+
let returnedFunction;
|
|
149
|
+
if (useMemoCallback.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
150
|
+
// For block body arrow functions or function expressions
|
|
151
|
+
const returnStatement = useMemoCallback.body.body[0];
|
|
152
|
+
returnedFunction = returnStatement.argument;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// For implicit return arrow functions
|
|
156
|
+
returnedFunction = useMemoCallback.body;
|
|
157
|
+
}
|
|
158
|
+
// Create the useCallback replacement
|
|
159
|
+
const returnedFunctionText = sourceCode.getText(returnedFunction);
|
|
160
|
+
// Check if useMemo has TypeScript generic type parameters
|
|
161
|
+
const hasTypeParameters = node.typeParameters !== undefined;
|
|
162
|
+
const typeParametersText = hasTypeParameters ? sourceCode.getText(node.typeParameters) : '';
|
|
163
|
+
context.report({
|
|
164
|
+
node,
|
|
165
|
+
messageId: 'preferUseCallback',
|
|
166
|
+
fix: (fixer) => {
|
|
167
|
+
return fixer.replaceText(node, `useCallback${typeParametersText}(${returnedFunctionText}, ${dependencyArray})`);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
exports.default = exports.preferUseCallbackOverUseMemoForFunctions;
|
|
172
|
+
//# sourceMappingURL=prefer-usecallback-over-usememo-for-functions.js.map
|
|
@@ -15,6 +15,71 @@ const isJsxElement = (node) => {
|
|
|
15
15
|
return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
16
16
|
node.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a variable is used multiple times in a component
|
|
20
|
+
*/
|
|
21
|
+
const isUsedMultipleTimes = (variableName, node) => {
|
|
22
|
+
// Find the function component that contains this node
|
|
23
|
+
let currentNode = node;
|
|
24
|
+
let functionNode;
|
|
25
|
+
// Walk up the AST to find the function component
|
|
26
|
+
while (currentNode.parent) {
|
|
27
|
+
currentNode = currentNode.parent;
|
|
28
|
+
if (currentNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
29
|
+
currentNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
30
|
+
currentNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
31
|
+
functionNode = currentNode;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!functionNode) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Count occurrences of the variable in the function body
|
|
39
|
+
let count = 0;
|
|
40
|
+
// Function to recursively search for references to the variable
|
|
41
|
+
const findReferences = (searchNode) => {
|
|
42
|
+
if (!searchNode)
|
|
43
|
+
return;
|
|
44
|
+
// Check if this node is a reference to our variable
|
|
45
|
+
if (searchNode.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
46
|
+
searchNode.name === variableName &&
|
|
47
|
+
// Exclude the declaration itself
|
|
48
|
+
!(searchNode.parent &&
|
|
49
|
+
searchNode.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
50
|
+
searchNode.parent.id === searchNode)) {
|
|
51
|
+
count++;
|
|
52
|
+
}
|
|
53
|
+
// Recursively check all properties of the node
|
|
54
|
+
for (const key in searchNode) {
|
|
55
|
+
if (key === 'parent')
|
|
56
|
+
continue; // Skip parent to avoid circular references
|
|
57
|
+
const child = searchNode[key];
|
|
58
|
+
if (child && typeof child === 'object') {
|
|
59
|
+
if (Array.isArray(child)) {
|
|
60
|
+
child.forEach(item => {
|
|
61
|
+
if (item && typeof item === 'object') {
|
|
62
|
+
findReferences(item);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
findReferences(child);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Start the search from the function body
|
|
73
|
+
if (functionNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
74
|
+
findReferences(functionNode.body);
|
|
75
|
+
}
|
|
76
|
+
else if (functionNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
77
|
+
functionNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
78
|
+
findReferences(functionNode.body);
|
|
79
|
+
}
|
|
80
|
+
// Return true if the variable is referenced more than once
|
|
81
|
+
return count > 1;
|
|
82
|
+
};
|
|
18
83
|
/**
|
|
19
84
|
* Checks if an object contains JSX elements in its properties
|
|
20
85
|
*/
|
|
@@ -338,6 +403,17 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
|
|
|
338
403
|
return {
|
|
339
404
|
CallExpression(node) {
|
|
340
405
|
if (containsJsxInUseMemo(node)) {
|
|
406
|
+
// Check if this is a variable declaration
|
|
407
|
+
if (node.parent &&
|
|
408
|
+
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
409
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
410
|
+
const variableName = node.parent.id.name;
|
|
411
|
+
// Check if the variable is used multiple times in the component
|
|
412
|
+
if (isUsedMultipleTimes(variableName, node)) {
|
|
413
|
+
// If the variable is used multiple times, allow it
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
341
417
|
context.report({
|
|
342
418
|
node,
|
|
343
419
|
messageId: 'useMemoShouldBeComponent',
|