@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2
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/dist/src/analysis.js +108 -10
- package/dist/src/augmentations.js +62 -18
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +98 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +172 -40
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +57 -10
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +38 -4
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +146 -11
- package/src/augmentations.ts +76 -22
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +128 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +15 -0
- package/src/findings/completion.ts +222 -46
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +104 -15
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +48 -3
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isForInStatement, isForOfStatement, isFunctionLikeDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAssignment, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isSourceFile, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableDeclarationList, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
8
|
+
import { SignatureKind } from "@typescript/native/sync";
|
|
9
|
+
import { exportsLocally, implicitReads, importBindings, } from "./import-bindings.js";
|
|
8
10
|
import { isParameterProperty, nodeKey } from "./inventory.js";
|
|
9
11
|
import { positionKey, renderPosition } from "./position.js";
|
|
12
|
+
import { importRef } from "./ref.js";
|
|
10
13
|
import { isAnswered, UNANSWERED } from "./query.js";
|
|
11
14
|
import { signatureOf } from "./signatures.js";
|
|
12
15
|
import { spellsNoBinding } from "./spelled-names.js";
|
|
@@ -304,6 +307,33 @@ function readsBinding(use, declared, ids) {
|
|
|
304
307
|
const own = ids.get(declared);
|
|
305
308
|
return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
|
|
306
309
|
}
|
|
310
|
+
/** Whether a name is spelled with a leading underscore, which declares it unread. */
|
|
311
|
+
function declaredUnread(name) {
|
|
312
|
+
return name.text.startsWith("_");
|
|
313
|
+
}
|
|
314
|
+
/** A file's leading comments, where the compiler reads its `@ts-check` and `@ts-nocheck` pragmas. */
|
|
315
|
+
const LEADING_COMMENTS = /^(?:#![^\n]*)?(?:\s|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/u;
|
|
316
|
+
/** A comment of the leading comments, a line comment's text after its slashes captured. */
|
|
317
|
+
const LEADING_COMMENT = /\/\*[\s\S]*?\*\/|\/\/\/?([^\n]*)/gu;
|
|
318
|
+
/**
|
|
319
|
+
* Whether a file's leading comments hold one pragma: as the compiler reads it, a line
|
|
320
|
+
* comment that starts with the pragma's name, in any case, followed by nothing or a space.
|
|
321
|
+
*/
|
|
322
|
+
function hasPragma(leading, pragma) {
|
|
323
|
+
return [...leading.matchAll(LEADING_COMMENT)].some(([, text]) => text !== undefined && pragma.test(text));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Whether the compiler checks a file, so its rules report what they find there: a
|
|
327
|
+
* JavaScript file only under `checkJs` or a `@ts-check` pragma, and no file with a
|
|
328
|
+
* `@ts-nocheck` pragma.
|
|
329
|
+
*/
|
|
330
|
+
function checkedByTheCompiler(file, checkJs) {
|
|
331
|
+
const leading = LEADING_COMMENTS.exec(file.text)?.[0] ?? "";
|
|
332
|
+
if (hasPragma(leading, /^\s*@ts-nocheck(?:\s|$)/iu)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return (!/\.[cm]?jsx?$/u.test(file.fileName) || checkJs || hasPragma(leading, /^\s*@ts-check(?:\s|$)/iu));
|
|
336
|
+
}
|
|
307
337
|
/**
|
|
308
338
|
* The parts of one project's own declarations. `references` are the project's own
|
|
309
339
|
* references, which say which declarations a discarded call reaches, so the checker is
|
|
@@ -319,7 +349,12 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
319
349
|
const constructions = new Set();
|
|
320
350
|
const sites = [];
|
|
321
351
|
const contextual = [];
|
|
352
|
+
const imports = [];
|
|
353
|
+
const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
|
|
354
|
+
const options = project.program.getCompilerOptions();
|
|
355
|
+
const jsx = options;
|
|
322
356
|
for (const file of files) {
|
|
357
|
+
const checked = checkedByTheCompiler(file, options.checkJs === true);
|
|
323
358
|
// A parameter property's own parts stay with the parameter when its member goes.
|
|
324
359
|
const declarationAt = (node) => isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
|
|
325
360
|
// A part no declaration holds belongs to the file, which is judged while the file is live.
|
|
@@ -338,6 +373,7 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
338
373
|
values: [],
|
|
339
374
|
catches: [],
|
|
340
375
|
blocks: [],
|
|
376
|
+
loops: [],
|
|
341
377
|
unreachable: [],
|
|
342
378
|
cases: [],
|
|
343
379
|
};
|
|
@@ -433,6 +469,15 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
433
469
|
if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
|
|
434
470
|
walk.blocks.push(node);
|
|
435
471
|
}
|
|
472
|
+
const loopHolder = (isForOfStatement(node) || isForInStatement(node)) && silenced === 0
|
|
473
|
+
? holderOf(node)
|
|
474
|
+
: undefined;
|
|
475
|
+
if (loopHolder !== undefined &&
|
|
476
|
+
(isForOfStatement(node) || isForInStatement(node)) &&
|
|
477
|
+
isVariableDeclarationList(node.initializer) &&
|
|
478
|
+
(node.initializer.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0) {
|
|
479
|
+
walk.loops.push({ loop: node, holder: loopHolder });
|
|
480
|
+
}
|
|
436
481
|
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
437
482
|
statementsOf(node.statements);
|
|
438
483
|
return;
|
|
@@ -462,7 +507,37 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
462
507
|
for (const store of stores) {
|
|
463
508
|
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
464
509
|
}
|
|
510
|
+
const loopVariables = walk.loops.map(({ loop, holder }) => {
|
|
511
|
+
const list = loop.initializer;
|
|
512
|
+
// A loop must bind a name, so one spelled declared unread is no subject.
|
|
513
|
+
const names = list.declarations
|
|
514
|
+
.flatMap((declaration) => boundNames(declaration.name))
|
|
515
|
+
.filter((name) => !declaredUnread(name));
|
|
516
|
+
const uses = identifiersNaming(loop.statement, new Set(names.map((one) => one.text)));
|
|
517
|
+
asked.push(...names, ...uses);
|
|
518
|
+
return { names, uses, holder };
|
|
519
|
+
});
|
|
520
|
+
const bindings = fileId === undefined ? [] : importBindings(file);
|
|
521
|
+
const bound = new Set(bindings);
|
|
522
|
+
const bindingUses = bindings.length === 0
|
|
523
|
+
? []
|
|
524
|
+
: identifiersNaming(file, new Set(bindings.map((one) => one.text))).filter((use) => !bound.has(use));
|
|
525
|
+
asked.push(...bindings, ...bindingUses);
|
|
465
526
|
const ids = resolvedIds(project, [...new Set(asked)]);
|
|
527
|
+
const implicit = bindings.length === 0 ? new Set() : implicitReads(file, jsx);
|
|
528
|
+
for (const binding of bindings) {
|
|
529
|
+
const fileRef = fileId === undefined ? undefined : refs.get(fileId);
|
|
530
|
+
if (fileId === undefined || fileRef === undefined) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
imports.push({
|
|
534
|
+
ref: importRef(fileRef, binding.text),
|
|
535
|
+
name: binding.text,
|
|
536
|
+
position: spanOf(file, targetRoot, binding),
|
|
537
|
+
used: implicit.has(binding.text) ||
|
|
538
|
+
bindingUses.some((use) => use.text === binding.text && (exportsLocally(use) || readsBinding(use, binding, ids))),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
466
541
|
for (const one of walk.declared) {
|
|
467
542
|
const constructor = isConstructorDeclaration(one.node);
|
|
468
543
|
if (!constructor) {
|
|
@@ -476,7 +551,10 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
476
551
|
});
|
|
477
552
|
const reader = readers.get(one.signature);
|
|
478
553
|
if (reader !== undefined) {
|
|
479
|
-
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids,
|
|
554
|
+
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
|
|
555
|
+
inValue: false,
|
|
556
|
+
checked,
|
|
557
|
+
});
|
|
480
558
|
parts.push(...found.parts);
|
|
481
559
|
contextual.push(...found.contextual);
|
|
482
560
|
}
|
|
@@ -484,7 +562,10 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
484
562
|
for (const one of walk.values) {
|
|
485
563
|
const reader = readers.get(one.signature);
|
|
486
564
|
if (reader !== undefined) {
|
|
487
|
-
const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids,
|
|
565
|
+
const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, {
|
|
566
|
+
inValue: true,
|
|
567
|
+
checked,
|
|
568
|
+
});
|
|
488
569
|
parts.push(...found.parts);
|
|
489
570
|
contextual.push(...found.contextual);
|
|
490
571
|
}
|
|
@@ -500,6 +581,19 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
500
581
|
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
501
582
|
});
|
|
502
583
|
});
|
|
584
|
+
for (const { names, uses, holder } of loopVariables) {
|
|
585
|
+
for (const name of names) {
|
|
586
|
+
parts.push({
|
|
587
|
+
code: DEAD_STORE,
|
|
588
|
+
declaration: holder,
|
|
589
|
+
kind: "store",
|
|
590
|
+
name: name.text,
|
|
591
|
+
position: spanOf(file, targetRoot, name),
|
|
592
|
+
message: `value written to ${name.text} is never read`,
|
|
593
|
+
used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
503
597
|
walk.blocks.forEach((block, at) => {
|
|
504
598
|
const holder = holderOf(block);
|
|
505
599
|
if (holder !== undefined) {
|
|
@@ -532,6 +626,7 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
532
626
|
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
533
627
|
return {
|
|
534
628
|
parts: withContextualRead(project, parts, contextual),
|
|
629
|
+
imports,
|
|
535
630
|
signatures,
|
|
536
631
|
calls,
|
|
537
632
|
specifiers,
|
|
@@ -558,7 +653,9 @@ function valueSignature(node) {
|
|
|
558
653
|
/**
|
|
559
654
|
* Every name one function's parameters bind, a binding pattern's names one by one. A
|
|
560
655
|
* `this` parameter receives no argument, a decorated one is read by its decorator, and a
|
|
561
|
-
* parameter property is read by the write to its member, so none binds a subject.
|
|
656
|
+
* parameter property is read by the write to its member, so none binds a subject. A name
|
|
657
|
+
* spelled with a leading underscore is declared unread, so it is listed for its reads and
|
|
658
|
+
* reported never.
|
|
562
659
|
*/
|
|
563
660
|
function parameterNames(signature) {
|
|
564
661
|
const found = [];
|
|
@@ -570,7 +667,7 @@ function parameterNames(signature) {
|
|
|
570
667
|
continue;
|
|
571
668
|
}
|
|
572
669
|
if (isIdentifier(element.name)) {
|
|
573
|
-
const exempt = rest && element.dotDotDotToken === undefined;
|
|
670
|
+
const exempt = (rest && element.dotDotDotToken === undefined) || declaredUnread(element.name);
|
|
574
671
|
found.push({ name: element.name, at, pattern: true, exempt });
|
|
575
672
|
}
|
|
576
673
|
else {
|
|
@@ -586,7 +683,7 @@ function parameterNames(signature) {
|
|
|
586
683
|
}
|
|
587
684
|
if (isIdentifier(name)) {
|
|
588
685
|
if (name.text !== "this") {
|
|
589
|
-
found.push({ name, at, pattern: false, exempt:
|
|
686
|
+
found.push({ name, at, pattern: false, exempt: declaredUnread(name) });
|
|
590
687
|
}
|
|
591
688
|
return;
|
|
592
689
|
}
|
|
@@ -594,11 +691,91 @@ function parameterNames(signature) {
|
|
|
594
691
|
});
|
|
595
692
|
return found;
|
|
596
693
|
}
|
|
694
|
+
/** How deep a type is searched for a type parameter: a callback of a callback is two levels. */
|
|
695
|
+
const TYPE_PARAMETER_DEPTH = 3;
|
|
696
|
+
/** Whether one type is or holds a type parameter, through constituents, type arguments and signatures. */
|
|
697
|
+
function namesTypeParameter(project, type, depth = 0) {
|
|
698
|
+
if (type.isTypeParameter()) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
if (depth >= TYPE_PARAMETER_DEPTH) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
const { queries } = project;
|
|
705
|
+
const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [];
|
|
706
|
+
const args = queries.typeArguments(type);
|
|
707
|
+
const signatures = queries.signaturesOf(type, SignatureKind.Call);
|
|
708
|
+
if (!isAnswered(parts) || !isAnswered(args) || !isAnswered(signatures)) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
const inner = [
|
|
712
|
+
...parts,
|
|
713
|
+
...args,
|
|
714
|
+
...signatures.flatMap((signature) => signature.parameters.map((_parameter, at) => queries.typeAtPosition(signature, at))),
|
|
715
|
+
];
|
|
716
|
+
return inner.some((one) => !isAnswered(one) || namesTypeParameter(project, one, depth + 1));
|
|
717
|
+
}
|
|
597
718
|
/**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
719
|
+
* Whether the call a function expression is written in may read the annotation of its
|
|
720
|
+
* parameter at `at`: the callee is generic, and its parameter type at the function's place
|
|
721
|
+
* reads a type parameter there. A function written inside an object literal a generic
|
|
722
|
+
* call is passed is read so too, and a question the checker leaves unanswered reads it.
|
|
723
|
+
*/
|
|
724
|
+
function calleeSteers(project, signature, at) {
|
|
725
|
+
const { queries } = project;
|
|
726
|
+
let value = signature;
|
|
727
|
+
while (isParenthesizedExpression(value.parent)) {
|
|
728
|
+
value = value.parent;
|
|
729
|
+
}
|
|
730
|
+
let nested = false;
|
|
731
|
+
while ((isPropertyAssignment(value.parent) && isObjectLiteralExpression(value.parent.parent)) ||
|
|
732
|
+
isObjectLiteralExpression(value.parent)) {
|
|
733
|
+
value = isObjectLiteralExpression(value.parent) ? value.parent : value.parent.parent;
|
|
734
|
+
nested = true;
|
|
735
|
+
}
|
|
736
|
+
const call = value.parent;
|
|
737
|
+
if (!isCallExpression(call) && !isNewExpression(call)) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
const argument = (call.arguments ?? []).findIndex((one) => one === value);
|
|
741
|
+
const resolved = argument < 0 ? undefined : queries.resolvedSignature(call);
|
|
742
|
+
const generic = resolved === undefined || !isAnswered(resolved) ? resolved : queries.genericOf(resolved);
|
|
743
|
+
if (argument < 0 || generic === undefined) {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
if (!isAnswered(generic) || nested) {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
const position = queries.typeAtPosition(generic, argument);
|
|
750
|
+
if (!isAnswered(position) || position.isTypeParameter()) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
const parts = position.isUnionType() || position.isIntersectionType()
|
|
754
|
+
? queries.constituents(position)
|
|
755
|
+
: [position];
|
|
756
|
+
if (!isAnswered(parts)) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
return parts.some((part) => {
|
|
760
|
+
if (part.isTypeParameter()) {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
const calls = queries.signaturesOf(part, SignatureKind.Call);
|
|
764
|
+
if (!isAnswered(calls)) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
return calls.some((one) => {
|
|
768
|
+
const type = at < one.parameters.length ? queries.typeAtPosition(one, at) : undefined;
|
|
769
|
+
return type !== undefined && (!isAnswered(type) || namesTypeParameter(project, type));
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
775
|
+
* contextual type: the code handing the argument may act on what an object pattern reads,
|
|
776
|
+
* so each such name is read, and an annotated parameter is marked with whether the call's
|
|
777
|
+
* inference may read its annotation. A question the checker leaves unanswered keeps the
|
|
778
|
+
* name read.
|
|
602
779
|
*/
|
|
603
780
|
function withContextualRead(project, parts, contextual) {
|
|
604
781
|
if (contextual.length === 0) {
|
|
@@ -606,9 +783,25 @@ function withContextualRead(project, parts, contextual) {
|
|
|
606
783
|
}
|
|
607
784
|
const signatures = [...new Set(contextual.map((one) => one.signature))];
|
|
608
785
|
const answers = project.queries.contextualTypes(signatures);
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
786
|
+
const typedBy = new Map(signatures.map((signature, at) => [signature, answers[at]]));
|
|
787
|
+
const judged = new Map();
|
|
788
|
+
for (const one of contextual) {
|
|
789
|
+
const answer = typedBy.get(one.signature);
|
|
790
|
+
if (answer === undefined) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const place = one.part.parameter;
|
|
794
|
+
judged.set(one.part, one.pattern || answer === UNANSWERED || place === undefined
|
|
795
|
+
? { ...one.part, used: true }
|
|
796
|
+
: {
|
|
797
|
+
...one.part,
|
|
798
|
+
parameter: {
|
|
799
|
+
...place,
|
|
800
|
+
annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return parts.map((part) => judged.get(part) ?? part);
|
|
612
805
|
}
|
|
613
806
|
/**
|
|
614
807
|
* Each name one function's parameters bind and whether the function reads it, and the
|
|
@@ -616,7 +809,7 @@ function withContextualRead(project, parts, contextual) {
|
|
|
616
809
|
* that carries a type annotation or is an object binding pattern. A binding pattern counts
|
|
617
810
|
* as read where its function reads any name it binds.
|
|
618
811
|
*/
|
|
619
|
-
function parameterParts(file, root, declaration, signature, reader, ids,
|
|
812
|
+
function parameterParts(file, root, declaration, signature, reader, ids, place) {
|
|
620
813
|
// A function that reads its `arguments` reads every parameter by position. A nested
|
|
621
814
|
// function other than an arrow has an `arguments` object of its own.
|
|
622
815
|
const readsArguments = !isArrowFunction(signature) &&
|
|
@@ -639,7 +832,7 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
|
|
|
639
832
|
position: spanOf(file, root, name),
|
|
640
833
|
message: `parameter ${name.text} is never read in the body`,
|
|
641
834
|
used: read[index] === true,
|
|
642
|
-
parameter: { beforeARead: !pattern && at < lastRead,
|
|
835
|
+
parameter: { beforeARead: !pattern && at < lastRead, ...place },
|
|
643
836
|
};
|
|
644
837
|
found.push(part);
|
|
645
838
|
const parameter = signature.parameters[at];
|
|
@@ -647,42 +840,79 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
|
|
|
647
840
|
!part.used &&
|
|
648
841
|
parameter !== undefined &&
|
|
649
842
|
(parameter.type !== undefined || isObjectBindingPattern(parameter.name))) {
|
|
650
|
-
contextual.push({
|
|
843
|
+
contextual.push({
|
|
844
|
+
part,
|
|
845
|
+
signature: expression,
|
|
846
|
+
at,
|
|
847
|
+
pattern: isObjectBindingPattern(parameter.name),
|
|
848
|
+
});
|
|
651
849
|
}
|
|
652
850
|
});
|
|
653
851
|
return { parts: found, contextual };
|
|
654
852
|
}
|
|
655
853
|
/**
|
|
656
854
|
* The names one declaration binds that a write of its initializer stores each on its own:
|
|
657
|
-
* the declared name, or each element name of
|
|
855
|
+
* the declared name, or each element name of a binding pattern at any depth. A name with a
|
|
856
|
+
* default is left out, because removing it skips the default's evaluation, and so is a name
|
|
857
|
+
* beside a rest element of its object pattern, whose removal changes what the rest holds.
|
|
658
858
|
*/
|
|
659
|
-
function
|
|
859
|
+
function boundNames(name) {
|
|
660
860
|
if (isIdentifier(name)) {
|
|
661
861
|
return [name];
|
|
662
862
|
}
|
|
663
|
-
|
|
664
|
-
|
|
863
|
+
const rest = isObjectBindingPattern(name) &&
|
|
864
|
+
name.elements.some((element) => element.dotDotDotToken !== undefined);
|
|
865
|
+
return name.elements.flatMap((element) => {
|
|
866
|
+
if (!isBindingElement(element) || element.name === undefined) {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
if (element.initializer !== undefined || (rest && element.dotDotDotToken === undefined)) {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
return boundNames(element.name);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
/** The function whose scope a `var` of one block belongs to, or none at a module's top level. */
|
|
876
|
+
function functionOf(block) {
|
|
877
|
+
for (let at = block.parent;; at = at.parent) {
|
|
878
|
+
if (isFunctionLikeDeclaration(at)) {
|
|
879
|
+
return at;
|
|
880
|
+
}
|
|
881
|
+
if (isSourceFile(at)) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
665
884
|
}
|
|
666
|
-
return name.elements.flatMap((element) => isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : []);
|
|
667
885
|
}
|
|
668
|
-
/**
|
|
886
|
+
/**
|
|
887
|
+
* The locals one block declares with `let`, `const` or `var`, each with the names of the
|
|
888
|
+
* block that spell it. A `var` of a nested block is one where no nested function of its
|
|
889
|
+
* function names it, since the function scope holds it past the block's end.
|
|
890
|
+
*/
|
|
669
891
|
function localsOf(block) {
|
|
670
892
|
const declared = [];
|
|
893
|
+
const scope = functionOf(block);
|
|
894
|
+
const outlives = scope?.body !== block;
|
|
671
895
|
block.statements.forEach((statement, at) => {
|
|
672
896
|
if (!isVariableStatement(statement)) {
|
|
673
897
|
return;
|
|
674
898
|
}
|
|
675
899
|
const list = statement.declarationList;
|
|
676
|
-
|
|
677
|
-
|
|
900
|
+
const blockScoped = (list.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0;
|
|
901
|
+
if ((!blockScoped && scope === undefined) || (list.flags & NodeFlags.Using) !== 0) {
|
|
678
902
|
return;
|
|
679
903
|
}
|
|
680
904
|
for (const declaration of list.declarations) {
|
|
681
|
-
for (const name of
|
|
905
|
+
for (const name of boundNames(declaration.name)) {
|
|
906
|
+
if (!blockScoped &&
|
|
907
|
+
outlives &&
|
|
908
|
+
identifiersNaming(scope?.body ?? block, new Set([name.text])).some((use) => inNestedFunction(use, scope ?? block))) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
682
911
|
declared.push({
|
|
683
912
|
name,
|
|
684
913
|
statement: at,
|
|
685
914
|
initialized: declaration.initializer !== undefined,
|
|
915
|
+
outlives: !blockScoped && outlives,
|
|
686
916
|
});
|
|
687
917
|
}
|
|
688
918
|
}
|
|
@@ -696,19 +926,39 @@ function localsOf(block) {
|
|
|
696
926
|
uses: spelled.filter((use) => use !== one.name && use.text === one.name.text),
|
|
697
927
|
}));
|
|
698
928
|
}
|
|
929
|
+
/** Whether one name is the whole target of a plain assignment, which writes it and reads nothing. */
|
|
930
|
+
function isPlainWrite(use) {
|
|
931
|
+
const parent = outerParent(use);
|
|
932
|
+
return (isBinaryExpression(parent) &&
|
|
933
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
934
|
+
unwrapped(parent.left) === use);
|
|
935
|
+
}
|
|
699
936
|
/**
|
|
700
937
|
* The writes to the locals of one block that no read reaches. A write is a declaration's
|
|
701
|
-
* initializer or a plain assignment written as a statement of the block
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
* and
|
|
706
|
-
* at any time.
|
|
938
|
+
* initializer or a plain assignment written as a statement of the block; it is dead when
|
|
939
|
+
* the block's later statements read the local nowhere before the next such write or the
|
|
940
|
+
* block's end. A statement writing it inside a nested construct ends the search with the
|
|
941
|
+
* write kept. A local a nested function reads is no subject, since the function may run at
|
|
942
|
+
* any time, and every write to a local nothing reads is dead, a nested function's included.
|
|
707
943
|
*/
|
|
708
944
|
function deadStores(file, root, block, holder, locals, ids) {
|
|
709
945
|
const found = [];
|
|
946
|
+
const deadAt = (local, target) => ({
|
|
947
|
+
code: DEAD_STORE,
|
|
948
|
+
declaration: holder,
|
|
949
|
+
kind: "store",
|
|
950
|
+
name: local.name.text,
|
|
951
|
+
position: spanOf(file, root, target),
|
|
952
|
+
message: `value written to ${local.name.text} is never read`,
|
|
953
|
+
used: false,
|
|
954
|
+
});
|
|
710
955
|
for (const local of locals) {
|
|
711
956
|
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
|
|
957
|
+
if (!local.outlives && uses.every(isPlainWrite)) {
|
|
958
|
+
const targets = [...(local.initialized ? [local.name] : []), ...uses];
|
|
959
|
+
found.push(...targets.map((target) => deadAt(local, target)));
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
712
962
|
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
713
963
|
continue;
|
|
714
964
|
}
|
|
@@ -746,7 +996,7 @@ function deadStores(file, root, block, holder, locals, ids) {
|
|
|
746
996
|
if (sameStatement.length > 0) {
|
|
747
997
|
continue;
|
|
748
998
|
}
|
|
749
|
-
let dead =
|
|
999
|
+
let dead = !local.outlives;
|
|
750
1000
|
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
751
1001
|
const statement = statements[at];
|
|
752
1002
|
if (statement === undefined) {
|
|
@@ -763,19 +1013,9 @@ function deadStores(file, root, block, holder, locals, ids) {
|
|
|
763
1013
|
break;
|
|
764
1014
|
}
|
|
765
1015
|
}
|
|
766
|
-
if (
|
|
767
|
-
|
|
1016
|
+
if (dead) {
|
|
1017
|
+
found.push(deadAt(local, write.target));
|
|
768
1018
|
}
|
|
769
|
-
const position = spanOf(file, root, write.target);
|
|
770
|
-
found.push({
|
|
771
|
-
code: DEAD_STORE,
|
|
772
|
-
declaration: holder,
|
|
773
|
-
kind: "store",
|
|
774
|
-
name: local.name.text,
|
|
775
|
-
position,
|
|
776
|
-
message: `value written to ${local.name.text} is never read`,
|
|
777
|
-
used: false,
|
|
778
|
-
});
|
|
779
1019
|
}
|
|
780
1020
|
}
|
|
781
1021
|
return found;
|
|
@@ -839,6 +1079,12 @@ export function intraFunctionFacts(projects, references, exempted) {
|
|
|
839
1079
|
const known = parts.get(key);
|
|
840
1080
|
parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
|
|
841
1081
|
}
|
|
1082
|
+
const imports = new Map();
|
|
1083
|
+
for (const binding of projects.flatMap((one) => one.imports)) {
|
|
1084
|
+
const key = positionKey(binding.position);
|
|
1085
|
+
const known = imports.get(key);
|
|
1086
|
+
imports.set(key, known === undefined ? binding : { ...known, used: known.used || binding.used });
|
|
1087
|
+
}
|
|
842
1088
|
const calls = new Map();
|
|
843
1089
|
const specifiers = new Set();
|
|
844
1090
|
for (const one of projects) {
|
|
@@ -885,5 +1131,11 @@ export function intraFunctionFacts(projects, references, exempted) {
|
|
|
885
1131
|
const discardedEverywhere = new Set([...called]
|
|
886
1132
|
.filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
|
|
887
1133
|
.map(([id]) => id));
|
|
888
|
-
return {
|
|
1134
|
+
return {
|
|
1135
|
+
parts: [...parts.values()],
|
|
1136
|
+
imports: [...imports.values()],
|
|
1137
|
+
free,
|
|
1138
|
+
freeButValued,
|
|
1139
|
+
discardedEverywhere,
|
|
1140
|
+
};
|
|
889
1141
|
}
|
package/dist/src/inventory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isClassDeclaration, isComputedPropertyName, isConstructorDeclaration, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParameterDeclaration, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/ast";
|
|
1
|
+
import { isArrayBindingPattern, isBindingElement, isClassDeclaration, isComputedPropertyName, isConstructorDeclaration, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isObjectBindingPattern, isParameterDeclaration, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/ast";
|
|
2
2
|
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
3
3
|
import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
|
|
4
4
|
import { computedComponent, nameComponent, renderRef } from "./ref.js";
|
|
@@ -65,6 +65,18 @@ function parentOf(path) {
|
|
|
65
65
|
function modifiersOf(node) {
|
|
66
66
|
return node.modifierFlags ?? 0;
|
|
67
67
|
}
|
|
68
|
+
/** Whether a declaration is ambient: in a declaration file, or under a `declare` modifier. */
|
|
69
|
+
function isAmbient(file, node) {
|
|
70
|
+
if (file.isDeclarationFile) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
for (let at = node; at !== undefined; at = at.parent) {
|
|
74
|
+
if ((modifiersOf(at) & ModifierFlags.Ambient) !== 0) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
68
80
|
/** The type parameters one node declares, and none for a node that declares none. */
|
|
69
81
|
function typeParametersOf(node) {
|
|
70
82
|
if (isFunctionDeclaration(node) ||
|
|
@@ -200,6 +212,31 @@ function visibilityOf(node, name) {
|
|
|
200
212
|
}
|
|
201
213
|
return "public";
|
|
202
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* The binding elements of a destructuring declaration that each declare a name of their
|
|
217
|
+
* own, at any depth, but one with a default, whose removal skips the default's
|
|
218
|
+
* evaluation, and one beside a rest element of its object pattern, whose removal
|
|
219
|
+
* changes what the rest holds.
|
|
220
|
+
*/
|
|
221
|
+
function patternElements(name) {
|
|
222
|
+
if (!isObjectBindingPattern(name) && !isArrayBindingPattern(name)) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const rest = isObjectBindingPattern(name) &&
|
|
226
|
+
name.elements.some((element) => element.dotDotDotToken !== undefined);
|
|
227
|
+
return name.elements.flatMap((element) => {
|
|
228
|
+
if (!isBindingElement(element) ||
|
|
229
|
+
element.initializer !== undefined ||
|
|
230
|
+
(rest && element.dotDotDotToken === undefined)) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
const bound = element.name;
|
|
234
|
+
if (bound === undefined) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
return isIdentifier(bound) ? [{ element, name: bound }] : patternElements(bound);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
203
240
|
/** The name node of one declaration, for the kinds that carry one. */
|
|
204
241
|
function nameNodeOf(node) {
|
|
205
242
|
return node.name;
|
|
@@ -491,15 +528,19 @@ export function inventory(project, host, targetRoot) {
|
|
|
491
528
|
if (isVariableStatement(node)) {
|
|
492
529
|
for (const declaration of node.declarationList.declarations) {
|
|
493
530
|
const component = componentOf(declaration.name);
|
|
494
|
-
if (component
|
|
495
|
-
|
|
496
|
-
|
|
531
|
+
if (component !== undefined) {
|
|
532
|
+
declare(declaration, owner, "variable", declaration.name, {
|
|
533
|
+
component,
|
|
534
|
+
localName: undefined,
|
|
535
|
+
});
|
|
497
536
|
continue;
|
|
498
537
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
538
|
+
for (const { element, name } of patternElements(declaration.name)) {
|
|
539
|
+
declare(element, owner, "variable", name, {
|
|
540
|
+
component: { text: name.text, computed: false },
|
|
541
|
+
localName: undefined,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
503
544
|
}
|
|
504
545
|
return;
|
|
505
546
|
}
|
|
@@ -754,6 +795,9 @@ export function inventory(project, host, targetRoot) {
|
|
|
754
795
|
.map((symbol) => symbol.id),
|
|
755
796
|
byName: namesOf(building),
|
|
756
797
|
modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
|
|
798
|
+
ambient: new Set(building
|
|
799
|
+
.filter((record) => isAmbient(record.file, record.node))
|
|
800
|
+
.map((record) => positionKey(record.position))),
|
|
757
801
|
dependents,
|
|
758
802
|
};
|
|
759
803
|
}
|