@blumintinc/eslint-plugin-blumint 1.20.201 → 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.201',
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;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.201",
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,18 @@
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
+ },
2
16
  {
3
17
  "version": "1.20.201",
4
18
  "date": "2026-09-01T19:58:16.746Z",