@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.1
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 +106 -10
- package/dist/src/augmentations.js +21 -8
- 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 +75 -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 +116 -21
- 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 +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- 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 +18 -9
- 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 +4 -1
- 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 +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +121 -17
- 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 +143 -10
- package/src/augmentations.ts +21 -10
- 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 +103 -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 +10 -0
- package/src/findings/completion.ts +147 -24
- 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 +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- 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 +32 -14
- 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 +10 -1
- 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 +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +149 -16
- 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
package/src/schema.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The serialization-contract class: the data members and `toJSON` of every type a value's
|
|
3
3
|
* static type reaches, where the value reaches `JSON.stringify` or a call `ts.serializers`
|
|
4
|
-
* names, and `toString` too where it reaches an
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* names, and `toString` too where it reaches an erased parameter of a function outside the
|
|
5
|
+
* analysis, or an erased property of a type the program does not declare. A function of
|
|
6
|
+
* the project handing such a parameter to a destination is one, to a fixpoint, and a
|
|
7
|
+
* function invoked through `call`, `apply` or `bind` is the callee.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -80,6 +80,9 @@ const SERIALIZED_CONVERSION = "toJSON";
|
|
|
80
80
|
/** The parameter types that keep nothing of the value they are given. */
|
|
81
81
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
82
82
|
|
|
83
|
+
/** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
|
|
84
|
+
const MENTION_DEPTH = 3;
|
|
85
|
+
|
|
83
86
|
/**
|
|
84
87
|
* What a destination holds back of a class: its properties, or its properties and its
|
|
85
88
|
* two conversion methods. The second holds back everything the first does.
|
|
@@ -564,6 +567,30 @@ function destinations<Brand>(
|
|
|
564
567
|
});
|
|
565
568
|
return containing.length === 0 ? target.declarations : containing;
|
|
566
569
|
};
|
|
570
|
+
/** Whether a type names a type parameter, through constituents, type arguments and call signatures. */
|
|
571
|
+
const mentions = (type: Type, parameter: Type, depth: number): boolean => {
|
|
572
|
+
// A conditional type an alias names, `Awaited<T>` among them, carries its arguments
|
|
573
|
+
// only on the alias the client's type object reports.
|
|
574
|
+
const aliased = (type as { readonly aliasTypeArguments?: readonly number[] })
|
|
575
|
+
.aliasTypeArguments;
|
|
576
|
+
if (type.id === parameter.id || aliased?.includes(parameter.id) === true) {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
if (depth >= MENTION_DEPTH) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const inner = [
|
|
583
|
+
...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
|
|
584
|
+
...must(queries.typeArguments(type)),
|
|
585
|
+
...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
|
|
586
|
+
...signature.parameters.map((_parameter, at) =>
|
|
587
|
+
must(queries.typeAtPosition(signature, at)),
|
|
588
|
+
),
|
|
589
|
+
...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
|
|
590
|
+
]),
|
|
591
|
+
];
|
|
592
|
+
return inner.some((one) => mentions(one, parameter, depth + 1));
|
|
593
|
+
};
|
|
567
594
|
const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
|
|
568
595
|
const erasedAt = (call: CallExpression, argument: number): boolean => {
|
|
569
596
|
let indexes = erasedArguments.get(call);
|
|
@@ -577,8 +604,32 @@ function destinations<Brand>(
|
|
|
577
604
|
? []
|
|
578
605
|
: must(queries.signaturesOf(type, SignatureKind.Call))
|
|
579
606
|
: [resolvedOf(call)].filter((one) => one !== undefined);
|
|
607
|
+
// A type parameter of the callee that nothing constrains and its result does not name
|
|
608
|
+
// keeps nothing of the value either: the instantiated signature names the value's own
|
|
609
|
+
// type there instead, and a result that names it hands the value back.
|
|
610
|
+
const generic = invoked.indirect ? undefined : resolvedOf(call);
|
|
611
|
+
const declared = generic === undefined ? undefined : must(queries.genericOf(generic));
|
|
612
|
+
const unconstrained = (index: number): boolean => {
|
|
613
|
+
const parameter =
|
|
614
|
+
declared === undefined ? undefined : must(queries.parameterType(declared, index));
|
|
615
|
+
const parts =
|
|
616
|
+
parameter === undefined
|
|
617
|
+
? []
|
|
618
|
+
: parameter.isUnionType()
|
|
619
|
+
? must(queries.constituents(parameter))
|
|
620
|
+
: [parameter];
|
|
621
|
+
const result = declared === undefined ? undefined : must(queries.returnTypeOf(declared));
|
|
622
|
+
return parts.some((part) => {
|
|
623
|
+
if (!part.isTypeParameter() || (result !== undefined && mentions(result, part, 0))) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const constraint = must(queries.constraintOf(part));
|
|
627
|
+
return constraint === undefined || (constraint.flags & ERASED) !== 0;
|
|
628
|
+
});
|
|
629
|
+
};
|
|
580
630
|
indexes = new Set(
|
|
581
631
|
invoked.arguments.flatMap((_unused, index) =>
|
|
632
|
+
unconstrained(index) ||
|
|
582
633
|
signatures.some((signature) => {
|
|
583
634
|
const parameter = must(queries.parameterType(signature, index));
|
|
584
635
|
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
package/src/session.ts
CHANGED
|
@@ -129,6 +129,7 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
129
129
|
...(mapper === undefined ? {} : { runExternalCode: true }),
|
|
130
130
|
});
|
|
131
131
|
let closed = false;
|
|
132
|
+
const parsed = new Map<string, { readonly text: string; readonly file: SourceFile }>();
|
|
132
133
|
return {
|
|
133
134
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
134
135
|
parseConfigJson: (json, configFile) =>
|
|
@@ -160,8 +161,14 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
160
161
|
moduleResolutions: { fallback: "resolve", entries: [...entries] },
|
|
161
162
|
}),
|
|
162
163
|
parseSourceFile: (fileName, text) => {
|
|
164
|
+
// Every project of a run reads the same files outside its program, so one parse serves all.
|
|
165
|
+
const known = parsed.get(fileName);
|
|
166
|
+
if (known?.text === text) {
|
|
167
|
+
return known.file;
|
|
168
|
+
}
|
|
163
169
|
const retained = api.createSourceFile(fileName, text);
|
|
164
170
|
try {
|
|
171
|
+
parsed.set(fileName, { text, file: retained.sourceFile });
|
|
165
172
|
return retained.sourceFile;
|
|
166
173
|
} finally {
|
|
167
174
|
retained.dispose();
|
package/src/source-files.ts
CHANGED
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
import type { SourceFile } from "@typescript/native/ast";
|
|
8
8
|
import { commonDirectory, emittedFrom } from "./emit-map.ts";
|
|
9
|
-
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
9
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
11
|
|
|
12
12
|
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
13
13
|
const SUBPATH_WILDCARD = "*";
|
|
14
14
|
|
|
15
|
+
/** The extension a source file's name drops when the project writes it out. */
|
|
16
|
+
const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
|
|
17
|
+
|
|
18
|
+
/** The extensions the project may write a source file's output under. */
|
|
19
|
+
const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* Whether one path is what a subpath pattern stands for: the text before the
|
|
17
23
|
* wildcard, any run of characters including the solidus, then the text after it.
|
|
@@ -40,6 +46,47 @@ export interface SourceFiles {
|
|
|
40
46
|
named(path: string): readonly SourceFile[];
|
|
41
47
|
/** The directories the project writes its output below, absolute: its output and declaration directories. */
|
|
42
48
|
readonly outputDirs: readonly string[];
|
|
49
|
+
/** The output paths, absolute, the project writes one own file to, each of which {@link named} reads back to it. */
|
|
50
|
+
writtenAs(file: SourceFile): readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* The own files one absolute path names read as an output of the project written below
|
|
53
|
+
* `outputDir`: a manifest that ships from its build directory names its entries there.
|
|
54
|
+
*/
|
|
55
|
+
namedAsOutput(outputDir: string, path: string): readonly SourceFile[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The first own file of the paths given that the project holds, or none. */
|
|
59
|
+
function heldAt(
|
|
60
|
+
byName: ReadonlyMap<string, SourceFile>,
|
|
61
|
+
paths: readonly string[],
|
|
62
|
+
): readonly SourceFile[] {
|
|
63
|
+
for (const candidate of paths) {
|
|
64
|
+
const file = byName.get(candidate);
|
|
65
|
+
if (file !== undefined) {
|
|
66
|
+
return [file];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Where one project writes its output, and the directory it writes it from, absolute. */
|
|
73
|
+
function layoutOf<Brand>(
|
|
74
|
+
project: ProjectView<Brand>,
|
|
75
|
+
fileNames: readonly string[],
|
|
76
|
+
): {
|
|
77
|
+
readonly outDir: string | undefined;
|
|
78
|
+
readonly declarationDir: string | undefined;
|
|
79
|
+
readonly sourceDir: string;
|
|
80
|
+
} {
|
|
81
|
+
const options = project.program.getCompilerOptions();
|
|
82
|
+
const configDir = dirnamePath(project.configFile);
|
|
83
|
+
const at = (option: string | undefined): string | undefined =>
|
|
84
|
+
option === undefined ? undefined : resolvePath(configDir, option);
|
|
85
|
+
return {
|
|
86
|
+
outDir: at(options.outDir),
|
|
87
|
+
declarationDir: at(options.declarationDir),
|
|
88
|
+
sourceDir: at(options.rootDir) ?? commonDirectory(fileNames),
|
|
89
|
+
};
|
|
43
90
|
}
|
|
44
91
|
|
|
45
92
|
/**
|
|
@@ -62,14 +109,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
|
|
|
62
109
|
}
|
|
63
110
|
}
|
|
64
111
|
|
|
65
|
-
const
|
|
66
|
-
const configDir = dirnamePath(project.configFile);
|
|
67
|
-
const at = (option: string | undefined): string | undefined =>
|
|
68
|
-
option === undefined ? undefined : resolvePath(configDir, option);
|
|
69
|
-
const outDir = at(options.outDir);
|
|
70
|
-
const declarationDir = at(options.declarationDir);
|
|
71
|
-
const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
|
|
72
|
-
|
|
112
|
+
const { outDir, declarationDir, sourceDir } = layoutOf(project, [...byName.keys()]);
|
|
73
113
|
const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
|
|
74
114
|
|
|
75
115
|
/** The source paths one written path may stand for, the path itself first. */
|
|
@@ -79,15 +119,25 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
|
|
|
79
119
|
byPath,
|
|
80
120
|
byName,
|
|
81
121
|
outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
|
|
122
|
+
writtenAs: (file) => {
|
|
123
|
+
const below = relativePath(sourceDir, file.fileName);
|
|
124
|
+
if (below === undefined) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const written = [outDir, declarationDir].flatMap((dir) =>
|
|
128
|
+
dir === undefined
|
|
129
|
+
? []
|
|
130
|
+
: OUTPUT_EXTENSIONS.map((extension) =>
|
|
131
|
+
joinPath(dir, below.replace(SOURCE_EXTENSION, extension)),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
return [...new Set(written)].filter((path) => heldAt(byName, candidates(path))[0] === file);
|
|
135
|
+
},
|
|
136
|
+
namedAsOutput: (outputDir, path) =>
|
|
137
|
+
heldAt(byName, emittedFrom(path, { outDir: outputDir, sourceDirs: [sourceDir] })),
|
|
82
138
|
named: (path) => {
|
|
83
139
|
if (!path.includes(SUBPATH_WILDCARD)) {
|
|
84
|
-
|
|
85
|
-
const held = byName.get(candidate);
|
|
86
|
-
if (held !== undefined) {
|
|
87
|
-
return [held];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return [];
|
|
140
|
+
return heldAt(byName, candidates(path));
|
|
91
141
|
}
|
|
92
142
|
const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
|
|
93
143
|
return [...byName.values()].filter((file) =>
|
package/src/suppress-file.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
UNSCOPED_ENTRY,
|
|
20
20
|
blankReason,
|
|
21
21
|
isEntryPath,
|
|
22
|
+
isPartKind,
|
|
22
23
|
isSymbolRef,
|
|
23
24
|
siteText,
|
|
24
25
|
type Mechanism,
|
|
@@ -155,13 +156,23 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
155
156
|
if (refusals.length > 0) {
|
|
156
157
|
return { records: [], refusals };
|
|
157
158
|
}
|
|
158
|
-
const named =
|
|
159
|
+
const named =
|
|
160
|
+
byReference.get(declarationKey(symbol, written.path)) ??
|
|
161
|
+
(isPartKind(code)
|
|
162
|
+
? byReference.get(declarationKey(parameterHolder(symbol), written.path))
|
|
163
|
+
: undefined) ??
|
|
164
|
+
[];
|
|
159
165
|
if (named.length === 0) {
|
|
160
166
|
return { records: [{ ...written, bound: "" }], refusals: [] };
|
|
161
167
|
}
|
|
162
168
|
return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
/** The reference of the declaration a parameter's reference names it under, or `ref` itself. */
|
|
172
|
+
function parameterHolder(ref: string): string {
|
|
173
|
+
return ref.endsWith(")") ? ref.slice(0, ref.lastIndexOf("(")) : ref;
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
|
|
166
177
|
const byReference = new Map<string, InventorySymbol[]>();
|
|
167
178
|
for (const symbol of symbols) {
|
package/src/suppress-inline.ts
CHANGED
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
type Node,
|
|
11
11
|
type SourceFile,
|
|
12
12
|
} from "@typescript/native/ast";
|
|
13
|
+
import { importBindings } from "./import-bindings.ts";
|
|
13
14
|
import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
14
15
|
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
16
|
+
import { importRef } from "./ref.ts";
|
|
15
17
|
import type { ProjectView } from "./session.ts";
|
|
16
18
|
import { signatureOf } from "./signatures.ts";
|
|
17
19
|
import {
|
|
@@ -200,6 +202,7 @@ function declarationLines(
|
|
|
200
202
|
file: SourceFile,
|
|
201
203
|
held: Inventory,
|
|
202
204
|
symbols: ReadonlyMap<string, Declared>,
|
|
205
|
+
targetRoot: string,
|
|
203
206
|
): ReadonlyMap<number, readonly Binding[]> {
|
|
204
207
|
const below = new Map<number, Binding[]>();
|
|
205
208
|
const add = (offset: number, one: Binding): void => {
|
|
@@ -245,6 +248,20 @@ function declarationLines(
|
|
|
245
248
|
node.forEachChild(visit);
|
|
246
249
|
};
|
|
247
250
|
file.forEachChild(visit);
|
|
251
|
+
// An import binding is a subject of its own, bound on its line as a declaration is.
|
|
252
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
253
|
+
const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
|
|
254
|
+
for (const name of moduleRef === "" ? [] : importBindings(file)) {
|
|
255
|
+
const position = renderPosition(file, targetRoot, name.getStart());
|
|
256
|
+
const one = { id: positionKey(position), ref: importRef(moduleRef, name.text), position };
|
|
257
|
+
let statement: Node = name;
|
|
258
|
+
while (statement.parent.kind !== SyntaxKind.SourceFile) {
|
|
259
|
+
statement = statement.parent;
|
|
260
|
+
}
|
|
261
|
+
for (const at of [statement.pos, name.getStart()]) {
|
|
262
|
+
add(at, { declared: one, part: one });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
248
265
|
return below;
|
|
249
266
|
}
|
|
250
267
|
|
|
@@ -285,7 +302,7 @@ export function inlineDirectives<Brand>(
|
|
|
285
302
|
if (comments.length === 0) {
|
|
286
303
|
continue;
|
|
287
304
|
}
|
|
288
|
-
const below = declarationLines(file, held, symbols);
|
|
305
|
+
const below = declarationLines(file, held, symbols, targetRoot);
|
|
289
306
|
for (const comment of comments) {
|
|
290
307
|
const site = renderPosition(file, targetRoot, comment.offset);
|
|
291
308
|
const target = trails(file, comment.offset) ? site.line : site.line + 1;
|
package/src/suppress.ts
CHANGED
|
@@ -243,10 +243,11 @@ function claimKey(finding: Finding): string {
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
246
|
-
* the declaration at the finding's position, or it names the finding's reference
|
|
247
|
-
*
|
|
248
|
-
* finding
|
|
249
|
-
*
|
|
246
|
+
* the declaration at the finding's position, or it names the finding's reference, or the
|
|
247
|
+
* reference of the declaration holding a part that has one of its own, and the file the
|
|
248
|
+
* finding is reported in, each exactly. The second is how an entry copied from a finding
|
|
249
|
+
* about a part of a declaration reaches the part, and the only way any record reaches a
|
|
250
|
+
* dependency row.
|
|
250
251
|
*/
|
|
251
252
|
export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
252
253
|
if (record.code !== finding.code) {
|
|
@@ -265,7 +266,10 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
265
266
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
266
267
|
return true;
|
|
267
268
|
}
|
|
268
|
-
return
|
|
269
|
+
return (
|
|
270
|
+
(record.symbol === finding.symbol.ref || record.symbol === finding.symbol.holder) &&
|
|
271
|
+
record.path === finding.position.path
|
|
272
|
+
);
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
/**
|
package/src/sweep.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
10
|
import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
|
|
11
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
11
12
|
import type { Position } from "./position.ts";
|
|
12
13
|
|
|
13
14
|
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
@@ -178,17 +179,32 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
178
179
|
return { liveUnder: live, candidates };
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The directory a test runner reads a manual mock from, whose exports it puts in place of
|
|
184
|
+
* the module's: https://jestjs.io/docs/manual-mocks
|
|
185
|
+
*/
|
|
186
|
+
const MANUAL_MOCKS = "__mocks__";
|
|
187
|
+
|
|
188
|
+
/** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
|
|
189
|
+
export function runnerCalls(symbol: InventorySymbol): boolean {
|
|
190
|
+
return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
/**
|
|
182
194
|
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
183
|
-
* That run's roots are the graph's own and each test file,
|
|
184
|
-
*
|
|
185
|
-
*
|
|
195
|
+
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
196
|
+
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
197
|
+
* which the runner reads; any other export of a test file, and test-support code, which
|
|
198
|
+
* runs where a test imports it, are judged as any file's are.
|
|
199
|
+
* `held` seeds it as a mark or an exemption seeds a sweep.
|
|
186
200
|
*/
|
|
187
201
|
export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
|
|
188
202
|
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
return graph.test[at] === true &&
|
|
203
|
+
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
204
|
+
const called = exported && runnerCalls(symbol);
|
|
205
|
+
return graph.test[at] === true &&
|
|
206
|
+
graph.support[at] !== true &&
|
|
207
|
+
(symbol.kind === "file" || called)
|
|
192
208
|
? [{ at, kind: "test-runner" as const }]
|
|
193
209
|
: [];
|
|
194
210
|
});
|
|
@@ -277,10 +293,11 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
277
293
|
|
|
278
294
|
/**
|
|
279
295
|
* Admits every test declaration whose set of referenced production declarations is
|
|
280
|
-
* non-empty and wholly dead, and returns which it admitted.
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
296
|
+
* non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
|
|
297
|
+
* code the target does not implement, so it is no production declaration here. The set is
|
|
298
|
+
* read from every reference the test makes, which a production sweep does not count. A
|
|
299
|
+
* test-support declaration a test referencing a live target also references is a live
|
|
300
|
+
* target of every test referencing it. A marked or exempt test is never admitted.
|
|
284
301
|
*/
|
|
285
302
|
function admitTestsOfDeadCode(
|
|
286
303
|
graph: Graph,
|
|
@@ -293,7 +310,11 @@ function admitTestsOfDeadCode(
|
|
|
293
310
|
let live = 0;
|
|
294
311
|
if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
|
|
295
312
|
for (const edge of graph.out[at] ?? []) {
|
|
296
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
testCode(edge.to) ||
|
|
315
|
+
graph.subject[edge.to] !== true ||
|
|
316
|
+
graph.ambient[edge.to] === true
|
|
317
|
+
) {
|
|
297
318
|
continue;
|
|
298
319
|
}
|
|
299
320
|
targets += 1;
|
package/src/symbol-keys.ts
CHANGED
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
* The members a unique symbol keys, used through element accesses. `value[key]`, where
|
|
3
3
|
* `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
|
|
4
4
|
* whose computed name is that same constant, and a store through the access writes it.
|
|
5
|
-
* No name node resolves to such a member: the key's name resolves to the constant.
|
|
5
|
+
* No name node resolves to such a member: the key's name resolves to the constant. An
|
|
6
|
+
* iteration reads the member its well-known iteration symbol keys.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
isComputedPropertyName,
|
|
11
|
+
isForOfStatement,
|
|
10
12
|
isIdentifier,
|
|
11
13
|
isNoSubstitutionTemplateLiteral,
|
|
12
14
|
isNumericLiteral,
|
|
13
15
|
isParenthesizedExpression,
|
|
14
16
|
isPropertyAccessExpression,
|
|
17
|
+
isSpreadElement,
|
|
15
18
|
isStringLiteral,
|
|
16
19
|
type ElementAccessExpression,
|
|
20
|
+
type Expression,
|
|
17
21
|
type Node,
|
|
22
|
+
type SourceFile,
|
|
18
23
|
} from "@typescript/native/ast";
|
|
19
24
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
20
25
|
import type { AliasChains } from "./alias-chain.ts";
|
|
21
|
-
import type
|
|
26
|
+
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
27
|
+
import { renderPosition } from "./position.ts";
|
|
22
28
|
import { isAnswered, type PropertyTables } from "./query.ts";
|
|
29
|
+
import type { Reference } from "./references.ts";
|
|
23
30
|
import type { ProjectView } from "./session.ts";
|
|
24
31
|
|
|
25
32
|
/** One element access whose key is a name, and how the access uses the member it selects. */
|
|
@@ -170,3 +177,123 @@ export function keyedUses<Brand>(
|
|
|
170
177
|
return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
|
|
171
178
|
});
|
|
172
179
|
}
|
|
180
|
+
|
|
181
|
+
/** The well-known symbol a value's iteration calls, by the construct that iterates it. */
|
|
182
|
+
type Protocol = "iterator" | "asyncIterator";
|
|
183
|
+
|
|
184
|
+
/** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
|
|
185
|
+
function wellKnownName(name: Node | undefined): string | undefined {
|
|
186
|
+
if (name === undefined || !isComputedPropertyName(name)) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const key = name.expression;
|
|
190
|
+
return isPropertyAccessExpression(key) &&
|
|
191
|
+
isIdentifier(key.expression) &&
|
|
192
|
+
key.expression.text === "Symbol" &&
|
|
193
|
+
isIdentifier(key.name)
|
|
194
|
+
? key.name.text
|
|
195
|
+
: undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The references the iterations of one project's own files make to the iteration members
|
|
200
|
+
* of the values they iterate: a `for…of` statement and a spread element read
|
|
201
|
+
* `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
|
|
202
|
+
* which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
|
|
203
|
+
*/
|
|
204
|
+
export function iterationReferences<Brand>(
|
|
205
|
+
project: ProjectView<Brand>,
|
|
206
|
+
held: Inventory,
|
|
207
|
+
chains: AliasChains,
|
|
208
|
+
root: string,
|
|
209
|
+
testFiles: ReadonlySet<string>,
|
|
210
|
+
): readonly Reference[] {
|
|
211
|
+
const queries = project.queries;
|
|
212
|
+
const ownPaths = project.ownPaths();
|
|
213
|
+
const iterated: {
|
|
214
|
+
readonly file: SourceFile;
|
|
215
|
+
readonly from: string;
|
|
216
|
+
readonly operand: Expression;
|
|
217
|
+
readonly protocols: readonly Protocol[];
|
|
218
|
+
}[] = [];
|
|
219
|
+
for (const file of project.ownSourceFiles()) {
|
|
220
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
221
|
+
if (fileId === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const visit = (node: Node, from: string): void => {
|
|
225
|
+
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
226
|
+
if (isForOfStatement(node)) {
|
|
227
|
+
const protocols: readonly Protocol[] =
|
|
228
|
+
node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
|
|
229
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols });
|
|
230
|
+
} else if (isSpreadElement(node)) {
|
|
231
|
+
iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
|
|
232
|
+
}
|
|
233
|
+
node.forEachChild((child) => {
|
|
234
|
+
visit(child, holder);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
file.forEachChild((child) => {
|
|
238
|
+
visit(child, fileId);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (iterated.length === 0) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const types = queries.typesAt(iterated.map((one) => one.operand));
|
|
245
|
+
const members = new Map<
|
|
246
|
+
number,
|
|
247
|
+
readonly { readonly name: string; readonly ids: readonly string[] }[]
|
|
248
|
+
>();
|
|
249
|
+
const membersOf = (
|
|
250
|
+
type: Type,
|
|
251
|
+
): readonly { readonly name: string; readonly ids: readonly string[] }[] => {
|
|
252
|
+
const known = members.get(type.id);
|
|
253
|
+
if (known !== undefined) {
|
|
254
|
+
return known;
|
|
255
|
+
}
|
|
256
|
+
const keyed: { name: string; ids: readonly string[] }[] = [];
|
|
257
|
+
const properties = queries.propertiesOf(type);
|
|
258
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
259
|
+
for (const handle of property.declarations) {
|
|
260
|
+
const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
|
|
261
|
+
const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
|
|
262
|
+
if (name !== undefined) {
|
|
263
|
+
keyed.push({ name, ids: chains.declarationsOf(property) });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
members.set(type.id, keyed);
|
|
269
|
+
return keyed;
|
|
270
|
+
};
|
|
271
|
+
const found: Reference[] = [];
|
|
272
|
+
iterated.forEach((one, index) => {
|
|
273
|
+
const type = types[index];
|
|
274
|
+
if (type === undefined || !isAnswered(type)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const parts =
|
|
278
|
+
type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
|
|
279
|
+
const position = renderPosition(one.file, root, one.operand.getStart());
|
|
280
|
+
for (const part of isAnswered(parts) ? parts : []) {
|
|
281
|
+
for (const member of membersOf(part)) {
|
|
282
|
+
if (!(one.protocols as readonly string[]).includes(member.name)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const to of member.ids) {
|
|
286
|
+
found.push({
|
|
287
|
+
from: one.from,
|
|
288
|
+
to,
|
|
289
|
+
position,
|
|
290
|
+
use: "read",
|
|
291
|
+
resolution: "keyed",
|
|
292
|
+
test: testFiles.has(position.path),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return found;
|
|
299
|
+
}
|