@blumintinc/eslint-plugin-blumint 1.14.0 → 1.16.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 +36 -14
- package/lib/index.js +20 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +68 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +119 -49
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
- 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-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- 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 +66 -15
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- 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 +3817 -4641
- package/lib/rules/global-const-style.js +25 -4
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
- package/lib/rules/memo-nested-react-components.js +243 -103
- package/lib/rules/no-array-length-in-deps.js +74 -3
- 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 +150 -489
- package/lib/rules/no-compositing-layer-props.js +31 -0
- 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 +147 -65
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-margin-properties.js +7 -38
- 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/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +117 -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 +75 -4
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- 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 +218 -13
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +49 -1
- package/lib/utils/ASTHelpers.js +394 -112
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -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,142 @@ const getVariableName = (node) => {
|
|
|
252
252
|
}
|
|
253
253
|
return null;
|
|
254
254
|
};
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
const isEnclosingFunction = (node) => {
|
|
256
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
257
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
258
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Nearest ancestor function whose body directly contains `node`. Climbs from
|
|
262
|
+
* the node's parent so a function node itself is not treated as its own
|
|
263
|
+
* enclosing function.
|
|
264
|
+
*/
|
|
265
|
+
const findEnclosingFunction = (node) => {
|
|
266
|
+
let parent = node.parent;
|
|
267
|
+
while (parent) {
|
|
268
|
+
if (isEnclosingFunction(parent)) {
|
|
269
|
+
return parent;
|
|
270
|
+
}
|
|
271
|
+
parent = parent.parent;
|
|
258
272
|
}
|
|
259
|
-
return
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
const isInsideFunction = (node) => {
|
|
276
|
+
return findEnclosingFunction(node) !== null;
|
|
260
277
|
};
|
|
261
|
-
const
|
|
262
|
-
|
|
278
|
+
const isPascalCaseName = (name) => /^[A-Z]/.test(name);
|
|
279
|
+
/**
|
|
280
|
+
* A return value is a *component* (vs. rendered output) when it is a
|
|
281
|
+
* memo()/forwardRef() call, a PascalCase identifier (returned component
|
|
282
|
+
* reference), or an inline function that itself creates a component. Such a
|
|
283
|
+
* value identifies the surrounding function as an HOC factory rather than a
|
|
284
|
+
* render body.
|
|
285
|
+
*/
|
|
286
|
+
const returnExpressionIsComponent = (expression, reactImports) => {
|
|
287
|
+
if (!expression) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const unwrapped = unwrapExpression(expression);
|
|
291
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
292
|
+
if (isMemoCall(unwrapped, reactImports) ||
|
|
293
|
+
isForwardRefCall(unwrapped, reactImports)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
299
|
+
return isPascalCaseName(unwrapped.name);
|
|
300
|
+
}
|
|
301
|
+
if (isFunctionExpression(unwrapped)) {
|
|
302
|
+
return Boolean(functionCreatesComponent(unwrapped, reactImports));
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
};
|
|
306
|
+
const returnExpressionIsJsx = (expression) => {
|
|
307
|
+
if (!expression) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const unwrapped = unwrapExpression(expression);
|
|
311
|
+
return (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
312
|
+
unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Direct return-statement arguments of a function, traversing control flow
|
|
316
|
+
* (if/switch/try/loops) but NOT descending into nested function definitions—
|
|
317
|
+
* returns inside nested functions belong to those functions, not this one.
|
|
318
|
+
*/
|
|
319
|
+
const collectDirectReturnExpressions = (fn) => {
|
|
320
|
+
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
321
|
+
return [fn.body];
|
|
322
|
+
}
|
|
323
|
+
const returns = [];
|
|
324
|
+
const visitStatement = (statement) => {
|
|
325
|
+
switch (statement.type) {
|
|
326
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
327
|
+
returns.push(statement.argument);
|
|
328
|
+
break;
|
|
329
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
330
|
+
statement.body.forEach(visitStatement);
|
|
331
|
+
break;
|
|
332
|
+
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
333
|
+
visitStatement(statement.consequent);
|
|
334
|
+
if (statement.alternate) {
|
|
335
|
+
visitStatement(statement.alternate);
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement:
|
|
339
|
+
for (const switchCase of statement.cases) {
|
|
340
|
+
switchCase.consequent.forEach(visitStatement);
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
344
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
345
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
346
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
347
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
348
|
+
visitStatement(statement.body);
|
|
349
|
+
break;
|
|
350
|
+
case utils_1.AST_NODE_TYPES.TryStatement:
|
|
351
|
+
visitStatement(statement.block);
|
|
352
|
+
if (statement.handler) {
|
|
353
|
+
visitStatement(statement.handler.body);
|
|
354
|
+
}
|
|
355
|
+
if (statement.finalizer) {
|
|
356
|
+
visitStatement(statement.finalizer);
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
fn.body.body.forEach(visitStatement);
|
|
364
|
+
return returns;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* True when the nearest enclosing function is an HOC factory—it returns a
|
|
368
|
+
* component (memo/forwardRef/component reference) and never returns JSX. Such
|
|
369
|
+
* a function runs once per call, so components defined inside it have stable
|
|
370
|
+
* identities and must NOT be flagged. A function that returns JSX is a render
|
|
371
|
+
* body, where nested components DO remount and remain flagged.
|
|
372
|
+
*/
|
|
373
|
+
const isInsideHocFactory = (node, reactImports) => {
|
|
374
|
+
const enclosing = findEnclosingFunction(node);
|
|
375
|
+
if (!enclosing) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const returns = collectDirectReturnExpressions(enclosing);
|
|
379
|
+
const returnsComponent = returns.some((expression) => returnExpressionIsComponent(expression, reactImports));
|
|
380
|
+
const returnsJsx = returns.some((expression) => returnExpressionIsJsx(expression));
|
|
381
|
+
return returnsComponent && !returnsJsx;
|
|
263
382
|
};
|
|
264
383
|
exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
265
384
|
name: 'memo-nested-react-components',
|
|
266
385
|
meta: {
|
|
267
386
|
type: 'suggestion',
|
|
268
387
|
docs: {
|
|
269
|
-
description: 'Disallow React components defined in
|
|
388
|
+
description: 'Disallow React components defined in render bodies, hooks, or passed as props',
|
|
270
389
|
recommended: 'error',
|
|
271
390
|
},
|
|
272
|
-
fixable: 'code',
|
|
273
391
|
schema: [
|
|
274
392
|
{
|
|
275
393
|
type: 'object',
|
|
@@ -284,9 +402,19 @@ exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
|
284
402
|
},
|
|
285
403
|
],
|
|
286
404
|
messages: {
|
|
287
|
-
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inside {{
|
|
288
|
-
|
|
289
|
-
|
|
405
|
+
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inline inside {{locationDescription}}.
|
|
406
|
+
|
|
407
|
+
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.
|
|
408
|
+
|
|
409
|
+
How to fix:
|
|
410
|
+
1. Define the component at MODULE SCOPE in its own file, wrapped with memo()
|
|
411
|
+
2. Use React Context and/or directly provide props to supply any dynamic data the component needs
|
|
412
|
+
3. Pass the stable, imported component reference to props like CatalogWrapper
|
|
413
|
+
|
|
414
|
+
Don't pass inline function components to component-type props (*Wrapper, *Component).
|
|
415
|
+
Render-prop callbacks (e.g., render={...}) are fine; this rule targets component-type props only.
|
|
416
|
+
|
|
417
|
+
See: https://react.dev/learn/your-first-component#nesting-and-organizing-components`,
|
|
290
418
|
},
|
|
291
419
|
},
|
|
292
420
|
defaultOptions: [{}],
|
|
@@ -298,7 +426,16 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
|
|
|
298
426
|
}
|
|
299
427
|
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
300
428
|
const reactImports = collectReactImports(sourceCode);
|
|
301
|
-
const
|
|
429
|
+
const reportNestedComponentViolation = (node, componentName, locationDescription) => {
|
|
430
|
+
context.report({
|
|
431
|
+
node,
|
|
432
|
+
messageId: 'memoizeNestedComponent',
|
|
433
|
+
data: {
|
|
434
|
+
componentName,
|
|
435
|
+
locationDescription,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
};
|
|
302
439
|
return {
|
|
303
440
|
CallExpression(node) {
|
|
304
441
|
const hook = isHookCall(node.callee);
|
|
@@ -314,95 +451,98 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
|
|
|
314
451
|
if (!componentMatch) {
|
|
315
452
|
return;
|
|
316
453
|
}
|
|
317
|
-
|
|
454
|
+
// For useMemo, it only counts as a component if it returns a function
|
|
455
|
+
if (hook.name.includes('useMemo') ||
|
|
456
|
+
hook.name.includes('useDeepCompareMemo')) {
|
|
457
|
+
if (componentMatch.componentIsCallback) {
|
|
458
|
+
// Returns JSX directly, so it's an element, not a component
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const variableName = getVariableName(node);
|
|
463
|
+
// A non-PascalCase binding (e.g. renderHit) is a render callback used
|
|
464
|
+
// with a render={...} prop, not a component—skip it.
|
|
465
|
+
if (variableName && !isPascalCaseName(variableName)) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Inside an HOC factory the binding has a stable identity, so it does
|
|
469
|
+
// not remount on re-render and must not be flagged.
|
|
470
|
+
if (isInsideHocFactory(node, reactImports)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const componentName = variableName ??
|
|
318
474
|
(isFunctionExpression(callback) &&
|
|
319
475
|
callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
|
|
320
476
|
? callback.id.name
|
|
321
477
|
: '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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
});
|
|
478
|
+
reportNestedComponentViolation(node, componentName, `${hook.name}()`);
|
|
479
|
+
},
|
|
480
|
+
VariableDeclarator(node) {
|
|
481
|
+
if (!node.init)
|
|
482
|
+
return;
|
|
483
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
484
|
+
return;
|
|
485
|
+
// Only check if name starts with uppercase (convention for components)
|
|
486
|
+
if (!isPascalCaseName(node.id.name))
|
|
487
|
+
return;
|
|
488
|
+
if (!isInsideFunction(node))
|
|
489
|
+
return;
|
|
490
|
+
// Inside an HOC factory the component has a stable identity (the factory
|
|
491
|
+
// runs once per call), so it does not remount and must not be flagged.
|
|
492
|
+
if (isInsideHocFactory(node, reactImports))
|
|
493
|
+
return;
|
|
494
|
+
// Skip if it's already a hook call (handled by CallExpression visitor)
|
|
495
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
496
|
+
const hook = isHookCall(node.init.callee);
|
|
497
|
+
if (hook)
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const componentMatch = expressionCreatesComponent(node.init, reactImports);
|
|
501
|
+
if (!componentMatch)
|
|
502
|
+
return;
|
|
503
|
+
// JSX elements assigned to variables are fine, only report function definitions
|
|
504
|
+
if (componentMatch.componentIsCallback)
|
|
505
|
+
return;
|
|
506
|
+
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
507
|
+
},
|
|
508
|
+
FunctionDeclaration(node) {
|
|
509
|
+
if (!node.id || !isPascalCaseName(node.id.name))
|
|
510
|
+
return;
|
|
511
|
+
if (!isInsideFunction(node))
|
|
512
|
+
return;
|
|
513
|
+
// Inside an HOC factory the component has a stable identity (the factory
|
|
514
|
+
// runs once per call), so it does not remount and must not be flagged.
|
|
515
|
+
if (isInsideHocFactory(node, reactImports))
|
|
516
|
+
return;
|
|
517
|
+
const componentMatch = functionCreatesComponent(node, reactImports);
|
|
518
|
+
if (!componentMatch)
|
|
519
|
+
return;
|
|
520
|
+
reportNestedComponentViolation(node.id, node.id.name, 'a render body');
|
|
521
|
+
},
|
|
522
|
+
JSXAttribute(node) {
|
|
523
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
524
|
+
return;
|
|
525
|
+
const attrName = node.name.name;
|
|
526
|
+
// Check if it's a component-type prop
|
|
527
|
+
if (!/(Wrapper|Component|Template|Header|Footer)$/.test(attrName)) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (!node.value ||
|
|
531
|
+
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const expression = node.value.expression;
|
|
535
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression)
|
|
536
|
+
return;
|
|
537
|
+
const componentMatch = expressionCreatesComponent(expression, reactImports);
|
|
538
|
+
if (!componentMatch)
|
|
539
|
+
return;
|
|
540
|
+
// For props, we only report if it's a function (component definition)
|
|
541
|
+
if (componentMatch.componentIsCallback) {
|
|
542
|
+
// It's a JSX element passed directly, which is usually fine
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
reportNestedComponentViolation(node, attrName, `the "${attrName}" prop`);
|
|
406
546
|
},
|
|
407
547
|
};
|
|
408
548
|
},
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noArrayLengthInDeps = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
// React hooks to check
|
|
7
8
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
8
9
|
const DEFAULT_HASH_IMPORT = {
|
|
@@ -58,7 +59,7 @@ function getLastPropertyName(expr) {
|
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
function generateUniqueName(base, taken) {
|
|
61
|
-
|
|
62
|
+
const candidate = `${base}Hash`;
|
|
62
63
|
if (!taken.has(candidate))
|
|
63
64
|
return candidate;
|
|
64
65
|
let i = 2;
|
|
@@ -120,6 +121,69 @@ function isUseMemoImported(sourceCode) {
|
|
|
120
121
|
}
|
|
121
122
|
return false;
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Hook callback = first function-typed argument (the effect/factory/memo fn).
|
|
126
|
+
* Deps array is the LAST argument; the callback precedes it.
|
|
127
|
+
*/
|
|
128
|
+
function getHookCallback(node) {
|
|
129
|
+
for (const arg of node.arguments) {
|
|
130
|
+
if (arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
131
|
+
arg.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
132
|
+
return arg;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function rangeContains(outer, inner) {
|
|
138
|
+
return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* True when `identifier` is the object of a non-computed `.length` access,
|
|
142
|
+
* i.e. the reference reads only the array's length, not its contents.
|
|
143
|
+
*/
|
|
144
|
+
function isLengthAccessOf(identifier) {
|
|
145
|
+
const parent = identifier.parent;
|
|
146
|
+
return (!!parent &&
|
|
147
|
+
parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
148
|
+
!parent.computed &&
|
|
149
|
+
parent.object === identifier &&
|
|
150
|
+
parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
151
|
+
parent.property.name === 'length');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decide whether a `<array>.length` dependency is safe to keep (suppress the
|
|
155
|
+
* report) by inspecting how the hook callback body uses the array binding.
|
|
156
|
+
*
|
|
157
|
+
* Safe (suppress) only when the array is referenced at least once inside the
|
|
158
|
+
* callback body AND every such reference is the object of a `.length` access —
|
|
159
|
+
* then depending on `.length` correctly avoids reruns on content changes.
|
|
160
|
+
*
|
|
161
|
+
* Returns false (keep reporting) for any non-`.length` use (element access,
|
|
162
|
+
* spread, iteration/method calls, bare reference, passed as an argument), when
|
|
163
|
+
* the array is never referenced in the body, and whenever the base cannot be
|
|
164
|
+
* confidently resolved (complex member-chain bases) — never hide a real bug.
|
|
165
|
+
*/
|
|
166
|
+
function isLengthOnlyUsage(context, callback, baseExpr) {
|
|
167
|
+
// Only resolvable for a bare identifier base (e.g. `items` in `items.length`).
|
|
168
|
+
// Member-chain bases (`a.b`, `data?.items`) have no single binding to track.
|
|
169
|
+
if (baseExpr.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// Resolve the binding the DEP refers to (from the deps-array identifier's
|
|
173
|
+
// scope), so a binding shadowed inside the callback body is not mistaken for
|
|
174
|
+
// it — the outer binding then has zero body references and we keep reporting.
|
|
175
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, baseExpr);
|
|
176
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, baseExpr.name);
|
|
177
|
+
if (!variable) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const bodyReferences = variable.references.filter((ref) => rangeContains(callback.body, ref.identifier));
|
|
181
|
+
// No body usage → not the content-vs-length bug, but keep current behavior.
|
|
182
|
+
if (bodyReferences.length === 0) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return bodyReferences.every((ref) => isLengthAccessOf(ref.identifier));
|
|
186
|
+
}
|
|
123
187
|
function isStableHashImported(sourceCode, hashSource, hashImportName) {
|
|
124
188
|
const program = sourceCode.ast;
|
|
125
189
|
for (const node of program.body) {
|
|
@@ -188,15 +252,22 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
188
252
|
return;
|
|
189
253
|
// Collect .length deps
|
|
190
254
|
const lengthDeps = [];
|
|
255
|
+
const callback = getHookCallback(node);
|
|
191
256
|
for (const el of depsArg.elements) {
|
|
192
257
|
if (!el)
|
|
193
258
|
continue;
|
|
194
259
|
if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
195
260
|
continue;
|
|
196
261
|
const member = getLengthMember(el);
|
|
197
|
-
if (member)
|
|
198
|
-
|
|
262
|
+
if (!member)
|
|
263
|
+
continue;
|
|
264
|
+
// Suppress when the callback body reads only `<array>.length`, never
|
|
265
|
+
// the array's contents — then `.length` is the correct dependency.
|
|
266
|
+
if (callback &&
|
|
267
|
+
isLengthOnlyUsage(context, callback, getBaseExpression(member))) {
|
|
268
|
+
continue;
|
|
199
269
|
}
|
|
270
|
+
lengthDeps.push({ element: el, member });
|
|
200
271
|
}
|
|
201
272
|
if (lengthDeps.length === 0)
|
|
202
273
|
return;
|
|
@@ -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>;
|