@blumintinc/eslint-plugin-blumint 1.20.113 → 1.20.115
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-serializable-params.d.ts +1 -0
- package/lib/rules/enforce-serializable-params.js +99 -35
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +1 -3
- package/lib/rules/fast-deep-equal-over-microdiff.js +0 -3
- package/lib/rules/no-jsx-in-hooks.js +39 -12
- package/lib/rules/prefer-fragment-component.d.ts +1 -3
- package/lib/rules/prefer-fragment-component.js +0 -1
- package/lib/rules/prefer-next-dynamic.d.ts +1 -2
- package/lib/rules/prefer-next-dynamic.js +0 -2
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").R
|
|
|
4
4
|
functionTypes: string[];
|
|
5
5
|
}[], {
|
|
6
6
|
TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration): void;
|
|
7
|
+
TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration): void;
|
|
7
8
|
TSTypeReference(node: TSESTree.TSTypeReference): void;
|
|
8
9
|
'Program:exit'(): void;
|
|
9
10
|
}>;
|
|
@@ -53,17 +53,63 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
53
53
|
...NON_SERIALIZABLE_TYPES,
|
|
54
54
|
...(options.additionalNonSerializableTypes || []),
|
|
55
55
|
]);
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Interfaces sit here alongside aliases because a request contract is as
|
|
58
|
+
* often an `interface` as a `type`, and resolving only one of the two makes
|
|
59
|
+
* the rule's coverage depend on which keyword the author reached for.
|
|
60
|
+
*/
|
|
61
|
+
const declarations = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* A node reports at most once. Resolution follows references, so a type
|
|
64
|
+
* reachable by two paths (`{ x: Inner; y: Inner }`) would otherwise produce
|
|
65
|
+
* duplicate reports at one location.
|
|
66
|
+
*/
|
|
67
|
+
const reported = new Set();
|
|
57
68
|
function isNonSerializableType(typeName) {
|
|
58
69
|
return allNonSerializableTypes.has(typeName);
|
|
59
70
|
}
|
|
60
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The rightmost identifier of a possibly-namespaced name, so
|
|
73
|
+
* `admin.firestore.Timestamp` is matched as `Timestamp`. The whole rule
|
|
74
|
+
* keys off simple names, and a namespaced spelling of a type is the same
|
|
75
|
+
* type — reading only `Identifier.name` leaves it `undefined` and silently
|
|
76
|
+
* exempts every firebase-admin-style reference.
|
|
77
|
+
*/
|
|
78
|
+
function simpleTypeNameOf(entity) {
|
|
79
|
+
if (!entity)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (entity.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
82
|
+
return entity.name;
|
|
83
|
+
if (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
84
|
+
return simpleTypeNameOf(entity.right);
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const propertyNameOf = (member) => {
|
|
89
|
+
const { key } = member;
|
|
90
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
91
|
+
return key.name;
|
|
92
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal)
|
|
93
|
+
return String(key.value);
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
96
|
+
function checkMembers(members, seen, propName) {
|
|
97
|
+
for (const member of members) {
|
|
98
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature)
|
|
99
|
+
continue;
|
|
100
|
+
checkTypeNode(member.typeAnnotation, propertyNameOf(member) ?? propName, seen);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function checkTypeNode(node, propName, seen = new Set()) {
|
|
61
104
|
if (!node)
|
|
62
105
|
return;
|
|
63
106
|
switch (node.type) {
|
|
64
107
|
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
65
|
-
const typeName = node.typeName
|
|
66
|
-
if (
|
|
108
|
+
const typeName = simpleTypeNameOf(node.typeName);
|
|
109
|
+
if (typeName &&
|
|
110
|
+
isNonSerializableType(typeName) &&
|
|
111
|
+
!reported.has(node)) {
|
|
112
|
+
reported.add(node);
|
|
67
113
|
context.report({
|
|
68
114
|
node,
|
|
69
115
|
messageId: propName
|
|
@@ -75,28 +121,51 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
75
121
|
},
|
|
76
122
|
});
|
|
77
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A reference to a locally declared type is followed so a payload
|
|
126
|
+
* assembled from named parts is inspected as a whole. `seen` is
|
|
127
|
+
* path-scoped rather than global: a self-referential type must not
|
|
128
|
+
* loop, but a type legitimately used by two siblings must still be
|
|
129
|
+
* checked under each of them.
|
|
130
|
+
*/
|
|
131
|
+
if (typeName && !seen.has(typeName)) {
|
|
132
|
+
const declaration = declarations.get(typeName);
|
|
133
|
+
if (declaration) {
|
|
134
|
+
seen.add(typeName);
|
|
135
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
136
|
+
checkTypeNode(declaration.typeAnnotation, propName, seen);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
checkMembers(declaration.body.body, seen, propName);
|
|
140
|
+
}
|
|
141
|
+
seen.delete(typeName);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
78
144
|
// Check type parameters of generic types (like Array<T>)
|
|
79
145
|
if (node.typeParameters) {
|
|
80
|
-
node.typeParameters.params.forEach((param) => checkTypeNode(param, propName));
|
|
146
|
+
node.typeParameters.params.forEach((param) => checkTypeNode(param, propName, seen));
|
|
81
147
|
}
|
|
82
148
|
break;
|
|
83
149
|
}
|
|
84
150
|
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
85
|
-
checkTypeNode(node.elementType, propName);
|
|
151
|
+
checkTypeNode(node.elementType, propName, seen);
|
|
86
152
|
break;
|
|
87
153
|
case utils_1.AST_NODE_TYPES.TSTypeAnnotation:
|
|
88
|
-
checkTypeNode(node.typeAnnotation, propName);
|
|
154
|
+
checkTypeNode(node.typeAnnotation, propName, seen);
|
|
89
155
|
break;
|
|
90
156
|
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
91
|
-
node.members
|
|
92
|
-
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
93
|
-
const propertyName = member.key.name;
|
|
94
|
-
checkTypeNode(member.typeAnnotation, propertyName);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
157
|
+
checkMembers(node.members, seen, propName);
|
|
97
158
|
break;
|
|
98
159
|
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
99
|
-
|
|
160
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
161
|
+
node.types.forEach((type) => checkTypeNode(type, propName, seen));
|
|
162
|
+
break;
|
|
163
|
+
case utils_1.AST_NODE_TYPES.TSTupleType:
|
|
164
|
+
node.elementTypes.forEach((type) => checkTypeNode(type, propName, seen));
|
|
165
|
+
break;
|
|
166
|
+
// `readonly T[]` wraps its operand rather than replacing it.
|
|
167
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
168
|
+
checkTypeNode(node.typeAnnotation, propName, seen);
|
|
100
169
|
break;
|
|
101
170
|
}
|
|
102
171
|
}
|
|
@@ -109,35 +178,30 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
109
178
|
const wrapperReferences = [];
|
|
110
179
|
return {
|
|
111
180
|
TSTypeAliasDeclaration(node) {
|
|
112
|
-
|
|
181
|
+
declarations.set(node.id.name, node);
|
|
182
|
+
},
|
|
183
|
+
TSInterfaceDeclaration(node) {
|
|
184
|
+
declarations.set(node.id.name, node);
|
|
113
185
|
},
|
|
114
186
|
TSTypeReference(node) {
|
|
115
|
-
const typeName = node.typeName
|
|
116
|
-
if (
|
|
187
|
+
const typeName = simpleTypeNameOf(node.typeName);
|
|
188
|
+
if (typeName &&
|
|
189
|
+
options.functionTypes.includes(typeName) &&
|
|
117
190
|
node.typeParameters?.params[0]) {
|
|
118
191
|
wrapperReferences.push(node);
|
|
119
192
|
}
|
|
120
193
|
},
|
|
121
194
|
'Program:exit'() {
|
|
122
195
|
for (const node of wrapperReferences) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* violation. Checking the node itself lets the non-serializable
|
|
133
|
-
* lookup and the generic descent apply; an unrecognized name simply
|
|
134
|
-
* yields no report, so an imported request type stays silent.
|
|
135
|
-
*/
|
|
136
|
-
checkTypeNode(typeAlias ? typeAlias.typeAnnotation : typeParam);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
checkTypeNode(typeParam);
|
|
140
|
-
}
|
|
196
|
+
/**
|
|
197
|
+
* The payload is handed to `checkTypeNode` whatever its shape: it
|
|
198
|
+
* resolves a reference to a local declaration itself, and a reference
|
|
199
|
+
* that names none is still a type in its own right —
|
|
200
|
+
* `CallableRequest<Timestamp>` is the plainest form of the violation.
|
|
201
|
+
* An unrecognized name yields no report, so an imported request type
|
|
202
|
+
* stays silent.
|
|
203
|
+
*/
|
|
204
|
+
checkTypeNode(node.typeParameters?.params[0]);
|
|
141
205
|
}
|
|
142
206
|
},
|
|
143
207
|
};
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
3
|
-
export declare const fastDeepEqualOverMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
4
|
-
export {};
|
|
2
|
+
export declare const fastDeepEqualOverMicrodiff: TSESLint.RuleModule<"useFastDeepEqual", [], TSESLint.RuleListener>;
|
|
@@ -89,9 +89,6 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
89
89
|
useFastDeepEqual: "What's wrong: This code uses `{{diffName}}(...).length` as a deep equality check.\n" +
|
|
90
90
|
'Why it matters: `{{diffName}}` allocates a full change list (paths, types, values) before you compare it to zero, which hides the boolean intent and wastes memory/time.\n' +
|
|
91
91
|
'How to fix: Call `{{fastEqualName}}(left, right)` for equality (or prefix with `!` for inequality) using the same two arguments instead of counting diff length.',
|
|
92
|
-
addFastDeepEqualImport: "What's wrong: This file checks equality via `{{diffName}}(...).length` but does not import a deep-equality function.\n" +
|
|
93
|
-
'Why it matters: Without `@blumintinc/fast-deep-equal`, equality checks keep building diff entries just to count them, adding overhead and obscuring intent.\n' +
|
|
94
|
-
'How to fix: Add a default import from `@blumintinc/fast-deep-equal` as `{{fastEqualName}}` and use `{{fastEqualName}}(a, b)` for equality checks.',
|
|
95
92
|
},
|
|
96
93
|
},
|
|
97
94
|
defaultOptions: [],
|
|
@@ -13,20 +13,47 @@ const isJsxElement = (node) => {
|
|
|
13
13
|
node.type === utils_1.AST_NODE_TYPES.JSXFragment ||
|
|
14
14
|
node.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer);
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* A type reference's name as dotted segments.
|
|
18
|
+
*
|
|
19
|
+
* A qualified name nests to the LEFT, so `React.JSX.Element` is a
|
|
20
|
+
* TSQualifiedName whose own `left` is another TSQualifiedName. Matching one
|
|
21
|
+
* nesting level at a time therefore recognizes `JSX.Element` but not the
|
|
22
|
+
* React-namespaced spelling of the same type; comparing whole paths recognizes
|
|
23
|
+
* both.
|
|
24
|
+
*/
|
|
25
|
+
const qualifiedSegments = (typeName) => {
|
|
26
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
27
|
+
return [typeName.name];
|
|
28
|
+
}
|
|
29
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
30
|
+
const left = qualifiedSegments(typeName.left);
|
|
31
|
+
return left && [...left, typeName.right.name];
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
/** JSX-producing type paths, written without the optional `React` qualifier. */
|
|
36
|
+
const JSX_TYPE_PATHS = new Set([
|
|
37
|
+
'JSX',
|
|
38
|
+
'JSX.Element',
|
|
39
|
+
'ReactNode',
|
|
40
|
+
'ReactElement',
|
|
41
|
+
]);
|
|
16
42
|
const isJsxReturnType = (node) => {
|
|
17
|
-
if (node.typeAnnotation.type
|
|
18
|
-
|
|
19
|
-
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
20
|
-
return ['JSX', 'ReactNode', 'ReactElement'].includes(typeName.name);
|
|
21
|
-
}
|
|
22
|
-
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
23
|
-
return (typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
24
|
-
typeName.left.name === 'JSX' &&
|
|
25
|
-
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
26
|
-
typeName.right.name === 'Element');
|
|
27
|
-
}
|
|
43
|
+
if (node.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
44
|
+
return false;
|
|
28
45
|
}
|
|
29
|
-
|
|
46
|
+
const segments = qualifiedSegments(node.typeAnnotation.typeName);
|
|
47
|
+
if (!segments) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `React.` re-exports the same declarations, so it carries no meaning for
|
|
52
|
+
* this question. Every other qualifier names a type from a different module
|
|
53
|
+
* and must not match — `Foo.ReactNode` is not React's.
|
|
54
|
+
*/
|
|
55
|
+
const path = segments[0] === 'React' ? segments.slice(1) : segments;
|
|
56
|
+
return path.length > 0 && JSX_TYPE_PATHS.has(path.join('.'));
|
|
30
57
|
};
|
|
31
58
|
const containsJsxInBlockStatement = (node) => {
|
|
32
59
|
const variablesWithJsx = new Set();
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
|
|
3
|
-
export declare const preferFragmentComponent: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
4
|
-
export {};
|
|
2
|
+
export declare const preferFragmentComponent: TSESLint.RuleModule<"preferFragment", [], TSESLint.RuleListener>;
|
|
@@ -121,7 +121,6 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
|
|
|
121
121
|
schema: [],
|
|
122
122
|
messages: {
|
|
123
123
|
preferFragment: 'Prefer Fragment imported from react over {{type}}. Shorthand fragments block props like "key" and mixing fragment styles makes JSX harder to refactor. Import { Fragment } from "react" and wrap the children with <Fragment>...</Fragment> so fragment usage stays explicit.',
|
|
124
|
-
addFragmentImport: "Fragment is used but not imported from react. Without an explicit import the fixer leaves <Fragment> undefined and the React dependency implicit. Add `import { Fragment } from 'react'` alongside your other React imports so the file compiles.",
|
|
125
124
|
},
|
|
126
125
|
},
|
|
127
126
|
defaultOptions: [],
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'preferNextDynamic' | 'addNextDynamicImport' | 'removeUseDynamicImport';
|
|
3
2
|
type Options = [
|
|
4
3
|
{
|
|
5
4
|
useDynamicSources?: string[];
|
|
6
5
|
}?
|
|
7
6
|
];
|
|
8
|
-
export declare const preferNextDynamic: TSESLint.RuleModule<
|
|
7
|
+
export declare const preferNextDynamic: TSESLint.RuleModule<"preferNextDynamic", Options, TSESLint.RuleListener>;
|
|
9
8
|
export {};
|
|
@@ -206,8 +206,6 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
206
206
|
],
|
|
207
207
|
messages: {
|
|
208
208
|
preferNextDynamic: 'Component "{{componentName}}" is created with useDynamic(import(...)), which bypasses Next.js dynamic() handling for client-only components and leaves SSR control to a custom wrapper. Wrap the import in dynamic(() => import(...), { ssr: false }) so Next.js manages code-splitting and disables server rendering safely.',
|
|
209
|
-
addNextDynamicImport: "The auto-fix will replace useDynamic(import(...)) with dynamic(() => import(...), { ssr: false }), which references Next.js's dynamic function. Without importing dynamic from 'next/dynamic', the fixed code will throw a ReferenceError at runtime when the module loads. Add `import dynamic from 'next/dynamic'` at the top of the file to make the dynamic identifier available.",
|
|
210
|
-
removeUseDynamicImport: 'Remove the unused useDynamic import after migrating to dynamic(); leaving the custom hook imported invites accidental reuse and keeps dead code in the bundle.',
|
|
211
209
|
},
|
|
212
210
|
},
|
|
213
211
|
defaultOptions: [{}],
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.115",
|
|
4
|
+
"date": "2026-08-05T18:41:41.153Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-jsx-in-hooks",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1753
|
|
11
|
+
],
|
|
12
|
+
"summary": "detect React-qualified JSX return types (closes #1753)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.114",
|
|
18
|
+
"date": "2026-08-05T18:16:40.596Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-serializable-params",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1751
|
|
25
|
+
],
|
|
26
|
+
"summary": "resolve namespaced names, interfaces and reference chains (closes #1751)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.113",
|
|
4
32
|
"date": "2026-08-05T17:37:08.020Z",
|