@blumintinc/eslint-plugin-blumint 1.20.84 → 1.20.85
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/prefer-spread-over-reassembly.js +109 -14
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -83,6 +83,75 @@ const ELEMENT_CALLBACK_METHODS = new Set([
|
|
|
83
83
|
'flatMap',
|
|
84
84
|
]);
|
|
85
85
|
const ARRAY_TYPE_NAMES = new Set(['Array', 'ReadonlyArray']);
|
|
86
|
+
/**
|
|
87
|
+
* Type operators that are homomorphic over their argument: each one rewrites
|
|
88
|
+
* the modifiers of every member and leaves the key set identical, so a member
|
|
89
|
+
* list read through them describes the wrapped type exactly. `Readonly<{...}>`
|
|
90
|
+
* is the idiomatic spelling of a data record, and refusing to see through it
|
|
91
|
+
* makes the narrowing proof inert on the code it exists to protect (#1643).
|
|
92
|
+
*
|
|
93
|
+
* `Pick`, `Omit`, `Record`, `Exclude` and `Extract` are deliberately absent:
|
|
94
|
+
* they rewrite the key set, and a wrong proof silences a report the rule owes
|
|
95
|
+
* rather than merely failing to find one.
|
|
96
|
+
*/
|
|
97
|
+
const KEY_PRESERVING_TYPE_OPERATORS = new Set([
|
|
98
|
+
'Readonly',
|
|
99
|
+
'Required',
|
|
100
|
+
'Partial',
|
|
101
|
+
]);
|
|
102
|
+
/**
|
|
103
|
+
* Collects every name the file itself binds in a way that can stand in front of
|
|
104
|
+
* a type argument list — a type alias, an interface, a class, an enum, a
|
|
105
|
+
* namespace or an import. A file spelling `Readonly` as one of these is talking
|
|
106
|
+
* about its own declaration rather than the lib utility, so the key-preserving
|
|
107
|
+
* unwrap must not apply to it.
|
|
108
|
+
*
|
|
109
|
+
* The walk covers nested declarations too, since a `type Partial<T>` inside a
|
|
110
|
+
* function body shadows the global just as effectively as a top-level one.
|
|
111
|
+
*/
|
|
112
|
+
function collectLocallyBoundNames(node, names) {
|
|
113
|
+
switch (node.type) {
|
|
114
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
115
|
+
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
|
|
116
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration:
|
|
117
|
+
case utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration:
|
|
118
|
+
names.add(node.id.name);
|
|
119
|
+
break;
|
|
120
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
121
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
122
|
+
if (node.id) {
|
|
123
|
+
names.add(node.id.name);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case utils_1.AST_NODE_TYPES.TSModuleDeclaration:
|
|
127
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
128
|
+
names.add(node.id.name);
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case utils_1.AST_NODE_TYPES.ImportSpecifier:
|
|
132
|
+
case utils_1.AST_NODE_TYPES.ImportDefaultSpecifier:
|
|
133
|
+
case utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier:
|
|
134
|
+
names.add(node.local.name);
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
for (const key of Object.keys(node)) {
|
|
140
|
+
if (key === 'parent')
|
|
141
|
+
continue;
|
|
142
|
+
const value = node[key];
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
for (const child of value) {
|
|
145
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
146
|
+
collectLocallyBoundNames(child, names);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (value && typeof value === 'object' && 'type' in value) {
|
|
151
|
+
collectLocallyBoundNames(value, names);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
86
155
|
/**
|
|
87
156
|
* Resolves `Promise<T>` to `T`, leaving anything else as it stands, which is
|
|
88
157
|
* what `await` does to the type of the expression it operates on.
|
|
@@ -172,32 +241,43 @@ function findLocalTypeDeclaration(program, name) {
|
|
|
172
241
|
* Enumerates every property name a type node declares, or null when the member
|
|
173
242
|
* list cannot be established with certainty.
|
|
174
243
|
*
|
|
175
|
-
* Only an unambiguous, fully written-out member list qualifies
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
244
|
+
* Only an unambiguous, fully written-out member list qualifies, reached either
|
|
245
|
+
* directly or through the key-preserving operators in
|
|
246
|
+
* {@link KEY_PRESERVING_TYPE_OPERATORS}. A union, an intersection, a mapped or
|
|
247
|
+
* conditional type, any other generic instantiation and an interface with an
|
|
248
|
+
* `extends` clause all describe a member set assembled elsewhere, so none of
|
|
249
|
+
* them can prove anything here.
|
|
179
250
|
*/
|
|
180
|
-
function memberNamesOf(typeNode,
|
|
251
|
+
function memberNamesOf(typeNode, scope, seen = new Set()) {
|
|
181
252
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
182
253
|
return namesOfMembers(typeNode.members);
|
|
183
254
|
}
|
|
184
255
|
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
185
|
-
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier
|
|
186
|
-
typeNode.typeParameters) {
|
|
256
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
187
257
|
return null;
|
|
188
258
|
}
|
|
189
259
|
const name = typeNode.typeName.name;
|
|
260
|
+
if (typeNode.typeParameters) {
|
|
261
|
+
// An arity other than one is not the lib utility this name spells, so
|
|
262
|
+
// nothing about the wrapped member list follows from it.
|
|
263
|
+
if (!KEY_PRESERVING_TYPE_OPERATORS.has(name) ||
|
|
264
|
+
typeNode.typeParameters.params.length !== 1 ||
|
|
265
|
+
scope.isLocallyBound(name)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return memberNamesOf(typeNode.typeParameters.params[0], scope, seen);
|
|
269
|
+
}
|
|
190
270
|
// A self-referential alias (`type T = T`) would otherwise recur forever.
|
|
191
271
|
if (seen.has(name)) {
|
|
192
272
|
return null;
|
|
193
273
|
}
|
|
194
274
|
seen.add(name);
|
|
195
|
-
const declaration = findLocalTypeDeclaration(program, name);
|
|
275
|
+
const declaration = findLocalTypeDeclaration(scope.program, name);
|
|
196
276
|
if (!declaration || declaration.typeParameters) {
|
|
197
277
|
return null;
|
|
198
278
|
}
|
|
199
279
|
if (declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
200
|
-
return memberNamesOf(declaration.typeAnnotation,
|
|
280
|
+
return memberNamesOf(declaration.typeAnnotation, scope, seen);
|
|
201
281
|
}
|
|
202
282
|
if (declaration.extends && declaration.extends.length > 0) {
|
|
203
283
|
return null;
|
|
@@ -627,6 +707,20 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
|
|
|
627
707
|
const minFields = options?.minFields ?? DEFAULT_MIN_FIELDS;
|
|
628
708
|
const sourceCode = context.getSourceCode();
|
|
629
709
|
const program = sourceCode.ast;
|
|
710
|
+
// The scan walks the whole file, so it is deferred until a key-preserving
|
|
711
|
+
// operator actually turns up in a position the proof depends on — most
|
|
712
|
+
// files never reach it.
|
|
713
|
+
let locallyBoundNames = null;
|
|
714
|
+
const typeScope = {
|
|
715
|
+
program,
|
|
716
|
+
isLocallyBound: (name) => {
|
|
717
|
+
if (!locallyBoundNames) {
|
|
718
|
+
locallyBoundNames = new Set();
|
|
719
|
+
collectLocallyBoundNames(program, locallyBoundNames);
|
|
720
|
+
}
|
|
721
|
+
return locallyBoundNames.has(name);
|
|
722
|
+
},
|
|
723
|
+
};
|
|
630
724
|
/**
|
|
631
725
|
* Walks an expression toward its syntactic root and returns the type node
|
|
632
726
|
* that root declares, mirroring the receiver trace in
|
|
@@ -753,7 +847,7 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
|
|
|
753
847
|
return null;
|
|
754
848
|
}
|
|
755
849
|
const elementType = arrayElementTypeOf(receiverType);
|
|
756
|
-
return elementType ? memberNamesOf(elementType,
|
|
850
|
+
return elementType ? memberNamesOf(elementType, typeScope) : null;
|
|
757
851
|
}
|
|
758
852
|
/**
|
|
759
853
|
* Reports whether the destructured pick is provably a PROPER subset of the
|
|
@@ -763,15 +857,16 @@ exports.preferSpreadOverReassembly = (0, createRule_1.createRule)({
|
|
|
763
857
|
*
|
|
764
858
|
* The proof runs in the safe direction only. A member set that matches the
|
|
765
859
|
* pick exactly is exhaustive, so the rewrite is behavior-preserving and the
|
|
766
|
-
* rule still reports; a type it cannot resolve — imported,
|
|
767
|
-
*
|
|
768
|
-
*
|
|
860
|
+
* rule still reports; a type it cannot resolve — imported, a union, an
|
|
861
|
+
* index signature, or an instantiation of anything but a key-preserving
|
|
862
|
+
* operator — yields no proof and the rule likewise still reports. Silence
|
|
863
|
+
* is reserved for the case where the widening is demonstrated.
|
|
769
864
|
*/
|
|
770
865
|
function isProvablyNarrowingPick(fn, param, destructuredNames) {
|
|
771
866
|
// An explicit annotation overrides whatever the call site would imply,
|
|
772
867
|
// so the contextual route is consulted only in its absence.
|
|
773
868
|
const memberNames = param.typeAnnotation
|
|
774
|
-
? memberNamesOf(param.typeAnnotation.typeAnnotation,
|
|
869
|
+
? memberNamesOf(param.typeAnnotation.typeAnnotation, typeScope)
|
|
775
870
|
: contextualElementMemberNames(fn);
|
|
776
871
|
if (!memberNames || memberNames.size <= destructuredNames.length) {
|
|
777
872
|
return false;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.85",
|
|
4
|
+
"date": "2026-08-03T03:21:29.711Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-spread-over-reassembly",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1643
|
|
11
|
+
],
|
|
12
|
+
"summary": "read the narrowing proof through Readonly (closes #1643)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.84",
|
|
4
18
|
"date": "2026-08-03T02:56:38.740Z",
|