@blumintinc/eslint-plugin-blumint 1.20.135 → 1.20.136
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/consistent-callback-naming.js +16 -0
- package/lib/rules/enforce-assert-safe-object-key.js +72 -9
- package/lib/rules/enforce-props-argument-name.js +28 -2
- package/lib/rules/enforce-props-naming-consistency.js +28 -2
- package/lib/rules/no-redundant-annotation-assertion.js +611 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +46 -4
- package/package.json +1 -1
- package/release-manifest.json +57 -0
package/lib/index.js
CHANGED
|
@@ -607,11 +607,27 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
607
607
|
globalVar.references.forEach((ref) => references.add(ref));
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
|
+
// A binding that leaves the module is one end of a cross-file
|
|
611
|
+
// contract. Renaming `export const handleClick` to `click` strands
|
|
612
|
+
// every `import { handleClick }` with TS2724, and a single-file fixer
|
|
613
|
+
// cannot reach those importers — the same reasoning that already
|
|
614
|
+
// withholds the JSX prop rename and the destructured one, which is
|
|
615
|
+
// where `isExportedBinding` was first needed. The violation still
|
|
616
|
+
// reports; only the rename is withheld.
|
|
617
|
+
const declaredVariable = context
|
|
618
|
+
.getDeclaredVariables(node)
|
|
619
|
+
.find((v) => v.identifiers.includes(node.id));
|
|
620
|
+
const leavesModule = declaredVariable
|
|
621
|
+
? isExportedBinding(declaredVariable)
|
|
622
|
+
: isExportedDeclaration(node);
|
|
610
623
|
context.report({
|
|
611
624
|
node,
|
|
612
625
|
messageId: 'callbackFunctionPrefix',
|
|
613
626
|
data: { functionName },
|
|
614
627
|
fix(fixer) {
|
|
628
|
+
if (leavesModule) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
615
631
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
616
632
|
const newName = stripHandlePrefix(functionName);
|
|
617
633
|
// `const handleDelete = fn` would become `const delete = fn`,
|
|
@@ -256,6 +256,54 @@ function isNumericCall(node) {
|
|
|
256
256
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
257
257
|
callee.object.name === 'Math');
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The property names `assertSafe` exists to reject. A key that provably cannot
|
|
261
|
+
* spell one of these cannot reach the prototype surface, which is the entire
|
|
262
|
+
* hazard — so proving it is what earns an exemption, the same standard the
|
|
263
|
+
* numeric analysis already meets.
|
|
264
|
+
*/
|
|
265
|
+
const PROTOTYPE_REACHING_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
266
|
+
/**
|
|
267
|
+
* Whether a template's FIXED text still leaves room to spell `target`.
|
|
268
|
+
*
|
|
269
|
+
* The producible set is `q0 + * + q1 + * + … + * + qN`, each `*` an arbitrary
|
|
270
|
+
* substitution. `target` is producible iff it starts with `q0`, ends with `qN`,
|
|
271
|
+
* and the interior quasis occur in order in between without overlapping. So
|
|
272
|
+
* `` `user-${id}` `` can never be `__proto__` (no such prefix) while
|
|
273
|
+
* `` `__pro${x}` `` can — with `x` = `'to__'`, which resolves to
|
|
274
|
+
* `Object.prototype` at runtime.
|
|
275
|
+
*
|
|
276
|
+
* Interior quasis are matched greedily from the left. That is sufficient
|
|
277
|
+
* because they are fixed strings: taking the earliest occurrence never consumes
|
|
278
|
+
* a character a later quasi needed, so no backtracking can succeed where the
|
|
279
|
+
* greedy pass fails.
|
|
280
|
+
*
|
|
281
|
+
* A template with no substitutions produces exactly one string and is a static
|
|
282
|
+
* key like any other string literal, so it is never treated as reaching.
|
|
283
|
+
*/
|
|
284
|
+
function templateCanSpell(quasis, target) {
|
|
285
|
+
if (quasis.length < 2) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const first = quasis[0];
|
|
289
|
+
const last = quasis[quasis.length - 1];
|
|
290
|
+
if (!target.startsWith(first) || !target.endsWith(last)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const limit = target.length - last.length;
|
|
294
|
+
let cursor = first.length;
|
|
295
|
+
if (cursor > limit) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
for (const middle of quasis.slice(1, -1)) {
|
|
299
|
+
const at = target.indexOf(middle, cursor);
|
|
300
|
+
if (at < 0 || at + middle.length > limit) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
cursor = at + middle.length;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
259
307
|
/**
|
|
260
308
|
* A `: number` annotation on a binding name. Parameters and variable
|
|
261
309
|
* declarators are the bindings that carry one, and TypeScript checks every
|
|
@@ -1133,19 +1181,34 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1133
1181
|
if (isLikelyArray) {
|
|
1134
1182
|
return;
|
|
1135
1183
|
}
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1184
|
+
// A template whose every substitution is provably numeric can only
|
|
1185
|
+
// widen into digits, and no dangerous property name is the string
|
|
1186
|
+
// form of a number — the same proof the identifier path accepts.
|
|
1187
|
+
const canWidenToText = property.expressions.some((expr) => !isStaticallyNumeric(expr));
|
|
1188
|
+
const quasis = property.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
1189
|
+
const reachesPrototype = canWidenToText &&
|
|
1190
|
+
PROTOTYPE_REACHING_KEYS.some((key) => templateCanSpell(quasis, key));
|
|
1191
|
+
// Fixed text on either side of the substitution can rule a property
|
|
1192
|
+
// name out — `user-${id}` is never `__proto__` — and the rule skips
|
|
1193
|
+
// a key it can prove harmless. What it must NOT do is assume that:
|
|
1194
|
+
// `__pro${x}` carries fixed text too and still reaches the
|
|
1195
|
+
// prototype (#1880).
|
|
1196
|
+
if (!reachesPrototype) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// `${id}` alone is the whole key, so the remedy names the inner
|
|
1200
|
+
// expression and the fix wraps it directly. A template carrying
|
|
1201
|
+
// fixed text has no such inner key — the string it builds is the
|
|
1202
|
+
// key — so that whole template is what gets wrapped, which is the
|
|
1203
|
+
// shape the docs show for `assertSafe(`${id}_suffix`)`.
|
|
1138
1204
|
const isSimpleVarInterpolation = property.expressions.length === 1 &&
|
|
1139
1205
|
property.quasis.length === 2 &&
|
|
1140
1206
|
property.quasis[0].value.raw === '' &&
|
|
1141
1207
|
property.quasis[1].value.raw === '';
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const expr = property.expressions[0];
|
|
1147
|
-
const exprText = context.sourceCode.getText(expr);
|
|
1148
|
-
reportWrittenKey(written, property, exprText);
|
|
1208
|
+
const unwrapped = isSimpleVarInterpolation
|
|
1209
|
+
? property.expressions[0]
|
|
1210
|
+
: property;
|
|
1211
|
+
reportWrittenKey(written, property, context.sourceCode.getText(unwrapped));
|
|
1149
1212
|
return;
|
|
1150
1213
|
}
|
|
1151
1214
|
// Check for direct variable usage (identifiers)
|
|
@@ -340,6 +340,33 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
340
340
|
}
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* The member name a `this.<x>` access reads, whatever its spelling.
|
|
345
|
+
*
|
|
346
|
+
* Keying the check on the dot spelling alone left `this['settings']`
|
|
347
|
+
* invisible, so the rename shipped and stranded it — the class no longer had
|
|
348
|
+
* the member the getter reads (#1881). A computed access with a static
|
|
349
|
+
* string is the SAME member as the dot form, and the fixer cannot rewrite it
|
|
350
|
+
* either, so it has to count. `null` marks a genuinely dynamic key, which
|
|
351
|
+
* names no member statically.
|
|
352
|
+
*/
|
|
353
|
+
function staticMemberName(node) {
|
|
354
|
+
if (!node.computed) {
|
|
355
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
356
|
+
? node.property.name
|
|
357
|
+
: null;
|
|
358
|
+
}
|
|
359
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
360
|
+
typeof node.property.value === 'string') {
|
|
361
|
+
return node.property.value;
|
|
362
|
+
}
|
|
363
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
364
|
+
node.property.expressions.length === 0 &&
|
|
365
|
+
node.property.quasis.length === 1) {
|
|
366
|
+
return node.property.quasis[0].value.cooked;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
343
370
|
// Determine whether renaming a constructor parameter property is unsafe to
|
|
344
371
|
// autofix. A parameter property (`private readonly foo: T`) creates BOTH a
|
|
345
372
|
// constructor-local binding and a `this.foo` class field, so a
|
|
@@ -355,8 +382,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
355
382
|
}
|
|
356
383
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
357
384
|
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
358
|
-
node
|
|
359
|
-
node.property.name === name) {
|
|
385
|
+
staticMemberName(node) === name) {
|
|
360
386
|
unsafe = true;
|
|
361
387
|
return;
|
|
362
388
|
}
|
|
@@ -42,6 +42,33 @@ const getEnclosingClass = (node) => {
|
|
|
42
42
|
* cannot be resolved through scope analysis, the fix is withheld whenever the
|
|
43
43
|
* name occurs anywhere in the class other than at its declaration.
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* The member name a `this.<x>` access reads, whatever its spelling.
|
|
47
|
+
*
|
|
48
|
+
* Keying the check on the dot spelling alone left `this['settings']` invisible,
|
|
49
|
+
* so the rename shipped and stranded it — the class no longer had the member the
|
|
50
|
+
* getter reads (#1882, the sibling of #1881). A computed access with a static
|
|
51
|
+
* string is the SAME member as the dot form, and the fixer cannot rewrite it
|
|
52
|
+
* either, so it has to count. `null` marks a genuinely dynamic key, which names
|
|
53
|
+
* no member statically and therefore strands nothing.
|
|
54
|
+
*/
|
|
55
|
+
const staticMemberName = (node) => {
|
|
56
|
+
if (!node.computed) {
|
|
57
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
58
|
+
? node.property.name
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
62
|
+
typeof node.property.value === 'string') {
|
|
63
|
+
return node.property.value;
|
|
64
|
+
}
|
|
65
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
66
|
+
node.property.expressions.length === 0 &&
|
|
67
|
+
node.property.quasis.length === 1) {
|
|
68
|
+
return node.property.quasis[0].value.cooked;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
45
72
|
const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
|
|
46
73
|
let unsafe = false;
|
|
47
74
|
const visit = (node) => {
|
|
@@ -50,8 +77,7 @@ const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
|
|
|
50
77
|
}
|
|
51
78
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
52
79
|
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
53
|
-
node
|
|
54
|
-
node.property.name === name) {
|
|
80
|
+
staticMemberName(node) === name) {
|
|
55
81
|
unsafe = true;
|
|
56
82
|
return;
|
|
57
83
|
}
|
|
@@ -77,12 +77,12 @@ function isTraversalBoundary(node) {
|
|
|
77
77
|
node.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
78
78
|
node.type === utils_1.AST_NODE_TYPES.ClassExpression);
|
|
79
79
|
}
|
|
80
|
-
function recordReturnStatement(node,
|
|
80
|
+
function recordReturnStatement(node, assertionSites) {
|
|
81
81
|
if (!node.argument)
|
|
82
82
|
return;
|
|
83
83
|
const assertion = extractAssertionTypeNode(node.argument);
|
|
84
84
|
if (assertion)
|
|
85
|
-
|
|
85
|
+
assertionSites.push({ assertion, expression: node.argument });
|
|
86
86
|
}
|
|
87
87
|
function addChildNodesToStack(node, stack) {
|
|
88
88
|
const keys = visitor_keys_1.visitorKeys[node.type];
|
|
@@ -103,22 +103,38 @@ function addChildNodesToStack(node, stack) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
function collectReturnArguments(body) {
|
|
107
|
+
const args = [];
|
|
108
|
+
const stack = [...body.body];
|
|
109
|
+
while (stack.length) {
|
|
110
|
+
const current = stack.pop();
|
|
111
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
112
|
+
if (current.argument)
|
|
113
|
+
args.push(current.argument);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (isTraversalBoundary(current))
|
|
117
|
+
continue;
|
|
118
|
+
addChildNodesToStack(current, stack);
|
|
119
|
+
}
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
106
122
|
function collectReturnInfo(body) {
|
|
107
|
-
const
|
|
123
|
+
const assertionSites = [];
|
|
108
124
|
let returnCount = 0;
|
|
109
125
|
const stack = [...body.body];
|
|
110
126
|
while (stack.length) {
|
|
111
127
|
const current = stack.pop();
|
|
112
128
|
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
113
129
|
returnCount += 1;
|
|
114
|
-
recordReturnStatement(current,
|
|
130
|
+
recordReturnStatement(current, assertionSites);
|
|
115
131
|
continue;
|
|
116
132
|
}
|
|
117
133
|
if (isTraversalBoundary(current))
|
|
118
134
|
continue;
|
|
119
135
|
addChildNodesToStack(current, stack);
|
|
120
136
|
}
|
|
121
|
-
return {
|
|
137
|
+
return { assertionSites, returnCount };
|
|
122
138
|
}
|
|
123
139
|
function findTypeAnnotationStart(typeAnnotation, sourceCode) {
|
|
124
140
|
const start = typeAnnotation.range[0];
|
|
@@ -176,6 +192,64 @@ function unwrapAlias(type, checker) {
|
|
|
176
192
|
}
|
|
177
193
|
return type;
|
|
178
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Readonly-ness reaches a property symbol by two disjoint routes, and a key
|
|
197
|
+
* built from only one of them equates shapes that differ:
|
|
198
|
+
*
|
|
199
|
+
* - A property *written* `readonly` (interface member, type-alias member, class
|
|
200
|
+
* field) carries the modifier on its declaration and no check flag.
|
|
201
|
+
* - A property *synthesized* as readonly — an `as const` object literal, a
|
|
202
|
+
* `Readonly<T>` mapped type — has no declaration modifier and carries
|
|
203
|
+
* `CheckFlags.Readonly` instead.
|
|
204
|
+
*
|
|
205
|
+
* Reading only declarations makes `as const` compare equal to a mutable
|
|
206
|
+
* annotation, and deleting that annotation changes the value's type (see #1883).
|
|
207
|
+
*
|
|
208
|
+
* `getCheckFlags` is not published in every TypeScript release's type
|
|
209
|
+
* definitions, so it is reached through a guarded lookup: where it is absent the
|
|
210
|
+
* key falls back to declaration modifiers alone, which is exactly the
|
|
211
|
+
* information available without it. The enum is dereferenced here rather than at
|
|
212
|
+
* module scope because the plugin barrel loads every rule eagerly and a missing
|
|
213
|
+
* compiler export at module scope fails the whole plugin, not just this rule.
|
|
214
|
+
*/
|
|
215
|
+
function isReadonlyProperty(prop) {
|
|
216
|
+
const compiler = ts;
|
|
217
|
+
const readonlyCheckFlag = compiler.CheckFlags?.Readonly;
|
|
218
|
+
if (typeof compiler.getCheckFlags === 'function' && readonlyCheckFlag) {
|
|
219
|
+
if ((compiler.getCheckFlags(prop) & readonlyCheckFlag) !== 0)
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
// A getter with no setter is readonly by shape rather than by modifier or
|
|
223
|
+
// check flag, so neither route above sees it — `{ get x(): number }` carries
|
|
224
|
+
// `getCheckFlags` 0 and `getCombinedModifierFlags` 0, and would otherwise
|
|
225
|
+
// format identically to a mutable `{ x: number }` (#1887).
|
|
226
|
+
const flags = prop.getFlags();
|
|
227
|
+
if ((flags & ts.SymbolFlags.Accessor) !== 0 &&
|
|
228
|
+
(flags & ts.SymbolFlags.SetAccessor) === 0) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return (prop.declarations ?? []).some((declaration) => (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Readonly) !==
|
|
232
|
+
0);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The index signatures a type declares, with their readonly-ness.
|
|
236
|
+
*
|
|
237
|
+
* They belong in the structural key for the same reason the properties do: an
|
|
238
|
+
* index-signature-only type otherwise keys to the empty string, so every such
|
|
239
|
+
* type compares equal to every other. Readonly-ness in particular does not
|
|
240
|
+
* affect bidirectional assignability of an index signature, so a readonly one
|
|
241
|
+
* would match a mutable annotation and removing that annotation ships TS2542
|
|
242
|
+
* (#1887).
|
|
243
|
+
*/
|
|
244
|
+
function getFormattedIndexSignatures(type, checker) {
|
|
245
|
+
const compiler = checker;
|
|
246
|
+
if (typeof compiler.getIndexInfosOfType !== 'function')
|
|
247
|
+
return [];
|
|
248
|
+
return compiler
|
|
249
|
+
.getIndexInfosOfType(type)
|
|
250
|
+
.map((info) => `${info.isReadonly ? 'readonly ' : ''}[${typeText(info.keyType, checker)}]:${typeText(unwrapAlias(info.type, checker), checker)}`)
|
|
251
|
+
.sort();
|
|
252
|
+
}
|
|
179
253
|
function formatPropertySignature(prop, parentType, checker) {
|
|
180
254
|
const declaration = prop.valueDeclaration ??
|
|
181
255
|
prop.declarations?.[0] ??
|
|
@@ -186,7 +260,8 @@ function formatPropertySignature(prop, parentType, checker) {
|
|
|
186
260
|
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
187
261
|
const text = typeText(unwrapAlias(propType, checker), checker);
|
|
188
262
|
const isOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
189
|
-
|
|
263
|
+
const readonlyPrefix = isReadonlyProperty(prop) ? 'readonly ' : '';
|
|
264
|
+
return `${readonlyPrefix}${prop.getName()}${isOptional ? '?' : ''}:${text}`;
|
|
190
265
|
}
|
|
191
266
|
function getFormattedTypeProperties(type, checker) {
|
|
192
267
|
return checker
|
|
@@ -205,7 +280,8 @@ function structuralKey(type, checker) {
|
|
|
205
280
|
const apparent = checker.getApparentType(type);
|
|
206
281
|
const properties = getFormattedTypeProperties(apparent, checker);
|
|
207
282
|
const signatures = getFormattedCallSignatures(apparent, checker);
|
|
208
|
-
|
|
283
|
+
const indexes = getFormattedIndexSignatures(apparent, checker);
|
|
284
|
+
return `${properties.join('|')}::${signatures.join('|')}::${indexes.join('|')}`;
|
|
209
285
|
}
|
|
210
286
|
function getComparableType(typeNode, checker, services) {
|
|
211
287
|
const tsNode = services.esTreeNodeToTSNodeMap.get(typeNode);
|
|
@@ -297,11 +373,11 @@ function haveMatchingTypes(annotation, assertion, checker, services) {
|
|
|
297
373
|
}
|
|
298
374
|
return selectMatchingTypeRepresentation(representations);
|
|
299
375
|
}
|
|
300
|
-
function
|
|
376
|
+
function getReturnAssertionSite(node) {
|
|
301
377
|
const value = node.type === utils_1.AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
302
378
|
const body = value.body;
|
|
303
379
|
if (body?.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
304
|
-
const {
|
|
380
|
+
const { assertionSites, returnCount } = collectReturnInfo(body);
|
|
305
381
|
// Skip functions with multiple returns because different branches can assert different types.
|
|
306
382
|
if (returnCount !== 1)
|
|
307
383
|
return null;
|
|
@@ -311,11 +387,426 @@ function getReturnAssertion(node) {
|
|
|
311
387
|
if (lastStatement?.type !== utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
312
388
|
return null;
|
|
313
389
|
}
|
|
314
|
-
return
|
|
390
|
+
return assertionSites[0] ?? null;
|
|
315
391
|
}
|
|
316
392
|
if (!body)
|
|
317
393
|
return null;
|
|
318
|
-
|
|
394
|
+
const assertion = extractAssertionTypeNode(body);
|
|
395
|
+
return assertion ? { assertion, expression: body } : null;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The identifiers that name `node`, in the AST. A self-reference inside the
|
|
399
|
+
* function's own return expression resolves to one of these, whichever shape
|
|
400
|
+
* the function is written in: a declaration's own name, a named function
|
|
401
|
+
* expression's name, the variable or property an anonymous function is assigned
|
|
402
|
+
* to, or a method's key.
|
|
403
|
+
*/
|
|
404
|
+
/**
|
|
405
|
+
* The key a member is declared under, whatever its spelling.
|
|
406
|
+
*
|
|
407
|
+
* A computed key whose key expression is a literal names exactly the member a
|
|
408
|
+
* dotted or bracketed read resolves to — `{ ['build']() {} }` declares `build`
|
|
409
|
+
* — and the reader side already resolves both spellings. Refusing the computed
|
|
410
|
+
* form left such a candidate with NO owner at all, so it could not be found
|
|
411
|
+
* circular even by a direct self-reference (#1888). A genuinely dynamic key
|
|
412
|
+
* names no member statically and still yields nothing.
|
|
413
|
+
*/
|
|
414
|
+
function memberKeyNameNode(node) {
|
|
415
|
+
if (!node.computed)
|
|
416
|
+
return node.key;
|
|
417
|
+
return node.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
418
|
+
(typeof node.key.value === 'string' || typeof node.key.value === 'number')
|
|
419
|
+
? node.key
|
|
420
|
+
: null;
|
|
421
|
+
}
|
|
422
|
+
/** Every binding a destructuring pattern introduces. */
|
|
423
|
+
function patternBindingNames(pattern) {
|
|
424
|
+
const names = [];
|
|
425
|
+
const visit = (node) => {
|
|
426
|
+
if (!node)
|
|
427
|
+
return;
|
|
428
|
+
switch (node.type) {
|
|
429
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
430
|
+
names.push(node);
|
|
431
|
+
return;
|
|
432
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
433
|
+
for (const property of node.properties) {
|
|
434
|
+
visit(property.type === utils_1.AST_NODE_TYPES.Property
|
|
435
|
+
? property.value
|
|
436
|
+
: property.argument);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
440
|
+
for (const element of node.elements)
|
|
441
|
+
visit(element);
|
|
442
|
+
return;
|
|
443
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
444
|
+
visit(node.left);
|
|
445
|
+
return;
|
|
446
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
447
|
+
visit(node.argument);
|
|
448
|
+
return;
|
|
449
|
+
default:
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
visit(pattern);
|
|
454
|
+
return names;
|
|
455
|
+
}
|
|
456
|
+
/** A pattern carries its annotation on the pattern node itself. */
|
|
457
|
+
function patternTypeAnnotation(pattern) {
|
|
458
|
+
return pattern
|
|
459
|
+
.typeAnnotation;
|
|
460
|
+
}
|
|
461
|
+
function ownerNameNodes(node) {
|
|
462
|
+
if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
463
|
+
const key = memberKeyNameNode(node);
|
|
464
|
+
return key ? [key] : [];
|
|
465
|
+
}
|
|
466
|
+
const names = [];
|
|
467
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression && node.id) {
|
|
468
|
+
names.push(node.id);
|
|
469
|
+
}
|
|
470
|
+
const parent = node.parent;
|
|
471
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
472
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
473
|
+
names.push(parent.id);
|
|
474
|
+
}
|
|
475
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
476
|
+
parent?.type === utils_1.AST_NODE_TYPES.Property) {
|
|
477
|
+
const key = memberKeyNameNode(parent);
|
|
478
|
+
if (key)
|
|
479
|
+
names.push(key);
|
|
480
|
+
}
|
|
481
|
+
return names;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* A binding is identified by the declarations behind its symbol rather than by
|
|
485
|
+
* the symbol object. Reading `obj.build` yields a *clone* of the symbol declared
|
|
486
|
+
* by `build: () => {}` — widening an object literal's type rebuilds its property
|
|
487
|
+
* symbols — so symbol identity reports a self-reference as unrelated. The clone
|
|
488
|
+
* keeps the original declaration, which therefore is the stable key.
|
|
489
|
+
*/
|
|
490
|
+
function declarationsOfSymbol(symbol) {
|
|
491
|
+
return symbol.declarations ?? [];
|
|
492
|
+
}
|
|
493
|
+
function declaredAt(nodes, checker, services) {
|
|
494
|
+
const declarations = new Set();
|
|
495
|
+
for (const node of nodes) {
|
|
496
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
497
|
+
const symbol = tsNode ? checker.getSymbolAtLocation(tsNode) : undefined;
|
|
498
|
+
if (!symbol)
|
|
499
|
+
continue;
|
|
500
|
+
for (const declaration of declarationsOfSymbol(symbol)) {
|
|
501
|
+
declarations.add(declaration);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return declarations;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Every binding the returned expression reads. Resolving through the checker
|
|
508
|
+
* rather than matching identifier text keeps a shadowing inner declaration of
|
|
509
|
+
* the same name from reading as a self-reference.
|
|
510
|
+
*/
|
|
511
|
+
function referencedDeclarationsOf(expression, checker, services, pruneTypedFunctions = false) {
|
|
512
|
+
const declarations = new Set();
|
|
513
|
+
const root = services.esTreeNodeToTSNodeMap.get(expression);
|
|
514
|
+
if (!root)
|
|
515
|
+
return declarations;
|
|
516
|
+
/**
|
|
517
|
+
* Only a VALUE read can make return-type inference circular.
|
|
518
|
+
*
|
|
519
|
+
* The returned expression subtree contains the assertion's own type node —
|
|
520
|
+
* `<Status>{…}` and `expr as Status` both carry `Status` inside it — so
|
|
521
|
+
* resolving every identifier would count that type reference as a
|
|
522
|
+
* self-reference whenever a binding and a type share a name
|
|
523
|
+
* (`type Status = …; const Status = (): Status => <Status>{…}`). TypeScript
|
|
524
|
+
* resolves a type annotation without needing any function's return type, so
|
|
525
|
+
* such a reference can never close the cycle.
|
|
526
|
+
*
|
|
527
|
+
* `typeof f` is the one type-position spelling that reads a VALUE, and it does
|
|
528
|
+
* depend on `f`'s return type, so it counts. Checking it before the general
|
|
529
|
+
* type-node test is what lets it through — a type query is itself a type node.
|
|
530
|
+
*/
|
|
531
|
+
const readsAValue = (identifier) => {
|
|
532
|
+
let current = identifier.parent;
|
|
533
|
+
while (current) {
|
|
534
|
+
if (ts.isTypeQueryNode(current))
|
|
535
|
+
return true;
|
|
536
|
+
if (ts.isTypeNode(current))
|
|
537
|
+
return false;
|
|
538
|
+
if (current === root)
|
|
539
|
+
return true;
|
|
540
|
+
current = current.parent;
|
|
541
|
+
}
|
|
542
|
+
return true;
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* A property read spells its name as an identifier in `obj.build` and as a
|
|
546
|
+
* string in `obj['build']`, and both resolve to the same property symbol —
|
|
547
|
+
* so testing for an identifier alone recognises only one of two spellings of
|
|
548
|
+
* one reference. The literal is only a name where it indexes something;
|
|
549
|
+
* elsewhere a string is data and resolves to nothing worth following.
|
|
550
|
+
*/
|
|
551
|
+
const isNameNode = (tsNode) => {
|
|
552
|
+
if (ts.isIdentifier(tsNode))
|
|
553
|
+
return true;
|
|
554
|
+
const parent = tsNode.parent;
|
|
555
|
+
return (ts.isStringLiteralLike(tsNode) &&
|
|
556
|
+
Boolean(parent) &&
|
|
557
|
+
ts.isElementAccessExpression(parent) &&
|
|
558
|
+
parent.argumentExpression === tsNode);
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* The name a declaration gives itself is not a read of it. Walking an object
|
|
562
|
+
* literal or a class body reaches the names of its own members, and counting
|
|
563
|
+
* those makes the literal look like it reads every function it contains —
|
|
564
|
+
* enough to close a cycle that TypeScript does not have, since it resolves an
|
|
565
|
+
* object literal's properties one at a time rather than as a whole.
|
|
566
|
+
*
|
|
567
|
+
* A shorthand property is the exception in both directions: `{ build }`
|
|
568
|
+
* declares a property and reads a binding, and it is the read that matters.
|
|
569
|
+
*/
|
|
570
|
+
const isDeclarationName = (tsNode) => {
|
|
571
|
+
const parent = tsNode.parent;
|
|
572
|
+
if (!parent)
|
|
573
|
+
return false;
|
|
574
|
+
if (ts.isShorthandPropertyAssignment(parent) ||
|
|
575
|
+
ts.isPropertyAccessExpression(parent) ||
|
|
576
|
+
ts.isElementAccessExpression(parent) ||
|
|
577
|
+
ts.isQualifiedName(parent)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return parent.name === tsNode;
|
|
581
|
+
};
|
|
582
|
+
const symbolAt = (tsNode) => {
|
|
583
|
+
const parent = tsNode.parent;
|
|
584
|
+
// A shorthand property's own identifier resolves to the property, so the
|
|
585
|
+
// binding it abbreviates has to be asked for by name.
|
|
586
|
+
if (parent && ts.isShorthandPropertyAssignment(parent)) {
|
|
587
|
+
return checker.getShorthandAssignmentValueSymbol(parent);
|
|
588
|
+
}
|
|
589
|
+
return checker.getSymbolAtLocation(tsNode);
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* A nested function that writes its own return type down answers without
|
|
593
|
+
* consulting anything, so nothing beneath it can be a link in a cycle. It is
|
|
594
|
+
* already its own graph node, related to its own dependencies, and reaching
|
|
595
|
+
* through it would attribute its body to the binding that merely contains it
|
|
596
|
+
* — `const cache = { get: (): Q => build() }` does not make `cache` depend on
|
|
597
|
+
* `build`, because `get` is typed by its annotation.
|
|
598
|
+
*/
|
|
599
|
+
const answersWithoutInference = (tsNode) => pruneTypedFunctions &&
|
|
600
|
+
(ts.isArrowFunction(tsNode) ||
|
|
601
|
+
ts.isFunctionExpression(tsNode) ||
|
|
602
|
+
ts.isFunctionDeclaration(tsNode) ||
|
|
603
|
+
ts.isMethodDeclaration(tsNode)) &&
|
|
604
|
+
tsNode.type !== undefined;
|
|
605
|
+
const visit = (tsNode) => {
|
|
606
|
+
if (isNameNode(tsNode) &&
|
|
607
|
+
!isDeclarationName(tsNode) &&
|
|
608
|
+
readsAValue(tsNode)) {
|
|
609
|
+
const symbol = symbolAt(tsNode);
|
|
610
|
+
if (symbol) {
|
|
611
|
+
for (const declaration of declarationsOfSymbol(symbol)) {
|
|
612
|
+
declarations.add(declaration);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (tsNode !== root && answersWithoutInference(tsNode)) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
ts.forEachChild(tsNode, visit);
|
|
620
|
+
};
|
|
621
|
+
visit(root);
|
|
622
|
+
return declarations;
|
|
623
|
+
}
|
|
624
|
+
function isFunctionLike(node) {
|
|
625
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
626
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
627
|
+
}
|
|
628
|
+
/** The expressions a function's own return type is inferred from. */
|
|
629
|
+
function returnExpressionsOf(node) {
|
|
630
|
+
const value = node.type === utils_1.AST_NODE_TYPES.MethodDefinition ? node.value : node;
|
|
631
|
+
const body = value.body;
|
|
632
|
+
if (!body)
|
|
633
|
+
return [];
|
|
634
|
+
return body.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
635
|
+
? collectReturnArguments(body)
|
|
636
|
+
: [body];
|
|
637
|
+
}
|
|
638
|
+
function returnTypeAnnotationOf(node) {
|
|
639
|
+
return node.type === utils_1.AST_NODE_TYPES.MethodDefinition
|
|
640
|
+
? node.value.returnType
|
|
641
|
+
: node.returnType;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Whether the binding this function is assigned to declares its own type. A
|
|
645
|
+
* contextually typed function is not inferred from its body — `const f: () => Q
|
|
646
|
+
* = () => g()` types `f` from the annotation regardless of what `g` returns —
|
|
647
|
+
* so it cannot carry a cycle even without a return annotation of its own.
|
|
648
|
+
*/
|
|
649
|
+
function ownerCarriesTypeAnnotation(node) {
|
|
650
|
+
const parent = node.parent;
|
|
651
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
652
|
+
return (parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
653
|
+
Boolean(parent.id.typeAnnotation));
|
|
654
|
+
}
|
|
655
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
656
|
+
return Boolean(parent.typeAnnotation);
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
function addInferenceEdges(graph, owners, dependencies, needsInference) {
|
|
661
|
+
for (const owner of owners) {
|
|
662
|
+
const existing = graph.get(owner);
|
|
663
|
+
if (!existing) {
|
|
664
|
+
graph.set(owner, {
|
|
665
|
+
needsInference,
|
|
666
|
+
dependencies: new Set(dependencies),
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
existing.needsInference ||= needsInference;
|
|
671
|
+
for (const dependency of dependencies) {
|
|
672
|
+
existing.dependencies.add(dependency);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function unionReferences(expressions, checker, services, pruneTypedFunctions = false) {
|
|
677
|
+
const references = new Set();
|
|
678
|
+
for (const expression of expressions) {
|
|
679
|
+
for (const declaration of referencedDeclarationsOf(expression, checker, services, pruneTypedFunctions)) {
|
|
680
|
+
references.add(declaration);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return references;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Every function-like node in the file, related to the declarations its return
|
|
687
|
+
* type is inferred from. Functions the rule is not reporting on belong in the
|
|
688
|
+
* graph too: an unannotated helper is precisely the kind of node a cycle runs
|
|
689
|
+
* through, and it is invisible to a relation drawn between candidates only.
|
|
690
|
+
*/
|
|
691
|
+
function addFunctionNodes(graph, functions, checker, services) {
|
|
692
|
+
for (const node of functions) {
|
|
693
|
+
const owners = declaredAt(ownerNameNodes(node), checker, services);
|
|
694
|
+
if (owners.size === 0)
|
|
695
|
+
continue;
|
|
696
|
+
// A written-down type normally answers without consulting anything, which
|
|
697
|
+
// is what lets the walk stop at it — unless the annotation itself reads a
|
|
698
|
+
// value through `typeof`, which is a dependency like any other and can
|
|
699
|
+
// close the cycle it was supposed to break (#1888). Such a node keeps
|
|
700
|
+
// needing inference, and contributes what its annotation reads.
|
|
701
|
+
const annotationReads = annotationValueReads(node, checker, services);
|
|
702
|
+
const writtenDown = Boolean(returnTypeAnnotationOf(node)) || ownerCarriesTypeAnnotation(node);
|
|
703
|
+
const dependencies = unionReferences(returnExpressionsOf(node), checker, services);
|
|
704
|
+
for (const declaration of annotationReads)
|
|
705
|
+
dependencies.add(declaration);
|
|
706
|
+
addInferenceEdges(graph, owners, dependencies, !writtenDown || annotationReads.size > 0);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The values this function's declared type reads through `typeof`.
|
|
711
|
+
*
|
|
712
|
+
* `referencedDeclarationsOf` already treats a type query as a value read and
|
|
713
|
+
* every other type position as not one, so handing it the annotation yields
|
|
714
|
+
* exactly the `typeof` operands and nothing else.
|
|
715
|
+
*/
|
|
716
|
+
function annotationValueReads(node, checker, services) {
|
|
717
|
+
const reads = new Set();
|
|
718
|
+
const parent = node.parent;
|
|
719
|
+
const annotations = [
|
|
720
|
+
returnTypeAnnotationOf(node),
|
|
721
|
+
parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
722
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
723
|
+
? parent.id.typeAnnotation
|
|
724
|
+
: undefined,
|
|
725
|
+
parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition
|
|
726
|
+
? parent.typeAnnotation
|
|
727
|
+
: undefined,
|
|
728
|
+
];
|
|
729
|
+
for (const annotation of annotations) {
|
|
730
|
+
if (!annotation)
|
|
731
|
+
continue;
|
|
732
|
+
for (const declaration of referencedDeclarationsOf(annotation.typeAnnotation, checker, services)) {
|
|
733
|
+
reads.add(declaration);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return reads;
|
|
737
|
+
}
|
|
738
|
+
function addValueNodes(graph, values, checker, services) {
|
|
739
|
+
for (const { name, init } of values) {
|
|
740
|
+
const owners = declaredAt([name], checker, services);
|
|
741
|
+
if (owners.size === 0)
|
|
742
|
+
continue;
|
|
743
|
+
addInferenceEdges(graph, owners, unionReferences([init], checker, services, true), true);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Whether following what this declaration's type is inferred from leads back to
|
|
748
|
+
* the declaration itself.
|
|
749
|
+
*
|
|
750
|
+
* The walk stops at any declaration whose type is written down: an annotated
|
|
751
|
+
* function, an annotated binding, an ambient declaration. Such a node answers
|
|
752
|
+
* the question it is asked without consulting anything further, which is
|
|
753
|
+
* exactly what breaks a cycle — TypeScript's own error says a type is inferred
|
|
754
|
+
* "directly or indirectly" from itself, and every link in that indirection has
|
|
755
|
+
* to be a type it must infer.
|
|
756
|
+
*/
|
|
757
|
+
function reachesOwnDeclaration(owners, dependencies, graph) {
|
|
758
|
+
const frontier = [...dependencies];
|
|
759
|
+
const seen = new Set();
|
|
760
|
+
while (frontier.length) {
|
|
761
|
+
const current = frontier.pop();
|
|
762
|
+
if (owners.has(current))
|
|
763
|
+
return true;
|
|
764
|
+
if (seen.has(current))
|
|
765
|
+
continue;
|
|
766
|
+
seen.add(current);
|
|
767
|
+
const node = graph.get(current);
|
|
768
|
+
if (!node?.needsInference)
|
|
769
|
+
continue;
|
|
770
|
+
for (const dependency of node.dependencies)
|
|
771
|
+
frontier.push(dependency);
|
|
772
|
+
}
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* The candidates whose annotation must survive because removing it would make
|
|
777
|
+
* the function's return type circular.
|
|
778
|
+
*
|
|
779
|
+
* A function that reaches itself through what its return type is inferred from
|
|
780
|
+
* can only be typed by its annotation: dropping it leaves TypeScript inferring
|
|
781
|
+
* the return type from an expression whose type depends on that same return
|
|
782
|
+
* type (TS7023/TS7022), or — where the assertion pins enough of the shape to
|
|
783
|
+
* break the cycle — silently widening a member to `any`. That circularity is
|
|
784
|
+
* invisible to the equality test that proves redundancy, because the equality
|
|
785
|
+
* only holds *while* the annotation is there.
|
|
786
|
+
*
|
|
787
|
+
* The reach is transitive rather than a hop between candidates, because a cycle
|
|
788
|
+
* closes through whatever happens to lie on it: an unannotated helper, an
|
|
789
|
+
* object holding a callback, a plain alias. Those are not candidates — they
|
|
790
|
+
* have no annotation to remove — yet they relay a dependency, and a relation
|
|
791
|
+
* drawn candidate-to-candidate cannot see them.
|
|
792
|
+
*
|
|
793
|
+
* Every candidate is treated as needing inference, since this rule's fixes ship
|
|
794
|
+
* as one batch and every annotation in it goes together. That over-approximates
|
|
795
|
+
* — declining one member of a cycle would free the rest — and the surplus costs
|
|
796
|
+
* a missed report rather than a broken fix. It also subsumes the direct
|
|
797
|
+
* self-reference case, which is a cycle of length one.
|
|
798
|
+
*/
|
|
799
|
+
function findCircularReturnCandidates(candidates, graph) {
|
|
800
|
+
for (const candidate of candidates) {
|
|
801
|
+
addInferenceEdges(graph, candidate.owners, candidate.references, true);
|
|
802
|
+
}
|
|
803
|
+
const circular = new Set();
|
|
804
|
+
for (const candidate of candidates) {
|
|
805
|
+
if (reachesOwnDeclaration(candidate.owners, candidate.references, graph)) {
|
|
806
|
+
circular.add(candidate.site);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return circular;
|
|
319
810
|
}
|
|
320
811
|
exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
321
812
|
name: 'no-redundant-annotation-assertion',
|
|
@@ -355,6 +846,17 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
355
846
|
* re-reports the debt.
|
|
356
847
|
*/
|
|
357
848
|
const sites = [];
|
|
849
|
+
/** Return-position sites, kept with the symbols that decide circularity. */
|
|
850
|
+
const returnCandidates = [];
|
|
851
|
+
/**
|
|
852
|
+
* The nodes the inference graph is built from, gathered as AST during the
|
|
853
|
+
* traversal and resolved to symbols only if a return-position candidate
|
|
854
|
+
* turns up. Resolving every binding in the file up front would charge a
|
|
855
|
+
* type-checker query per declaration to answer a question no file without a
|
|
856
|
+
* return annotation ever asks.
|
|
857
|
+
*/
|
|
858
|
+
const functionLikeNodes = [];
|
|
859
|
+
const inferredValueDeclarations = [];
|
|
358
860
|
/**
|
|
359
861
|
* Suppression is applied to reports after a rule emits them, so a suppressed
|
|
360
862
|
* site keeps its annotation while losing its fix. Counting its removal
|
|
@@ -365,12 +867,32 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
365
867
|
function collectIfRedundant(annotation, assertion, reportNode, fixerTarget) {
|
|
366
868
|
const matchingType = haveMatchingTypes(annotation.typeAnnotation, assertion, checker, parserServices);
|
|
367
869
|
if (!matchingType)
|
|
368
|
-
return;
|
|
369
|
-
|
|
870
|
+
return null;
|
|
871
|
+
const site = {
|
|
370
872
|
reportNode,
|
|
371
873
|
removal: annotationRemovalRange(fixerTarget, sourceCode),
|
|
372
874
|
matchingType,
|
|
373
|
-
}
|
|
875
|
+
};
|
|
876
|
+
sites.push(site);
|
|
877
|
+
return site;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* The one gate every return-position visitor passes through, so a declined
|
|
881
|
+
* return annotation loses both its report and its fix whichever of the four
|
|
882
|
+
* function shapes carries it.
|
|
883
|
+
*/
|
|
884
|
+
function collectReturnSite(node, annotation, reportNode) {
|
|
885
|
+
const assertionSite = getReturnAssertionSite(node);
|
|
886
|
+
if (!assertionSite)
|
|
887
|
+
return;
|
|
888
|
+
const owners = declaredAt(ownerNameNodes(node), checker, parserServices);
|
|
889
|
+
const references = referencedDeclarationsOf(assertionSite.expression, checker, parserServices);
|
|
890
|
+
// Whether the annotation is load-bearing is decided at `Program:exit`:
|
|
891
|
+
// the cycle can run through a function elsewhere in the file, and every
|
|
892
|
+
// annotation in it goes in the same batched fix.
|
|
893
|
+
const site = collectIfRedundant(annotation, assertionSite.assertion, reportNode, annotation);
|
|
894
|
+
if (site)
|
|
895
|
+
returnCandidates.push({ site, owners, references });
|
|
374
896
|
}
|
|
375
897
|
/**
|
|
376
898
|
* The sites whose fixes actually ship. A site is excluded when its report
|
|
@@ -384,15 +906,36 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
384
906
|
* vetoing the rest: orphanhood grows monotonically with the removed set, so
|
|
385
907
|
* a site that cannot be planned alone can only ever poison the batch.
|
|
386
908
|
*/
|
|
387
|
-
function selectFixableSites() {
|
|
388
|
-
return
|
|
909
|
+
function selectFixableSites(candidates) {
|
|
910
|
+
return candidates.filter((site) => !isReportSuppressed(site.reportNode) &&
|
|
389
911
|
(0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.removal]) !== null);
|
|
390
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* The file's inference graph, built only where a return annotation is at
|
|
915
|
+
* stake. Both the functions and the value bindings go in: a cycle runs
|
|
916
|
+
* through whichever of them happens to lie on it.
|
|
917
|
+
*/
|
|
918
|
+
function buildInferenceGraph() {
|
|
919
|
+
const graph = new Map();
|
|
920
|
+
addFunctionNodes(graph, functionLikeNodes, checker, parserServices);
|
|
921
|
+
addValueNodes(graph, inferredValueDeclarations, checker, parserServices);
|
|
922
|
+
return graph;
|
|
923
|
+
}
|
|
391
924
|
return {
|
|
392
925
|
'Program:exit'() {
|
|
393
926
|
if (sites.length === 0)
|
|
394
927
|
return;
|
|
395
|
-
|
|
928
|
+
// Recursion only becomes circular once every annotation on the cycle is
|
|
929
|
+
// gone, which is exactly what this rule's single batched fix does, so
|
|
930
|
+
// the check has to see the whole batch — and the whole file, because
|
|
931
|
+
// what closes the cycle need not be a candidate at all.
|
|
932
|
+
const circular = returnCandidates.length > 0
|
|
933
|
+
? findCircularReturnCandidates(returnCandidates, buildInferenceGraph())
|
|
934
|
+
: new Set();
|
|
935
|
+
const reportable = sites.filter((site) => !circular.has(site));
|
|
936
|
+
if (reportable.length === 0)
|
|
937
|
+
return;
|
|
938
|
+
const fixable = selectFixableSites(reportable);
|
|
396
939
|
const removals = fixable.map((site) => site.removal);
|
|
397
940
|
// One plan over every surviving removal: an import referenced solely by
|
|
398
941
|
// annotations that all go in this pass is orphaned by their union, even
|
|
@@ -404,7 +947,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
404
947
|
// others that the import's orphanhood was judged against. The rest
|
|
405
948
|
// report without a fixer; the carrier's pass already resolves them.
|
|
406
949
|
const carrier = importRanges ? fixable[0] : undefined;
|
|
407
|
-
for (const site of
|
|
950
|
+
for (const site of reportable) {
|
|
408
951
|
context.report({
|
|
409
952
|
node: site.reportNode,
|
|
410
953
|
messageId: 'redundantAnnotationAndAssertion',
|
|
@@ -419,10 +962,29 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
419
962
|
}
|
|
420
963
|
},
|
|
421
964
|
VariableDeclarator(node) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
965
|
+
// A pattern introduces bindings that relay a dependency exactly as a
|
|
966
|
+
// plain one does — `const { run } = { run: () => build() }` is the
|
|
967
|
+
// destructured spelling of a shape already covered — but the early
|
|
968
|
+
// return below skipped registering them at all (#1888). Each name gets
|
|
969
|
+
// the whole initializer, which is the same over-approximation a plain
|
|
970
|
+
// binding carries.
|
|
971
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
972
|
+
if (node.init && !patternTypeAnnotation(node.id)) {
|
|
973
|
+
for (const name of patternBindingNames(node.id)) {
|
|
974
|
+
inferredValueDeclarations.push({ name, init: node.init });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
// A binding whose initializer is a function is already related to what
|
|
980
|
+
// it reads by that function's own graph node, and by its return
|
|
981
|
+
// expressions rather than its whole body.
|
|
982
|
+
if (node.init &&
|
|
983
|
+
!node.id.typeAnnotation &&
|
|
984
|
+
!isFunctionLike(node.init)) {
|
|
985
|
+
inferredValueDeclarations.push({ name: node.id, init: node.init });
|
|
986
|
+
}
|
|
987
|
+
if (!node.id.typeAnnotation || node.id.optional || node.definite) {
|
|
426
988
|
return;
|
|
427
989
|
}
|
|
428
990
|
const assertionType = extractAssertionTypeNode(node.init);
|
|
@@ -430,7 +992,26 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
430
992
|
return;
|
|
431
993
|
collectIfRedundant(node.id.typeAnnotation, assertionType, node.id, node.id.typeAnnotation);
|
|
432
994
|
},
|
|
995
|
+
// A parameter default relays a dependency the same way a body `const`
|
|
996
|
+
// does — `function helper(seed = build())` is the parameter spelling of
|
|
997
|
+
// `const seed = build()` — and nothing visited parameters, so the link was
|
|
998
|
+
// invisible (#1888). An annotated parameter is typed without consulting
|
|
999
|
+
// its default, so it breaks the chain like any written-down type.
|
|
1000
|
+
AssignmentPattern(node) {
|
|
1001
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.Property ||
|
|
1002
|
+
node.left.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
1003
|
+
node.left.typeAnnotation) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
inferredValueDeclarations.push({ name: node.left, init: node.right });
|
|
1007
|
+
},
|
|
433
1008
|
PropertyDefinition(node) {
|
|
1009
|
+
if (node.value &&
|
|
1010
|
+
!node.typeAnnotation &&
|
|
1011
|
+
!node.computed &&
|
|
1012
|
+
!isFunctionLike(node.value)) {
|
|
1013
|
+
inferredValueDeclarations.push({ name: node.key, init: node.value });
|
|
1014
|
+
}
|
|
434
1015
|
if (!node.typeAnnotation ||
|
|
435
1016
|
!node.value ||
|
|
436
1017
|
node.optional ||
|
|
@@ -442,39 +1023,31 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
|
|
|
442
1023
|
collectIfRedundant(node.typeAnnotation, assertionType, node.key, node.typeAnnotation);
|
|
443
1024
|
},
|
|
444
1025
|
FunctionDeclaration(node) {
|
|
1026
|
+
functionLikeNodes.push(node);
|
|
445
1027
|
if (!node.returnType)
|
|
446
1028
|
return;
|
|
447
|
-
|
|
448
|
-
if (!assertionType)
|
|
449
|
-
return;
|
|
450
|
-
collectIfRedundant(node.returnType, assertionType, node.id ?? node, node.returnType);
|
|
1029
|
+
collectReturnSite(node, node.returnType, node.id ?? node);
|
|
451
1030
|
},
|
|
452
1031
|
FunctionExpression(node) {
|
|
453
1032
|
if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
454
1033
|
return;
|
|
455
1034
|
}
|
|
1035
|
+
functionLikeNodes.push(node);
|
|
456
1036
|
if (!node.returnType)
|
|
457
1037
|
return;
|
|
458
|
-
|
|
459
|
-
if (!assertionType)
|
|
460
|
-
return;
|
|
461
|
-
collectIfRedundant(node.returnType, assertionType, node, node.returnType);
|
|
1038
|
+
collectReturnSite(node, node.returnType, node);
|
|
462
1039
|
},
|
|
463
1040
|
ArrowFunctionExpression(node) {
|
|
1041
|
+
functionLikeNodes.push(node);
|
|
464
1042
|
if (!node.returnType)
|
|
465
1043
|
return;
|
|
466
|
-
|
|
467
|
-
if (!assertionType)
|
|
468
|
-
return;
|
|
469
|
-
collectIfRedundant(node.returnType, assertionType, node, node.returnType);
|
|
1044
|
+
collectReturnSite(node, node.returnType, node);
|
|
470
1045
|
},
|
|
471
1046
|
MethodDefinition(node) {
|
|
1047
|
+
functionLikeNodes.push(node);
|
|
472
1048
|
if (!node.value.returnType)
|
|
473
1049
|
return;
|
|
474
|
-
|
|
475
|
-
if (!assertionType)
|
|
476
|
-
return;
|
|
477
|
-
collectIfRedundant(node.value.returnType, assertionType, node.key, node.value.returnType);
|
|
1050
|
+
collectReturnSite(node, node.value.returnType, node.key);
|
|
478
1051
|
},
|
|
479
1052
|
};
|
|
480
1053
|
},
|
|
@@ -248,6 +248,27 @@ function declaredTypeNode(node) {
|
|
|
248
248
|
function checksExcessProperties(typeNode) {
|
|
249
249
|
return typeNode !== null && !UNCHECKED_ANNOTATION_TYPES.has(typeNode.type);
|
|
250
250
|
}
|
|
251
|
+
/** `as const` declares no members of its own, so it pins no name. */
|
|
252
|
+
function isConstAssertionType(typeNode) {
|
|
253
|
+
return (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
254
|
+
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
255
|
+
typeNode.typeName.name === 'const');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* An `as T` / `<T>` whose target type declares members the value must still
|
|
259
|
+
* have. Renaming one of them makes the assertion uncomparable (TS2352), so the
|
|
260
|
+
* name is dictated by `T` rather than chosen by the author — the same reasoning
|
|
261
|
+
* the annotation arms use, applied to the check an assertion actually performs.
|
|
262
|
+
*/
|
|
263
|
+
function isCheckedTypeAssertion(node) {
|
|
264
|
+
if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
265
|
+
node.type !== utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const { typeAnnotation } = node;
|
|
269
|
+
return (checksExcessProperties(typeAnnotation) &&
|
|
270
|
+
!isConstAssertionType(typeAnnotation));
|
|
271
|
+
}
|
|
251
272
|
function isFunctionNode(node) {
|
|
252
273
|
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
253
274
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
@@ -310,10 +331,18 @@ const EXPRESSION_ASSERTION_TYPES = new Set([
|
|
|
310
331
|
* members, and climbs THROUGH assertion wrappers, which change no runtime value
|
|
311
332
|
* and so cannot detach a literal from the declared type it is assigned to
|
|
312
333
|
* (#1597) — `enforce-object-literal-as-const` ships in the same recommended
|
|
313
|
-
* config and appends `as const` to exactly these literals by `--fix`.
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
334
|
+
* config and appends `as const` to exactly these literals by `--fix`.
|
|
335
|
+
*
|
|
336
|
+
* An `as T` is itself a signal when `T` declares members. It is true that an
|
|
337
|
+
* `as` clause does not reject EXCESS members the way an annotation does
|
|
338
|
+
* (`{ orderBy, extra } as Q` compiles; `const c: Q = { orderBy, extra }` is
|
|
339
|
+
* TS2322) — but the operation being gated is a RENAME, which removes a REQUIRED
|
|
340
|
+
* member, and that an assertion does reject: `{ order } as Q` is TS2352 when `Q`
|
|
341
|
+
* requires `orderBy`. Reasoning from excess properties alone made the rule
|
|
342
|
+
* demand a rename that does not compile (#1885). `as const` is the exception
|
|
343
|
+
* and stays transparent, declaring no members of its own.
|
|
344
|
+
*
|
|
345
|
+
* The walk stops at anything else.
|
|
317
346
|
*/
|
|
318
347
|
function hasConformanceSignal(node) {
|
|
319
348
|
let current = node;
|
|
@@ -330,6 +359,19 @@ function hasConformanceSignal(node) {
|
|
|
330
359
|
checksExcessProperties(parent.typeAnnotation)) {
|
|
331
360
|
return true;
|
|
332
361
|
}
|
|
362
|
+
// An `as T` DOES pin the member names it requires, even though it does not
|
|
363
|
+
// reject excess ones. The two are different checks: excess-property
|
|
364
|
+
// checking is what an annotation adds, but a RENAME removes a REQUIRED
|
|
365
|
+
// member, and that breaks comparability — `{ beta: 1 } as T` is TS2352 when
|
|
366
|
+
// `T` requires `alpha`. Reasoning from excess properties alone made the rule
|
|
367
|
+
// demand a rename that does not compile (#1885). `as const` stays
|
|
368
|
+
// transparent: it declares no members of its own, and
|
|
369
|
+
// `enforce-object-literal-as-const` appends one to exactly these literals.
|
|
370
|
+
if (isCheckedTypeAssertion(parent) &&
|
|
371
|
+
parent
|
|
372
|
+
.expression === current) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
333
375
|
if (EXPRESSION_ASSERTION_TYPES.has(parent.type)) {
|
|
334
376
|
current = parent;
|
|
335
377
|
continue;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.136",
|
|
4
|
+
"date": "2026-08-08T14:34:06.547Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "consistent-callback-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1878
|
|
11
|
+
],
|
|
12
|
+
"summary": "withhold the rename on exported bindings"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-assert-safe-object-key",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1880
|
|
19
|
+
],
|
|
20
|
+
"summary": "check that a template's fixed text rules out a dangerous key (closes #1880)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-props-argument-name",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1881
|
|
27
|
+
],
|
|
28
|
+
"summary": "withhold the parameter-property rename on every static spelling of the member (closes #1881)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "enforce-props-naming-consistency",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1882
|
|
35
|
+
],
|
|
36
|
+
"summary": "withhold the parameter-property rename on every static spelling of the member (closes #1882)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no-redundant-annotation-assertion",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
1883,
|
|
43
|
+
1886,
|
|
44
|
+
1887,
|
|
45
|
+
1888
|
|
46
|
+
],
|
|
47
|
+
"summary": "make every spelling of a relay a node in the inference graph (closes #1888); compare index signatures, and read readonly through the accessor route (closes #1887); follow the circular-return check transitively (closes #1886); keep annotations that are load-bearing (closes #1883)"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "no-unnecessary-verb-suffix",
|
|
51
|
+
"changeType": "fix",
|
|
52
|
+
"issues": [
|
|
53
|
+
1885
|
|
54
|
+
],
|
|
55
|
+
"summary": "treat an `as T` as the conformance signal it is (closes #1885)"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
2
59
|
{
|
|
3
60
|
"version": "1.20.135",
|
|
4
61
|
"date": "2026-08-08T08:06:35.865Z",
|