@blumintinc/eslint-plugin-blumint 1.21.9 → 1.21.10
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/lib/index.js
CHANGED
|
@@ -361,7 +361,8 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
361
361
|
const declarator = value.parent;
|
|
362
362
|
if (declarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
363
363
|
declarator.init === value &&
|
|
364
|
-
declarator.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
364
|
+
(declarator.id.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
365
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern)) {
|
|
365
366
|
return declarator;
|
|
366
367
|
}
|
|
367
368
|
const container = storageContainerOf(value);
|
|
@@ -376,12 +377,42 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
376
377
|
* bracketed form — read through `accessedPropertyName` so the two spellings
|
|
377
378
|
* cannot diverge from how the mutation walk already reads a method name.
|
|
378
379
|
*/
|
|
379
|
-
const
|
|
380
|
+
const isNamespacedCallee = (callee, namespace, method) => {
|
|
380
381
|
const value = outermostValueOf(callee);
|
|
381
382
|
return (value.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
382
383
|
value.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
383
|
-
value.object.name ===
|
|
384
|
-
accessedPropertyName(value) ===
|
|
384
|
+
value.object.name === namespace &&
|
|
385
|
+
accessedPropertyName(value) === method);
|
|
386
|
+
};
|
|
387
|
+
const isObjectAssignCallee = (callee) => isNamespacedCallee(callee, 'Object', 'assign');
|
|
388
|
+
/** Whether a callee is the bare global `structuredClone`. */
|
|
389
|
+
const isStructuredCloneCallee = (callee) => {
|
|
390
|
+
const value = outermostValueOf(callee);
|
|
391
|
+
return (value.type === utils_1.AST_NODE_TYPES.Identifier && value.name === 'structuredClone');
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Whether a call COPIES the argument at `index` while keeping its type.
|
|
395
|
+
*
|
|
396
|
+
* `Array.from(X)` and `structuredClone(X)` both hand back a fresh, mutable
|
|
397
|
+
* value whose element or property types are the argument's — so freezing the
|
|
398
|
+
* argument narrows the copy exactly as a spread does. `Array.from(X, fn)` is
|
|
399
|
+
* excluded for the same reason `map` is: a mapper retypes the result, so
|
|
400
|
+
* nothing of the constant's type survives into it.
|
|
401
|
+
*/
|
|
402
|
+
const isCopyingCall = (call, index) => {
|
|
403
|
+
if (isObjectAssignCallee(call.callee)) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (index !== 0) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
if (isStructuredCloneCallee(call.callee)) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
if (isNamespacedCallee(call.callee, 'Array', 'from')) {
|
|
413
|
+
return call.arguments.length === 1;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
385
416
|
};
|
|
386
417
|
/**
|
|
387
418
|
* Array methods whose result keeps the receiver's ELEMENT type. `map` is
|
|
@@ -390,7 +421,19 @@ const isObjectAssignCallee = (callee) => {
|
|
|
390
421
|
* `map`. Admitting it would withhold the assertion from every derived array
|
|
391
422
|
* anything is computed from, to cover a spelling nobody writes.
|
|
392
423
|
*/
|
|
393
|
-
const TYPE_PRESERVING_COPY_METHODS = new Set([
|
|
424
|
+
const TYPE_PRESERVING_COPY_METHODS = new Set([
|
|
425
|
+
'concat',
|
|
426
|
+
'slice',
|
|
427
|
+
'filter',
|
|
428
|
+
'flat',
|
|
429
|
+
// The ES2023 copying methods. Listed even though this repo's TypeScript
|
|
430
|
+
// predates them, because they are the same category and admitting them costs
|
|
431
|
+
// nothing: a name that does not resolve produces no reports to lose.
|
|
432
|
+
'toSorted',
|
|
433
|
+
'toReversed',
|
|
434
|
+
'toSpliced',
|
|
435
|
+
'with',
|
|
436
|
+
]);
|
|
394
437
|
/**
|
|
395
438
|
* The expression that builds a COPY carrying this value's type — the literal
|
|
396
439
|
* around a spread of it, the call of a copying array method on it, or an
|
|
@@ -415,10 +458,11 @@ const copyExpressionOf = (node) => {
|
|
|
415
458
|
parent.parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
|
|
416
459
|
return parent.parent;
|
|
417
460
|
}
|
|
418
|
-
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression
|
|
419
|
-
parent.arguments.
|
|
420
|
-
|
|
421
|
-
|
|
461
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
462
|
+
const index = parent.arguments.indexOf(node);
|
|
463
|
+
if (index !== -1 && isCopyingCall(parent, index)) {
|
|
464
|
+
return parent;
|
|
465
|
+
}
|
|
422
466
|
}
|
|
423
467
|
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
424
468
|
parent.object === node) {
|
|
@@ -442,6 +486,10 @@ const PATTERN_CONTAINERS = new Set([
|
|
|
442
486
|
utils_1.AST_NODE_TYPES.ObjectPattern,
|
|
443
487
|
utils_1.AST_NODE_TYPES.ArrayPattern,
|
|
444
488
|
utils_1.AST_NODE_TYPES.RestElement,
|
|
489
|
+
// A parameter property is a parameter AND declares a class property, so it
|
|
490
|
+
// infers twice over. Without it the walk stops before reaching the
|
|
491
|
+
// constructor's params and `constructor(public stage = DEFAULT)` narrows.
|
|
492
|
+
utils_1.AST_NODE_TYPES.TSParameterProperty,
|
|
445
493
|
]);
|
|
446
494
|
const FUNCTION_TYPES = new Set([
|
|
447
495
|
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
@@ -347,6 +347,90 @@ const isInsideFunction = (node) => {
|
|
|
347
347
|
return findEnclosingFunction(node) !== null;
|
|
348
348
|
};
|
|
349
349
|
const isPascalCaseName = (name) => /^[A-Z]/.test(name);
|
|
350
|
+
/**
|
|
351
|
+
* Prop names whose value a parent MOUNTS rather than calls. Kept identical to
|
|
352
|
+
* the `JSXAttribute` visitor's own test so the two paths cannot disagree about
|
|
353
|
+
* what a component-type prop is — the disagreement between them is #2334.
|
|
354
|
+
*/
|
|
355
|
+
const COMPONENT_PROP_SUFFIX = /(Wrapper|Component|Template|Header|Footer)$/;
|
|
356
|
+
const isComponentPropName = (name) => isPascalCaseName(name) && COMPONENT_PROP_SUFFIX.test(name);
|
|
357
|
+
const consumptionOfReference = (identifier, reactImports) => {
|
|
358
|
+
// `<Binding />`. The scope manager reports the tag name as a reference whose
|
|
359
|
+
// identifier is a `JSXIdentifier`, which no other position produces.
|
|
360
|
+
if (identifier.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
361
|
+
return 'component';
|
|
362
|
+
}
|
|
363
|
+
let current = identifier;
|
|
364
|
+
for (;;) {
|
|
365
|
+
const parent = parentBeyondChain(current);
|
|
366
|
+
if (!parent) {
|
|
367
|
+
return 'unknown';
|
|
368
|
+
}
|
|
369
|
+
// `Binding as Something` / `Binding!` keep the value on its way to a use.
|
|
370
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
371
|
+
parent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
372
|
+
parent.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) &&
|
|
373
|
+
parent.expression === current) {
|
|
374
|
+
current = parent;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
378
|
+
// `createElement(Binding, ...)` mounts it exactly as a tag name does.
|
|
379
|
+
if (parent.arguments[0] === current &&
|
|
380
|
+
isReactCreateElementCall(parent, reactImports)) {
|
|
381
|
+
return 'component';
|
|
382
|
+
}
|
|
383
|
+
// `Binding(onClose)` — the parent INVOKES it, which is what a render
|
|
384
|
+
// callback is for. A component is never called directly.
|
|
385
|
+
if (parent.callee === current) {
|
|
386
|
+
return 'callback';
|
|
387
|
+
}
|
|
388
|
+
return 'unknown';
|
|
389
|
+
}
|
|
390
|
+
// `<Host ContentComponent={Binding} />` mounts it; `<Host render={Binding} />`
|
|
391
|
+
// calls it. The prop name is the parent's contract, and it is read on the
|
|
392
|
+
// same terms the `JSXAttribute` visitor uses so the two cannot disagree.
|
|
393
|
+
if (parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
394
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
395
|
+
parent.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
396
|
+
return isComponentPropName(parent.parent.name.name)
|
|
397
|
+
? 'component'
|
|
398
|
+
: 'callback';
|
|
399
|
+
}
|
|
400
|
+
return 'unknown';
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Whether the binding a memo-hook call initializes is MOUNTED, CALLED, or
|
|
405
|
+
* neither, read from the scope manager's reference list rather than a textual
|
|
406
|
+
* search so a same-named binding in a sibling scope cannot answer for this one.
|
|
407
|
+
*
|
|
408
|
+
* `unknown` is the honest answer for a binding with no informative reference —
|
|
409
|
+
* an exported component has none in its own file, and so does a fixture
|
|
410
|
+
* fragment. Falling back to the name there keeps the rule's reach while letting
|
|
411
|
+
* evidence override the guess wherever evidence exists.
|
|
412
|
+
*/
|
|
413
|
+
const consumptionOfBinding = (node, context, reactImports) => {
|
|
414
|
+
const declarator = parentBeyondChain(node);
|
|
415
|
+
if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
416
|
+
return 'unknown';
|
|
417
|
+
}
|
|
418
|
+
let sawCallback = false;
|
|
419
|
+
for (const variable of context.getDeclaredVariables(declarator)) {
|
|
420
|
+
for (const reference of variable.references) {
|
|
421
|
+
const consumption = consumptionOfReference(reference.identifier, reactImports);
|
|
422
|
+
// A single mounting use settles it: the identity churn happens there
|
|
423
|
+
// regardless of how many other places merely call it.
|
|
424
|
+
if (consumption === 'component') {
|
|
425
|
+
return 'component';
|
|
426
|
+
}
|
|
427
|
+
if (consumption === 'callback') {
|
|
428
|
+
sawCallback = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return sawCallback ? 'callback' : 'unknown';
|
|
433
|
+
};
|
|
350
434
|
/**
|
|
351
435
|
* The values a container hands to its caller: object property values and array
|
|
352
436
|
* elements. Mirrors `containedValues` in the paired `require-memo` rule (#1919),
|
|
@@ -725,10 +809,20 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
725
809
|
}
|
|
726
810
|
}
|
|
727
811
|
const variableName = getVariableName(node);
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
812
|
+
// The NAME was the whole discriminator here, which reported every
|
|
813
|
+
// PascalCase render callback the message explicitly exempts and missed
|
|
814
|
+
// every lowercase binding handed to a component-type prop. Evidence
|
|
815
|
+
// from the use site overrides it in both directions; the name still
|
|
816
|
+
// decides where there is no evidence, which is where an exported
|
|
817
|
+
// component lives (#2334).
|
|
818
|
+
if (variableName) {
|
|
819
|
+
const consumption = consumptionOfBinding(node, context, reactImports);
|
|
820
|
+
if (consumption === 'callback') {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (consumption === 'unknown' && !isPascalCaseName(variableName)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
732
826
|
}
|
|
733
827
|
// Inside an HOC factory the binding has a stable identity, so it does
|
|
734
828
|
// not remount on re-render and must not be flagged.
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.10",
|
|
4
|
+
"date": "2026-09-05T09:28:38.523Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2333
|
|
11
|
+
],
|
|
12
|
+
"summary": "follow every copy that carries the frozen type, and the parameter property that infers from it (closes #2333)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "memo-nested-react-components",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2334
|
|
19
|
+
],
|
|
20
|
+
"summary": "decide by the use site, not the binding's first letter (closes #2334)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.21.9",
|
|
4
26
|
"date": "2026-09-05T05:55:33.427Z",
|