@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
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.2',
225
+ version: '1.19.4',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -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 function in the file references the
622
- * candidate by name in its body. When sibling exports build on the candidate,
623
- * the candidate is the module's shared internal primitive — a cohesive
624
- * multi-export utility module is already a utility file, and extracting its core
625
- * would orphan the siblings from the primitive they depend on.
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) && name !== componentName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.2",
3
+ "version": "1.19.4",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",