@blumintinc/eslint-plugin-blumint 1.20.134 → 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 +403 -9
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +40 -4
- package/lib/rules/enforce-id-capitalization.js +18 -0
- package/lib/rules/enforce-props-argument-name.js +28 -2
- package/lib/rules/enforce-props-naming-consistency.js +28 -2
- package/lib/rules/no-inline-component-prop.js +8 -5
- package/lib/rules/no-object-values-on-strings.js +23 -6
- package/lib/rules/no-passthrough-getters.js +10 -2
- package/lib/rules/no-redundant-annotation-assertion.js +611 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +46 -4
- package/lib/rules/no-useless-usememo-primitives.js +30 -9
- package/lib/rules/no-usememo-for-pass-by-value.js +185 -31
- package/lib/rules/parallelize-async-operations.js +10 -1
- package/lib/rules/prefer-map-over-conditional-dispatch.js +36 -8
- package/lib/utils/fixtureCorpus.d.ts +148 -8
- package/lib/utils/fixtureCorpus.js +207 -10
- package/lib/utils/harvestRuleTesterCases.d.ts +15 -0
- package/lib/utils/harvestRuleTesterCases.js +30 -3
- package/lib/utils/importRemoval.js +35 -6
- package/lib/utils/replacementSegments.d.ts +35 -0
- package/lib/utils/replacementSegments.js +55 -0
- package/package.json +1 -1
- package/release-manifest.json +143 -0
|
@@ -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
|
},
|