@blumintinc/eslint-plugin-blumint 1.20.43 → 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 +1 -1
- package/lib/rules/enforce-firestore-doc-ref-generic.js +57 -0
- package/lib/rules/enforce-mui-rounded-icons.js +90 -33
- package/lib/rules/enforce-object-literal-as-const.js +15 -6
- package/lib/rules/enforce-transform-memoization.js +9 -0
- package/package.json +1 -1
- package/release-manifest.json +44 -0
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.enforceFirestoreDocRefGeneric = void 0;
|
|
|
10
10
|
*/
|
|
11
11
|
const utils_1 = require("@typescript-eslint/utils");
|
|
12
12
|
const createRule_1 = require("../utils/createRule");
|
|
13
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
13
14
|
/**
|
|
14
15
|
* @type {import('eslint').Rule.RuleModule}
|
|
15
16
|
*/
|
|
@@ -305,8 +306,64 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
305
306
|
if (findVariableDeclaration(node)) {
|
|
306
307
|
return true;
|
|
307
308
|
}
|
|
309
|
+
// Resolve the binding through the scope chain so that a typed collection
|
|
310
|
+
// stored in a variable still supplies the document generic to .doc().
|
|
311
|
+
if (isTypedCollectionBinding(node)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
308
314
|
return false;
|
|
309
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Resolves an identifier to its declaration and reports whether that
|
|
318
|
+
* declaration provably yields a typed CollectionReference.
|
|
319
|
+
*
|
|
320
|
+
* Deliberately conservative: only immutable (`const`) bindings with a
|
|
321
|
+
* single definition are followed, and only for one hop. An alias such as
|
|
322
|
+
* `const b = a;` is not resolved because chasing arbitrary dataflow
|
|
323
|
+
* syntactically produces unsound exemptions; `let`/`var` are refused
|
|
324
|
+
* because a later assignment can replace the value with an untyped
|
|
325
|
+
* collection. Anything unresolvable (parameters, imports, destructuring)
|
|
326
|
+
* keeps reporting, since the rule cannot prove the reference is typed.
|
|
327
|
+
*/
|
|
328
|
+
function isTypedCollectionBinding(node) {
|
|
329
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
330
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, node.name);
|
|
331
|
+
if (!variable || variable.defs.length !== 1) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const def = variable.defs[0];
|
|
335
|
+
if (def.type !== 'Variable' ||
|
|
336
|
+
def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
337
|
+
def.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
338
|
+
def.parent.kind !== 'const') {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const declarator = def.node;
|
|
342
|
+
if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
343
|
+
declarator.id.typeAnnotation) {
|
|
344
|
+
return hasCollectionReferenceType(declarator.id.typeAnnotation.typeAnnotation);
|
|
345
|
+
}
|
|
346
|
+
return isTypedCollectionInitializer(declarator.init);
|
|
347
|
+
}
|
|
348
|
+
function isTypedCollectionInitializer(init) {
|
|
349
|
+
if (!init) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// An explicit assertion states the schema just as an annotation does.
|
|
353
|
+
if (init.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
354
|
+
return hasCollectionReferenceType(init.typeAnnotation);
|
|
355
|
+
}
|
|
356
|
+
// Mirrors the chained `db.collection<T>('x').doc('y')` detection: the
|
|
357
|
+
// presence of the type argument is what matters here. An `any`/`{}`
|
|
358
|
+
// argument is already reported on the collection call itself, so it is
|
|
359
|
+
// not reported a second time on the derived document reference.
|
|
360
|
+
return (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
361
|
+
init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
362
|
+
init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
363
|
+
init.callee.property.name === 'collection' &&
|
|
364
|
+
!!init.typeParameters &&
|
|
365
|
+
init.typeParameters.params.length > 0);
|
|
366
|
+
}
|
|
310
367
|
function checkCallExpressionForCollectionReference(node) {
|
|
311
368
|
// Check if this is a method call that returns CollectionReference
|
|
312
369
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
119
|
-
//
|
|
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
|
-
|
|
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, `${
|
|
134
|
+
return fixer.replaceText(argument, `${sourceCode.getText(argument)} as const`);
|
|
126
135
|
},
|
|
127
136
|
});
|
|
128
137
|
},
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceTransformMemoization = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
7
8
|
name: 'enforce-transform-memoization',
|
|
8
9
|
meta: {
|
|
@@ -395,6 +396,14 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
|
395
396
|
!adaptValueNames.has(node.callee.name)) {
|
|
396
397
|
return;
|
|
397
398
|
}
|
|
399
|
+
// Every message this rule emits prescribes useMemo/useCallback, and a
|
|
400
|
+
// hook call is legal only inside a component or another hook. An
|
|
401
|
+
// adaptValue call at module scope, in a plain helper, or in a test body
|
|
402
|
+
// is not on a render path: nothing is "recreated on every render"
|
|
403
|
+
// there, and the prescribed fix would throw "Invalid hook call".
|
|
404
|
+
if (!ASTHelpers_1.ASTHelpers.isInsideComponentOrHook(node, context)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
398
407
|
const optionsArg = node.arguments[0];
|
|
399
408
|
if (!optionsArg)
|
|
400
409
|
return;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
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
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.44",
|
|
26
|
+
"date": "2026-07-31T05:47:19.150Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-firestore-doc-ref-generic",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1498
|
|
33
|
+
],
|
|
34
|
+
"summary": "honor a typed collection bound to a const (closes #1498)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "enforce-transform-memoization",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1497
|
|
41
|
+
],
|
|
42
|
+
"summary": "only report inside a component or hook (closes #1497)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.20.43",
|
|
4
48
|
"date": "2026-07-31T04:38:54.532Z",
|