@cplieger/deadset-ts 8.0.0-dev.16 → 8.0.0-dev.17
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/alias-chain.js +0 -5
- package/dist/src/amd.js +81 -0
- package/dist/src/analysis.js +58 -22
- package/dist/src/augmentations.js +23 -24
- package/dist/src/bindings.js +382 -0
- package/dist/src/calls.js +25 -8
- package/dist/src/commands/tools.js +5 -9
- package/dist/src/component-files.js +3 -2
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +6 -9
- package/dist/src/contextual-uses.js +1 -17
- package/dist/src/contract/config.generated.js +4 -1
- package/dist/src/contract/report.generated.js +6 -2
- package/dist/src/dependencies.js +25 -29
- package/dist/src/destructuring.js +1 -19
- package/dist/src/element-access.js +179 -164
- package/dist/src/entries/build-scripts.js +2 -2
- package/dist/src/entries/configuration-files.js +3 -7
- package/dist/src/entries/conventions.js +2 -2
- package/dist/src/entries/html-entries.js +7 -10
- package/dist/src/entries/mdx-entries.js +2 -2
- package/dist/src/entries/static-module.js +2 -1
- package/dist/src/facts.js +4 -4
- package/dist/src/findings/completion.js +40 -4
- package/dist/src/{implementations.js → findings/implementations.js} +3 -3
- package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
- package/dist/src/framework-lifecycle.js +41 -14
- package/dist/src/graph.js +12 -4
- package/dist/src/import-bindings.js +73 -28
- package/dist/src/index/sites.js +557 -0
- package/dist/src/indexed-access.js +523 -478
- package/dist/src/interface-satisfaction.js +1 -1
- package/dist/src/keyof-comparisons.js +124 -103
- package/dist/src/load/expected-module.js +2 -2
- package/dist/src/load/specifiers.js +2 -4
- package/dist/src/load/synthesize.js +5 -0
- package/dist/src/load/workspace-resolution.js +2 -2
- package/dist/src/matrix.js +6 -4
- package/dist/src/module-declarations.js +70 -85
- package/dist/src/overrides.js +134 -112
- package/dist/src/paths.js +46 -7
- package/dist/src/platform-calls.js +2 -2
- package/dist/src/platform-symbols.js +13 -19
- package/dist/src/references/read-rule.js +1 -0
- package/dist/src/references/read-rules.js +48 -0
- package/dist/src/references/sink.js +151 -0
- package/dist/src/references/sources.js +450 -0
- package/dist/src/references.js +110 -1285
- package/dist/src/roots/configured-declarations.js +3 -0
- package/dist/src/roots/root-set.js +1 -1
- package/dist/src/roots/unseen-loads.js +38 -10
- package/dist/src/serialization-contract.js +8 -3
- package/dist/src/stores.js +4 -2
- package/dist/src/sweep.js +25 -13
- package/dist/src/symbol-keys.js +89 -83
- package/dist/src/tests/file-class.js +76 -0
- package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
- package/dist/src/{test-support.js → tests/support.js} +2 -11
- package/dist/src/type-parameter-reads.js +178 -139
- package/dist/src/unimplemented-callees.js +262 -242
- package/dist/src/verbs/analyze.js +3 -3
- package/dist/src/verbs/explain.js +50 -28
- package/dist/src/verbs/print-retained.js +2 -2
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/alias-chain.ts +0 -7
- package/src/amd.ts +130 -0
- package/src/analysis.ts +86 -23
- package/src/augmentations.ts +41 -27
- package/src/bindings.ts +460 -0
- package/src/calls.ts +25 -7
- package/src/commands/tools.ts +5 -11
- package/src/component-files.ts +4 -2
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +8 -11
- package/src/contextual-uses.ts +2 -22
- package/src/contract/config.generated.ts +4 -1
- package/src/contract/report.generated.ts +6 -2
- package/src/dependencies.ts +27 -28
- package/src/destructuring.ts +1 -31
- package/src/element-access.ts +188 -182
- package/src/entries/build-scripts.ts +2 -2
- package/src/entries/configuration-files.ts +11 -8
- package/src/entries/conventions.ts +2 -1
- package/src/entries/html-entries.ts +15 -14
- package/src/entries/mdx-entries.ts +2 -2
- package/src/entries/static-module.ts +2 -1
- package/src/facts.ts +5 -3
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +43 -4
- package/src/findings/emitter.ts +1 -1
- package/src/{implementations.ts → findings/implementations.ts} +4 -4
- package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
- package/src/framework-lifecycle.ts +42 -14
- package/src/graph.ts +24 -6
- package/src/import-bindings.ts +98 -29
- package/src/index/sites.ts +781 -0
- package/src/indexed-access.ts +570 -527
- package/src/interface-satisfaction.ts +1 -1
- package/src/keyof-comparisons.ts +131 -116
- package/src/load/expected-module.ts +8 -2
- package/src/load/specifiers.ts +8 -5
- package/src/load/synthesize.ts +6 -0
- package/src/load/workspace-resolution.ts +9 -2
- package/src/matrix.ts +6 -3
- package/src/module-declarations.ts +74 -98
- package/src/overrides.ts +162 -134
- package/src/paths.ts +54 -8
- package/src/platform-calls.ts +3 -3
- package/src/platform-symbols.ts +11 -27
- package/src/references/read-rule.ts +60 -0
- package/src/references/read-rules.ts +55 -0
- package/src/references/sink.ts +213 -0
- package/src/references/sources.ts +632 -0
- package/src/references.ts +168 -1733
- package/src/report.ts +1 -1
- package/src/roots/configured-declarations.ts +3 -0
- package/src/roots/root-set.ts +1 -1
- package/src/roots/unseen-loads.ts +49 -11
- package/src/serialization-contract.ts +8 -3
- package/src/stores.ts +13 -2
- package/src/sweep.ts +37 -14
- package/src/symbol-keys.ts +102 -102
- package/src/tests/file-class.ts +118 -0
- package/src/{outside-tests.ts → tests/outside.ts} +16 -10
- package/src/{test-support.ts → tests/support.ts} +9 -20
- package/src/type-parameter-reads.ts +187 -158
- package/src/unimplemented-callees.ts +297 -280
- package/src/verbs/analyze.ts +3 -2
- package/src/verbs/explain.ts +74 -31
- package/src/verbs/print-retained.ts +2 -2
package/src/report.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
import type { AppliedConvention } from "./entries/conventions.ts";
|
|
19
19
|
import type { PassResult } from "./findings-pass.ts";
|
|
20
20
|
import type { StaleSuppression } from "./findings/self-check.ts";
|
|
21
|
-
import type { TestFileRule } from "./
|
|
21
|
+
import type { TestFileRule } from "./tests/file-class.ts";
|
|
22
22
|
import { bytewise } from "./text/compare.ts";
|
|
23
23
|
import { LANGUAGE, PRODUCT } from "./product.ts";
|
|
24
24
|
|
|
@@ -204,6 +204,9 @@ function modulesNamed<Brand>(
|
|
|
204
204
|
project: ProjectView<Brand>,
|
|
205
205
|
specifiers: ReadonlySet<string>,
|
|
206
206
|
): ReadonlyMap<string, TSSymbol> {
|
|
207
|
+
if (specifiers.size === 0) {
|
|
208
|
+
return new Map();
|
|
209
|
+
}
|
|
207
210
|
const written = new Map<string, Node>();
|
|
208
211
|
for (const file of project.ownSourceFiles()) {
|
|
209
212
|
const visit = (node: Node): void => {
|
package/src/roots/root-set.ts
CHANGED
|
@@ -621,7 +621,7 @@ export function rootSet<Brand>(
|
|
|
621
621
|
for (const id of skippedRoots(project, held, options.skips)) {
|
|
622
622
|
add(id, "type-error", "");
|
|
623
623
|
}
|
|
624
|
-
for (const id of mergedGlobalInterfaces(project, held, files)) {
|
|
624
|
+
for (const id of mergedGlobalInterfaces(project, held, files).kept) {
|
|
625
625
|
add(id, "global-augmentation", "");
|
|
626
626
|
}
|
|
627
627
|
|
|
@@ -19,9 +19,18 @@ import {
|
|
|
19
19
|
type Node,
|
|
20
20
|
type SourceFile,
|
|
21
21
|
} from "@typescript/native/ast";
|
|
22
|
+
import { amdLoadOf, amdUnresolved } from "../amd.ts";
|
|
22
23
|
import { membersNamed, textsOf, writtenValue, type Value } from "../entries/static-module.ts";
|
|
23
24
|
import { writtenName } from "../platform-calls.ts";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
cannotStartPackageName,
|
|
27
|
+
dirnamePath,
|
|
28
|
+
hasURLScheme,
|
|
29
|
+
isAbsolutePath,
|
|
30
|
+
relativePath,
|
|
31
|
+
resolvePath,
|
|
32
|
+
startsRelativePath,
|
|
33
|
+
} from "../paths.ts";
|
|
25
34
|
import type { Position } from "../position.ts";
|
|
26
35
|
import { unwrapped } from "../unwrap.ts";
|
|
27
36
|
import { globExpression } from "../text/glob.ts";
|
|
@@ -54,7 +63,7 @@ function leadingLiteral(node: Node): string | undefined {
|
|
|
54
63
|
* it, each of which would name every sibling.
|
|
55
64
|
*/
|
|
56
65
|
function computedLoadDirectory(dir: string, prefix: string): string | undefined {
|
|
57
|
-
if (!(prefix
|
|
66
|
+
if (!startsRelativePath(prefix) || !prefix.endsWith("/")) {
|
|
58
67
|
return undefined;
|
|
59
68
|
}
|
|
60
69
|
const target = resolvePath(dir, prefix);
|
|
@@ -74,11 +83,13 @@ interface Loadable {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
/** One load by a computed path whose files the analysis resolves. */
|
|
77
|
-
interface ComputedLoad {
|
|
86
|
+
export interface ComputedLoad {
|
|
78
87
|
readonly call: CallExpression;
|
|
79
88
|
readonly from: string;
|
|
80
89
|
/** The name a `require()` call is written with; {@link callsPlatform} owns which one loads. */
|
|
81
90
|
readonly require: Identifier | undefined;
|
|
91
|
+
/** Whether the call is an AMD `define` or `require` (references-ts.37). */
|
|
92
|
+
readonly amd: boolean;
|
|
82
93
|
readonly files: readonly Loadable[];
|
|
83
94
|
/** Whether the load reads each file as text or as a URL, which references no declaration of it. */
|
|
84
95
|
readonly asset: boolean;
|
|
@@ -136,7 +147,7 @@ function globbed(
|
|
|
136
147
|
const dir = dirnamePath(file.fileName);
|
|
137
148
|
const root = (): string | undefined => rootOf(file.fileName);
|
|
138
149
|
const against = (at: string): string | undefined => {
|
|
139
|
-
if (!at
|
|
150
|
+
if (!isAbsolutePath(at)) {
|
|
140
151
|
return resolvePath(dir, at);
|
|
141
152
|
}
|
|
142
153
|
const top = root();
|
|
@@ -149,9 +160,9 @@ function globbed(
|
|
|
149
160
|
const negated = written.startsWith("!");
|
|
150
161
|
const pattern = negated ? written.slice(1) : written;
|
|
151
162
|
let path: string | undefined;
|
|
152
|
-
if (pattern
|
|
163
|
+
if (isAbsolutePath(pattern)) {
|
|
153
164
|
path = against(pattern);
|
|
154
|
-
} else if (
|
|
165
|
+
} else if (startsRelativePath(pattern)) {
|
|
155
166
|
path = from === undefined ? undefined : resolvePath(from, pattern);
|
|
156
167
|
} else if (pattern.startsWith("**")) {
|
|
157
168
|
path = pattern;
|
|
@@ -173,7 +184,8 @@ function globbed(
|
|
|
173
184
|
* file itself: an `import()` or `require()` whose path starts with a relative literal naming
|
|
174
185
|
* a directory below the file's loads every candidate below it, as a bundler's context module
|
|
175
186
|
* reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
|
|
176
|
-
* call of a computed path that names no such directory and no package is an unresolved load
|
|
187
|
+
* call of a computed path that names no such directory and no package is an unresolved load,
|
|
188
|
+
* and so is an AMD call one of whose dependencies names no path ({@link amdUnresolved}).
|
|
177
189
|
*/
|
|
178
190
|
export function computedLoadsOf(
|
|
179
191
|
file: SourceFile,
|
|
@@ -196,6 +208,7 @@ export function computedLoadsOf(
|
|
|
196
208
|
call,
|
|
197
209
|
from,
|
|
198
210
|
require: undefined,
|
|
211
|
+
amd: false,
|
|
199
212
|
files: files ?? [],
|
|
200
213
|
resolved: files !== undefined,
|
|
201
214
|
asset,
|
|
@@ -203,6 +216,23 @@ export function computedLoadsOf(
|
|
|
203
216
|
},
|
|
204
217
|
];
|
|
205
218
|
}
|
|
219
|
+
const amd = amdLoadOf(call);
|
|
220
|
+
if (amd !== undefined) {
|
|
221
|
+
return amdUnresolved(amd)
|
|
222
|
+
? [
|
|
223
|
+
{
|
|
224
|
+
call,
|
|
225
|
+
from,
|
|
226
|
+
require: amd.callee,
|
|
227
|
+
amd: true,
|
|
228
|
+
files: [],
|
|
229
|
+
resolved: false,
|
|
230
|
+
asset: false,
|
|
231
|
+
eager: false,
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
: [];
|
|
235
|
+
}
|
|
206
236
|
const argument = unwrapped(first);
|
|
207
237
|
const require =
|
|
208
238
|
call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
|
|
@@ -219,7 +249,16 @@ export function computedLoadsOf(
|
|
|
219
249
|
? []
|
|
220
250
|
: others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
|
|
221
251
|
return [
|
|
222
|
-
{
|
|
252
|
+
{
|
|
253
|
+
call,
|
|
254
|
+
from,
|
|
255
|
+
require,
|
|
256
|
+
amd: false,
|
|
257
|
+
files,
|
|
258
|
+
resolved: target !== undefined,
|
|
259
|
+
asset: false,
|
|
260
|
+
eager: false,
|
|
261
|
+
},
|
|
223
262
|
];
|
|
224
263
|
});
|
|
225
264
|
}
|
|
@@ -300,8 +339,7 @@ function namesPackage(prefix: string | undefined): boolean {
|
|
|
300
339
|
return (
|
|
301
340
|
prefix !== undefined &&
|
|
302
341
|
prefix !== "" &&
|
|
303
|
-
!prefix
|
|
304
|
-
!prefix
|
|
305
|
-
!/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix)
|
|
342
|
+
!cannotStartPackageName(prefix) &&
|
|
343
|
+
!hasURLScheme(prefix)
|
|
306
344
|
);
|
|
307
345
|
}
|
|
@@ -1220,7 +1220,8 @@ export interface Decoded {
|
|
|
1220
1220
|
|
|
1221
1221
|
/**
|
|
1222
1222
|
* The data members the decodes and copies of `files` fill, as {@link Decoded} names them.
|
|
1223
|
-
* A generic declared outside the analysis, or only ambiently,
|
|
1223
|
+
* A generic declared outside the analysis, or only ambiently, or one that reads a value
|
|
1224
|
+
* {@link readTypes} finds untyped as its own type parameter, fills the type that a type
|
|
1224
1225
|
* parameter of its own no parameter names binds, where the awaited result carries that
|
|
1225
1226
|
* type through its data members rather than only naming it, as a store's methods do: no
|
|
1226
1227
|
* argument hands the value over, so it fills it where the analysis cannot see, and may
|
|
@@ -1282,6 +1283,10 @@ function decodedOf<Brand>(
|
|
|
1282
1283
|
for (const file of files) {
|
|
1283
1284
|
file.forEachChild(visit);
|
|
1284
1285
|
}
|
|
1286
|
+
const read = readTypes(project, untyped);
|
|
1287
|
+
// A generic of the analysis that reads an untyped value as its own type parameter is a
|
|
1288
|
+
// client of what made the value, and fills what that parameter binds as an outside one does.
|
|
1289
|
+
const clients = new Set(read.made.flatMap((type) => (type.isTypeParameter() ? [type.id] : [])));
|
|
1285
1290
|
const filled: { readonly carried: Type; readonly types: readonly Type[] }[] = [];
|
|
1286
1291
|
const resolved = queries.resolvedSignatures(calls).map(must);
|
|
1287
1292
|
for (const signature of resolved) {
|
|
@@ -1296,7 +1301,8 @@ function decodedOf<Brand>(
|
|
|
1296
1301
|
if (
|
|
1297
1302
|
declaration === undefined ||
|
|
1298
1303
|
(inAnalysis(declaration.path) &&
|
|
1299
|
-
(node === undefined || (node.flags & NodeFlags.Ambient) === 0)
|
|
1304
|
+
(node === undefined || (node.flags & NodeFlags.Ambient) === 0) &&
|
|
1305
|
+
!generic.some((id) => clients.has(id)))
|
|
1300
1306
|
) {
|
|
1301
1307
|
continue;
|
|
1302
1308
|
}
|
|
@@ -1318,7 +1324,6 @@ function decodedOf<Brand>(
|
|
|
1318
1324
|
filled.push({ carried: awaited(project, given), types });
|
|
1319
1325
|
}
|
|
1320
1326
|
}
|
|
1321
|
-
const read = readTypes(project, untyped);
|
|
1322
1327
|
const copies = copiedNames(project, spreads, calls);
|
|
1323
1328
|
if (
|
|
1324
1329
|
filled.length === 0 &&
|
package/src/stores.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { nodeKey, type Inventory } from "./inventory.ts";
|
|
|
13
13
|
import type { Reference } from "./references.ts";
|
|
14
14
|
import type { ProjectView } from "./session.ts";
|
|
15
15
|
import type { Exemption, Mode } from "./sweep.ts";
|
|
16
|
+
import { classedAsTest, madeByTestCode, withheldBy } from "./tests/file-class.ts";
|
|
16
17
|
|
|
17
18
|
/** One run's stores and reads, over every configuration. */
|
|
18
19
|
export interface Stores {
|
|
@@ -39,6 +40,8 @@ export interface Stores {
|
|
|
39
40
|
/** What one configuration contributes to the run's stores. */
|
|
40
41
|
interface ConfigurationStores {
|
|
41
42
|
readonly references: readonly Reference[];
|
|
43
|
+
/** The configuration's test-support files, whose references are test code's. */
|
|
44
|
+
readonly support: ReadonlySet<string>;
|
|
42
45
|
readonly accessors: readonly string[];
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -71,12 +74,20 @@ export function storesOf(
|
|
|
71
74
|
): Stores {
|
|
72
75
|
return {
|
|
73
76
|
references: per.flatMap((one) =>
|
|
74
|
-
one.references.filter((reference) => !mode
|
|
77
|
+
one.references.filter((reference) => !withheldBy(mode, reference, one.support)),
|
|
75
78
|
),
|
|
76
79
|
readInTests: new Set(
|
|
77
80
|
per.flatMap((one) =>
|
|
78
81
|
one.references
|
|
79
|
-
.filter(
|
|
82
|
+
.filter(
|
|
83
|
+
(reference) =>
|
|
84
|
+
reference.use !== "write" &&
|
|
85
|
+
classedAsTest(
|
|
86
|
+
madeByTestCode(reference, one.support),
|
|
87
|
+
reference.consumer !== undefined,
|
|
88
|
+
mode,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
80
91
|
.map((reference) => reference.to),
|
|
81
92
|
),
|
|
82
93
|
),
|
package/src/sweep.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
|
-
import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
|
|
10
|
+
import { namesACaller, OUTSIDE, type Counts, type Edge, type Graph } from "./graph.ts";
|
|
11
|
+
import { edgeWithheldBy } from "./tests/file-class.ts";
|
|
11
12
|
import type { InventorySymbol } from "./inventory.ts";
|
|
12
13
|
import type { Position } from "./position.ts";
|
|
13
14
|
|
|
@@ -54,6 +55,32 @@ export interface Mode {
|
|
|
54
55
|
* in a test file makes is such a reference, so nothing only a test reaches is live.
|
|
55
56
|
*/
|
|
56
57
|
readonly production: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Classes a reference a loaded consumer's test file made as a production one rather than a
|
|
60
|
+
* test one, `analysis.consumer_tests` at `production`: a classification, not a filter, so it
|
|
61
|
+
* decides the split a candidate reports as well as what a production sweep counts.
|
|
62
|
+
*/
|
|
63
|
+
readonly consumerTestsProduction?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The references to one declaration the mode counts: under `production` none test code made but those it classes as production. */
|
|
67
|
+
function countedRefs(made: Counts | undefined, mode: Mode): number {
|
|
68
|
+
if (made === undefined) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (!mode.production) {
|
|
72
|
+
return made.production + made.test;
|
|
73
|
+
}
|
|
74
|
+
return made.production + (mode.consumerTestsProduction === true ? made.consumerTest : 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The references to one declaration, split as the mode classes them, whatever it counts. */
|
|
78
|
+
export function splitRefs(
|
|
79
|
+
made: Counts | undefined,
|
|
80
|
+
mode: Mode,
|
|
81
|
+
): { readonly production: number; readonly test: number } {
|
|
82
|
+
const moved = mode.consumerTestsProduction === true ? (made?.consumerTest ?? 0) : 0;
|
|
83
|
+
return { production: (made?.production ?? 0) + moved, test: (made?.test ?? 0) - moved };
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
/** What one sweep runs over besides the graph. */
|
|
@@ -134,13 +161,7 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
const counted = (at: number): number =>
|
|
138
|
-
const made = graph.made[at];
|
|
139
|
-
if (made === undefined) {
|
|
140
|
-
return 0;
|
|
141
|
-
}
|
|
142
|
-
return input.mode.production ? made.production : made.production + made.test;
|
|
143
|
-
};
|
|
164
|
+
const counted = (at: number): number => countedRefs(graph.made[at], input.mode);
|
|
144
165
|
const reached = reachable(graph, held, input.mode);
|
|
145
166
|
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
146
167
|
const relations: Relation[] = [];
|
|
@@ -169,10 +190,11 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
169
190
|
if (dead[at] !== true) {
|
|
170
191
|
return;
|
|
171
192
|
}
|
|
193
|
+
const split = splitRefs(graph.made[at], input.mode);
|
|
172
194
|
candidates.push({
|
|
173
195
|
id: symbol.id,
|
|
174
|
-
productionRefs:
|
|
175
|
-
testRefs:
|
|
196
|
+
productionRefs: split.production,
|
|
197
|
+
testRefs: split.test,
|
|
176
198
|
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
177
199
|
testOfDeadCode: testOfDeadCode[at] === true,
|
|
178
200
|
});
|
|
@@ -223,11 +245,12 @@ export function reachedWithTheTests(
|
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
/**
|
|
226
|
-
* The references a loaded consumer makes that the mode counts. A
|
|
227
|
-
*
|
|
248
|
+
* The references a loaded consumer or an unheld file makes that the mode counts. A
|
|
249
|
+
* production mode counts no test reference, but a loaded consumer's where the mode classes
|
|
250
|
+
* a consumer's tests as production.
|
|
228
251
|
*/
|
|
229
252
|
function callsOf(graph: Graph, mode: Mode): readonly Edge[] {
|
|
230
|
-
return graph.consumed.filter((edge) => !mode
|
|
253
|
+
return graph.consumed.filter((edge) => !edgeWithheldBy(mode, edge));
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
@@ -280,7 +303,7 @@ function reachable(
|
|
|
280
303
|
const walk = (holding: boolean): void => {
|
|
281
304
|
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
282
305
|
for (const edge of graph.out[at] ?? []) {
|
|
283
|
-
if (!holding && mode
|
|
306
|
+
if (!holding && edgeWithheldBy(mode, edge)) {
|
|
284
307
|
continue;
|
|
285
308
|
}
|
|
286
309
|
enter(edge.to, namespaces && !edge.evaluation);
|
package/src/symbol-keys.ts
CHANGED
|
@@ -23,10 +23,9 @@ import {
|
|
|
23
23
|
import { parenthesesRemoved } from "./unwrap.ts";
|
|
24
24
|
import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
|
|
25
25
|
import type { AliasChains } from "./alias-chain.ts";
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import type { Reference } from "./references.ts";
|
|
26
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
27
|
+
import { isAnswered, UNANSWERED, type PropertyTables } from "./query.ts";
|
|
28
|
+
import type { Read, ReadRule } from "./references/read-rule.ts";
|
|
30
29
|
import type { ProjectView } from "./session.ts";
|
|
31
30
|
|
|
32
31
|
/** One element access whose key is a name, and how the access uses the member it selects. */
|
|
@@ -193,104 +192,105 @@ function wellKnownName(name: Node | undefined): string | undefined {
|
|
|
193
192
|
}
|
|
194
193
|
|
|
195
194
|
/**
|
|
196
|
-
* The
|
|
197
|
-
*
|
|
198
|
-
* `
|
|
199
|
-
*
|
|
195
|
+
* The reads the iterations of a side's files make of the iteration members of the values they
|
|
196
|
+
* iterate: a `for…of` statement and a spread element read `[Symbol.iterator]`, and a
|
|
197
|
+
* `for await…of` statement reads `[Symbol.asyncIterator]` and, which it falls back to,
|
|
198
|
+
* `[Symbol.iterator]`. Each is made by the declaration holding it. A value whose type or
|
|
199
|
+
* members the checker leaves unanswered holds the members those names could spell.
|
|
200
200
|
*/
|
|
201
|
-
export
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const fileId = held.declarations.get(nodeKey(file, file));
|
|
218
|
-
if (fileId === undefined) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
const visit = (node: Node, from: string): void => {
|
|
222
|
-
const holder = held.declarations.get(nodeKey(file, node)) ?? from;
|
|
223
|
-
if (isForOfStatement(node)) {
|
|
224
|
-
const protocols: readonly Protocol[] =
|
|
225
|
-
node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
|
|
226
|
-
iterated.push({ file, from: holder, operand: node.expression, protocols });
|
|
227
|
-
} else if (isSpreadElement(node)) {
|
|
228
|
-
iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
|
|
229
|
-
}
|
|
230
|
-
node.forEachChild((child) => {
|
|
231
|
-
visit(child, holder);
|
|
232
|
-
});
|
|
233
|
-
};
|
|
234
|
-
file.forEachChild((child) => {
|
|
235
|
-
visit(child, fileId);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
if (iterated.length === 0) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
const types = queries.typesAt(iterated.map((one) => one.operand));
|
|
242
|
-
const members = new Map<
|
|
243
|
-
number,
|
|
244
|
-
readonly { readonly name: string; readonly ids: readonly string[] }[]
|
|
245
|
-
>();
|
|
246
|
-
const membersOf = (
|
|
247
|
-
type: Type,
|
|
248
|
-
): readonly { readonly name: string; readonly ids: readonly string[] }[] => {
|
|
249
|
-
const known = members.get(type.id);
|
|
250
|
-
if (known !== undefined) {
|
|
251
|
-
return known;
|
|
252
|
-
}
|
|
253
|
-
const keyed: { name: string; ids: readonly string[] }[] = [];
|
|
254
|
-
const properties = queries.propertiesOf(type);
|
|
255
|
-
for (const property of isAnswered(properties) ? properties : []) {
|
|
256
|
-
for (const handle of property.declarations) {
|
|
257
|
-
const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
|
|
258
|
-
const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
|
|
259
|
-
if (name !== undefined) {
|
|
260
|
-
keyed.push({ name, ids: chains.declarationsOf(property) });
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
members.set(type.id, keyed);
|
|
266
|
-
return keyed;
|
|
267
|
-
};
|
|
268
|
-
const found: Reference[] = [];
|
|
269
|
-
iterated.forEach((one, index) => {
|
|
270
|
-
const type = types[index];
|
|
271
|
-
if (type === undefined || !isAnswered(type)) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const parts =
|
|
275
|
-
type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
|
|
276
|
-
const position = renderPosition(one.file, root, one.operand.getStart());
|
|
277
|
-
for (const part of isAnswered(parts) ? parts : []) {
|
|
278
|
-
for (const member of membersOf(part)) {
|
|
279
|
-
if (!(one.protocols as readonly string[]).includes(member.name)) {
|
|
280
|
-
continue;
|
|
201
|
+
export const iteration: ReadRule = {
|
|
202
|
+
start() {
|
|
203
|
+
const iterated: {
|
|
204
|
+
readonly file: SourceFile;
|
|
205
|
+
readonly from: string;
|
|
206
|
+
readonly operand: Expression;
|
|
207
|
+
readonly protocols: readonly Protocol[];
|
|
208
|
+
}[] = [];
|
|
209
|
+
return {
|
|
210
|
+
visit(node, { file, from }) {
|
|
211
|
+
if (isForOfStatement(node)) {
|
|
212
|
+
const protocols: readonly Protocol[] =
|
|
213
|
+
node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
|
|
214
|
+
iterated.push({ file, from, operand: node.expression, protocols });
|
|
215
|
+
} else if (isSpreadElement(node)) {
|
|
216
|
+
iterated.push({ file, from, operand: node.expression, protocols: ["iterator"] });
|
|
281
217
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
use: "read",
|
|
288
|
-
resolution: "keyed",
|
|
289
|
-
test: testFiles.has(position.path),
|
|
290
|
-
});
|
|
218
|
+
},
|
|
219
|
+
reads({ project, chains }) {
|
|
220
|
+
const found: Read[] = [];
|
|
221
|
+
if (iterated.length === 0) {
|
|
222
|
+
return found;
|
|
291
223
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
224
|
+
const queries = project.queries;
|
|
225
|
+
const ownPaths = project.ownPaths();
|
|
226
|
+
const types = queries.typesAt(iterated.map((one) => one.operand));
|
|
227
|
+
const members = new Map<
|
|
228
|
+
number,
|
|
229
|
+
readonly { readonly name: string; readonly ids: readonly string[] }[] | typeof UNANSWERED
|
|
230
|
+
>();
|
|
231
|
+
const membersOf = (type: Type) => {
|
|
232
|
+
const known = members.get(type.id);
|
|
233
|
+
if (known !== undefined) {
|
|
234
|
+
return known;
|
|
235
|
+
}
|
|
236
|
+
const properties = queries.propertiesOf(type);
|
|
237
|
+
const keyed: { name: string; ids: readonly string[] }[] = [];
|
|
238
|
+
for (const property of isAnswered(properties) ? properties : []) {
|
|
239
|
+
for (const handle of property.declarations) {
|
|
240
|
+
const node = ownPaths.has(handle.path)
|
|
241
|
+
? project.declarationAt(handle)?.node
|
|
242
|
+
: undefined;
|
|
243
|
+
const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
|
|
244
|
+
if (name !== undefined) {
|
|
245
|
+
keyed.push({ name, ids: chains.declarationsOf(property) });
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const result = isAnswered(properties) ? keyed : UNANSWERED;
|
|
251
|
+
members.set(type.id, result);
|
|
252
|
+
return result;
|
|
253
|
+
};
|
|
254
|
+
// A member a well-known symbol keys is named by the symbol's computed name.
|
|
255
|
+
const spelled = (protocols: readonly Protocol[]): readonly string[] =>
|
|
256
|
+
protocols.map((protocol) => `Symbol.${protocol}`);
|
|
257
|
+
iterated.forEach((one, index) => {
|
|
258
|
+
const type = types[index];
|
|
259
|
+
if (type === UNANSWERED) {
|
|
260
|
+
found.push({ kind: "held", from: one.from, names: spelled(one.protocols) });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (type === undefined) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const parts =
|
|
267
|
+
type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
|
|
268
|
+
for (const part of isAnswered(parts) ? parts : [type]) {
|
|
269
|
+
const keyed = membersOf(part);
|
|
270
|
+
if (keyed === UNANSWERED) {
|
|
271
|
+
found.push({ kind: "held", from: one.from, names: spelled(one.protocols) });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const member of keyed) {
|
|
275
|
+
if (!(one.protocols as readonly string[]).includes(member.name)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
for (const to of member.ids) {
|
|
279
|
+
found.push({
|
|
280
|
+
kind: "named",
|
|
281
|
+
from: one.from,
|
|
282
|
+
to,
|
|
283
|
+
file: one.file,
|
|
284
|
+
at: one.operand,
|
|
285
|
+
use: "read",
|
|
286
|
+
via: "keyed",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return found;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file classes the run counts references by. A test file is one a `ts.test_files`
|
|
3
|
+
* pattern or a test runner's row names (the test-runner entry source decides which), a test
|
|
4
|
+
* file no project holds, or a consumer's file a pattern names below the consumer's root.
|
|
5
|
+
* A test-support file is test code as well, decided once the references are known
|
|
6
|
+
* (`support.ts`), so a reference carries only whether a test file made it and the counters
|
|
7
|
+
* read the support class beside it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SourceFile } from "@typescript/native/ast";
|
|
11
|
+
import type { Edge } from "../graph.ts";
|
|
12
|
+
import { renderPosition } from "../position.ts";
|
|
13
|
+
import type { Reference } from "../references.ts";
|
|
14
|
+
import type { ProjectView } from "../session.ts";
|
|
15
|
+
import type { Mode } from "../sweep.ts";
|
|
16
|
+
import { bytewise } from "../text/compare.ts";
|
|
17
|
+
import { globExpression } from "../text/glob.ts";
|
|
18
|
+
|
|
19
|
+
/** One rule that classified files as test files, and how many files it matched. */
|
|
20
|
+
export interface TestFileRule {
|
|
21
|
+
/** The rule's name, as a report's own rule list spells one. */
|
|
22
|
+
readonly rule: string;
|
|
23
|
+
readonly matched: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether one file of a side is a test file. */
|
|
27
|
+
export type TestFile = (file: SourceFile) => boolean;
|
|
28
|
+
|
|
29
|
+
/** The test files of the target: each file the test-runner source names, by its file name. */
|
|
30
|
+
export function targetTestFile(named: ReadonlySet<string>): TestFile {
|
|
31
|
+
return (file) => named.has(file.fileName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The test files a pattern names, each by its path below `root`: a consumer's, below the consumer's root. */
|
|
35
|
+
export function patternTestFile(patterns: readonly string[], root: string): TestFile {
|
|
36
|
+
const expressions = patterns.map(globExpression);
|
|
37
|
+
return (file) => {
|
|
38
|
+
const path = renderPosition(file, root, 0).path;
|
|
39
|
+
return expressions.some((expression) => expression.test(path));
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The own files of a project that are test files, by their path below `root`, ascending. */
|
|
44
|
+
export function testFilePaths<Brand>(
|
|
45
|
+
project: ProjectView<Brand>,
|
|
46
|
+
root: string,
|
|
47
|
+
testFile: TestFile,
|
|
48
|
+
): readonly string[] {
|
|
49
|
+
return project
|
|
50
|
+
.ownSourceFiles()
|
|
51
|
+
.filter(testFile)
|
|
52
|
+
.map((file) => renderPosition(file, root, 0).path)
|
|
53
|
+
.sort(bytewise);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether test code made one reference: a test file, or a test-support file of the target. */
|
|
57
|
+
export function madeByTestCode(reference: Reference, support: ReadonlySet<string>): boolean {
|
|
58
|
+
return (
|
|
59
|
+
reference.test || (reference.consumer === undefined && support.has(reference.position.path))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether the mode classes a reference test code made as a test one: any but a loaded consumer's it classes as production. */
|
|
64
|
+
export function classedAsTest(test: boolean, consumer: boolean, mode: Mode): boolean {
|
|
65
|
+
return test && !(consumer && mode.consumerTestsProduction === true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether a production mode counts no reference of this one: test code made it, as the mode classes it. */
|
|
69
|
+
export function withheldBy(
|
|
70
|
+
mode: Mode,
|
|
71
|
+
reference: Reference,
|
|
72
|
+
support: ReadonlySet<string>,
|
|
73
|
+
): boolean {
|
|
74
|
+
return (
|
|
75
|
+
mode.production &&
|
|
76
|
+
classedAsTest(madeByTestCode(reference, support), reference.consumer !== undefined, mode)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether a production mode follows no edge like this one: {@link withheldBy} for a graph edge. */
|
|
81
|
+
export function edgeWithheldBy(mode: Mode, edge: Edge): boolean {
|
|
82
|
+
return mode.production && classedAsTest(edge.test, edge.consumer, mode);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The rule a file the analysis classified as test-support code is counted under. */
|
|
86
|
+
const TEST_SUPPORT_RULE = "test-support";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
90
|
+
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
91
|
+
* its pattern in the configured list.
|
|
92
|
+
*/
|
|
93
|
+
function patternRule(index: number): string {
|
|
94
|
+
return `test-file-pattern-${String(index + 1)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
99
|
+
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
100
|
+
*/
|
|
101
|
+
export function testFileRulesOf(
|
|
102
|
+
testFiles: readonly string[],
|
|
103
|
+
paths: readonly string[],
|
|
104
|
+
supportFiles: readonly string[] = [],
|
|
105
|
+
): readonly TestFileRule[] {
|
|
106
|
+
const distinct = [...new Set(paths)];
|
|
107
|
+
const support = new Set(supportFiles).size;
|
|
108
|
+
return [
|
|
109
|
+
...testFiles.map((pattern, index) => {
|
|
110
|
+
const expression = globExpression(pattern);
|
|
111
|
+
return {
|
|
112
|
+
rule: patternRule(index),
|
|
113
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
|
|
117
|
+
].sort((a, b) => bytewise(a.rule, b.rule));
|
|
118
|
+
}
|