@blumintinc/eslint-plugin-blumint 1.14.0 → 1.15.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 +10 -11
- package/lib/index.js +14 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +42 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +132 -41
- package/lib/rules/enforce-boolean-naming-prefixes.js +0 -212
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-global-constants.js +3 -3
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-memoize-async.js +69 -15
- package/lib/rules/enforce-props-argument-name.js +42 -16
- package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
- package/lib/rules/enforce-transform-memoization.js +1 -1
- package/lib/rules/enforce-verb-noun-naming.js +3814 -4641
- package/lib/rules/global-const-style.js +16 -4
- package/lib/rules/memo-compare-deeply-complex-props.js +16 -3
- package/lib/rules/memo-nested-react-components.js +108 -103
- package/lib/rules/no-async-foreach.js +7 -2
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +13 -15
- package/lib/rules/no-console-error.js +12 -10
- package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +48 -1
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +83 -1
- package/lib/rules/no-redundant-this-params.js +50 -1
- package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
- package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
- package/lib/rules/parallelize-async-operations.js +119 -54
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
- package/lib/rules/prefer-params-over-parent-id.js +1 -1
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prevent-children-clobber.js +9 -5
- package/lib/rules/react-memoize-literals.js +131 -12
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/utils/ASTHelpers.d.ts +34 -1
- package/lib/utils/ASTHelpers.js +346 -112
- package/package.json +1 -1
|
@@ -31,6 +31,21 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
31
31
|
}
|
|
32
32
|
return target;
|
|
33
33
|
};
|
|
34
|
+
const isDynamicValue = (node) => {
|
|
35
|
+
const target = unwrapAssertions(node);
|
|
36
|
+
if (target.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
37
|
+
target.type === utils_1.AST_NODE_TYPES.NewExpression ||
|
|
38
|
+
target.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (target.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
42
|
+
return isDynamicValue(target.expression);
|
|
43
|
+
}
|
|
44
|
+
if (target.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
45
|
+
return isDynamicValue(target.object);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
34
49
|
const describeValueKind = (node) => {
|
|
35
50
|
const target = unwrapAssertions(node);
|
|
36
51
|
if (target.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
@@ -102,10 +117,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
102
117
|
const { name } = declaration.id;
|
|
103
118
|
const init = declaration.init;
|
|
104
119
|
// Skip if no initializer or if it's a dynamic value or class instance
|
|
105
|
-
if (!init ||
|
|
106
|
-
init.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
107
|
-
init.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
108
|
-
init.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
120
|
+
if (!init || isDynamicValue(init)) {
|
|
109
121
|
return;
|
|
110
122
|
}
|
|
111
123
|
const sourceCode = context.sourceCode;
|
|
@@ -6,6 +6,16 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
function isUtilMemoModulePath(path) {
|
|
7
7
|
return /(?:^|\/|\\)util\/memo$/.test(path);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a property name is handled by the default deep equality logic
|
|
11
|
+
* in our custom memo implementation (blumintAreEqual).
|
|
12
|
+
*/
|
|
13
|
+
function isDefaultDeepCompareProp(name) {
|
|
14
|
+
return (name === 'sx' ||
|
|
15
|
+
name.endsWith('Sx') ||
|
|
16
|
+
name === 'style' ||
|
|
17
|
+
name.endsWith('Style'));
|
|
18
|
+
}
|
|
9
19
|
function unwrapExpression(expression) {
|
|
10
20
|
let node = expression;
|
|
11
21
|
// Unwrap harmless wrappers so detection treats casted/parenthesized expressions the same.
|
|
@@ -733,13 +743,16 @@ exports.memoCompareDeeplyComplexProps = (0, createRule_1.createRule)({
|
|
|
733
743
|
const comparatorArg = node.arguments[1];
|
|
734
744
|
return { memoCall, componentArg, comparatorArg };
|
|
735
745
|
}
|
|
736
|
-
function analyzeComponentAndProps(componentArg, comparatorArg, currentScope) {
|
|
746
|
+
function analyzeComponentAndProps(componentArg, comparatorArg, currentScope, memoSource) {
|
|
737
747
|
const componentTargets = findComponentAnalysisTargets(componentArg, (name) => initializerTracking.getInitializer(name, currentScope));
|
|
738
748
|
if (!componentTargets)
|
|
739
749
|
return null;
|
|
740
750
|
if (isComparatorProvided(comparatorArg, currentScope))
|
|
741
751
|
return null;
|
|
742
|
-
|
|
752
|
+
let complexProps = collectComplexPropsForTargets(componentTargets, (expr) => collectComplexProps(expr, sourceCode, complexPropsCache));
|
|
753
|
+
if (isUtilMemoModulePath(memoSource)) {
|
|
754
|
+
complexProps = complexProps.filter((prop) => !isDefaultDeepCompareProp(prop));
|
|
755
|
+
}
|
|
743
756
|
if (complexProps.length === 0)
|
|
744
757
|
return null;
|
|
745
758
|
const componentName = resolveComponentName(componentTargets, componentArg);
|
|
@@ -763,7 +776,7 @@ exports.memoCompareDeeplyComplexProps = (0, createRule_1.createRule)({
|
|
|
763
776
|
return;
|
|
764
777
|
const { memoCall, componentArg, comparatorArg } = validationResult;
|
|
765
778
|
const currentScope = context.getScope();
|
|
766
|
-
const analysisResult = analyzeComponentAndProps(componentArg, comparatorArg, currentScope);
|
|
779
|
+
const analysisResult = analyzeComponentAndProps(componentArg, comparatorArg, currentScope, memoCall.source);
|
|
767
780
|
if (!analysisResult)
|
|
768
781
|
return;
|
|
769
782
|
const { complexProps, componentName } = analysisResult;
|
|
@@ -4,12 +4,12 @@ exports.memoNestedReactComponents = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const minimatch_1 = require("minimatch");
|
|
6
6
|
const createRule_1 = require("../utils/createRule");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const CALLBACK_HOOKS = new Set([
|
|
8
|
+
'useCallback',
|
|
9
|
+
'useDeepCompareCallback',
|
|
10
|
+
'useMemo',
|
|
11
|
+
'useDeepCompareMemo',
|
|
12
|
+
]);
|
|
13
13
|
const collectReactImports = (sourceCode) => {
|
|
14
14
|
const reactImports = { namespace: null, named: {} };
|
|
15
15
|
for (const statement of sourceCode.ast.body) {
|
|
@@ -252,24 +252,26 @@ const getVariableName = (node) => {
|
|
|
252
252
|
}
|
|
253
253
|
return null;
|
|
254
254
|
};
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
const isInsideFunction = (node) => {
|
|
256
|
+
let parent = node.parent;
|
|
257
|
+
while (parent) {
|
|
258
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
259
|
+
parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
260
|
+
parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
parent = parent.parent;
|
|
258
264
|
}
|
|
259
|
-
return
|
|
260
|
-
};
|
|
261
|
-
const hasIdentifierInScope = (identifierName, scope) => {
|
|
262
|
-
return !!ASTHelpers_1.ASTHelpers.findVariableInScope(scope, identifierName);
|
|
265
|
+
return false;
|
|
263
266
|
};
|
|
264
267
|
exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
265
268
|
name: 'memo-nested-react-components',
|
|
266
269
|
meta: {
|
|
267
270
|
type: 'suggestion',
|
|
268
271
|
docs: {
|
|
269
|
-
description: 'Disallow React components defined in
|
|
272
|
+
description: 'Disallow React components defined in render bodies, hooks, or passed as props',
|
|
270
273
|
recommended: 'error',
|
|
271
274
|
},
|
|
272
|
-
fixable: 'code',
|
|
273
275
|
schema: [
|
|
274
276
|
{
|
|
275
277
|
type: 'object',
|
|
@@ -284,9 +286,19 @@ exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
|
284
286
|
},
|
|
285
287
|
],
|
|
286
288
|
messages: {
|
|
287
|
-
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inside {{
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inline inside {{locationDescription}}.
|
|
290
|
+
|
|
291
|
+
Why it matters: Inline components get new identities when their containing scope re-renders, causing React to unmount and remount them—dropping state, replaying animations, and causing UI flashes. Wrapping with memo() does NOT fix this—memo() only prevents re-renders when props change, not when the component identity itself changes.
|
|
292
|
+
|
|
293
|
+
How to fix:
|
|
294
|
+
1. Define the component at MODULE SCOPE in its own file, wrapped with memo()
|
|
295
|
+
2. Use React Context and/or directly provide props to supply any dynamic data the component needs
|
|
296
|
+
3. Pass the stable, imported component reference to props like CatalogWrapper
|
|
297
|
+
|
|
298
|
+
Don't pass inline function components to component-type props (*Wrapper, *Component).
|
|
299
|
+
Render-prop callbacks (e.g., render={...}) are fine; this rule targets component-type props only.
|
|
300
|
+
|
|
301
|
+
See: https://react.dev/learn/your-first-component#nesting-and-organizing-components`,
|
|
290
302
|
},
|
|
291
303
|
},
|
|
292
304
|
defaultOptions: [{}],
|
|
@@ -298,7 +310,16 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
|
|
|
298
310
|
}
|
|
299
311
|
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
300
312
|
const reactImports = collectReactImports(sourceCode);
|
|
301
|
-
const
|
|
313
|
+
const reportNestedComponentViolation = (node, componentName, locationDescription) => {
|
|
314
|
+
context.report({
|
|
315
|
+
node,
|
|
316
|
+
messageId: 'memoizeNestedComponent',
|
|
317
|
+
data: {
|
|
318
|
+
componentName,
|
|
319
|
+
locationDescription,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
};
|
|
302
323
|
return {
|
|
303
324
|
CallExpression(node) {
|
|
304
325
|
const hook = isHookCall(node.callee);
|
|
@@ -314,95 +335,79 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
|
|
|
314
335
|
if (!componentMatch) {
|
|
315
336
|
return;
|
|
316
337
|
}
|
|
338
|
+
// For useMemo, it only counts as a component if it returns a function
|
|
339
|
+
if (hook.name.includes('useMemo') ||
|
|
340
|
+
hook.name.includes('useDeepCompareMemo')) {
|
|
341
|
+
if (componentMatch.componentIsCallback) {
|
|
342
|
+
// Returns JSX directly, so it's an element, not a component
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
317
346
|
const componentName = getVariableName(node) ??
|
|
318
347
|
(isFunctionExpression(callback) &&
|
|
319
348
|
callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
|
|
320
349
|
? callback.id.name
|
|
321
350
|
: 'component');
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// Prevent mixing namespaces: don't apply fix when hook and memo use different React imports.
|
|
385
|
-
if (memoNamespace &&
|
|
386
|
-
node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
387
|
-
node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
388
|
-
node.callee.object.name !== memoNamespace) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
const callbackText = sourceCode.getText(callback);
|
|
392
|
-
const callbackReplacement = `() => ${memoReference}(${callbackText})`;
|
|
393
|
-
const fixes = [
|
|
394
|
-
fixer.replaceText(callback, callbackReplacement),
|
|
395
|
-
];
|
|
396
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
397
|
-
fixes.push(fixer.replaceText(node.callee, HOOK_REPLACEMENT[node.callee.name]));
|
|
398
|
-
}
|
|
399
|
-
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
400
|
-
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
401
|
-
fixes.push(fixer.replaceText(node.callee.property, HOOK_REPLACEMENT[node.callee.property.name]));
|
|
402
|
-
}
|
|
403
|
-
return fixes;
|
|
404
|
-
},
|
|
405
|
-
});
|
|
351
|
+
reportNestedComponentViolation(node, componentName, `${hook.name}()`);
|
|
352
|
+
},
|
|
353
|
+
VariableDeclarator(node) {
|
|
354
|
+
if (!node.init)
|
|
355
|
+
return;
|
|
356
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
357
|
+
return;
|
|
358
|
+
// Only check if name starts with uppercase (convention for components)
|
|
359
|
+
if (!/^[A-Z]/.test(node.id.name))
|
|
360
|
+
return;
|
|
361
|
+
if (!isInsideFunction(node))
|
|
362
|
+
return;
|
|
363
|
+
// Skip if it's already a hook call (handled by CallExpression visitor)
|
|
364
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
365
|
+
const hook = isHookCall(node.init.callee);
|
|
366
|
+
if (hook)
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const componentMatch = expressionCreatesComponent(node.init, reactImports);
|
|
370
|
+
if (!componentMatch)
|
|
371
|
+
return;
|
|
372
|
+
// JSX elements assigned to variables are fine, only report function definitions
|
|
373
|
+
if (componentMatch.componentIsCallback)
|
|
374
|
+
return;
|
|
375
|
+
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
376
|
+
},
|
|
377
|
+
FunctionDeclaration(node) {
|
|
378
|
+
if (!node.id || !/^[A-Z]/.test(node.id.name))
|
|
379
|
+
return;
|
|
380
|
+
if (!isInsideFunction(node))
|
|
381
|
+
return;
|
|
382
|
+
const componentMatch = functionCreatesComponent(node, reactImports);
|
|
383
|
+
if (!componentMatch)
|
|
384
|
+
return;
|
|
385
|
+
reportNestedComponentViolation(node.id, node.id.name, 'a render body');
|
|
386
|
+
},
|
|
387
|
+
JSXAttribute(node) {
|
|
388
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
389
|
+
return;
|
|
390
|
+
const attrName = node.name.name;
|
|
391
|
+
// Check if it's a component-type prop
|
|
392
|
+
if (!/(Wrapper|Component|Template|Header|Footer)$/.test(attrName)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!node.value ||
|
|
396
|
+
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const expression = node.value.expression;
|
|
400
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression)
|
|
401
|
+
return;
|
|
402
|
+
const componentMatch = expressionCreatesComponent(expression, reactImports);
|
|
403
|
+
if (!componentMatch)
|
|
404
|
+
return;
|
|
405
|
+
// For props, we only report if it's a function (component definition)
|
|
406
|
+
if (componentMatch.componentIsCallback) {
|
|
407
|
+
// It's a JSX element passed directly, which is usually fine
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
reportNestedComponentViolation(node, attrName, `the "${attrName}" prop`);
|
|
406
411
|
},
|
|
407
412
|
};
|
|
408
413
|
},
|
|
@@ -82,8 +82,13 @@ const getSourceCode = (context) => {
|
|
|
82
82
|
`getSourceCode=${typeof context.getSourceCode}.`);
|
|
83
83
|
};
|
|
84
84
|
const getScope = (context, sourceCode, node) => {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
try {
|
|
86
|
+
const typedSourceCode = sourceCode;
|
|
87
|
+
return typedSourceCode.getScope?.(node) ?? context.getScope?.() ?? null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return context.getScope?.() ?? null;
|
|
91
|
+
}
|
|
87
92
|
};
|
|
88
93
|
const analyzeInlineCallback = (callback) => {
|
|
89
94
|
if (!callback.async) {
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noCircularReferences: TSESLint.RuleModule<"circularReference", [], TSESLint.RuleListener>;
|
|
@@ -107,11 +107,20 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
107
107
|
node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
108
108
|
node.property.name === 'method'));
|
|
109
109
|
}
|
|
110
|
+
function getVariable(name) {
|
|
111
|
+
let scope = context.getScope();
|
|
112
|
+
while (scope) {
|
|
113
|
+
const variable = scope.variables.find((v) => v.name === name);
|
|
114
|
+
if (variable) {
|
|
115
|
+
return variable;
|
|
116
|
+
}
|
|
117
|
+
scope = scope.upper;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
110
121
|
function getReferencedObject(node) {
|
|
111
122
|
if (isIdentifier(node)) {
|
|
112
|
-
const
|
|
113
|
-
const scopeId = getScopeId(scope);
|
|
114
|
-
const variable = scope.variables.find((v) => v.name === node.name);
|
|
123
|
+
const variable = getVariable(node.name);
|
|
115
124
|
if (variable?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
116
125
|
const init = variable.defs[0].node.init;
|
|
117
126
|
if (init) {
|
|
@@ -127,24 +136,13 @@ exports.noCircularReferences = (0, createRule_1.createRule)({
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
|
-
// Check objects in the current scope
|
|
131
|
-
const scopeObjects = scopeMap.get(scopeId);
|
|
132
|
-
if (scopeObjects) {
|
|
133
|
-
for (const obj of scopeObjects) {
|
|
134
|
-
const info = objectMap.get(obj);
|
|
135
|
-
if (info && info.scope === scopeId && !info.isCircular) {
|
|
136
|
-
return obj;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
139
|
}
|
|
141
140
|
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
142
141
|
const property = node.property;
|
|
143
142
|
if (property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
144
143
|
const object = node.object;
|
|
145
144
|
if (isIdentifier(object)) {
|
|
146
|
-
const
|
|
147
|
-
const variable = scope.variables.find((v) => v.name === object.name);
|
|
145
|
+
const variable = getVariable(object.name);
|
|
148
146
|
if (variable?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
149
147
|
const init = variable.defs[0].node.init;
|
|
150
148
|
if (init) {
|
|
@@ -4,6 +4,7 @@ exports.noConsoleError = void 0;
|
|
|
4
4
|
const minimatch_1 = require("minimatch");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
6
|
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
8
|
const defaultIgnorePatterns = [
|
|
8
9
|
'**/__tests__/**',
|
|
9
10
|
'**/__mocks__/**',
|
|
@@ -69,19 +70,20 @@ const getResolvedVariable = (scope, identifier) => {
|
|
|
69
70
|
return findVariable(scope, identifier.name);
|
|
70
71
|
};
|
|
71
72
|
const getScopeForNode = (context, node) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
try {
|
|
74
|
+
const sourceCode = context.getSourceCode();
|
|
75
|
+
const sourceCodeWithScope = sourceCode;
|
|
76
|
+
if (typeof sourceCodeWithScope.getScope === 'function') {
|
|
77
|
+
return sourceCodeWithScope.getScope(node) ?? context.getScope();
|
|
78
|
+
}
|
|
79
|
+
return context.getScope();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return context.getScope();
|
|
76
83
|
}
|
|
77
|
-
return context.getScope();
|
|
78
84
|
};
|
|
79
85
|
const getDeclaredVariablesForNode = (context, node) => {
|
|
80
|
-
|
|
81
|
-
if (typeof sourceCodeWithDeclaredVariables.getDeclaredVariables === 'function') {
|
|
82
|
-
return sourceCodeWithDeclaredVariables.getDeclaredVariables(node);
|
|
83
|
-
}
|
|
84
|
-
return context.getDeclaredVariables(node);
|
|
86
|
+
return ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, node);
|
|
85
87
|
};
|
|
86
88
|
const isErrorKey = (key, computed) => {
|
|
87
89
|
if (!computed && key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -515,7 +515,7 @@ exports.noEmptyDependencyUseCallbacks = (0, createRule_1.createRule)({
|
|
|
515
515
|
const callback = getCallbackArg(callExpression);
|
|
516
516
|
if (!callback)
|
|
517
517
|
return;
|
|
518
|
-
if (ASTHelpers_1.ASTHelpers.returnsJSX(callback.body))
|
|
518
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(callback.body, context))
|
|
519
519
|
return;
|
|
520
520
|
const extraTypeRoots = [];
|
|
521
521
|
if (callExpression.parent &&
|
|
@@ -61,6 +61,7 @@ context) {
|
|
|
61
61
|
const usages = new Map(); // Track usage and its position
|
|
62
62
|
const visited = new Set();
|
|
63
63
|
let needsEntireObject = false;
|
|
64
|
+
let isUsed = false;
|
|
64
65
|
// Built-in array methods that indicate usage of the entire array
|
|
65
66
|
const ARRAY_METHODS = new Set([
|
|
66
67
|
'map',
|
|
@@ -230,6 +231,52 @@ context) {
|
|
|
230
231
|
if (!node || visited.has(node))
|
|
231
232
|
return;
|
|
232
233
|
visited.add(node);
|
|
234
|
+
// Direct usage of the target identifier to distinguish between:
|
|
235
|
+
// 1. Never used (not even present) → suggest removal
|
|
236
|
+
// 2. Used in ways requiring entire object → no suggestion
|
|
237
|
+
// 3. Used only via specific fields → suggest replacing with fields
|
|
238
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === objectName) {
|
|
239
|
+
const parent = node.parent;
|
|
240
|
+
// Exclude: property name in `other.objectName` (not our target object)
|
|
241
|
+
const isMemberProperty = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
242
|
+
parent.property === node &&
|
|
243
|
+
!parent.computed;
|
|
244
|
+
// Exclude: object in `objectName.prop` (handled by MemberExpression visitor for field tracking)
|
|
245
|
+
const isMemberObject = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
246
|
+
parent.object === node;
|
|
247
|
+
// Exclude: key in `{ objectName: value }` (not usage, just a label)
|
|
248
|
+
// Include: shorthand `{ objectName }` (actual usage)
|
|
249
|
+
const isPropertyKey = parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
250
|
+
parent.key === node &&
|
|
251
|
+
!parent.computed &&
|
|
252
|
+
!parent.shorthand;
|
|
253
|
+
if (!isMemberProperty && !isMemberObject && !isPropertyKey) {
|
|
254
|
+
isUsed = true;
|
|
255
|
+
// Patterns that require the entire object (cannot refactor to specific fields)
|
|
256
|
+
const isTypeAUsage = parent?.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
257
|
+
parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
258
|
+
parent?.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
259
|
+
parent?.type === utils_1.AST_NODE_TYPES.LogicalExpression ||
|
|
260
|
+
parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression ||
|
|
261
|
+
parent?.type === utils_1.AST_NODE_TYPES.UnaryExpression ||
|
|
262
|
+
(parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
263
|
+
(parent.value === node ||
|
|
264
|
+
parent.shorthand ||
|
|
265
|
+
(parent.key === node && parent.computed))) ||
|
|
266
|
+
parent?.type === utils_1.AST_NODE_TYPES.TemplateLiteral ||
|
|
267
|
+
parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
268
|
+
parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
269
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer ||
|
|
270
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute ||
|
|
271
|
+
parent?.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
272
|
+
parent?.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
273
|
+
parent?.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
|
|
274
|
+
parent?.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
275
|
+
if (isTypeAUsage) {
|
|
276
|
+
needsEntireObject = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
233
280
|
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
234
281
|
// Check if the object is being called as a function
|
|
235
282
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -431,7 +478,7 @@ context) {
|
|
|
431
478
|
return posA - posB;
|
|
432
479
|
});
|
|
433
480
|
const filteredUsages = new Set(sortedPaths);
|
|
434
|
-
const notUsed = !needsEntireObject && filteredUsages.size === 0;
|
|
481
|
+
const notUsed = !needsEntireObject && !isUsed && filteredUsages.size === 0;
|
|
435
482
|
return {
|
|
436
483
|
usages: filteredUsages,
|
|
437
484
|
needsEntireObject,
|
|
@@ -190,9 +190,9 @@ function isInModuleScope(node) {
|
|
|
190
190
|
}
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
|
-
function isComponentLikeFunction(fn, displayName) {
|
|
193
|
+
function isComponentLikeFunction(fn, context, displayName) {
|
|
194
194
|
const body = fn.body;
|
|
195
|
-
const hasJSX = !!body && ASTHelpers_1.ASTHelpers.returnsJSX(body);
|
|
195
|
+
const hasJSX = !!body && ASTHelpers_1.ASTHelpers.returnsJSX(body, context);
|
|
196
196
|
const expressionBody = fn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
197
197
|
fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement
|
|
198
198
|
? fn.body
|
|
@@ -204,7 +204,15 @@ function isComponentLikeFunction(fn, displayName) {
|
|
|
204
204
|
}
|
|
205
205
|
function findVariableInScopes(context, identifier) {
|
|
206
206
|
const sourceCode = context.sourceCode;
|
|
207
|
-
let scope =
|
|
207
|
+
let scope = null;
|
|
208
|
+
try {
|
|
209
|
+
// Tolerate ESLint API variations across versions/configurations.
|
|
210
|
+
// Some contexts may not provide getScope or may throw unexpectedly.
|
|
211
|
+
scope = sourceCode.getScope?.(identifier) ?? context.getScope();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
scope = null;
|
|
215
|
+
}
|
|
208
216
|
while (scope) {
|
|
209
217
|
const variable = scope.variables.find((v) => v.name === identifier.name);
|
|
210
218
|
if (variable)
|
|
@@ -340,7 +348,7 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
340
348
|
}
|
|
341
349
|
if (!fnNode)
|
|
342
350
|
return false;
|
|
343
|
-
return isComponentLikeFunction(fnNode, displayName);
|
|
351
|
+
return isComponentLikeFunction(fnNode, context, displayName);
|
|
344
352
|
}
|
|
345
353
|
function report(node, propName, componentName) {
|
|
346
354
|
context.report({
|
|
@@ -394,13 +402,14 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
394
402
|
return;
|
|
395
403
|
}
|
|
396
404
|
const fnNode = findObjectPropertyFunction(defNode.init, member.property.name);
|
|
397
|
-
if (fnNode &&
|
|
405
|
+
if (fnNode &&
|
|
406
|
+
isComponentLikeFunction(fnNode, context, member.property.name)) {
|
|
398
407
|
report(member, propName, member.property.name);
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
function handleInlineFunctionExpression(fn, propName) {
|
|
402
411
|
const explicitName = fn.type === utils_1.AST_NODE_TYPES.FunctionExpression ? fn.id?.name : undefined;
|
|
403
|
-
if (!isComponentLikeFunction(fn, explicitName)) {
|
|
412
|
+
if (!isComponentLikeFunction(fn, context, explicitName)) {
|
|
404
413
|
return;
|
|
405
414
|
}
|
|
406
415
|
const displayName = (fn.type === utils_1.AST_NODE_TYPES.FunctionExpression && fn.id?.name) ||
|
|
@@ -411,7 +420,7 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
411
420
|
const fnNode = getFunctionFromCall(call);
|
|
412
421
|
if (!fnNode)
|
|
413
422
|
return;
|
|
414
|
-
if (isComponentLikeFunction(fnNode)) {
|
|
423
|
+
if (isComponentLikeFunction(fnNode, context)) {
|
|
415
424
|
report(call, propName, INLINE_COMPONENT_NAME);
|
|
416
425
|
}
|
|
417
426
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
export declare const noPassthroughGetters:
|
|
1
|
+
import { TSESTree, TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noPassthroughGetters: TSESLint.RuleModule<"noPassthroughGetter", never[], {
|
|
3
3
|
MethodDefinition(node: TSESTree.MethodDefinition): void;
|
|
4
4
|
}>;
|