@blumintinc/eslint-plugin-blumint 1.20.80 → 1.20.81
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
|
@@ -156,6 +156,44 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
156
156
|
}
|
|
157
157
|
return null;
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* The props type carried by a component annotation on the DECLARATOR —
|
|
161
|
+
* `const C: React.FC<X> = ({ ... }) => ...` — used only when the parameter
|
|
162
|
+
* itself is unannotated (#1620). Gated to FC-shaped annotations so an
|
|
163
|
+
* arbitrary declarator type is never mistaken for a props source; the
|
|
164
|
+
* returned argument runs through the same resolver as a direct annotation,
|
|
165
|
+
* so wrappers like `React.FC<Readonly<XProps>>` resolve for free.
|
|
166
|
+
*/
|
|
167
|
+
const FC_TYPE_NAMES = new Set([
|
|
168
|
+
'FC',
|
|
169
|
+
'FunctionComponent',
|
|
170
|
+
'VFC',
|
|
171
|
+
'VoidFunctionComponent',
|
|
172
|
+
]);
|
|
173
|
+
const propsArgumentOfFcAnnotation = (declarationId) => {
|
|
174
|
+
if (declarationId.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const annotation = declarationId.typeAnnotation?.typeAnnotation;
|
|
178
|
+
if (annotation?.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const { typeName } = annotation;
|
|
182
|
+
const localName = typeName.type === utils_1.AST_NODE_TYPES.Identifier
|
|
183
|
+
? typeName.name
|
|
184
|
+
: typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
185
|
+
typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
186
|
+
typeName.left.name === 'React' &&
|
|
187
|
+
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier
|
|
188
|
+
? typeName.right.name
|
|
189
|
+
: null;
|
|
190
|
+
if (localName === null || !FC_TYPE_NAMES.has(localName)) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const typeArgs = annotation.typeParameters ??
|
|
194
|
+
annotation.typeArguments;
|
|
195
|
+
return typeArgs?.params[0];
|
|
196
|
+
};
|
|
159
197
|
const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used, knownProps) => {
|
|
160
198
|
const spreadTypeProps = spreadTypeToPropNames.get(spreadTypeName);
|
|
161
199
|
if (!spreadTypeProps || spreadTypeProps.size === 0) {
|
|
@@ -769,8 +807,10 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
769
807
|
const param = fn.params[0];
|
|
770
808
|
if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
771
809
|
// Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
|
|
772
|
-
// as the plain `FooProps` annotation
|
|
773
|
-
|
|
810
|
+
// as the plain `FooProps` annotation; an unannotated pattern under an
|
|
811
|
+
// FC-annotated declarator resolves from the annotation's argument.
|
|
812
|
+
const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
|
|
813
|
+
propsArgumentOfFcAnnotation(declaration.id));
|
|
774
814
|
if (typeName) {
|
|
775
815
|
const used = new Set();
|
|
776
816
|
const restUsed = collectUsedFromObjectPattern(param, typeName, used);
|
|
@@ -782,7 +822,8 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
782
822
|
// Identifier param (`props: FooProps`): the destructuring happens in
|
|
783
823
|
// the body. Resolve the Props type (unwrapping generic wrappers) and
|
|
784
824
|
// scan the body for `const { ... } = props`.
|
|
785
|
-
const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation
|
|
825
|
+
const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
|
|
826
|
+
propsArgumentOfFcAnnotation(declaration.id));
|
|
786
827
|
if (!typeName) {
|
|
787
828
|
return;
|
|
788
829
|
}
|
|
@@ -9,6 +9,59 @@ const defaultOptions = [
|
|
|
9
9
|
enforceForRenamedProperties: false,
|
|
10
10
|
},
|
|
11
11
|
];
|
|
12
|
+
/**
|
|
13
|
+
* Names of every class declared anywhere in the file. A purely syntactic rule
|
|
14
|
+
* cannot see an imported class, so same-file declarations are the entire
|
|
15
|
+
* population a type annotation can be resolved against (#1619).
|
|
16
|
+
*/
|
|
17
|
+
function collectClassNames(sourceCode) {
|
|
18
|
+
const names = new Set();
|
|
19
|
+
const stack = [sourceCode.ast];
|
|
20
|
+
while (stack.length > 0) {
|
|
21
|
+
const current = stack.pop();
|
|
22
|
+
if ((current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
23
|
+
current.type === utils_1.AST_NODE_TYPES.ClassExpression) &&
|
|
24
|
+
current.id) {
|
|
25
|
+
names.add(current.id.name);
|
|
26
|
+
}
|
|
27
|
+
for (const key of sourceCode.visitorKeys[current.type] ?? []) {
|
|
28
|
+
const value = current[key];
|
|
29
|
+
const children = Array.isArray(value) ? value : [value];
|
|
30
|
+
for (const child of children) {
|
|
31
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
32
|
+
stack.push(child);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reports whether an identifier's declared type names a class declared in this
|
|
41
|
+
* file — the annotation-carried form of a class instance (`user: User` as a
|
|
42
|
+
* parameter or an annotated variable), which the docs promise the same
|
|
43
|
+
* exemption as a `new User()` initializer (#1619).
|
|
44
|
+
*/
|
|
45
|
+
function annotationNamesFileClass(identifier, classNames) {
|
|
46
|
+
if (!identifier || identifier.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
50
|
+
return (annotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
51
|
+
annotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
52
|
+
classNames.has(annotation.typeName.name));
|
|
53
|
+
}
|
|
54
|
+
const fileClassNames = new WeakMap();
|
|
55
|
+
function classNamesFor(context) {
|
|
56
|
+
const sourceCode = context.getSourceCode();
|
|
57
|
+
const cached = fileClassNames.get(sourceCode.ast);
|
|
58
|
+
if (cached) {
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
const names = collectClassNames(sourceCode);
|
|
62
|
+
fileClassNames.set(sourceCode.ast, names);
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
12
65
|
function isClassInstance(node, context) {
|
|
13
66
|
// Check if node is a MemberExpression
|
|
14
67
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
@@ -22,12 +75,22 @@ function isClassInstance(node, context) {
|
|
|
22
75
|
const variable = object.name;
|
|
23
76
|
const scope = context.getScope();
|
|
24
77
|
const ref = scope.references.find((ref) => ref.identifier.name === variable);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
const def = ref?.resolved?.defs[0];
|
|
79
|
+
if (def?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
80
|
+
const init = def.node.init;
|
|
81
|
+
return (init?.type === utils_1.AST_NODE_TYPES.NewExpression ||
|
|
82
|
+
// `const user: User = getUser();` — the annotation, not the
|
|
83
|
+
// initializer, is what marks the value as a class instance.
|
|
84
|
+
annotationNamesFileClass(def.node.id, classNamesFor(context)));
|
|
85
|
+
}
|
|
86
|
+
// `function greet(user: User)` — a parameter typed with a same-file
|
|
87
|
+
// class is a class instance the initializer-based check cannot see.
|
|
88
|
+
if (def?.type === 'Parameter' &&
|
|
89
|
+
annotationNamesFileClass(def.name, classNamesFor(context))) {
|
|
90
|
+
return true;
|
|
28
91
|
}
|
|
29
92
|
// Check if the identifier refers to a class (not an instance)
|
|
30
|
-
if (
|
|
93
|
+
if (def?.node.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
|
|
31
94
|
return false;
|
|
32
95
|
}
|
|
33
96
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.81",
|
|
4
|
+
"date": "2026-08-02T12:22:56.245Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-unused-props",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1620
|
|
11
|
+
],
|
|
12
|
+
"summary": "resolve the props type from an FC-annotated declarator (closes #1620)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-destructuring-no-class",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1619
|
|
19
|
+
],
|
|
20
|
+
"summary": "recognize annotation-carried class instances (closes #1619)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.20.80",
|
|
4
26
|
"date": "2026-08-02T11:35:36.730Z",
|