@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
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
isArrayBindingPattern,
|
|
10
9
|
isArrowFunction,
|
|
11
10
|
isAwaitExpression,
|
|
12
11
|
isBigIntLiteral,
|
|
@@ -29,6 +28,8 @@ import {
|
|
|
29
28
|
isExpressionStatement,
|
|
30
29
|
isFunctionDeclaration,
|
|
31
30
|
isFunctionExpression,
|
|
31
|
+
isForInStatement,
|
|
32
|
+
isForOfStatement,
|
|
32
33
|
isFunctionLikeDeclaration,
|
|
33
34
|
isHeritageClause,
|
|
34
35
|
isIdentifier,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
isObjectLiteralExpression,
|
|
47
48
|
isParenthesizedExpression,
|
|
48
49
|
isPrefixUnaryExpression,
|
|
50
|
+
isPropertyAssignment,
|
|
49
51
|
isPropertyAccessExpression,
|
|
50
52
|
isQualifiedName,
|
|
51
53
|
isReturnStatement,
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
isThrowStatement,
|
|
57
59
|
isTypeAliasDeclaration,
|
|
58
60
|
isTypeReferenceNode,
|
|
61
|
+
isVariableDeclarationList,
|
|
59
62
|
isVariableStatement,
|
|
60
63
|
isVoidExpression,
|
|
61
64
|
NodeFlags,
|
|
@@ -66,15 +69,27 @@ import {
|
|
|
66
69
|
type CallExpression,
|
|
67
70
|
type CatchClause,
|
|
68
71
|
type ConstructorDeclaration,
|
|
72
|
+
type ForInStatement,
|
|
73
|
+
type ForOfStatement,
|
|
74
|
+
type FunctionLikeDeclaration,
|
|
69
75
|
type FunctionExpression,
|
|
70
76
|
type Identifier,
|
|
71
77
|
type Node,
|
|
72
78
|
type SourceFile,
|
|
73
79
|
type Statement,
|
|
80
|
+
type VariableDeclarationList,
|
|
74
81
|
} from "@typescript/native/ast";
|
|
82
|
+
import { SignatureKind, type Type } from "@typescript/native/sync";
|
|
75
83
|
import type { FindingPosition } from "./finding.ts";
|
|
84
|
+
import {
|
|
85
|
+
exportsLocally,
|
|
86
|
+
implicitReads,
|
|
87
|
+
importBindings,
|
|
88
|
+
type JsxOptions,
|
|
89
|
+
} from "./import-bindings.ts";
|
|
76
90
|
import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
77
91
|
import { positionKey, renderPosition } from "./position.ts";
|
|
92
|
+
import { importRef } from "./ref.ts";
|
|
78
93
|
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
79
94
|
import type { Reference } from "./references.ts";
|
|
80
95
|
import type { ProjectView } from "./session.ts";
|
|
@@ -116,6 +131,14 @@ interface ParameterPlace {
|
|
|
116
131
|
readonly beforeARead: boolean;
|
|
117
132
|
/** Whether no declaration of the inventory declares the function, which is then a value. */
|
|
118
133
|
readonly inValue: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Where the parameter carries an annotation and the compiler gives its function a
|
|
136
|
+
* contextual type: whether a type parameter of the callee reads the parameter's type,
|
|
137
|
+
* so the annotation may steer the call's inference.
|
|
138
|
+
*/
|
|
139
|
+
readonly annotated?: "steers" | "free";
|
|
140
|
+
/** Whether the compiler checks the parameter's file, so its unused-parameter rule may report it. */
|
|
141
|
+
readonly checked: boolean;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/** What one declaration's signature answers to beside its body. */
|
|
@@ -129,9 +152,18 @@ interface SignatureFact {
|
|
|
129
152
|
readonly constructor: boolean;
|
|
130
153
|
}
|
|
131
154
|
|
|
155
|
+
/** One name an import declaration binds, and whether its file reads it. */
|
|
156
|
+
interface ImportFact {
|
|
157
|
+
readonly ref: string;
|
|
158
|
+
readonly name: string;
|
|
159
|
+
readonly position: FindingPosition;
|
|
160
|
+
readonly used: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
/** What one project answers about the parts of its declarations. */
|
|
133
164
|
export interface ProjectParts {
|
|
134
165
|
readonly parts: readonly PartFact[];
|
|
166
|
+
readonly imports: readonly ImportFact[];
|
|
135
167
|
readonly signatures: readonly SignatureFact[];
|
|
136
168
|
/** Each name written as the callee of a call, by position, and what the call does with its value. */
|
|
137
169
|
readonly calls: ReadonlyMap<string, CallUse>;
|
|
@@ -510,10 +542,49 @@ interface ParameterName {
|
|
|
510
542
|
/** The parameter's index in its list. */
|
|
511
543
|
readonly at: number;
|
|
512
544
|
readonly pattern: boolean;
|
|
513
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* Whether the name is never reported: it is spelled declared unread, or it stands beside a
|
|
547
|
+
* rest element of its object pattern, whose removal changes what the rest holds.
|
|
548
|
+
*/
|
|
514
549
|
readonly exempt: boolean;
|
|
515
550
|
}
|
|
516
551
|
|
|
552
|
+
/** Whether a name is spelled with a leading underscore, which declares it unread. */
|
|
553
|
+
function declaredUnread(name: Identifier): boolean {
|
|
554
|
+
return name.text.startsWith("_");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A file's leading comments, where the compiler reads its `@ts-check` and `@ts-nocheck` pragmas. */
|
|
558
|
+
const LEADING_COMMENTS = /^(?:#![^\n]*)?(?:\s|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/u;
|
|
559
|
+
|
|
560
|
+
/** A comment of the leading comments, a line comment's text after its slashes captured. */
|
|
561
|
+
const LEADING_COMMENT = /\/\*[\s\S]*?\*\/|\/\/\/?([^\n]*)/gu;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Whether a file's leading comments hold one pragma: as the compiler reads it, a line
|
|
565
|
+
* comment that starts with the pragma's name, in any case, followed by nothing or a space.
|
|
566
|
+
*/
|
|
567
|
+
function hasPragma(leading: string, pragma: RegExp): boolean {
|
|
568
|
+
return [...leading.matchAll(LEADING_COMMENT)].some(
|
|
569
|
+
([, text]) => text !== undefined && pragma.test(text),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Whether the compiler checks a file, so its rules report what they find there: a
|
|
575
|
+
* JavaScript file only under `checkJs` or a `@ts-check` pragma, and no file with a
|
|
576
|
+
* `@ts-nocheck` pragma.
|
|
577
|
+
*/
|
|
578
|
+
function checkedByTheCompiler(file: SourceFile, checkJs: boolean): boolean {
|
|
579
|
+
const leading = LEADING_COMMENTS.exec(file.text)?.[0] ?? "";
|
|
580
|
+
if (hasPragma(leading, /^\s*@ts-nocheck(?:\s|$)/iu)) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return (
|
|
584
|
+
!/\.[cm]?jsx?$/u.test(file.fileName) || checkJs || hasPragma(leading, /^\s*@ts-check(?:\s|$)/iu)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
517
588
|
/** What one file's walk collects before the batch that resolves its names. */
|
|
518
589
|
interface FileWalk {
|
|
519
590
|
readonly declared: Declared[];
|
|
@@ -524,6 +595,8 @@ interface FileWalk {
|
|
|
524
595
|
readonly holder: string;
|
|
525
596
|
}[];
|
|
526
597
|
readonly blocks: Block[];
|
|
598
|
+
/** The loops whose `let` or `const` variables each iteration writes. */
|
|
599
|
+
readonly loops: { readonly loop: ForOfStatement | ForInStatement; readonly holder: string }[];
|
|
527
600
|
readonly unreachable: { readonly statement: Statement; readonly holder: string }[];
|
|
528
601
|
readonly cases: { readonly clause: Node; readonly text: string; readonly holder: string }[];
|
|
529
602
|
}
|
|
@@ -548,8 +621,13 @@ export function projectParts<Brand>(
|
|
|
548
621
|
const constructions = new Set<string>();
|
|
549
622
|
const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
|
|
550
623
|
const contextual: ContextualParameter[] = [];
|
|
624
|
+
const imports: ImportFact[] = [];
|
|
625
|
+
const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
|
|
626
|
+
const options = project.program.getCompilerOptions();
|
|
627
|
+
const jsx = options as JsxOptions;
|
|
551
628
|
|
|
552
629
|
for (const file of files) {
|
|
630
|
+
const checked = checkedByTheCompiler(file, options.checkJs === true);
|
|
553
631
|
// A parameter property's own parts stay with the parameter when its member goes.
|
|
554
632
|
const declarationAt = (node: Node): string | undefined =>
|
|
555
633
|
isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
|
|
@@ -569,6 +647,7 @@ export function projectParts<Brand>(
|
|
|
569
647
|
values: [],
|
|
570
648
|
catches: [],
|
|
571
649
|
blocks: [],
|
|
650
|
+
loops: [],
|
|
572
651
|
unreachable: [],
|
|
573
652
|
cases: [],
|
|
574
653
|
};
|
|
@@ -667,6 +746,18 @@ export function projectParts<Brand>(
|
|
|
667
746
|
if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
|
|
668
747
|
walk.blocks.push(node);
|
|
669
748
|
}
|
|
749
|
+
const loopHolder =
|
|
750
|
+
(isForOfStatement(node) || isForInStatement(node)) && silenced === 0
|
|
751
|
+
? holderOf(node)
|
|
752
|
+
: undefined;
|
|
753
|
+
if (
|
|
754
|
+
loopHolder !== undefined &&
|
|
755
|
+
(isForOfStatement(node) || isForInStatement(node)) &&
|
|
756
|
+
isVariableDeclarationList(node.initializer) &&
|
|
757
|
+
(node.initializer.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0
|
|
758
|
+
) {
|
|
759
|
+
walk.loops.push({ loop: node, holder: loopHolder });
|
|
760
|
+
}
|
|
670
761
|
if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
|
|
671
762
|
statementsOf(node.statements);
|
|
672
763
|
return;
|
|
@@ -699,7 +790,44 @@ export function projectParts<Brand>(
|
|
|
699
790
|
for (const store of stores) {
|
|
700
791
|
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
701
792
|
}
|
|
793
|
+
const loopVariables = walk.loops.map(({ loop, holder }) => {
|
|
794
|
+
const list = loop.initializer as VariableDeclarationList;
|
|
795
|
+
// A loop must bind a name, so one spelled declared unread is no subject.
|
|
796
|
+
const names = list.declarations
|
|
797
|
+
.flatMap((declaration) => boundNames(declaration.name))
|
|
798
|
+
.filter((name) => !declaredUnread(name));
|
|
799
|
+
const uses = identifiersNaming(loop.statement, new Set(names.map((one) => one.text)));
|
|
800
|
+
asked.push(...names, ...uses);
|
|
801
|
+
return { names, uses, holder };
|
|
802
|
+
});
|
|
803
|
+
const bindings = fileId === undefined ? [] : importBindings(file);
|
|
804
|
+
const bound = new Set<Node>(bindings);
|
|
805
|
+
const bindingUses =
|
|
806
|
+
bindings.length === 0
|
|
807
|
+
? []
|
|
808
|
+
: identifiersNaming(file, new Set(bindings.map((one) => one.text))).filter(
|
|
809
|
+
(use) => !bound.has(use),
|
|
810
|
+
);
|
|
811
|
+
asked.push(...bindings, ...bindingUses);
|
|
702
812
|
const ids = resolvedIds(project, [...new Set(asked)]);
|
|
813
|
+
const implicit = bindings.length === 0 ? new Set<string>() : implicitReads(file, jsx);
|
|
814
|
+
for (const binding of bindings) {
|
|
815
|
+
const fileRef = fileId === undefined ? undefined : refs.get(fileId);
|
|
816
|
+
if (fileId === undefined || fileRef === undefined) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
imports.push({
|
|
820
|
+
ref: importRef(fileRef, binding.text),
|
|
821
|
+
name: binding.text,
|
|
822
|
+
position: spanOf(file, targetRoot, binding),
|
|
823
|
+
used:
|
|
824
|
+
implicit.has(binding.text) ||
|
|
825
|
+
bindingUses.some(
|
|
826
|
+
(use) =>
|
|
827
|
+
use.text === binding.text && (exportsLocally(use) || readsBinding(use, binding, ids)),
|
|
828
|
+
),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
703
831
|
|
|
704
832
|
for (const one of walk.declared) {
|
|
705
833
|
const constructor = isConstructorDeclaration(one.node);
|
|
@@ -714,7 +842,10 @@ export function projectParts<Brand>(
|
|
|
714
842
|
});
|
|
715
843
|
const reader = readers.get(one.signature);
|
|
716
844
|
if (reader !== undefined) {
|
|
717
|
-
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids,
|
|
845
|
+
const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
|
|
846
|
+
inValue: false,
|
|
847
|
+
checked,
|
|
848
|
+
});
|
|
718
849
|
parts.push(...found.parts);
|
|
719
850
|
contextual.push(...found.contextual);
|
|
720
851
|
}
|
|
@@ -722,15 +853,10 @@ export function projectParts<Brand>(
|
|
|
722
853
|
for (const one of walk.values) {
|
|
723
854
|
const reader = readers.get(one.signature);
|
|
724
855
|
if (reader !== undefined) {
|
|
725
|
-
const found = parameterParts(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
one.signature,
|
|
730
|
-
reader,
|
|
731
|
-
ids,
|
|
732
|
-
true,
|
|
733
|
-
);
|
|
856
|
+
const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, {
|
|
857
|
+
inValue: true,
|
|
858
|
+
checked,
|
|
859
|
+
});
|
|
734
860
|
parts.push(...found.parts);
|
|
735
861
|
contextual.push(...found.contextual);
|
|
736
862
|
}
|
|
@@ -746,6 +872,19 @@ export function projectParts<Brand>(
|
|
|
746
872
|
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
747
873
|
});
|
|
748
874
|
});
|
|
875
|
+
for (const { names, uses, holder } of loopVariables) {
|
|
876
|
+
for (const name of names) {
|
|
877
|
+
parts.push({
|
|
878
|
+
code: DEAD_STORE,
|
|
879
|
+
declaration: holder,
|
|
880
|
+
kind: "store",
|
|
881
|
+
name: name.text,
|
|
882
|
+
position: spanOf(file, targetRoot, name),
|
|
883
|
+
message: `value written to ${name.text} is never read`,
|
|
884
|
+
used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
749
888
|
walk.blocks.forEach((block, at) => {
|
|
750
889
|
const holder = holderOf(block);
|
|
751
890
|
if (holder !== undefined) {
|
|
@@ -779,6 +918,7 @@ export function projectParts<Brand>(
|
|
|
779
918
|
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
780
919
|
return {
|
|
781
920
|
parts: withContextualRead(project, parts, contextual),
|
|
921
|
+
imports,
|
|
782
922
|
signatures,
|
|
783
923
|
calls,
|
|
784
924
|
specifiers,
|
|
@@ -807,7 +947,9 @@ function valueSignature(node: Node): SignatureNode | undefined {
|
|
|
807
947
|
/**
|
|
808
948
|
* Every name one function's parameters bind, a binding pattern's names one by one. A
|
|
809
949
|
* `this` parameter receives no argument, a decorated one is read by its decorator, and a
|
|
810
|
-
* parameter property is read by the write to its member, so none binds a subject.
|
|
950
|
+
* parameter property is read by the write to its member, so none binds a subject. A name
|
|
951
|
+
* spelled with a leading underscore is declared unread, so it is listed for its reads and
|
|
952
|
+
* reported never.
|
|
811
953
|
*/
|
|
812
954
|
function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
813
955
|
const found: ParameterName[] = [];
|
|
@@ -820,7 +962,8 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
|
820
962
|
continue;
|
|
821
963
|
}
|
|
822
964
|
if (isIdentifier(element.name)) {
|
|
823
|
-
const exempt =
|
|
965
|
+
const exempt =
|
|
966
|
+
(rest && element.dotDotDotToken === undefined) || declaredUnread(element.name);
|
|
824
967
|
found.push({ name: element.name, at, pattern: true, exempt });
|
|
825
968
|
} else {
|
|
826
969
|
patternNames(element.name, at);
|
|
@@ -837,7 +980,7 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
|
837
980
|
}
|
|
838
981
|
if (isIdentifier(name)) {
|
|
839
982
|
if (name.text !== "this") {
|
|
840
|
-
found.push({ name, at, pattern: false, exempt:
|
|
983
|
+
found.push({ name, at, pattern: false, exempt: declaredUnread(name) });
|
|
841
984
|
}
|
|
842
985
|
return;
|
|
843
986
|
}
|
|
@@ -850,13 +993,110 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
|
850
993
|
interface ContextualParameter {
|
|
851
994
|
readonly part: PartFact;
|
|
852
995
|
readonly signature: ArrowFunction | FunctionExpression;
|
|
996
|
+
/** The parameter's index in its list. */
|
|
997
|
+
readonly at: number;
|
|
998
|
+
/** Whether the parameter is an object binding pattern, rather than an annotated name. */
|
|
999
|
+
readonly pattern: boolean;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** How deep a type is searched for a type parameter: a callback of a callback is two levels. */
|
|
1003
|
+
const TYPE_PARAMETER_DEPTH = 3;
|
|
1004
|
+
|
|
1005
|
+
/** Whether one type is or holds a type parameter, through constituents, type arguments and signatures. */
|
|
1006
|
+
function namesTypeParameter<Brand>(project: ProjectView<Brand>, type: Type, depth = 0): boolean {
|
|
1007
|
+
if (type.isTypeParameter()) {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
if (depth >= TYPE_PARAMETER_DEPTH) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
const { queries } = project;
|
|
1014
|
+
const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [];
|
|
1015
|
+
const args = queries.typeArguments(type);
|
|
1016
|
+
const signatures = queries.signaturesOf(type, SignatureKind.Call);
|
|
1017
|
+
if (!isAnswered(parts) || !isAnswered(args) || !isAnswered(signatures)) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
const inner = [
|
|
1021
|
+
...parts,
|
|
1022
|
+
...args,
|
|
1023
|
+
...signatures.flatMap((signature) =>
|
|
1024
|
+
signature.parameters.map((_parameter, at) => queries.typeAtPosition(signature, at)),
|
|
1025
|
+
),
|
|
1026
|
+
];
|
|
1027
|
+
return inner.some((one) => !isAnswered(one) || namesTypeParameter(project, one, depth + 1));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Whether the call a function expression is written in may read the annotation of its
|
|
1032
|
+
* parameter at `at`: the callee is generic, and its parameter type at the function's place
|
|
1033
|
+
* reads a type parameter there. A function written inside an object literal a generic
|
|
1034
|
+
* call is passed is read so too, and a question the checker leaves unanswered reads it.
|
|
1035
|
+
*/
|
|
1036
|
+
function calleeSteers<Brand>(
|
|
1037
|
+
project: ProjectView<Brand>,
|
|
1038
|
+
signature: ArrowFunction | FunctionExpression,
|
|
1039
|
+
at: number,
|
|
1040
|
+
): boolean {
|
|
1041
|
+
const { queries } = project;
|
|
1042
|
+
let value: Node = signature;
|
|
1043
|
+
while (isParenthesizedExpression(value.parent)) {
|
|
1044
|
+
value = value.parent;
|
|
1045
|
+
}
|
|
1046
|
+
let nested = false;
|
|
1047
|
+
while (
|
|
1048
|
+
(isPropertyAssignment(value.parent) && isObjectLiteralExpression(value.parent.parent)) ||
|
|
1049
|
+
isObjectLiteralExpression(value.parent)
|
|
1050
|
+
) {
|
|
1051
|
+
value = isObjectLiteralExpression(value.parent) ? value.parent : value.parent.parent;
|
|
1052
|
+
nested = true;
|
|
1053
|
+
}
|
|
1054
|
+
const call = value.parent;
|
|
1055
|
+
if (!isCallExpression(call) && !isNewExpression(call)) {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
const argument = (call.arguments ?? []).findIndex((one) => one === value);
|
|
1059
|
+
const resolved = argument < 0 ? undefined : queries.resolvedSignature(call);
|
|
1060
|
+
const generic =
|
|
1061
|
+
resolved === undefined || !isAnswered(resolved) ? resolved : queries.genericOf(resolved);
|
|
1062
|
+
if (argument < 0 || generic === undefined) {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
if (!isAnswered(generic) || nested) {
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
const position = queries.typeAtPosition(generic, argument);
|
|
1069
|
+
if (!isAnswered(position) || position.isTypeParameter()) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
const parts =
|
|
1073
|
+
position.isUnionType() || position.isIntersectionType()
|
|
1074
|
+
? queries.constituents(position)
|
|
1075
|
+
: [position];
|
|
1076
|
+
if (!isAnswered(parts)) {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
return parts.some((part) => {
|
|
1080
|
+
if (part.isTypeParameter()) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
const calls = queries.signaturesOf(part, SignatureKind.Call);
|
|
1084
|
+
if (!isAnswered(calls)) {
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
return calls.some((one) => {
|
|
1088
|
+
const type = at < one.parameters.length ? queries.typeAtPosition(one, at) : undefined;
|
|
1089
|
+
return type !== undefined && (!isAnswered(type) || namesTypeParameter(project, type));
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
853
1092
|
}
|
|
854
1093
|
|
|
855
1094
|
/**
|
|
856
|
-
* The parts with each name of `contextual`
|
|
857
|
-
* contextual type: the
|
|
858
|
-
*
|
|
859
|
-
* A question the checker leaves unanswered keeps the
|
|
1095
|
+
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
1096
|
+
* contextual type: the code handing the argument may act on what an object pattern reads,
|
|
1097
|
+
* so each such name is read, and an annotated parameter is marked with whether the call's
|
|
1098
|
+
* inference may read its annotation. A question the checker leaves unanswered keeps the
|
|
1099
|
+
* name read.
|
|
860
1100
|
*/
|
|
861
1101
|
function withContextualRead<Brand>(
|
|
862
1102
|
project: ProjectView<Brand>,
|
|
@@ -868,9 +1108,28 @@ function withContextualRead<Brand>(
|
|
|
868
1108
|
}
|
|
869
1109
|
const signatures = [...new Set(contextual.map((one) => one.signature))];
|
|
870
1110
|
const answers = project.queries.contextualTypes(signatures);
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
1111
|
+
const typedBy = new Map(signatures.map((signature, at) => [signature, answers[at]]));
|
|
1112
|
+
const judged = new Map<PartFact, PartFact>();
|
|
1113
|
+
for (const one of contextual) {
|
|
1114
|
+
const answer = typedBy.get(one.signature);
|
|
1115
|
+
if (answer === undefined) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const place = one.part.parameter;
|
|
1119
|
+
judged.set(
|
|
1120
|
+
one.part,
|
|
1121
|
+
one.pattern || answer === UNANSWERED || place === undefined
|
|
1122
|
+
? { ...one.part, used: true }
|
|
1123
|
+
: {
|
|
1124
|
+
...one.part,
|
|
1125
|
+
parameter: {
|
|
1126
|
+
...place,
|
|
1127
|
+
annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return parts.map((part) => judged.get(part) ?? part);
|
|
874
1133
|
}
|
|
875
1134
|
|
|
876
1135
|
/**
|
|
@@ -886,7 +1145,7 @@ function parameterParts(
|
|
|
886
1145
|
signature: SignatureNode,
|
|
887
1146
|
reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
|
|
888
1147
|
ids: ReadonlyMap<Node, Answer<number>>,
|
|
889
|
-
|
|
1148
|
+
place: Pick<ParameterPlace, "inValue" | "checked">,
|
|
890
1149
|
): { readonly parts: PartFact[]; readonly contextual: ContextualParameter[] } {
|
|
891
1150
|
// A function that reads its `arguments` reads every parameter by position. A nested
|
|
892
1151
|
// function other than an arrow has an `arguments` object of its own.
|
|
@@ -917,7 +1176,7 @@ function parameterParts(
|
|
|
917
1176
|
position: spanOf(file, root, name),
|
|
918
1177
|
message: `parameter ${name.text} is never read in the body`,
|
|
919
1178
|
used: read[index] === true,
|
|
920
|
-
parameter: { beforeARead: !pattern && at < lastRead,
|
|
1179
|
+
parameter: { beforeARead: !pattern && at < lastRead, ...place },
|
|
921
1180
|
};
|
|
922
1181
|
found.push(part);
|
|
923
1182
|
const parameter = signature.parameters[at];
|
|
@@ -927,7 +1186,12 @@ function parameterParts(
|
|
|
927
1186
|
parameter !== undefined &&
|
|
928
1187
|
(parameter.type !== undefined || isObjectBindingPattern(parameter.name))
|
|
929
1188
|
) {
|
|
930
|
-
contextual.push({
|
|
1189
|
+
contextual.push({
|
|
1190
|
+
part,
|
|
1191
|
+
signature: expression,
|
|
1192
|
+
at,
|
|
1193
|
+
pattern: isObjectBindingPattern(parameter.name),
|
|
1194
|
+
});
|
|
931
1195
|
}
|
|
932
1196
|
});
|
|
933
1197
|
return { parts: found, contextual };
|
|
@@ -939,44 +1203,85 @@ interface Local {
|
|
|
939
1203
|
readonly statement: number;
|
|
940
1204
|
readonly initialized: boolean;
|
|
941
1205
|
readonly uses: readonly Identifier[];
|
|
1206
|
+
/** A `var` of a nested block, whose function scope outlives the block's end. */
|
|
1207
|
+
readonly outlives: boolean;
|
|
942
1208
|
}
|
|
943
1209
|
|
|
944
1210
|
/**
|
|
945
1211
|
* The names one declaration binds that a write of its initializer stores each on its own:
|
|
946
|
-
* the declared name, or each element name of
|
|
1212
|
+
* the declared name, or each element name of a binding pattern at any depth. A name with a
|
|
1213
|
+
* default is left out, because removing it skips the default's evaluation, and so is a name
|
|
1214
|
+
* beside a rest element of its object pattern, whose removal changes what the rest holds.
|
|
947
1215
|
*/
|
|
948
|
-
function
|
|
1216
|
+
function boundNames(name: BindingName): readonly Identifier[] {
|
|
949
1217
|
if (isIdentifier(name)) {
|
|
950
1218
|
return [name];
|
|
951
1219
|
}
|
|
952
|
-
|
|
953
|
-
|
|
1220
|
+
const rest =
|
|
1221
|
+
isObjectBindingPattern(name) &&
|
|
1222
|
+
name.elements.some((element) => element.dotDotDotToken !== undefined);
|
|
1223
|
+
return name.elements.flatMap((element) => {
|
|
1224
|
+
if (!isBindingElement(element) || element.name === undefined) {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
if (element.initializer !== undefined || (rest && element.dotDotDotToken === undefined)) {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
return boundNames(element.name);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** The function whose scope a `var` of one block belongs to, or none at a module's top level. */
|
|
1235
|
+
function functionOf(block: Block): FunctionLikeDeclaration | undefined {
|
|
1236
|
+
for (let at: Node = block.parent; ; at = at.parent) {
|
|
1237
|
+
if (isFunctionLikeDeclaration(at)) {
|
|
1238
|
+
return at;
|
|
1239
|
+
}
|
|
1240
|
+
if (isSourceFile(at)) {
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
954
1243
|
}
|
|
955
|
-
return name.elements.flatMap((element) =>
|
|
956
|
-
isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : [],
|
|
957
|
-
);
|
|
958
1244
|
}
|
|
959
1245
|
|
|
960
|
-
/**
|
|
1246
|
+
/**
|
|
1247
|
+
* The locals one block declares with `let`, `const` or `var`, each with the names of the
|
|
1248
|
+
* block that spell it. A `var` of a nested block is one where no nested function of its
|
|
1249
|
+
* function names it, since the function scope holds it past the block's end.
|
|
1250
|
+
*/
|
|
961
1251
|
function localsOf(block: Block): readonly Local[] {
|
|
962
|
-
const declared: {
|
|
1252
|
+
const declared: {
|
|
1253
|
+
name: Identifier;
|
|
1254
|
+
statement: number;
|
|
1255
|
+
initialized: boolean;
|
|
1256
|
+
outlives: boolean;
|
|
1257
|
+
}[] = [];
|
|
1258
|
+
const scope = functionOf(block);
|
|
1259
|
+
const outlives = scope?.body !== block;
|
|
963
1260
|
block.statements.forEach((statement, at) => {
|
|
964
1261
|
if (!isVariableStatement(statement)) {
|
|
965
1262
|
return;
|
|
966
1263
|
}
|
|
967
1264
|
const list = statement.declarationList;
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
(list.flags & NodeFlags.Using) !== 0
|
|
971
|
-
) {
|
|
1265
|
+
const blockScoped = (list.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0;
|
|
1266
|
+
if ((!blockScoped && scope === undefined) || (list.flags & NodeFlags.Using) !== 0) {
|
|
972
1267
|
return;
|
|
973
1268
|
}
|
|
974
1269
|
for (const declaration of list.declarations) {
|
|
975
|
-
for (const name of
|
|
1270
|
+
for (const name of boundNames(declaration.name)) {
|
|
1271
|
+
if (
|
|
1272
|
+
!blockScoped &&
|
|
1273
|
+
outlives &&
|
|
1274
|
+
identifiersNaming(scope?.body ?? block, new Set([name.text])).some((use) =>
|
|
1275
|
+
inNestedFunction(use, scope ?? block),
|
|
1276
|
+
)
|
|
1277
|
+
) {
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
976
1280
|
declared.push({
|
|
977
1281
|
name,
|
|
978
1282
|
statement: at,
|
|
979
1283
|
initialized: declaration.initializer !== undefined,
|
|
1284
|
+
outlives: !blockScoped && outlives,
|
|
980
1285
|
});
|
|
981
1286
|
}
|
|
982
1287
|
}
|
|
@@ -991,14 +1296,23 @@ function localsOf(block: Block): readonly Local[] {
|
|
|
991
1296
|
}));
|
|
992
1297
|
}
|
|
993
1298
|
|
|
1299
|
+
/** Whether one name is the whole target of a plain assignment, which writes it and reads nothing. */
|
|
1300
|
+
function isPlainWrite(use: Identifier): boolean {
|
|
1301
|
+
const parent = outerParent(use);
|
|
1302
|
+
return (
|
|
1303
|
+
isBinaryExpression(parent) &&
|
|
1304
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
1305
|
+
unwrapped(parent.left) === use
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
994
1309
|
/**
|
|
995
1310
|
* The writes to the locals of one block that no read reaches. A write is a declaration's
|
|
996
|
-
* initializer or a plain assignment written as a statement of the block
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
* and
|
|
1001
|
-
* at any time.
|
|
1311
|
+
* initializer or a plain assignment written as a statement of the block; it is dead when
|
|
1312
|
+
* the block's later statements read the local nowhere before the next such write or the
|
|
1313
|
+
* block's end. A statement writing it inside a nested construct ends the search with the
|
|
1314
|
+
* write kept. A local a nested function reads is no subject, since the function may run at
|
|
1315
|
+
* any time, and every write to a local nothing reads is dead, a nested function's included.
|
|
1002
1316
|
*/
|
|
1003
1317
|
function deadStores(
|
|
1004
1318
|
file: SourceFile,
|
|
@@ -1009,8 +1323,22 @@ function deadStores(
|
|
|
1009
1323
|
ids: ReadonlyMap<Node, Answer<number>>,
|
|
1010
1324
|
): PartFact[] {
|
|
1011
1325
|
const found: PartFact[] = [];
|
|
1326
|
+
const deadAt = (local: Local, target: Identifier): PartFact => ({
|
|
1327
|
+
code: DEAD_STORE,
|
|
1328
|
+
declaration: holder,
|
|
1329
|
+
kind: "store",
|
|
1330
|
+
name: local.name.text,
|
|
1331
|
+
position: spanOf(file, root, target),
|
|
1332
|
+
message: `value written to ${local.name.text} is never read`,
|
|
1333
|
+
used: false,
|
|
1334
|
+
});
|
|
1012
1335
|
for (const local of locals) {
|
|
1013
1336
|
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
|
|
1337
|
+
if (!local.outlives && uses.every(isPlainWrite)) {
|
|
1338
|
+
const targets = [...(local.initialized ? [local.name] : []), ...uses];
|
|
1339
|
+
found.push(...targets.map((target) => deadAt(local, target)));
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1014
1342
|
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
1015
1343
|
continue;
|
|
1016
1344
|
}
|
|
@@ -1050,7 +1378,7 @@ function deadStores(
|
|
|
1050
1378
|
if (sameStatement.length > 0) {
|
|
1051
1379
|
continue;
|
|
1052
1380
|
}
|
|
1053
|
-
let dead =
|
|
1381
|
+
let dead = !local.outlives;
|
|
1054
1382
|
for (let at = write.at + 1; at < statements.length; at += 1) {
|
|
1055
1383
|
const statement = statements[at];
|
|
1056
1384
|
if (statement === undefined) {
|
|
@@ -1067,19 +1395,9 @@ function deadStores(
|
|
|
1067
1395
|
break;
|
|
1068
1396
|
}
|
|
1069
1397
|
}
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1398
|
+
if (dead) {
|
|
1399
|
+
found.push(deadAt(local, write.target));
|
|
1072
1400
|
}
|
|
1073
|
-
const position = spanOf(file, root, write.target);
|
|
1074
|
-
found.push({
|
|
1075
|
-
code: DEAD_STORE,
|
|
1076
|
-
declaration: holder,
|
|
1077
|
-
kind: "store",
|
|
1078
|
-
name: local.name.text,
|
|
1079
|
-
position,
|
|
1080
|
-
message: `value written to ${local.name.text} is never read`,
|
|
1081
|
-
used: false,
|
|
1082
|
-
});
|
|
1083
1401
|
}
|
|
1084
1402
|
}
|
|
1085
1403
|
return found;
|
|
@@ -1142,6 +1460,8 @@ function resultParts<Brand>(
|
|
|
1142
1460
|
export interface IntraFunctionFacts {
|
|
1143
1461
|
/** Every part of the run, each once, in use where any project found it in use. */
|
|
1144
1462
|
readonly parts: readonly PartFact[];
|
|
1463
|
+
/** Every import binding of the run, each once, read where any project found it read. */
|
|
1464
|
+
readonly imports: readonly ImportFact[];
|
|
1145
1465
|
/** The declarations whose signature is free to change. */
|
|
1146
1466
|
readonly free: ReadonlySet<string>;
|
|
1147
1467
|
/** The declarations whose signature is free but for a use as a value. */
|
|
@@ -1172,6 +1492,15 @@ export function intraFunctionFacts(
|
|
|
1172
1492
|
const known = parts.get(key);
|
|
1173
1493
|
parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
|
|
1174
1494
|
}
|
|
1495
|
+
const imports = new Map<string, ImportFact>();
|
|
1496
|
+
for (const binding of projects.flatMap((one) => one.imports)) {
|
|
1497
|
+
const key = positionKey(binding.position);
|
|
1498
|
+
const known = imports.get(key);
|
|
1499
|
+
imports.set(
|
|
1500
|
+
key,
|
|
1501
|
+
known === undefined ? binding : { ...known, used: known.used || binding.used },
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1175
1504
|
const calls = new Map<string, CallUse>();
|
|
1176
1505
|
const specifiers = new Set<string>();
|
|
1177
1506
|
for (const one of projects) {
|
|
@@ -1231,5 +1560,11 @@ export function intraFunctionFacts(
|
|
|
1231
1560
|
.filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
|
|
1232
1561
|
.map(([id]) => id),
|
|
1233
1562
|
);
|
|
1234
|
-
return {
|
|
1563
|
+
return {
|
|
1564
|
+
parts: [...parts.values()],
|
|
1565
|
+
imports: [...imports.values()],
|
|
1566
|
+
free,
|
|
1567
|
+
freeButValued,
|
|
1568
|
+
discardedEverywhere,
|
|
1569
|
+
};
|
|
1235
1570
|
}
|