@blumintinc/eslint-plugin-blumint 1.20.44 → 1.20.45

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.44',
226
+ version: '1.20.45',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceMuiRoundedIcons = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const utils_1 = require("@typescript-eslint/utils");
6
+ const MUI_ICONS_BARREL = '@mui/icons-material';
7
+ const MUI_ICONS_DEEP_PREFIX = `${MUI_ICONS_BARREL}/`;
6
8
  /**
7
9
  * Non-Rounded MUI variant suffixes. An icon imported as one of these variants
8
10
  * (e.g. `AddReactionOutlined`) maps to the Rounded variant of its BASE name
@@ -31,11 +33,39 @@ const ICONS_WITHOUT_ROUNDED = new Set([
31
33
  'X',
32
34
  'YouTube',
33
35
  ]);
36
+ /**
37
+ * Names the @mui/icons-material barrel exports that are not icons, so a
38
+ * `*Rounded` sibling never exists for them. `SvgIconComponent` is the icon
39
+ * component *type*, reached through the same barrel as the icons themselves.
40
+ */
41
+ const NON_ICON_EXPORTS = new Set(['SvgIconComponent']);
34
42
  /** Strips a trailing non-Rounded variant suffix to recover the base icon name. */
35
43
  const toBaseIconName = (iconName) => {
36
44
  const suffix = VARIANT_SUFFIXES.find((variant) => iconName.endsWith(variant));
37
45
  return suffix ? iconName.slice(0, -suffix.length) : iconName;
38
46
  };
47
+ /**
48
+ * Single source of truth for both import forms: maps the name an icon is
49
+ * imported under to the Rounded name that should replace it, or null when the
50
+ * name needs no change — it is already Rounded, it is not an icon, or MUI ships
51
+ * that icon without a Rounded variant.
52
+ */
53
+ const toRoundedIconName = (iconName) => {
54
+ if (!iconName ||
55
+ iconName.endsWith('Rounded') ||
56
+ NON_ICON_EXPORTS.has(iconName)) {
57
+ return null;
58
+ }
59
+ // Map a non-Rounded variant to its base icon so the suggestion/fix targets a
60
+ // name that actually exists.
61
+ const baseIconName = toBaseIconName(iconName);
62
+ // Brand icons have no Rounded variant — demanding one is a false positive and
63
+ // the fix would point at a name that does not exist.
64
+ if (ICONS_WITHOUT_ROUNDED.has(baseIconName)) {
65
+ return null;
66
+ }
67
+ return `${baseIconName}Rounded`;
68
+ };
39
69
  exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
40
70
  name: 'enforce-mui-rounded-icons',
41
71
  meta: {
@@ -52,41 +82,68 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
52
82
  },
53
83
  defaultOptions: [],
