@blumintinc/eslint-plugin-blumint 1.20.200 → 1.20.202

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.200',
226
+ version: '1.20.202',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -64,6 +64,52 @@ const DEFAULT_TARGET_PATHS = ['src/components/**/*.tsx'];
64
64
  function isDecorativeIcon(name) {
65
65
  return /Icon$/.test(name);
66
66
  }
67
+ const MUI_ICONS_PACKAGE = '@mui/icons-material';
68
+ /**
69
+ * Whether a specifier names the MUI icon package: the barrel
70
+ * (`@mui/icons-material`) or a per-icon deep path
71
+ * (`@mui/icons-material/CheckRounded`). Sibling packages such as
72
+ * `@mui/material` are deliberately not matched — the carve-out covers icons,
73
+ * not MUI at large, so a `@mui/lab` or `@mui/material` child stays a
74
+ * composition dependency.
75
+ */
76
+ function isMuiIconSource(source) {
77
+ return (source === MUI_ICONS_PACKAGE || source.startsWith(`${MUI_ICONS_PACKAGE}/`));
78
+ }
79
+ /**
80
+ * Whether `localName` is bound by a value import from the MUI icon package.
81
+ * The `*Icon` suffix alone cannot decide this: MUI's own export names carry no
82
+ * such suffix (`NotificationsNoneRounded`, `CheckRounded`), and the plugin's
83
+ * own `enforce-mui-rounded-icons` fixer emits exactly that spelling, so the
84
+ * binding cannot be renamed to opt in (issue #2282). An icon is a decorative
85
+ * leaf wherever it comes from, so the import source answers the same question
86
+ * the suffix does.
87
+ *
88
+ * A sibling of `findRelativeImport`, which accepts only relative specifiers by
89
+ * design (issue #1316 reads a child module off disk and must not chase a
90
+ * package). Type-only imports bind no renderable value and are skipped.
91
+ */
92
+ function isMuiIconImport(program, localName) {
93
+ for (const stmt of program.body) {
94
+ if (stmt.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
95
+ stmt.importKind === 'type' ||
96
+ typeof stmt.source.value !== 'string' ||
97
+ !isMuiIconSource(stmt.source.value)) {
98
+ continue;
99
+ }
100
+ for (const specifier of stmt.specifiers) {
101
+ if (specifier.local.name !== localName) {
102
+ continue;
103
+ }
104
+ // A module-scope name has one binding, so the first specifier that
105
+ // introduces it settles the question.
106
+ return (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
107
+ (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
108
+ specifier.importKind !== 'type'));
109
+ }
110
+ }
111
+ return false;
112
+ }
67
113
  /**
68
114
  * Derives the expected Props type name for a JSX element name.
69
115
  * e.g. "LoadingButton" → "LoadingButtonProps"
@@ -1704,6 +1750,7 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
1704
1750
  !isDecorativeIcon(name) &&
1705
1751
  name !== componentName &&
1706
1752
  !propSlots.has(name) &&
1753
+ !isMuiIconImport(prog, name) &&
1707
1754
  !isZeroPropComponent(bodyScope, name));
1708
1755
  if (depComponents.length < minDependencyCount) {
1709
1756
  return;
@@ -71,23 +71,27 @@ exports.useCustomLink = (0, createRule_1.createRule)({
71
71
  const defaultAsSpecifier = importSpecifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
72
72
  specifier.imported.name === 'default');
73
73
  if (defaultSpecifier || defaultAsSpecifier) {
74
+ // The fix rebuilds the whole declaration from the default binding
75
+ // alone, so any other specifier would be deleted outright and
76
+ // whatever still references it left dangling — including an
77
+ // `export { LinkProps }` that keeps the exported NAME while losing
78
+ // what it resolves to. Relocating them instead is not available:
79
+ // whether the wrapper re-exports a given specifier is unknowable
80
+ // from here, so carrying them over would trade a silent dangling
81
+ // binding for a possibly unresolvable import. Decline the fix and
82
+ // keep the report, so the migration stays a deliberate edit.
83
+ const droppedSpecifiers = importSpecifiers.filter((specifier) => specifier !== defaultSpecifier &&
84
+ specifier !== defaultAsSpecifier);
85
+ const localName = defaultSpecifier?.local?.name ||
86
+ defaultAsSpecifier?.local?.name ||
87
+ 'Link';
74
88
  context.report({
75
89
  node,
76
90
  messageId: 'useCustomLink',
77
- data: {
78
- localName: defaultSpecifier?.local?.name ||
79
- defaultAsSpecifier?.local?.name ||
80
- 'Link',
81
- },
82
- fix(fixer) {
83
- // Get the local name of the imported Link component
84
- const localName = defaultSpecifier?.local?.name ||
85
- defaultAsSpecifier?.local?.name ||
86
- 'Link';
87
- // Create the new import statement
88
- const newImport = `import ${localName} from '${LINK_MODULE_PATH}';`;
89
- return fixer.replaceText(node, newImport);
90
- },
91
+ data: { localName },
92
+ fix: droppedSpecifiers.length > 0
93
+ ? undefined
94
+ : (fixer) => fixer.replaceText(node, `import ${localName} from '${LINK_MODULE_PATH}';`),
91
95
  });
92
96
  }
93
97
  }
@@ -32,10 +32,28 @@ export declare const PLUGIN_PREFIX = "@blumintinc/blumint/";
32
32
  */
