@cplieger/deadset-ts 3.1.0 → 3.3.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/analysis.js +39 -13
- package/dist/src/decorator.js +64 -0
- package/dist/src/exempt-classes.js +83 -0
- package/dist/src/exempt.js +152 -0
- package/dist/src/interface-satisfaction.js +310 -0
- package/dist/src/member-names.js +76 -0
- package/dist/src/references.js +22 -3
- package/dist/src/reflective-lookup.js +96 -0
- package/dist/src/run.js +33 -1
- package/dist/src/sweep.js +35 -25
- package/dist/src/template-field.js +173 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/analysis.ts +59 -14
- package/src/decorator.ts +74 -0
- package/src/exempt-classes.ts +132 -0
- package/src/exempt.ts +226 -0
- package/src/interface-satisfaction.ts +417 -0
- package/src/member-names.ts +89 -0
- package/src/references.ts +41 -6
- package/src/reflective-lookup.ts +127 -0
- package/src/run.ts +39 -1
- package/src/sweep.ts +60 -25
- package/src/template-field.ts +214 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members of one project's inventory by the name each is written with, for the
|
|
3
|
+
* classes whose evidence is a name rather than a type relation: a template and a
|
|
4
|
+
* string key both hold a name and nothing else, so each holds back every member of
|
|
5
|
+
* that name its own mechanism can reach.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isIdentifier,
|
|
10
|
+
isNumericLiteral,
|
|
11
|
+
isPrivateIdentifier,
|
|
12
|
+
isStringLiteral,
|
|
13
|
+
type Node,
|
|
14
|
+
type SourceFile,
|
|
15
|
+
} from "@typescript/native/unstable/ast";
|
|
16
|
+
import { nodeKey, type Inventory, type InventorySymbol, type SymbolKind } from "./inventory.ts";
|
|
17
|
+
|
|
18
|
+
/** The kinds of declaration a member name can name. */
|
|
19
|
+
const MEMBER_KINDS: ReadonlySet<SymbolKind> = new Set([
|
|
20
|
+
"method",
|
|
21
|
+
"class-member",
|
|
22
|
+
"interface-method",
|
|
23
|
+
"type-member",
|
|
24
|
+
"enum-member",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The name a member declaration is written with: an identifier's text, a private
|
|
29
|
+
* name's text without its `#`, a string key's value and a numeric key's text. A
|
|
30
|
+
* private name is indexed under the name a template or a string would spell, so the
|
|
31
|
+
* rule that no such evidence holds it back is the framework's alone. A computed key
|
|
32
|
+
* names nothing a string can be equal to before the program runs.
|
|
33
|
+
*/
|
|
34
|
+
function writtenName(name: Node | undefined): string | undefined {
|
|
35
|
+
if (name === undefined) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (isPrivateIdentifier(name)) {
|
|
39
|
+
return name.text.replace(/^#/u, "");
|
|
40
|
+
}
|
|
41
|
+
if (isIdentifier(name)) {
|
|
42
|
+
return name.text;
|
|
43
|
+
}
|
|
44
|
+
if (isStringLiteral(name) || isNumericLiteral(name)) {
|
|
45
|
+
return name.text;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every member the inventory holds, keyed by the name it is written with, each
|
|
52
|
+
* member once and in inventory order. A getter and a setter of one name are one
|
|
53
|
+
* member.
|
|
54
|
+
*/
|
|
55
|
+
export function memberNames(
|
|
56
|
+
files: readonly SourceFile[],
|
|
57
|
+
held: Inventory,
|
|
58
|
+
): ReadonlyMap<string, readonly InventorySymbol[]> {
|
|
59
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
60
|
+
const named = new Map<string, string>();
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const visit = (node: Node): void => {
|
|
63
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
64
|
+
const symbol = id === undefined ? undefined : byId.get(id);
|
|
65
|
+
if (symbol !== undefined && MEMBER_KINDS.has(symbol.kind)) {
|
|
66
|
+
const name = writtenName((node as { readonly name?: Node }).name);
|
|
67
|
+
if (name !== undefined) {
|
|
68
|
+
named.set(symbol.id, name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
node.forEachChild(visit);
|
|
72
|
+
};
|
|
73
|
+
visit(file);
|
|
74
|
+
}
|
|
75
|
+
const index = new Map<string, InventorySymbol[]>();
|
|
76
|
+
for (const symbol of held.symbols) {
|
|
77
|
+
const name = named.get(symbol.id);
|
|
78
|
+
if (name === undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const members = index.get(name);
|
|
82
|
+
if (members === undefined) {
|
|
83
|
+
index.set(name, [symbol]);
|
|
84
|
+
} else {
|
|
85
|
+
members.push(symbol);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return index;
|
|
89
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isArrayLiteralExpression,
|
|
3
3
|
isBinaryExpression,
|
|
4
4
|
isCallExpression,
|
|
5
|
+
isDecorator,
|
|
5
6
|
isDeleteExpression,
|
|
6
7
|
isElementAccessExpression,
|
|
7
8
|
isExportDeclaration,
|
|
@@ -53,9 +54,11 @@ import type { ProjectView } from "./session.ts";
|
|
|
53
54
|
/**
|
|
54
55
|
* How one reference uses the declaration it names. An `evaluation` names a file: the
|
|
55
56
|
* 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
|
+
* none of what it exports. A `read` of a file reads the module's namespace. A
|
|
58
|
+
* `decorator` names the declaration a decorator expression is attached to, which the
|
|
59
|
+
* decorator receives when the class is defined.
|
|
57
60
|
*/
|
|
58
|
-
export type Use = "read" | "write" | "evaluation";
|
|
61
|
+
export type Use = "read" | "write" | "evaluation" | "decorator";
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Which accessor answered for one reference, so that the cost of a run is attributable
|
|
@@ -67,8 +70,9 @@ export type Use = "read" | "write" | "evaluation";
|
|
|
67
70
|
* - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
|
|
68
71
|
* the literal declares rather than to the declaration that name reads.
|
|
69
72
|
* - `alias` is a step along an import or export chain, from one link to what it names.
|
|
73
|
+
* - `syntax` is the tree alone, which says what a decorator is attached to.
|
|
70
74
|
*/
|
|
71
|
-
export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias";
|
|
75
|
+
export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax";
|
|
72
76
|
|
|
73
77
|
/** One use of one declaration by one declaration. */
|
|
74
78
|
export interface Reference {
|
|
@@ -318,11 +322,18 @@ interface Evaluation {
|
|
|
318
322
|
readonly from: string;
|
|
319
323
|
}
|
|
320
324
|
|
|
321
|
-
/**
|
|
325
|
+
/** One decorator expression, and the declaration it is attached to. */
|
|
326
|
+
interface Decorated {
|
|
327
|
+
readonly decorator: Node;
|
|
328
|
+
readonly declaration: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
|
|
322
332
|
interface FileSites {
|
|
323
333
|
readonly uses: readonly Site[];
|
|
324
334
|
readonly links: readonly Link[];
|
|
325
335
|
readonly evaluations: readonly Evaluation[];
|
|
336
|
+
readonly decorated: readonly Decorated[];
|
|
326
337
|
}
|
|
327
338
|
|
|
328
339
|
/** Whether one node is a string literal or a template literal with no substitution. */
|
|
@@ -418,6 +429,7 @@ function sitesOf(
|
|
|
418
429
|
const found: Site[] = [];
|
|
419
430
|
const links: Link[] = [];
|
|
420
431
|
const evaluations: Evaluation[] = [];
|
|
432
|
+
const decorated: Decorated[] = [];
|
|
421
433
|
const writes = new Set<number>();
|
|
422
434
|
const declared = new Set<number>();
|
|
423
435
|
const shorthands = new Map<number, Node>();
|
|
@@ -464,13 +476,18 @@ function sitesOf(
|
|
|
464
476
|
const outer = enclosing;
|
|
465
477
|
if (held !== undefined) {
|
|
466
478
|
enclosing = held;
|
|
479
|
+
for (const modifier of (node as { readonly modifiers?: readonly Node[] }).modifiers ?? []) {
|
|
480
|
+
if (isDecorator(modifier)) {
|
|
481
|
+
decorated.push({ decorator: modifier, declaration: held });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
467
484
|
}
|
|
468
485
|
node.forEachChild(visit);
|
|
469
486
|
enclosing = outer;
|
|
470
487
|
};
|
|
471
488
|
|
|
472
489
|
file.forEachChild(visit);
|
|
473
|
-
return { uses: found, links, evaluations };
|
|
490
|
+
return { uses: found, links, evaluations, decorated };
|
|
474
491
|
}
|
|
475
492
|
|
|
476
493
|
/**
|
|
@@ -567,7 +584,25 @@ export function references<Brand>(
|
|
|
567
584
|
if (test) {
|
|
568
585
|
tests.push(path);
|
|
569
586
|
}
|
|
570
|
-
const { uses, links, evaluations } = sitesOf(
|
|
587
|
+
const { uses, links, evaluations, decorated } = sitesOf(
|
|
588
|
+
file,
|
|
589
|
+
declarations,
|
|
590
|
+
declaredAt(file) ?? "",
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// A decorator is written inside the declaration it is attached to and receives it,
|
|
594
|
+
// so the declaration is used where the decorator is written, whatever the
|
|
595
|
+
// decorator does with it.
|
|
596
|
+
for (const { decorator, declaration } of decorated) {
|
|
597
|
+
found.push({
|
|
598
|
+
from: declaration,
|
|
599
|
+
to: declaration,
|
|
600
|
+
position: renderPosition(file, targetRoot, decorator.getStart()),
|
|
601
|
+
use: "decorator",
|
|
602
|
+
resolution: "syntax",
|
|
603
|
+
test,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
571
606
|
|
|
572
607
|
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
573
608
|
// the uses and the evaluated modules' specifiers last, each node once: a bare star
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reflective-lookup class: a member is held back, at the lowest confidence, when
|
|
3
|
+
* a string literal equal to its name is the key of an element access or the property
|
|
4
|
+
* key of a `Reflect.get`, `Reflect.set` or `Reflect.has` call, because the member is
|
|
5
|
+
* then read by a name the program computes nowhere the reference pass can follow.
|
|
6
|
+
*
|
|
7
|
+
* The evidence is a string beside a lookup rather than a type relation, so the
|
|
8
|
+
* string reaches every member of that name on any type, static members included,
|
|
9
|
+
* and every record names the string's own position. A key that is not literal text
|
|
10
|
+
* holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
|
|
11
|
+
* the global one only: a local declaration of that name is a different object.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
isCallExpression,
|
|
16
|
+
isElementAccessExpression,
|
|
17
|
+
isIdentifier,
|
|
18
|
+
isNoSubstitutionTemplateLiteral,
|
|
19
|
+
isPropertyAccessExpression,
|
|
20
|
+
isStringLiteral,
|
|
21
|
+
type Node,
|
|
22
|
+
type SourceFile,
|
|
23
|
+
} from "@typescript/native/unstable/ast";
|
|
24
|
+
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
25
|
+
import { memberNames } from "./member-names.ts";
|
|
26
|
+
import { renderPosition } from "./position.ts";
|
|
27
|
+
|
|
28
|
+
/** The `Reflect` methods whose second argument is a property key. */
|
|
29
|
+
const REFLECT_KEYED: ReadonlySet<string> = new Set(["get", "set", "has"]);
|
|
30
|
+
|
|
31
|
+
/** One literal key at one lookup, and how the lookup is spelled in a record. */
|
|
32
|
+
interface Lookup {
|
|
33
|
+
readonly key: Node;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly callee: string;
|
|
36
|
+
/** The `Reflect` the call names, where the lookup is a `Reflect` call. */
|
|
37
|
+
readonly reflect: Node | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The text one key denotes, where it is literal text. */
|
|
41
|
+
function literalText(node: Node | undefined): string | undefined {
|
|
42
|
+
if (node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))) {
|
|
43
|
+
return node.text;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Every lookup one file writes with a literal key, in source order. */
|
|
49
|
+
function lookupsOf(file: SourceFile): readonly Lookup[] {
|
|
50
|
+
const found: Lookup[] = [];
|
|
51
|
+
const visit = (node: Node): void => {
|
|
52
|
+
if (isElementAccessExpression(node)) {
|
|
53
|
+
const name = literalText(node.argumentExpression);
|
|
54
|
+
if (name !== undefined) {
|
|
55
|
+
found.push({
|
|
56
|
+
key: node.argumentExpression,
|
|
57
|
+
name,
|
|
58
|
+
callee: `[${JSON.stringify(name)}]`,
|
|
59
|
+
reflect: undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} else if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
63
|
+
const target = node.expression.expression;
|
|
64
|
+
const method = node.expression.name;
|
|
65
|
+
const key = node.arguments[1];
|
|
66
|
+
const name = literalText(key);
|
|
67
|
+
if (
|
|
68
|
+
isIdentifier(target) &&
|
|
69
|
+
target.text === "Reflect" &&
|
|
70
|
+
isIdentifier(method) &&
|
|
71
|
+
REFLECT_KEYED.has(method.text) &&
|
|
72
|
+
key !== undefined &&
|
|
73
|
+
name !== undefined
|
|
74
|
+
) {
|
|
75
|
+
found.push({ key, name, callee: `Reflect.${method.text}`, reflect: target });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
node.forEachChild(visit);
|
|
79
|
+
};
|
|
80
|
+
file.forEachChild(visit);
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The reflective-lookup detector: every member whose name a literal key of a lookup
|
|
86
|
+
* spells, recorded at the key.
|
|
87
|
+
*/
|
|
88
|
+
export function reflectiveLookup<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
89
|
+
const { project, held, targetRoot } = input;
|
|
90
|
+
const files = project.ownSourceFiles();
|
|
91
|
+
const lookups = files.flatMap(lookupsOf);
|
|
92
|
+
if (lookups.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
97
|
+
// declaration of that name in one of the project's own files shadows it.
|
|
98
|
+
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
99
|
+
const reflects = lookups.flatMap((lookup) =>
|
|
100
|
+
lookup.reflect === undefined ? [] : [lookup.reflect],
|
|
101
|
+
);
|
|
102
|
+
const global = new Set<Node>();
|
|
103
|
+
if (reflects.length > 0) {
|
|
104
|
+
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
105
|
+
const node = reflects[index];
|
|
106
|
+
const declaredHere = symbol?.declarations.some((declaration) =>
|
|
107
|
+
ownFiles.has(declaration.path),
|
|
108
|
+
);
|
|
109
|
+
if (node !== undefined && declaredHere === false) {
|
|
110
|
+
global.add(node);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const members = memberNames(files, held);
|
|
116
|
+
const found: Evidence[] = [];
|
|
117
|
+
for (const lookup of lookups) {
|
|
118
|
+
if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
|
|
122
|
+
for (const member of members.get(lookup.name) ?? []) {
|
|
123
|
+
found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return found;
|
|
127
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runRoots, type RunRoot } from "./analysis.ts";
|
|
1
|
+
import { runRoots, runSweep, type RunRoot } from "./analysis.ts";
|
|
2
2
|
import { ConfigError, REPOSITORY_DOCUMENT, type Inputs } from "./config.ts";
|
|
3
3
|
import {
|
|
4
4
|
diagnosticErrors,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DiscoveryError,
|
|
7
7
|
renderDiagnostic,
|
|
8
8
|
} from "./discover.ts";
|
|
9
|
+
import { retainedLines } from "./exempt.ts";
|
|
9
10
|
import type { Host } from "./host.ts";
|
|
10
11
|
import { joinPath, resolvePath } from "./paths.ts";
|
|
11
12
|
import { printConfig } from "./print.ts";
|
|
@@ -221,6 +222,9 @@ export function run(
|
|
|
221
222
|
if (verb === "print-roots") {
|
|
222
223
|
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
223
224
|
}
|
|
225
|
+
if (verb === "print-retained") {
|
|
226
|
+
return printRetainedVerb(args.slice(1), out, err, host, openClient);
|
|
227
|
+
}
|
|
224
228
|
if (verb !== undefined && (VERBS as readonly string[]).includes(verb)) {
|
|
225
229
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
226
230
|
return EXIT_USAGE;
|
|
@@ -337,6 +341,40 @@ function printRootsVerb(
|
|
|
337
341
|
}
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Writes every declaration an exemption held back in the production sweep a report is
|
|
346
|
+
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
347
|
+
* several configurations found is one record, so the line names no configuration. The
|
|
348
|
+
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
349
|
+
* read it.
|
|
350
|
+
*/
|
|
351
|
+
function printRetainedVerb(
|
|
352
|
+
args: readonly string[],
|
|
353
|
+
out: Writer,
|
|
354
|
+
err: Writer,
|
|
355
|
+
host: Host,
|
|
356
|
+
openClient: (collectTiming: boolean) => Engine,
|
|
357
|
+
): number {
|
|
358
|
+
let engine: Engine | undefined;
|
|
359
|
+
try {
|
|
360
|
+
const options = readOptions(args);
|
|
361
|
+
const { config } = resolve(inputsOf(host, options));
|
|
362
|
+
const scope = scopeOf(host, options);
|
|
363
|
+
engine = openClient(false);
|
|
364
|
+
const swept = runSweep(engine, host, scope, config, {
|
|
365
|
+
marked: [],
|
|
366
|
+
mode: { production: true },
|
|
367
|
+
});
|
|
368
|
+
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
369
|
+
out.write(`${line}\n`);
|
|
370
|
+
}
|
|
371
|
+
return EXIT_CLEAN;
|
|
372
|
+
} catch (error: unknown) {
|
|
373
|
+
engine?.close();
|
|
374
|
+
return refuse(error, err);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
340
378
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
341
379
|
function refuse(error: unknown, err: Writer): number {
|
|
342
380
|
if (error instanceof ConfigError) {
|
package/src/sweep.ts
CHANGED
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
9
10
|
import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
|
|
11
|
+
import type { Position } from "./position.ts";
|
|
10
12
|
|
|
11
13
|
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
12
14
|
export type Relation = "reference-counting" | "reachability";
|
|
13
15
|
|
|
16
|
+
/** One declaration an exemption class holds back, and the evidence it was found by. */
|
|
17
|
+
export interface Exemption {
|
|
18
|
+
/** The identifier of the declaration held back. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly class: TSExemptionClass;
|
|
21
|
+
/** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
|
|
22
|
+
readonly detail: string;
|
|
23
|
+
/** Where the evidence is written. */
|
|
24
|
+
readonly site: Position;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* The run's reference mode, decided once by the composition and read by every stage
|
|
16
29
|
* that counts references.
|
|
@@ -31,6 +44,13 @@ export interface SweepInput {
|
|
|
31
44
|
* nothing only a marked declaration references is dead.
|
|
32
45
|
*/
|
|
33
46
|
readonly marked: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* The exemptions the run computed. An exempt declaration is never a candidate, and it
|
|
49
|
+
* seeds reachability as a mark does, so nothing it references is dead; it is live
|
|
50
|
+
* under reference counting only where something references it. Absent, the sweep
|
|
51
|
+
* holds nothing back.
|
|
52
|
+
*/
|
|
53
|
+
readonly exempt?: readonly Exemption[];
|
|
34
54
|
readonly mode: Mode;
|
|
35
55
|
}
|
|
36
56
|
|
|
@@ -69,19 +89,19 @@ export interface Liveness {
|
|
|
69
89
|
*
|
|
70
90
|
* The order is the order the answers depend on: the callers and the marks are live
|
|
71
91
|
* before either relation runs, the candidate set is every judged declaration at least
|
|
72
|
-
* one relation does not hold live, and the tests of dead
|
|
92
|
+
* one relation does not hold live and no exemption holds back, and the tests of dead
|
|
93
|
+
* code join it.
|
|
73
94
|
*/
|
|
74
95
|
export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
75
|
-
const marked = graph.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// caller is not.
|
|
96
|
+
const marked = flagged(graph, input.marked);
|
|
97
|
+
const exempt = flagged(
|
|
98
|
+
graph,
|
|
99
|
+
(input.exempt ?? []).map((record) => record.id),
|
|
100
|
+
);
|
|
101
|
+
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
102
|
+
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
103
|
+
// names a caller, and a mark, are live under reference counting as well; a root that
|
|
104
|
+
// only supposes a caller is not, and neither is an exemption.
|
|
85
105
|
const called = graph.symbols.map(() => false);
|
|
86
106
|
for (const root of graph.rooted) {
|
|
87
107
|
if (namesACaller(root.kind)) {
|
|
@@ -96,7 +116,7 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
96
116
|
}
|
|
97
117
|
return input.mode.production ? made.production : made.production + made.test;
|
|
98
118
|
};
|
|
99
|
-
const reached = reachable(graph,
|
|
119
|
+
const reached = reachable(graph, held, input.mode);
|
|
100
120
|
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
101
121
|
const relations: Relation[] = [];
|
|
102
122
|
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
@@ -109,9 +129,10 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
109
129
|
});
|
|
110
130
|
|
|
111
131
|
const dead = graph.symbols.map(
|
|
112
|
-
(_symbol, at) =>
|
|
132
|
+
(_symbol, at) =>
|
|
133
|
+
graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2,
|
|
113
134
|
);
|
|
114
|
-
const testOfDeadCode = admitTestsOfDeadCode(graph, dead,
|
|
135
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, held);
|
|
115
136
|
|
|
116
137
|
const candidates: Candidate[] = [];
|
|
117
138
|
const live = new Map<string, readonly Relation[]>();
|
|
@@ -135,13 +156,26 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
135
156
|
return { liveUnder: live, candidates };
|
|
136
157
|
}
|
|
137
158
|
|
|
159
|
+
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
160
|
+
function flagged(graph: Graph, ids: readonly string[]): boolean[] {
|
|
161
|
+
const flags = graph.symbols.map(() => false);
|
|
162
|
+
for (const id of ids) {
|
|
163
|
+
const at = graph.at(id);
|
|
164
|
+
if (at !== OUTSIDE) {
|
|
165
|
+
flags[at] = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return flags;
|
|
169
|
+
}
|
|
170
|
+
|
|
138
171
|
/**
|
|
139
172
|
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
140
|
-
* reference sets: a
|
|
141
|
-
* no mode withholds a reference it makes,
|
|
142
|
-
*
|
|
173
|
+
* reference sets: a held declaration, marked or exempt, is live by a mechanism the
|
|
174
|
+
* analysis cannot see, so no mode withholds a reference it makes, a test declaration's
|
|
175
|
+
* included, while a root reaches through the references the mode counts. The held seed
|
|
176
|
+
* runs first, so what it reaches is expanded under the wider rule.
|
|
143
177
|
*/
|
|
144
|
-
function reachable(graph: Graph,
|
|
178
|
+
function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly boolean[] {
|
|
145
179
|
const reached = graph.symbols.map(() => false);
|
|
146
180
|
const expanded = graph.symbols.map(() => false);
|
|
147
181
|
const queue: number[] = [];
|
|
@@ -164,10 +198,10 @@ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readon
|
|
|
164
198
|
enter(exported, false);
|
|
165
199
|
}
|
|
166
200
|
};
|
|
167
|
-
const walk = (
|
|
201
|
+
const walk = (holding: boolean): void => {
|
|
168
202
|
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
169
203
|
for (const edge of graph.out[at] ?? []) {
|
|
170
|
-
if (!
|
|
204
|
+
if (!holding && mode.production && edge.test) {
|
|
171
205
|
continue;
|
|
172
206
|
}
|
|
173
207
|
enter(edge.to, !edge.evaluation);
|
|
@@ -175,8 +209,8 @@ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readon
|
|
|
175
209
|
}
|
|
176
210
|
};
|
|
177
211
|
|
|
178
|
-
|
|
179
|
-
if (
|
|
212
|
+
held.forEach((hold, at) => {
|
|
213
|
+
if (hold) {
|
|
180
214
|
enter(at, false);
|
|
181
215
|
}
|
|
182
216
|
});
|
|
@@ -192,16 +226,17 @@ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readon
|
|
|
192
226
|
* Admits every test declaration whose set of referenced production declarations is
|
|
193
227
|
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
194
228
|
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
195
|
-
* which is what makes the targets dead. A test referencing one live target is never
|
|
229
|
+
* which is what makes the targets dead. A test referencing one live target is never
|
|
230
|
+
* admitted, and neither is a marked or exempt test.
|
|
196
231
|
*/
|
|
197
232
|
function admitTestsOfDeadCode(
|
|
198
233
|
graph: Graph,
|
|
199
234
|
dead: boolean[],
|
|
200
|
-
|
|
235
|
+
held: readonly boolean[],
|
|
201
236
|
): readonly boolean[] {
|
|
202
237
|
const admitted = graph.symbols.map(() => false);
|
|
203
238
|
graph.symbols.forEach((_symbol, at) => {
|
|
204
|
-
if (graph.test[at] !== true || graph.subject[at] !== true ||
|
|
239
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
|
|
205
240
|
return;
|
|
206
241
|
}
|
|
207
242
|
let targets = 0;
|