@blumintinc/eslint-plugin-blumint 1.19.2 → 1.19.4
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
|
@@ -367,6 +367,13 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
367
367
|
return {};
|
|
368
368
|
const basename = fileBasename(filename);
|
|
369
369
|
const topLevelFunctions = [];
|
|
370
|
+
// Top-level `VariableDeclarator` initializer expressions that are NOT
|
|
371
|
+
// themselves functions — e.g. a `Record` registry object literal or an
|
|
372
|
+
// array literal whose entries invoke a factory helper. These are the
|
|
373
|
+
// sibling consumers a reverse-closure walk over function bodies alone would
|
|
374
|
+
// miss, so we retain them to complete the "does the file need the
|
|
375
|
+
// candidate?" check.
|
|
376
|
+
const nonFunctionInitializers = [];
|
|
370
377
|
let hasExportDefault = false;
|
|
371
378
|
// Names of every top-level binding (functions, consts, lets, destructured
|
|
372
379
|
// bindings). Used by the closure exemption: a function that references any
|
|
@@ -478,8 +485,14 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
478
485
|
continue;
|
|
479
486
|
const name = declarator.id.name;
|
|
480
487
|
const fn = extractFunctionInit(declarator.init);
|
|
481
|
-
if (!fn)
|
|
488
|
+
if (!fn) {
|
|
489
|
+
// A non-function initializer (object/array literal, call, etc.) may
|
|
490
|
+
// still consume a sibling helper by name; retain it for the
|
|
491
|
+
// reverse-closure exemption.
|
|
492
|
+
if (declarator.init)
|
|
493
|
+
nonFunctionInitializers.push(declarator.init);
|
|
482
494
|
continue;
|
|
495
|
+
}
|
|
483
496
|
topLevelFunctions.push({
|
|
484
497
|
node: declarator,
|
|
485
498
|
name,
|
|
@@ -559,7 +572,7 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
559
572
|
// primitive its sibling exports build on. A cohesive multi-export
|
|
560
573
|
// utility module is already a utility file; extracting its core
|
|
561
574
|
// would sever the file's internal cohesion.
|
|
562
|
-
if (isReferencedBySibling(info, topLevelFunctions))
|
|
575
|
+
if (isReferencedBySibling(info, topLevelFunctions, nonFunctionInitializers))
|
|
563
576
|
continue;
|
|
564
577
|
}
|
|
565
578
|
// --- Co-location gate: file must have a distinct primary export ---
|
|
@@ -618,17 +631,25 @@ function functionClosesOverModuleScope(fn, topLevelBindingNames) {
|
|
|
618
631
|
return false;
|
|
619
632
|
}
|
|
620
633
|
/**
|
|
621
|
-
* Returns true if any *other* top-level
|
|
622
|
-
* candidate by name
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
634
|
+
* Returns true if any *other* top-level sibling in the file references the
|
|
635
|
+
* candidate by name. When sibling exports build on the candidate, the candidate
|
|
636
|
+
* is the module's shared internal primitive — a cohesive multi-export utility
|
|
637
|
+
* module is already a utility file, and extracting its core would orphan the
|
|
638
|
+
* siblings from the primitive they depend on.
|
|
639
|
+
*
|
|
640
|
+
* Two kinds of sibling can consume the candidate:
|
|
641
|
+
* - Another top-level function that references it in its body.
|
|
642
|
+
* - A non-function top-level initializer expression that references it — e.g. a
|
|
643
|
+
* `Record` registry object literal or array literal whose entries invoke the
|
|
644
|
+
* candidate factory (`export const RENDERERS = { b: buildRenderer('b') }`).
|
|
645
|
+
* The function-body walk cannot see these, so the initializer expressions are
|
|
646
|
+
* inspected directly.
|
|
626
647
|
*
|
|
627
648
|
* This is the mirror of the closure exemption: `functionClosesOverModuleScope`
|
|
628
649
|
* asks "does the candidate need the file?"; this asks "does the file need the
|
|
629
650
|
* candidate?". Either direction of dependency means extraction severs cohesion.
|
|
630
651
|
*/
|
|
631
|
-
function isReferencedBySibling(candidate, allFunctions) {
|
|
652
|
+
function isReferencedBySibling(candidate, allFunctions, nonFunctionInitializers = []) {
|
|
632
653
|
for (const other of allFunctions) {
|
|
633
654
|
if (other.name === candidate.name)
|
|
634
655
|
continue;
|
|
@@ -639,6 +660,11 @@ function isReferencedBySibling(candidate, allFunctions) {
|
|
|
639
660
|
if (refs.has(candidate.name))
|
|
640
661
|
return true;
|
|
641
662
|
}
|
|
663
|
+
for (const init of nonFunctionInitializers) {
|
|
664
|
+
const refs = collectReferencedIdentifiers(init);
|
|
665
|
+
if (refs.has(candidate.name))
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
642
668
|
return false;
|
|
643
669
|
}
|
|
644
670
|
/**
|
|
@@ -46,6 +46,18 @@ const DEFAULT_EXCLUDED_COMPONENTS = new Set([
|
|
|
46
46
|
'React.StrictMode',
|
|
47
47
|
]);
|
|
48
48
|
const DEFAULT_TARGET_PATHS = ['src/components/**/*.tsx'];
|
|
49
|
+
/**
|
|
50
|
+
* Icon components (e.g. CheckIcon, RefreshIcon from @mui/icons-material) are
|
|
51
|
+
* decorative leaf elements — the same category as the layout/decorative
|
|
52
|
+
* primitives in DEFAULT_EXCLUDED_COMPONENTS. They expose no composable
|
|
53
|
+
* customization surface a parent should re-expose, so rendering one is never a
|
|
54
|
+
* composition dependency. Matched by the conventional `*Icon` suffix, which
|
|
55
|
+
* excludes CheckIcon/LinkIcon/RefreshIcon without touching interactive
|
|
56
|
+
* components like IconButton (issue #1307).
|
|
57
|
+
*/
|
|
58
|
+
function isDecorativeIcon(name) {
|
|
59
|
+
return /Icon$/.test(name);
|
|
60
|
+
}
|
|
49
61
|
/**
|
|
50
62
|
* Derives the expected Props type name for a JSX element name.
|
|
51
63
|
* e.g. "LoadingButton" → "LoadingButtonProps"
|
|
@@ -432,7 +444,9 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
|
|
|
432
444
|
const body = funcNode.body ?? funcNode;
|
|
433
445
|
const allJsxNames = collectJsxElementNames(body);
|
|
434
446
|
// Filter to non-excluded custom components
|
|
435
|
-
const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) &&
|
|
447
|
+
const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) &&
|
|
448
|
+
!isDecorativeIcon(name) &&
|
|
449
|
+
name !== componentName);
|
|
436
450
|
if (depComponents.length < minDependencyCount) {
|
|
437
451
|
return;
|
|
438
452
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.4",
|
|
4
|
+
"date": "2026-07-14T18:24:36.895Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "require-props-composition",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1307
|
|
11
|
+
],
|
|
12
|
+
"summary": "exclude decorative *Icon leaf components (closes #1307)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.3",
|
|
18
|
+
"date": "2026-07-14T11:28:07.708Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "prefer-utility-function-own-file",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1305
|
|
25
|
+
],
|
|
26
|
+
"summary": "exempt factory consumed only by sibling const initializer literal (closes #1305)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.2",
|
|
4
32
|
"date": "2026-07-14T07:32:08.575Z",
|