@blumintinc/eslint-plugin-blumint 1.21.9 → 1.21.11
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
|
@@ -350,9 +350,14 @@ const storageContainerOf = (node) => {
|
|
|
350
350
|
* the file compiling is not.
|
|
351
351
|
*
|
|
352
352
|
* A reference STORED INTO a composite literal is followed through that
|
|
353
|
-
* container, since storing does not copy — see `storageContainerOf`.
|
|
354
|
-
*
|
|
355
|
-
*
|
|
353
|
+
* container, since storing does not copy — see `storageContainerOf`.
|
|
354
|
+
*
|
|
355
|
+
* A destructuring id is accepted in BOTH spellings. It does not name the whole
|
|
356
|
+
* value, but every binding it introduces is typed from that value, and a rest
|
|
357
|
+
* element is itself a fresh container the assertion narrows: `const [, ...rest]
|
|
358
|
+
* = ITEMS` gives `rest` the frozen element type, so `rest.push(4)` is TS2345
|
|
359
|
+
* for an input that compiled. Admitting only the object spelling gave the same
|
|
360
|
+
* construct opposite verdicts (#2336).
|
|
356
361
|
*/
|
|
357
362
|
const aliasDeclaratorOf = (identifier) => {
|
|
358
363
|
// Ascends strictly, so reaching a node with no parent terminates the walk.
|
|
@@ -361,7 +366,9 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
361
366
|
const declarator = value.parent;
|
|
362
367
|
if (declarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
363
368
|
declarator.init === value &&
|
|
364
|
-
declarator.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
369
|
+
(declarator.id.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
370
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
371
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.ArrayPattern)) {
|
|
365
372
|
return declarator;
|
|
366
373
|
}
|
|
367
374
|
const container = storageContainerOf(value);
|
|
@@ -376,12 +383,42 @@ const aliasDeclaratorOf = (identifier) => {
|
|
|
376
383
|
* bracketed form — read through `accessedPropertyName` so the two spellings
|
|
377
384
|
* cannot diverge from how the mutation walk already reads a method name.
|
|
378
385
|
*/
|
|
379
|
-
const
|
|
386
|
+
const isNamespacedCallee = (callee, namespace, method) => {
|
|
380
387
|
const value = outermostValueOf(callee);
|
|
381
388
|
return (value.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
382
389
|
value.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
383
|
-
value.object.name ===
|
|
384
|
-
accessedPropertyName(value) ===
|
|
390
|
+
value.object.name === namespace &&
|
|
391
|
+
accessedPropertyName(value) === method);
|
|
392
|
+
};
|
|
393
|
+
const isObjectAssignCallee = (callee) => isNamespacedCallee(callee, 'Object', 'assign');
|
|
394
|
+
/** Whether a callee is the bare global `structuredClone`. */
|
|
395
|
+
const isStructuredCloneCallee = (callee) => {
|
|
396
|
+
const value = outermostValueOf(callee);
|
|
397
|
+
return (value.type === utils_1.AST_NODE_TYPES.Identifier && value.name === 'structuredClone');
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* Whether a call COPIES the argument at `index` while keeping its type.
|
|
401
|
+
*
|
|
402
|
+
* `Array.from(X)` and `structuredClone(X)` both hand back a fresh, mutable
|
|
403
|
+
* value whose element or property types are the argument's — so freezing the
|
|
404
|
+
* argument narrows the copy exactly as a spread does. `Array.from(X, fn)` is
|
|
405
|
+
* excluded for the same reason `map` is: a mapper retypes the result, so
|
|
406
|
+
* nothing of the constant's type survives into it.
|
|
407
|
+
*/
|
|
408
|
+
const isCopyingCall = (call, index) => {
|
|
409
|
+
if (isObjectAssignCallee(call.callee)) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
if (index !== 0) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (isStructuredCloneCallee(call.callee)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (isNamespacedCallee(call.callee, 'Array', 'from')) {
|
|
419
|
+
return call.arguments.length === 1;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
385
422
|
};
|
|
386
423
|
/**
|
|
387
424
|
* Array methods whose result keeps the receiver's ELEMENT type. `map` is
|
|
@@ -390,7 +427,19 @@ const isObjectAssignCallee = (callee) => {
|
|
|
390
427
|
* `map`. Admitting it would withhold the assertion from every derived array
|
|
391
428
|
* anything is computed from, to cover a spelling nobody writes.
|
|
392
429
|
*/
|
|
393
|
-
const TYPE_PRESERVING_COPY_METHODS = new Set([
|
|
430
|
+
const TYPE_PRESERVING_COPY_METHODS = new Set([
|
|
431
|
+
'concat',
|
|
432
|
+
'slice',
|
|
433
|
+
'filter',
|
|
434
|
+
'flat',
|
|
435
|
+
// The ES2023 copying methods. Listed even though this repo's TypeScript
|
|
436
|
+
// predates them, because they are the same category and admitting them costs
|
|
437
|
+
// nothing: a name that does not resolve produces no reports to lose.
|
|
438
|
+
'toSorted',
|
|
439
|
+
'toReversed',
|
|
440
|
+
'toSpliced',
|
|
441
|
+
'with',
|
|
442
|
+
]);
|
|
394
443
|
/**
|
|
395
444
|
* The expression that builds a COPY carrying this value's type — the literal
|
|
396
445
|
* around a spread of it, the call of a copying array method on it, or an
|
|
@@ -415,10 +464,11 @@ const copyExpressionOf = (node) => {
|
|
|
415
464
|
parent.parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
|
|
416
465
|
return parent.parent;
|
|
417
466
|
}
|
|
418
|
-
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression
|
|
419
|
-
parent.arguments.
|
|
420
|
-
|
|
421
|
-
|
|
467
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
468
|
+
const index = parent.arguments.indexOf(node);
|
|
469
|
+
if (index !== -1 && isCopyingCall(parent, index)) {
|
|
470
|
+
return parent;
|
|
471
|
+
}
|
|
422
472
|
}
|
|
423
473
|
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
424
474
|
parent.object === node) {
|
|
@@ -442,6 +492,10 @@ const PATTERN_CONTAINERS = new Set([
|
|
|
442
492
|
utils_1.AST_NODE_TYPES.ObjectPattern,
|
|
443
493
|
utils_1.AST_NODE_TYPES.ArrayPattern,
|
|
444
494
|
utils_1.AST_NODE_TYPES.RestElement,
|
|
495
|
+
// A parameter property is a parameter AND declares a class property, so it
|
|
496
|
+
// infers twice over. Without it the walk stops before reaching the
|
|
497
|
+
// constructor's params and `constructor(public stage = DEFAULT)` narrows.
|
|
498
|
+
utils_1.AST_NODE_TYPES.TSParameterProperty,
|
|
445
499
|
]);
|
|
446
500
|
const FUNCTION_TYPES = new Set([
|
|
447
501
|
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
@@ -347,6 +347,101 @@ 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 consumptionOfDeclaration = (declaration, id, context, reactImports) => {
|
|
414
|
+
// `getDeclaredVariables` on a FunctionDeclaration yields its PARAMETERS
|
|
415
|
+
// alongside the function name, and a parameter handed to a non-component prop
|
|
416
|
+
// votes `callback` for a binding it says nothing about. Keep only the
|
|
417
|
+
// variable this declaration's own id introduces.
|
|
418
|
+
const declared = context
|
|
419
|
+
.getDeclaredVariables(declaration)
|
|
420
|
+
.filter((variable) => variable.defs.some((def) => def.name === id));
|
|
421
|
+
let sawCallback = false;
|
|
422
|
+
for (const variable of declared) {
|
|
423
|
+
for (const reference of variable.references) {
|
|
424
|
+
const consumption = consumptionOfReference(reference.identifier, reactImports);
|
|
425
|
+
// A single mounting use settles it: the identity churn happens there
|
|
426
|
+
// regardless of how many other places merely call it.
|
|
427
|
+
if (consumption === 'component') {
|
|
428
|
+
return 'component';
|
|
429
|
+
}
|
|
430
|
+
if (consumption === 'callback') {
|
|
431
|
+
sawCallback = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return sawCallback ? 'callback' : 'unknown';
|
|
436
|
+
};
|
|
437
|
+
const consumptionOfBinding = (node, context, reactImports) => {
|
|
438
|
+
const declarator = parentBeyondChain(node);
|
|
439
|
+
if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
440
|
+
declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
441
|
+
return 'unknown';
|
|
442
|
+
}
|
|
443
|
+
return consumptionOfDeclaration(declarator, declarator.id, context, reactImports);
|
|
444
|
+
};
|
|
350
445
|
/**
|
|
351
446
|
* The values a container hands to its caller: object property values and array
|
|
352
447
|
* elements. Mirrors `containedValues` in the paired `require-memo` rule (#1919),
|
|
@@ -725,10 +820,20 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
725
820
|
}
|
|
726
821
|
}
|
|
727
822
|
const variableName = getVariableName(node);
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
823
|
+
// The NAME was the whole discriminator here, which reported every
|
|
824
|
+
// PascalCase render callback the message explicitly exempts and missed
|
|
825
|
+
// every lowercase binding handed to a component-type prop. Evidence
|
|
826
|
+
// from the use site overrides it in both directions; the name still
|
|
827
|
+
// decides where there is no evidence, which is where an exported
|
|
828
|
+
// component lives (#2334).
|
|
829
|
+
if (variableName) {
|
|
830
|
+
const consumption = consumptionOfBinding(node, context, reactImports);
|
|
831
|
+
if (consumption === 'callback') {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (consumption === 'unknown' && !isPascalCaseName(variableName)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
732
837
|
}
|
|
733
838
|
// Inside an HOC factory the binding has a stable identity, so it does
|
|
734
839
|
// not remount on re-render and must not be flagged.
|
|
@@ -747,8 +852,10 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
747
852
|
return;
|
|
748
853
|
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
749
854
|
return;
|
|
750
|
-
|
|
751
|
-
if (
|
|
855
|
+
const vdConsumption = consumptionOfDeclaration(node, node.id, context, reactImports);
|
|
856
|
+
if (vdConsumption === 'callback')
|
|
857
|
+
return;
|
|
858
|
+
if (vdConsumption === 'unknown' && !isPascalCaseName(node.id.name))
|
|
752
859
|
return;
|
|
753
860
|
if (!isInsideFunction(node))
|
|
754
861
|
return;
|
|
@@ -772,7 +879,12 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
772
879
|
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
773
880
|
},
|
|
774
881
|
FunctionDeclaration(node) {
|
|
775
|
-
if (!node.id
|
|
882
|
+
if (!node.id)
|
|
883
|
+
return;
|
|
884
|
+
const fdConsumption = consumptionOfDeclaration(node, node.id, context, reactImports);
|
|
885
|
+
if (fdConsumption === 'callback')
|
|
886
|
+
return;
|
|
887
|
+
if (fdConsumption === 'unknown' && !isPascalCaseName(node.id.name))
|
|
776
888
|
return;
|
|
777
889
|
if (!isInsideFunction(node))
|
|
778
890
|
return;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.11",
|
|
4
|
+
"date": "2026-09-05T11:06:01.167Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2336
|
|
11
|
+
],
|
|
12
|
+
"summary": "follow the array spelling of a destructured copy, and the rest element that needs no copy (closes #2336)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "memo-nested-react-components",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2335
|
|
19
|
+
],
|
|
20
|
+
"summary": "decide by the use site in the two non-hook visitors too (closes #2335)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.21.10",
|
|
26
|
+
"date": "2026-09-05T09:28:38.523Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "global-const-style",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2333
|
|
33
|
+
],
|
|
34
|
+
"summary": "follow every copy that carries the frozen type, and the parameter property that infers from it (closes #2333)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "memo-nested-react-components",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
2334
|
|
41
|
+
],
|
|
42
|
+
"summary": "decide by the use site, not the binding's first letter (closes #2334)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.21.9",
|
|
4
48
|
"date": "2026-09-05T05:55:33.427Z",
|