@cplieger/deadset-ts 3.0.0 → 3.2.0
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/README.md +3 -3
- package/dist/src/alias-chain.js +96 -0
- package/dist/src/analysis.js +32 -17
- package/dist/src/exempt-classes.js +36 -0
- package/dist/src/exempt.js +132 -0
- package/dist/src/graph.js +5 -1
- package/dist/src/matrix.js +138 -0
- package/dist/src/references.js +78 -100
- package/dist/src/roots.js +14 -37
- package/dist/src/run.js +33 -1
- package/dist/src/session.js +16 -5
- package/dist/src/sweep.js +47 -41
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/alias-chain.ts +135 -0
- package/src/analysis.ts +53 -32
- package/src/components.ts +1 -1
- package/src/exempt-classes.ts +69 -0
- package/src/exempt.ts +200 -0
- package/src/graph.ts +12 -3
- package/src/matrix.ts +212 -0
- package/src/references.ts +108 -113
- package/src/roots.ts +15 -46
- package/src/run.ts +39 -1
- package/src/session.ts +18 -5
- package/src/sweep.ts +75 -46
- package/src/version.ts +1 -1
package/src/matrix.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One run's projects as one matrix: every declaration any project holds, keyed by its
|
|
3
|
+
* position and carrying the configurations that hold it, swept once per configuration
|
|
4
|
+
* and intersected, with the dead components computed once over every configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { componentsOf, type Component } from "./components.ts";
|
|
8
|
+
import { graphOf, type Graph } from "./graph.ts";
|
|
9
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
10
|
+
import { byPosition } from "./position.ts";
|
|
11
|
+
import type { Reference } from "./references.ts";
|
|
12
|
+
import type { Root } from "./roots.ts";
|
|
13
|
+
import { sweep, type Candidate, type Liveness, type Relation, type SweepInput } from "./sweep.ts";
|
|
14
|
+
|
|
15
|
+
/** What the passes answered for one configuration of the run. */
|
|
16
|
+
export interface Configured {
|
|
17
|
+
/** The name discovery gives the configuration's project. */
|
|
18
|
+
readonly configuration: string;
|
|
19
|
+
readonly symbols: readonly InventorySymbol[];
|
|
20
|
+
readonly references: readonly Reference[];
|
|
21
|
+
readonly roots: readonly Root[];
|
|
22
|
+
/** The paths the reference pass classified as test files. */
|
|
23
|
+
readonly testFiles: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The run's configurations, indexed for the sweep. */
|
|
27
|
+
export interface Matrix {
|
|
28
|
+
/** The configurations, in the run's order. */
|
|
29
|
+
readonly configurations: readonly string[];
|
|
30
|
+
/** Per configuration, in the same order, the graph of what that configuration holds. */
|
|
31
|
+
readonly graphs: readonly Graph[];
|
|
32
|
+
/**
|
|
33
|
+
* The graph over every configuration at once: every declaration any configuration
|
|
34
|
+
* holds, once per position and in site order, and every reference any configuration
|
|
35
|
+
* makes, a reference several configurations make at one position counted once.
|
|
36
|
+
*/
|
|
37
|
+
readonly union: Graph;
|
|
38
|
+
/**
|
|
39
|
+
* Per declaration of {@link Matrix.union}, at its position there, the configurations
|
|
40
|
+
* whose project holds it, in the run's order.
|
|
41
|
+
*/
|
|
42
|
+
readonly heldIn: readonly (readonly string[])[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One declaration dead in every configuration that holds it. */
|
|
46
|
+
export interface MatrixCandidate extends Candidate {
|
|
47
|
+
/** The configurations the declaration is dead in, which are every one that holds it. */
|
|
48
|
+
readonly configurations: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One run's sweep. */
|
|
52
|
+
export interface SweepResult {
|
|
53
|
+
/**
|
|
54
|
+
* Per declaration, the relations that hold it live in at least one configuration,
|
|
55
|
+
* reference counting first. A declaration no relation holds live anywhere has no entry.
|
|
56
|
+
*/
|
|
57
|
+
readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
|
|
58
|
+
/** The declarations dead in every configuration that holds them, in site order. */
|
|
59
|
+
readonly candidates: readonly MatrixCandidate[];
|
|
60
|
+
/** The dead components, each before every component it reaches, each identifier once per run. */
|
|
61
|
+
readonly components: readonly Component[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Two strings ordered bytewise. */
|
|
65
|
+
function compare(a: string, b: string): number {
|
|
66
|
+
if (a === b) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
return a < b ? -1 : 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What makes one reference one reference, whichever configuration made it. */
|
|
73
|
+
function referenceKey(reference: Reference): string {
|
|
74
|
+
const { path, line, column } = reference.position;
|
|
75
|
+
return [reference.from, reference.to, path, String(line), String(column), reference.use].join(
|
|
76
|
+
"\u0000",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Indexes what the passes answered for each configuration of one run, in the run's order.
|
|
82
|
+
*
|
|
83
|
+
* A declaration is keyed across configurations by its identifier, which is its rendered
|
|
84
|
+
* position and so is the same in every configuration that compiles its file; the fields
|
|
85
|
+
* of the first configuration that holds it are the ones kept. A declaration one
|
|
86
|
+
* configuration alone holds is in the union with that one configuration, so it is
|
|
87
|
+
* judged where it exists rather than skipped.
|
|
88
|
+
*
|
|
89
|
+
* One reference written at one position of a file several configurations compile is seen
|
|
90
|
+
* once per configuration and is one reference, so the union holds it once; two references
|
|
91
|
+
* one configuration makes at one position are two.
|
|
92
|
+
*/
|
|
93
|
+
export function matrixOf(per: readonly Configured[]): Matrix {
|
|
94
|
+
const symbols: InventorySymbol[] = [];
|
|
95
|
+
const holders = new Map<string, string[]>();
|
|
96
|
+
const references: Reference[] = [];
|
|
97
|
+
const firstSeen = new Map<string, number>();
|
|
98
|
+
per.forEach((one, index) => {
|
|
99
|
+
for (const symbol of one.symbols) {
|
|
100
|
+
const held = holders.get(symbol.id);
|
|
101
|
+
if (held === undefined) {
|
|
102
|
+
holders.set(symbol.id, [one.configuration]);
|
|
103
|
+
symbols.push(symbol);
|
|
104
|
+
} else if (!held.includes(one.configuration)) {
|
|
105
|
+
held.push(one.configuration);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const reference of one.references) {
|
|
109
|
+
const key = referenceKey(reference);
|
|
110
|
+
const seen = firstSeen.get(key);
|
|
111
|
+
if (seen === undefined) {
|
|
112
|
+
firstSeen.set(key, index);
|
|
113
|
+
} else if (seen !== index) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
references.push(reference);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
configurations: per.map((one) => one.configuration),
|
|
123
|
+
graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
|
|
124
|
+
union: graphOf(
|
|
125
|
+
symbols,
|
|
126
|
+
references,
|
|
127
|
+
per.flatMap((one) => one.roots),
|
|
128
|
+
[...new Set(per.flatMap((one) => one.testFiles))],
|
|
129
|
+
),
|
|
130
|
+
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Answers which declarations of the run are dead in every configuration that holds them.
|
|
136
|
+
*
|
|
137
|
+
* Liveness is a question about one configuration, because a file another configuration
|
|
138
|
+
* does not compile makes no reference there and a root it does not hold seeds nothing
|
|
139
|
+
* there, so each configuration is swept on its own graph under the run's input. A
|
|
140
|
+
* declaration is a candidate when every configuration holding it judged it one: a
|
|
141
|
+
* reference under one configuration is a reference, so what one project uses is never
|
|
142
|
+
* reported, and a configuration that does not hold a declaration says nothing about it.
|
|
143
|
+
* The candidate's relation is reference counting where every configuration found it so,
|
|
144
|
+
* and reachability otherwise; its counts are the union's, one per reference.
|
|
145
|
+
*
|
|
146
|
+
* The components are computed once, over the union and the intersected dead set, so one
|
|
147
|
+
* dead component is one entry of the run with one identifier.
|
|
148
|
+
*/
|
|
149
|
+
export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
150
|
+
const per = matrix.graphs.map((graph) => sweep(graph, input));
|
|
151
|
+
const byConfiguration = new Map<string, ReadonlyMap<string, Candidate>>(
|
|
152
|
+
matrix.configurations.map((configuration, index) => [
|
|
153
|
+
configuration,
|
|
154
|
+
new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const union = matrix.union;
|
|
159
|
+
const dead = union.symbols.map(() => false);
|
|
160
|
+
const testOfDeadCode = union.symbols.map(() => false);
|
|
161
|
+
const candidates: MatrixCandidate[] = [];
|
|
162
|
+
union.symbols.forEach((symbol, at) => {
|
|
163
|
+
const held = matrix.heldIn[at] ?? [];
|
|
164
|
+
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
165
|
+
if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const all = found.filter((candidate): candidate is Candidate => candidate !== undefined);
|
|
169
|
+
const admitted = all.every((candidate) => candidate.testOfDeadCode);
|
|
170
|
+
dead[at] = true;
|
|
171
|
+
testOfDeadCode[at] = admitted;
|
|
172
|
+
candidates.push({
|
|
173
|
+
id: symbol.id,
|
|
174
|
+
productionRefs: union.made[at]?.production ?? 0,
|
|
175
|
+
testRefs: union.made[at]?.test ?? 0,
|
|
176
|
+
relation: all.every((candidate) => candidate.relation === "reference-counting")
|
|
177
|
+
? "reference-counting"
|
|
178
|
+
: "reachability",
|
|
179
|
+
testOfDeadCode: admitted,
|
|
180
|
+
configurations: held,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
liveUnder: liveAnywhere(union.symbols, per),
|
|
186
|
+
candidates,
|
|
187
|
+
components: componentsOf(union, dead, testOfDeadCode),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The relations in the order a declaration's entry lists them. */
|
|
192
|
+
const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Per declaration, in site order, every relation that holds it live in at least one
|
|
196
|
+
* configuration.
|
|
197
|
+
*/
|
|
198
|
+
function liveAnywhere(
|
|
199
|
+
symbols: readonly InventorySymbol[],
|
|
200
|
+
per: readonly Liveness[],
|
|
201
|
+
): ReadonlyMap<string, readonly Relation[]> {
|
|
202
|
+
const live = new Map<string, readonly Relation[]>();
|
|
203
|
+
for (const symbol of symbols) {
|
|
204
|
+
const relations = RELATIONS.filter((relation) =>
|
|
205
|
+
per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)),
|
|
206
|
+
);
|
|
207
|
+
if (relations.length > 0) {
|
|
208
|
+
live.set(symbol.id, relations);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return live;
|
|
212
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isArrayLiteralExpression,
|
|
3
3
|
isBinaryExpression,
|
|
4
|
+
isCallExpression,
|
|
4
5
|
isDeleteExpression,
|
|
5
6
|
isElementAccessExpression,
|
|
6
7
|
isExportDeclaration,
|
|
8
|
+
isExternalModuleReference,
|
|
7
9
|
isForInStatement,
|
|
8
10
|
isForOfStatement,
|
|
11
|
+
isImportDeclaration,
|
|
12
|
+
isImportEqualsDeclaration,
|
|
9
13
|
isNamespaceExport,
|
|
14
|
+
isNoSubstitutionTemplateLiteral,
|
|
10
15
|
isObjectLiteralExpression,
|
|
11
16
|
isParenthesizedExpression,
|
|
12
17
|
isPostfixUnaryExpression,
|
|
@@ -16,11 +21,13 @@ import {
|
|
|
16
21
|
isShorthandPropertyAssignment,
|
|
17
22
|
isSpreadAssignment,
|
|
18
23
|
isSpreadElement,
|
|
24
|
+
isStringLiteral,
|
|
19
25
|
SyntaxKind,
|
|
20
26
|
type Node,
|
|
21
27
|
type SourceFile,
|
|
22
28
|
} from "@typescript/native/unstable/ast";
|
|
23
|
-
import {
|
|
29
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
30
|
+
import { aliasChains } from "./alias-chain.ts";
|
|
24
31
|
import { globExpression } from "./glob.ts";
|
|
25
32
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
26
33
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
@@ -37,13 +44,18 @@ import type { ProjectView } from "./session.ts";
|
|
|
37
44
|
*
|
|
38
45
|
* What the pass records and what it does not: a reference carries its use, its
|
|
39
46
|
* position, the declaration that encloses it and the classification of the file that
|
|
40
|
-
* made it
|
|
41
|
-
*
|
|
42
|
-
*
|
|
47
|
+
* made it, and every place that imports one of the project's own modules is an
|
|
48
|
+
* evaluation of that module's file. It carries no mode. Which of the references a run
|
|
49
|
+
* counts is the reading the sweep makes of this set, so the mode is one value the run
|
|
50
|
+
* decides once and this pass never sees it.
|
|
43
51
|
*/
|
|
44
52
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
53
|
+
/**
|
|
54
|
+
* How one reference uses the declaration it names. An `evaluation` names a file: the
|
|
55
|
+
* place it is written imports the module, which runs the module's top level and reads
|
|
56
|
+
* none of what it exports. A `read` of a file reads the module's namespace.
|
|
57
|
+
*/
|
|
58
|
+
export type Use = "read" | "write" | "evaluation";
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
61
|
* Which accessor answered for one reference, so that the cost of a run is attributable
|
|
@@ -299,10 +311,52 @@ interface Link {
|
|
|
299
311
|
readonly alias: boolean;
|
|
300
312
|
}
|
|
301
313
|
|
|
314
|
+
/** One place the walk found that evaluates a module, and the declaration that holds it. */
|
|
315
|
+
interface Evaluation {
|
|
316
|
+
/** The module specifier, whose symbol is the module evaluated. */
|
|
317
|
+
readonly node: Node;
|
|
318
|
+
readonly from: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
302
321
|
/** Everything one file's walk found to resolve. */
|
|
303
322
|
interface FileSites {
|
|
304
323
|
readonly uses: readonly Site[];
|
|
305
324
|
readonly links: readonly Link[];
|
|
325
|
+
readonly evaluations: readonly Evaluation[];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Whether one node is a string literal or a template literal with no substitution. */
|
|
329
|
+
function isLiteralText(node: Node | undefined): node is Node {
|
|
330
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The module specifier of the module one node evaluates, if it evaluates one: an import
|
|
335
|
+
* declaration, an import-equals declaration of an external module, an export
|
|
336
|
+
* declaration that names a module, and an `import()` call whose argument is literal
|
|
337
|
+
* text. A type-only form evaluates nothing. An `import()` of a computed argument names
|
|
338
|
+
* no module this pass can know.
|
|
339
|
+
*/
|
|
340
|
+
function evaluatedBy(node: Node): Node | undefined {
|
|
341
|
+
if (isImportDeclaration(node)) {
|
|
342
|
+
return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword
|
|
343
|
+
? undefined
|
|
344
|
+
: node.moduleSpecifier;
|
|
345
|
+
}
|
|
346
|
+
if (isExportDeclaration(node)) {
|
|
347
|
+
return node.isTypeOnly ? undefined : node.moduleSpecifier;
|
|
348
|
+
}
|
|
349
|
+
if (isImportEqualsDeclaration(node)) {
|
|
350
|
+
const reference = node.moduleReference;
|
|
351
|
+
return !node.isTypeOnly && isExternalModuleReference(reference)
|
|
352
|
+
? reference.expression
|
|
353
|
+
: undefined;
|
|
354
|
+
}
|
|
355
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
356
|
+
const [argument] = node.arguments;
|
|
357
|
+
return isLiteralText(argument) ? argument : undefined;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
306
360
|
}
|
|
307
361
|
|
|
308
362
|
/**
|
|
@@ -363,6 +417,7 @@ function sitesOf(
|
|
|
363
417
|
): FileSites {
|
|
364
418
|
const found: Site[] = [];
|
|
365
419
|
const links: Link[] = [];
|
|
420
|
+
const evaluations: Evaluation[] = [];
|
|
366
421
|
const writes = new Set<number>();
|
|
367
422
|
const declared = new Set<number>();
|
|
368
423
|
const shorthands = new Map<number, Node>();
|
|
@@ -401,6 +456,10 @@ function sitesOf(
|
|
|
401
456
|
markNames(node);
|
|
402
457
|
markStores(node, writes);
|
|
403
458
|
links.push(...linksOf(file, node, declarations, enclosing));
|
|
459
|
+
const evaluated = evaluatedBy(node);
|
|
460
|
+
if (evaluated !== undefined) {
|
|
461
|
+
evaluations.push({ node: evaluated, from: enclosing });
|
|
462
|
+
}
|
|
404
463
|
const held = declarations.get(nodeKey(file, node));
|
|
405
464
|
const outer = enclosing;
|
|
406
465
|
if (held !== undefined) {
|
|
@@ -411,7 +470,7 @@ function sitesOf(
|
|
|
411
470
|
};
|
|
412
471
|
|
|
413
472
|
file.forEachChild(visit);
|
|
414
|
-
return { uses: found, links };
|
|
473
|
+
return { uses: found, links, evaluations };
|
|
415
474
|
}
|
|
416
475
|
|
|
417
476
|
/**
|
|
@@ -431,12 +490,6 @@ function compare(a: string, b: string): number {
|
|
|
431
490
|
return a < b ? -1 : 1;
|
|
432
491
|
}
|
|
433
492
|
|
|
434
|
-
/** One declaration one symbol reaches, and whether a step along an alias reached it. */
|
|
435
|
-
interface Target {
|
|
436
|
-
readonly id: string;
|
|
437
|
-
readonly stepped: boolean;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
493
|
/**
|
|
441
494
|
* Every reference one project's own files make to the project's own declarations, in
|
|
442
495
|
* position order, and the test-file rules that classified those files.
|
|
@@ -448,7 +501,8 @@ interface Target {
|
|
|
448
501
|
* The order is by the referencing position, then by the declaration named, the
|
|
449
502
|
* declaration referencing it and the use. That is a total order because one name
|
|
450
503
|
* references one declaration once from one declaration, while a chain of aliases reaches
|
|
451
|
-
* several declarations at one position
|
|
504
|
+
* several declarations at one position and a bare star re-export's specifier both reads
|
|
505
|
+
* and evaluates the module it names.
|
|
452
506
|
*/
|
|
453
507
|
export function references<Brand>(
|
|
454
508
|
project: ProjectView<Brand>,
|
|
@@ -462,92 +516,21 @@ export function references<Brand>(
|
|
|
462
516
|
const matched = options.testFiles.map(() => 0);
|
|
463
517
|
const found: Reference[] = [];
|
|
464
518
|
const tests: string[] = [];
|
|
465
|
-
const
|
|
466
|
-
const steps = new Map<number, TSSymbol | undefined>();
|
|
519
|
+
const chains = aliasChains(project, held);
|
|
467
520
|
let batched = 0;
|
|
468
521
|
let fileBatches = 0;
|
|
469
522
|
let residueFallbacks = 0;
|
|
470
523
|
let shorthandLookups = 0;
|
|
471
|
-
let aliasSteps = 0;
|
|
472
524
|
|
|
473
525
|
const files = project.ownSourceFiles();
|
|
474
|
-
const
|
|
526
|
+
const fileIds = new Set(
|
|
527
|
+
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
528
|
+
);
|
|
475
529
|
|
|
476
530
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
477
531
|
const declaredAt = (node: Node): string | undefined =>
|
|
478
532
|
declarations.get(nodeKey(node.getSourceFile(), node));
|
|
479
533
|
|
|
480
|
-
/**
|
|
481
|
-
* The declarations of the inventory one symbol is.
|
|
482
|
-
*
|
|
483
|
-
* A declaration handle carries the file it is in, so the question is answered before
|
|
484
|
-
* the handle is resolved: a declaration outside the project's own files belongs to
|
|
485
|
-
* another program and is never read, which is what keeps the pass from fetching a
|
|
486
|
-
* library file to discover that the inventory does not hold what is in it.
|
|
487
|
-
*/
|
|
488
|
-
const declarationsOf = (symbol: TSSymbol): string[] => {
|
|
489
|
-
const ids: string[] = [];
|
|
490
|
-
for (const handle of symbol.declarations) {
|
|
491
|
-
if (!ownFiles.has(handle.path)) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const node = project.declarationAt(handle)?.node;
|
|
495
|
-
const id = node === undefined ? undefined : declaredAt(node);
|
|
496
|
-
if (id !== undefined && !ids.includes(id)) {
|
|
497
|
-
ids.push(id);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return ids;
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* What one alias names, one link along, asked once per alias whatever the number of
|
|
505
|
-
* chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
|
|
506
|
-
* the condition the step asserts on: a default export of an expression is written in
|
|
507
|
-
* an alias form and names nothing declared elsewhere, so its chain ends at itself.
|
|
508
|
-
*/
|
|
509
|
-
const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
|
|
510
|
-
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
511
|
-
return undefined;
|
|
512
|
-
}
|
|
513
|
-
if (steps.has(symbol.id)) {
|
|
514
|
-
return steps.get(symbol.id);
|
|
515
|
-
}
|
|
516
|
-
const next = project.aliasStepOf(symbol);
|
|
517
|
-
aliasSteps += 1;
|
|
518
|
-
steps.set(symbol.id, next);
|
|
519
|
-
return next;
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* The declarations one symbol names: the declaration it is, and every link of the
|
|
524
|
-
* alias chain it stands at the head of. A re-export something imports is used and so
|
|
525
|
-
* is the declaration behind it, so both are named; a link this project does not
|
|
526
|
-
* declare is stepped over rather than recorded.
|
|
527
|
-
*/
|
|
528
|
-
const targetsOf = (symbol: TSSymbol): readonly Target[] => {
|
|
529
|
-
const cached = reached.get(symbol.id);
|
|
530
|
-
if (cached !== undefined) {
|
|
531
|
-
return cached;
|
|
532
|
-
}
|
|
533
|
-
const targets: Target[] = [];
|
|
534
|
-
const walked = new Set<number>();
|
|
535
|
-
let at: TSSymbol | undefined = symbol;
|
|
536
|
-
let stepped = false;
|
|
537
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
538
|
-
walked.add(at.id);
|
|
539
|
-
for (const id of declarationsOf(at)) {
|
|
540
|
-
if (!targets.some((target) => target.id === id)) {
|
|
541
|
-
targets.push({ id, stepped });
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
at = stepOf(at);
|
|
545
|
-
stepped = true;
|
|
546
|
-
}
|
|
547
|
-
reached.set(symbol.id, targets);
|
|
548
|
-
return targets;
|
|
549
|
-
};
|
|
550
|
-
|
|
551
534
|
/**
|
|
552
535
|
* The declarations one re-export carries forward: for an alias, the first link along
|
|
553
536
|
* its chain that the inventory declares, stepping over the links it does not, such as
|
|
@@ -555,22 +538,8 @@ export function references<Brand>(
|
|
|
555
538
|
* Each later link is a declaration of its own that references the next, so one link
|
|
556
539
|
* per re-export is what lets a chain be followed one declaration at a time.
|
|
557
540
|
*/
|
|
558
|
-
const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] =>
|
|
559
|
-
|
|
560
|
-
return declarationsOf(symbol);
|
|
561
|
-
}
|
|
562
|
-
const walked = new Set<number>([symbol.id]);
|
|
563
|
-
let at = stepOf(symbol);
|
|
564
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
565
|
-
walked.add(at.id);
|
|
566
|
-
const ids = declarationsOf(at);
|
|
567
|
-
if (ids.length > 0) {
|
|
568
|
-
return ids;
|
|
569
|
-
}
|
|
570
|
-
at = stepOf(at);
|
|
571
|
-
}
|
|
572
|
-
return [];
|
|
573
|
-
};
|
|
541
|
+
const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] =>
|
|
542
|
+
link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
|
|
574
543
|
|
|
575
544
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
576
545
|
const symbolFor = (site: Site, answered: ReadonlyMap<Node, TSSymbol>): TSSymbol | undefined => {
|
|
@@ -598,15 +567,19 @@ export function references<Brand>(
|
|
|
598
567
|
if (test) {
|
|
599
568
|
tests.push(path);
|
|
600
569
|
}
|
|
601
|
-
const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
570
|
+
const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
602
571
|
|
|
603
572
|
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
604
|
-
// the uses
|
|
605
|
-
//
|
|
606
|
-
//
|
|
573
|
+
// the uses and the evaluated modules' specifiers last, each node once: a bare star
|
|
574
|
+
// re-export's specifier is a link and an evaluation both. A shorthand is left out
|
|
575
|
+
// of the batch: its name resolves to the property the literal declares, so the
|
|
576
|
+
// batch's answer for it would name something this project's inventory never holds.
|
|
607
577
|
const batching = [
|
|
608
|
-
...
|
|
609
|
-
|
|
578
|
+
...new Set([
|
|
579
|
+
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
580
|
+
...links.map((link) => link.node),
|
|
581
|
+
...evaluations.map((evaluation) => evaluation.node),
|
|
582
|
+
]),
|
|
610
583
|
];
|
|
611
584
|
const answered = new Map<Node, TSSymbol>();
|
|
612
585
|
batched += batching.length;
|
|
@@ -640,6 +613,26 @@ export function references<Brand>(
|
|
|
640
613
|
}
|
|
641
614
|
}
|
|
642
615
|
|
|
616
|
+
for (const evaluation of evaluations) {
|
|
617
|
+
const module = answered.get(evaluation.node);
|
|
618
|
+
if (module === undefined) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const position = renderPosition(file, targetRoot, evaluation.node.getStart());
|
|
622
|
+
for (const id of chains.declarationsOf(module)) {
|
|
623
|
+
if (fileIds.has(id)) {
|
|
624
|
+
found.push({
|
|
625
|
+
from: evaluation.from,
|
|
626
|
+
to: id,
|
|
627
|
+
position,
|
|
628
|
+
use: "evaluation",
|
|
629
|
+
resolution: "batch",
|
|
630
|
+
test,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
643
636
|
for (const site of uses) {
|
|
644
637
|
const direct: Resolution =
|
|
645
638
|
site.shorthand !== undefined
|
|
@@ -652,7 +645,9 @@ export function references<Brand>(
|
|
|
652
645
|
continue;
|
|
653
646
|
}
|
|
654
647
|
const position = renderPosition(file, targetRoot, site.node.getStart());
|
|
655
|
-
|
|
648
|
+
// A re-export something imports is used and so is the declaration behind it, so
|
|
649
|
+
// the use names every link of the chain its name stands at the head of.
|
|
650
|
+
for (const target of chains.chainOf(symbol)) {
|
|
656
651
|
found.push({
|
|
657
652
|
from: site.from,
|
|
658
653
|
to: target.id,
|
|
@@ -680,6 +675,6 @@ export function references<Brand>(
|
|
|
680
675
|
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
681
676
|
.sort((a, b) => compare(a.rule, b.rule)),
|
|
682
677
|
testFilePaths: tests.sort(compare),
|
|
683
|
-
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
|
|
678
|
+
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
|
|
684
679
|
};
|
|
685
680
|
}
|
package/src/roots.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* The root set of one project: every declaration the analysis keeps live without a
|
|
3
3
|
* reference, and why.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* something outside the program reaches is live under
|
|
9
|
-
* candidate under
|
|
5
|
+
* Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
|
|
6
|
+
* that names a caller is live under reference counting as well, which the sweep
|
|
7
|
+
* decides; a root that only supposes a consumer is not, so an exported declaration
|
|
8
|
+
* something outside the program reaches is live under reachability and still a
|
|
9
|
+
* candidate under reference counting.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
13
|
-
import
|
|
13
|
+
import { aliasChains } from "./alias-chain.ts";
|
|
14
14
|
import { REPOSITORY_DOCUMENT } from "./config.ts";
|
|
15
15
|
import { entryPoints, type EntryRule } from "./entry-points.ts";
|
|
16
16
|
import type { Finding } from "./finding.ts";
|
|
@@ -63,7 +63,8 @@ export interface Roots {
|
|
|
63
63
|
* reason it is live.
|
|
64
64
|
*
|
|
65
65
|
* The member says which relation the set feeds: every declaration here is live
|
|
66
|
-
* under reachability
|
|
66
|
+
* under reachability. A root of a kind that names a caller is also live under
|
|
67
|
+
* reference counting, which the sweep decides.
|
|
67
68
|
*/
|
|
68
69
|
readonly liveUnderReachability: readonly Root[];
|
|
69
70
|
/**
|
|
@@ -230,12 +231,10 @@ export function roots<Brand>(
|
|
|
230
231
|
enter(point.file, point.rule, point.source, point.exports);
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
const exportsOf = exportTables(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
files.byName,
|
|
238
|
-
);
|
|
234
|
+
const exportsOf = exportTables(project, held, [
|
|
235
|
+
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
236
|
+
...published,
|
|
237
|
+
]);
|
|
239
238
|
for (const entry of entered) {
|
|
240
239
|
const id = fileIds.get(entry.file.fileName);
|
|
241
240
|
if (id === undefined) {
|
|
@@ -282,7 +281,6 @@ function exportTables<Brand>(
|
|
|
282
281
|
project: ProjectView<Brand>,
|
|
283
282
|
held: Inventory,
|
|
284
283
|
wanted: readonly SourceFile[],
|
|
285
|
-
ownFiles: ReadonlyMap<string, SourceFile>,
|
|
286
284
|
): ReadonlyMap<string, readonly string[]> {
|
|
287
285
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
288
286
|
const tables = new Map<string, readonly string[]>();
|
|
@@ -290,36 +288,7 @@ function exportTables<Brand>(
|
|
|
290
288
|
return tables;
|
|
291
289
|
}
|
|
292
290
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
293
|
-
const chains =
|
|
294
|
-
|
|
295
|
-
const chainOf = (symbol: TSSymbol): readonly string[] => {
|
|
296
|
-
const cached = chains.get(symbol.id);
|
|
297
|
-
if (cached !== undefined) {
|
|
298
|
-
return cached;
|
|
299
|
-
}
|
|
300
|
-
const ids: string[] = [];
|
|
301
|
-
const walked = new Set<number>();
|
|
302
|
-
let at: TSSymbol | undefined = symbol;
|
|
303
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
304
|
-
walked.add(at.id);
|
|
305
|
-
for (const handle of at.declarations) {
|
|
306
|
-
if (!ownFiles.has(handle.path)) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const node = project.declarationAt(handle)?.node;
|
|
310
|
-
const id =
|
|
311
|
-
node === undefined
|
|
312
|
-
? undefined
|
|
313
|
-
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
314
|
-
if (id !== undefined && !ids.includes(id)) {
|
|
315
|
-
ids.push(id);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
at = project.aliasStepOf(at);
|
|
319
|
-
}
|
|
320
|
-
chains.set(symbol.id, ids);
|
|
321
|
-
return ids;
|
|
322
|
-
};
|
|
291
|
+
const chains = aliasChains(project, held);
|
|
323
292
|
|
|
324
293
|
unique.forEach((file, index) => {
|
|
325
294
|
const module = modules[index];
|
|
@@ -329,8 +298,8 @@ function exportTables<Brand>(
|
|
|
329
298
|
}
|
|
330
299
|
const ids = new Set<string>();
|
|
331
300
|
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
332
|
-
for (const
|
|
333
|
-
ids.add(id);
|
|
301
|
+
for (const target of chains.chainOf(exported)) {
|
|
302
|
+
ids.add(target.id);
|
|
334
303
|
}
|
|
335
304
|
}
|
|
336
305
|
tables.set(file.fileName, [...ids]);
|