@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 +1 -1
- package/lib/rules/require-props-composition.js +47 -0
- package/lib/rules/use-custom-link.js +18 -14
- package/lib/utils/composedFixConfig.d.ts +21 -3
- package/lib/utils/composedFixConfig.js +49 -5
- package/lib/utils/harvestRuleTesterCases.d.ts +1 -1
- package/lib/utils/harvestRuleTesterCases.js +1 -1
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
36
|
-
*
|
|
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
|
|
58
|
-
*
|
|
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 = {
|
|
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
|
|
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
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|