54
84
  create(context) {
85
+ /** `import LogoutIcon from '@mui/icons-material/Logout'` — the icon is named by the module path. */
86
+ const checkDeepImport = (node, iconPath) => {
87
+ const iconName = iconPath.split('/').pop();
88
+ const roundedVariant = iconName ? toRoundedIconName(iconName) : null;
89
+ if (!roundedVariant) {
90
+ return;
91
+ }
92
+ context.report({
93
+ node,
94
+ messageId: 'enforceRoundedVariant',
95
+ // The local binding is untouched: only the module the icon comes from
96
+ // changes.
97
+ fix: (fixer) => fixer.replaceText(node.source, `'${MUI_ICONS_DEEP_PREFIX}${roundedVariant}'`),
98
+ });
99
+ };
100
+ /** `import { Logout } from '@mui/icons-material'` — the icon is named by each specifier. */
101
+ const checkBarrelImport = (node) => {
102
+ // A type-only import names a type, and the barrel's type exports are not
103
+ // icons; nothing it introduces can be rendered.
104
+ if (node.importKind === 'type') {
105
+ return;
106
+ }
107
+ for (const specifier of node.specifiers) {
108
+ // Default and namespace imports name no individual icon.
109
+ if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
110
+ specifier.importKind === 'type') {
111
+ continue;
112
+ }
113
+ const importedName = specifier.imported.name;
114
+ const roundedVariant = toRoundedIconName(importedName);
115
+ if (!roundedVariant) {
116
+ continue;
117
+ }
118
+ // An alias keeps the local binding independent of the imported name, so
119
+ // swapping the imported name is a self-contained edit. Without one, a
120
+ // fix would rename the binding and would have to rewrite every
121
+ // reference to it (shorthand properties, export specifiers, JSX) — so
122
+ // that form is reported without a fix rather than risking a corrupting
123
+ // edit.
124
+ const isAliased = specifier.local.name !== importedName;
125
+ context.report({
126
+ node: specifier,
127
+ messageId: 'enforceRoundedVariant',
128
+ fix: isAliased
129
+ ? (fixer) => fixer.replaceText(specifier.imported, roundedVariant)
130
+ : null,
131
+ });
132
+ }
133
+ };
55
134
  return {
56
135
  ImportDeclaration(node) {
57
- // Only check imports from @mui/icons-material
58
- if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
59
- typeof node.source.value === 'string' &&
60
- node.source.value.startsWith('@mui/icons-material/')) {
61
- const iconPath = node.source.value;
62
- // Skip if already using -Rounded variant
63
- if (iconPath.endsWith('Rounded')) {
64
- return;
65
- }
66
- // Extract the icon name from the path
67
- const iconName = iconPath.split('/').pop();
68
- // Skip if the icon name is not a string (shouldn't happen)
69
- if (!iconName) {
70
- return;
71
- }
72
- // Map a non-Rounded variant import to its base icon so the Rounded
73
- // suggestion/fix targets a name that actually exists.
74
- const baseIconName = toBaseIconName(iconName);
75
- // Brand icons have no Rounded variant — demanding one is a false
76
- // positive and the fix would point at a non-existent module.
77
- if (ICONS_WITHOUT_ROUNDED.has(baseIconName)) {
78
- return;
79
- }
80
- // Create the rounded variant name
81
- const roundedVariant = `${baseIconName}Rounded`;
82
- context.report({
83
- node,
84
- messageId: 'enforceRoundedVariant',
85
- fix: (fixer) => {
86
- // Replace the import path with the rounded variant
87
- return fixer.replaceText(node.source, `'@mui/icons-material/${roundedVariant}'`);
88
- },
89
- });
136
+ if (node.source.type !== utils_1.AST_NODE_TYPES.Literal ||
137
+ typeof node.source.value !== 'string') {
138
+ return;
139
+ }
140
+ const source = node.source.value;
141
+ if (source.startsWith(MUI_ICONS_DEEP_PREFIX)) {
142
+ checkDeepImport(node, source);
143
+ return;
144
+ }
145
+ if (source === MUI_ICONS_BARREL) {
146
+ checkBarrelImport(node);
90
147
  }
91
148
  },
92
149
  };
@@ -115,14 +115,23 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
115
115
  node,
116
116
  messageId: 'enforceAsConst',
117
117
  fix(fixer) {
118
- const text = sourceCode.getText(argument);
119
- // If it's already a type assertion but not 'as const'
118
+ // A literal already carrying an explicit assertion states a contract
119
+ // the fix cannot preserve: `as const` produces a readonly,
120
+ // literal-typed shape that is structurally different from the
121
+ // asserted type, so rewriting `as SomeType` into `as const` silently
122
+ // changes what the function returns and can break the signature the
123
+ // assertion was written to satisfy. The diagnostic still holds — the
124
+ // author may well want `as const` — but choosing between the two
125
+ // types needs a human, so decline to fix (#1503).
126
+ //
127
+ // Any `TSAsExpression` reaching here is a non-`const` assertion; the
128
+ // `as const` case returns before the report. The angle-bracket form
129
+ // (`<SomeType>{...}`, a `TSTypeAssertion`) needs no branch here
130
+ // because it is never detected above — tsc rejects testing for it.
120
131
  if (argument.type === 'TSAsExpression') {
121
- // Get the expression part (before the 'as')
122
- const expressionText = sourceCode.getText(argument.expression);
123
- return fixer.replaceText(argument, `${expressionText} as const`);
132
+ return null;
124
133
  }
125
- return fixer.replaceText(argument, `${text} as const`);
134
+ return fixer.replaceText(argument, `${sourceCode.getText(argument)} as const`);
126
135
  },
127
136
  });
128
137
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.44",
3
+ "version": "1.20.45",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.45",
4
+ "date": "2026-07-31T06:37:56.216Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-mui-rounded-icons",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1502
11
+ ],
12
+ "summary": "examine barrel imports, not just deep ones (closes #1502)"
13
+ },
14
+ {
15
+ "name": "enforce-object-literal-as-const",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1503
19
+ ],
20
+ "summary": "stop the autofix destroying an existing type assertion (closes #1503)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.44",
4
26
  "date": "2026-07-31T05:47:19.150Z",