33
33
  export declare const recommendedRulesExcluding: (excluded: ReadonlySet<string>) => Record<string, unknown>;
34
34
  /**
35
- * The composed set for one fixture: the shipped severities, plus the owner's own
36
- * entry carrying the options its author wrote.
35
+ * The severities an OVERRIDE contributes for one filename.
36
+ *
37
+ * `configs.recommended` is not the flat `rules` map alone — it also carries an
38
+ * `overrides` array, and a rule reachable only through one is still enabled in
39
+ * recommended. Reading `rules` by itself leaves such a rule outside every guard
40
+ * that composes the config, both on its own fixtures and, more importantly, as
41
+ * a SIBLING while some other rule's fixtures are linted. `enforce-date-ttime`
42
+ * is enabled exactly that way, for `src/**` (#1734 fixed the same blind read in
43
+ * `recommended-severity-consistency`; the model stayed flat everywhere else).
44
+ *
45
+ * Resolved per FILENAME rather than merged unconditionally, because the glob is
46
+ * what decides whether a consumer gets the rule on a given file. Merging it
47
+ * everywhere would enable it on paths no consumer runs it on, which is a
48
+ * different configuration and not the one being modelled.
49
+ */
50
+ export declare const overrideRulesFor: (filename: string, excluded: ReadonlySet<string>) => Record<string, unknown>;
51
+ /**
52
+ * The composed set for one fixture: the shipped severities, the severities any
53
+ * matching override adds for this fixture's path, plus the owner's own entry
54
+ * carrying the options its author wrote.
37
55
  */
38
- export declare const composedRulesFor: (recommended: Record<string, unknown>, excluded: ReadonlySet<string>, owner: string, testCase: FixtureCase) => Record<string, unknown>;
56
+ export declare const composedRulesFor: (recommended: Record<string, unknown>, excluded: ReadonlySet<string>, owner: string, testCase: FixtureCase, filename?: string) => Record<string, unknown>;
39
57
  export type OptionCarriage = {
40
58
  /** Screens whose composed config carried author-declared options. */
41
59
  carried: number;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.subsetInConfigOrder = exports.noteOptionCarriage = exports.composedRulesFor = exports.recommendedRulesExcluding = exports.PLUGIN_PREFIX = void 0;
3
+ exports.subsetInConfigOrder = exports.noteOptionCarriage = exports.composedRulesFor = exports.overrideRulesFor = exports.recommendedRulesExcluding = exports.PLUGIN_PREFIX = void 0;
4
+ const minimatch_1 = require("minimatch");
4
5
  const fixtureCorpus_1 = require("./fixtureCorpus");
5
6
  /* eslint-disable @typescript-eslint/no-var-requires */
6
7
  const plugin = require('../index');
@@ -54,11 +55,54 @@ const recommendedRulesExcluding = (excluded) => {
54
55
  };
55
56
  exports.recommendedRulesExcluding = recommendedRulesExcluding;
56
57
  /**
57
- * The composed set for one fixture: the shipped severities, plus the owner's own
58
- * entry carrying the options its author wrote.
58
+ * The severities an OVERRIDE contributes for one filename.
59
+ *
60
+ * `configs.recommended` is not the flat `rules` map alone — it also carries an
61
+ * `overrides` array, and a rule reachable only through one is still enabled in
62
+ * recommended. Reading `rules` by itself leaves such a rule outside every guard
63
+ * that composes the config, both on its own fixtures and, more importantly, as
64
+ * a SIBLING while some other rule's fixtures are linted. `enforce-date-ttime`
65
+ * is enabled exactly that way, for `src/**` (#1734 fixed the same blind read in
66
+ * `recommended-severity-consistency`; the model stayed flat everywhere else).
67
+ *
68
+ * Resolved per FILENAME rather than merged unconditionally, because the glob is
69
+ * what decides whether a consumer gets the rule on a given file. Merging it
70
+ * everywhere would enable it on paths no consumer runs it on, which is a
71
+ * different configuration and not the one being modelled.
72
+ */
73
+ const overrideRulesFor = (filename, excluded) => {
74
+ const rules = {};
75
+ for (const override of plugin.configs.recommended.overrides || []) {
76
+ const globs = override.files || [];
77
+ if (!globs.some((glob) => (0, minimatch_1.minimatch)(filename, glob, { dot: true }))) {
78
+ continue;
79
+ }
80
+ for (const [id, severity] of Object.entries(override.rules || {})) {
81
+ if (!id.startsWith(exports.PLUGIN_PREFIX))
82
+ continue;
83
+ if (severity === 'off' || severity === 0)
84
+ continue;
85
+ const name = id.slice(exports.PLUGIN_PREFIX.length);
86
+ if (!plugin.rules[name])
87
+ continue;
88
+ if (excluded.has(name))
89
+ continue;
90
+ rules[id] = severity;
91
+ }
92
+ }
93
+ return rules;
94
+ };
95
+ exports.overrideRulesFor = overrideRulesFor;
96
+ /**
97
+ * The composed set for one fixture: the shipped severities, the severities any
98
+ * matching override adds for this fixture's path, plus the owner's own entry
99
+ * carrying the options its author wrote.
59
100
  */
60
- const composedRulesFor = (recommended, excluded, owner, testCase) => {
61
- const rules = { ...recommended };
101
+ const composedRulesFor = (recommended, excluded, owner, testCase, filename = (0, fixtureCorpus_1.defaultFilenameFor)(testCase)) => {
102
+ const rules = {
103
+ ...recommended,
104
+ ...(0, exports.overrideRulesFor)(filename, excluded),
105
+ };
62
106
  if (!excluded.has(owner)) {
63
107
  rules[`${exports.PLUGIN_PREFIX}${owner}`] = (0, fixtureCorpus_1.severityWithOptions)(testCase);
64
108
  }
@@ -49,7 +49,7 @@ export type HarvestResult = {
49
49
  * Only suites that import the shared tester module can declare a case, since
50
50
  * `src/tests/no-local-rule-tester.test.ts` forbids a locally-built tester. That
51
51
  * makes the import a sound admission test rather than a heuristic, and it is
52
- * what keeps this affordable: the ~28 files it excludes are the meta-suites
52
+ * what keeps this affordable: the files it excludes are the meta-suites
53
53
  * (`fixer-type-safety`, `docs-examples-conformance`, `rule-crash-robustness`,
54
54
  * this guard's own siblings) which run full corpus sweeps at module scope and
55
55
  * cost more to load than every rule suite combined.
@@ -33,7 +33,7 @@ const TESTS_DIR = path.join(__dirname, '..', 'tests');
33
33
  * Only suites that import the shared tester module can declare a case, since
34
34
  * `src/tests/no-local-rule-tester.test.ts` forbids a locally-built tester. That
35
35
  * makes the import a sound admission test rather than a heuristic, and it is
36
- * what keeps this affordable: the ~28 files it excludes are the meta-suites
36
+ * what keeps this affordable: the files it excludes are the meta-suites
37
37
  * (`fixer-type-safety`, `docs-examples-conformance`, `rule-crash-robustness`,
38
38
  * this guard's own siblings) which run full corpus sweeps at module scope and
39
39
  * cost more to load than every rule suite combined.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.200",
3
+ "version": "1.20.202",
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.20.202",
4
+ "date": "2026-09-02T04:11:09.443Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-props-composition",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2282
11
+ ],
12
+ "summary": "treat an @mui/icons-material import as decorative whatever its binding is called (closes #2282)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.201",
18
+ "date": "2026-09-01T19:58:16.746Z",
19
+ "rules": [
20
+ {
21
+ "name": "use-custom-link",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2272
25
+ ],
26
+ "summary": "decline the autofix when it would drop a specifier (closes #2272)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.200",
4
32
  "date": "2026-09-01T14:50:44.328Z",