@cplieger/deadset-ts 3.3.1 → 3.5.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 +1 -1
- package/dist/src/analysis.js +8 -1
- package/dist/src/calls.js +154 -0
- package/dist/src/class-members.js +92 -0
- package/dist/src/components.js +61 -106
- package/dist/src/config.js +9 -0
- package/dist/src/configured-declarations.js +232 -0
- package/dist/src/decorator.js +3 -10
- package/dist/src/framework-lifecycle.js +192 -0
- package/dist/src/injection-container.js +101 -0
- package/dist/src/kinds.js +40 -0
- package/dist/src/print.js +28 -1
- package/dist/src/resolve.js +188 -9
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +55 -2
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +405 -0
- package/dist/src/value-reach.js +120 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +8 -1
- package/src/calls.ts +224 -0
- package/src/class-members.ts +136 -0
- package/src/components.ts +67 -130
- package/src/config.ts +68 -2
- package/src/configured-declarations.ts +308 -0
- package/src/decorator.ts +4 -16
- package/src/exempt.ts +5 -1
- package/src/framework-lifecycle.ts +280 -0
- package/src/injection-container.ts +141 -0
- package/src/kinds.ts +41 -0
- package/src/print.ts +36 -1
- package/src/resolve.ts +278 -9
- package/src/run.ts +10 -3
- package/src/schema.ts +66 -3
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +610 -0
- package/src/value-reach.ts +152 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 3.
|
|
11
|
+
**Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
package/dist/src/analysis.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { decorator } from "./decorator.js";
|
|
11
11
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
|
|
12
12
|
import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
|
|
13
|
+
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
14
|
+
import { injectionContainer } from "./injection-container.js";
|
|
13
15
|
import { interfaceSatisfaction } from "./interface-satisfaction.js";
|
|
14
16
|
import { inventory } from "./inventory.js";
|
|
15
17
|
import { readManifest } from "./manifest.js";
|
|
@@ -19,6 +21,7 @@ import { byPosition } from "./position.js";
|
|
|
19
21
|
import { reflectiveLookup } from "./reflective-lookup.js";
|
|
20
22
|
import { references } from "./references.js";
|
|
21
23
|
import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
|
|
24
|
+
import { serializationContract } from "./serialization-contract.js";
|
|
22
25
|
import { diagnosticsOf, runSession } from "./session.js";
|
|
23
26
|
import { readTemplates, templateField } from "./template-field.js";
|
|
24
27
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
@@ -128,6 +131,9 @@ const DETECTORS = new Map([
|
|
|
128
131
|
["template-field", templateField],
|
|
129
132
|
["reflective-lookup", reflectiveLookup],
|
|
130
133
|
["decorator", decorator],
|
|
134
|
+
["injection-container", injectionContainer],
|
|
135
|
+
["framework-lifecycle", frameworkLifecycle],
|
|
136
|
+
["serialization-contract", serializationContract],
|
|
131
137
|
]);
|
|
132
138
|
/**
|
|
133
139
|
* Sweeps the run the scope and the configuration describe. Each project's references
|
|
@@ -145,6 +151,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
145
151
|
const templates = disabled.has("template-field")
|
|
146
152
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
147
153
|
: readTemplates(host, targetRoot, config.analysis);
|
|
154
|
+
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
148
155
|
const read = readProjects(engine, host, scope, config, (project, projectRead) => {
|
|
149
156
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
150
157
|
testFiles: config.ts.testFiles,
|
|
@@ -156,7 +163,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
156
163
|
roots: projectRead.rooted.liveUnderReachability,
|
|
157
164
|
testFiles: resolved.testFilePaths,
|
|
158
165
|
};
|
|
159
|
-
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates }, detectors, {
|
|
166
|
+
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
|
|
160
167
|
disabled,
|
|
161
168
|
mode: input.mode,
|
|
162
169
|
testFiles: new Set(resolved.testFilePaths),
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calls and decorators of one project that the configured classes read: what each
|
|
3
|
+
* callee resolves to, and which of the target's classes a call passes or a decorator
|
|
4
|
+
* is attached to. A class is passed as an argument, or as an element of an array
|
|
5
|
+
* literal or a property value of an object literal written as one, at any depth, and a
|
|
6
|
+
* decorator written as a call is a call.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { nodeKey } from "./inventory.js";
|
|
11
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
12
|
+
/** The name node a callee or a decorator expression resolves through, where it has one. */
|
|
13
|
+
export function calleeName(expression) {
|
|
14
|
+
if (isParenthesizedExpression(expression)) {
|
|
15
|
+
return calleeName(expression.expression);
|
|
16
|
+
}
|
|
17
|
+
if (isPropertyAccessExpression(expression)) {
|
|
18
|
+
return expression.name;
|
|
19
|
+
}
|
|
20
|
+
return isIdentifier(expression) ? expression : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** The callee one decorator names: its expression, or the callee of the call it writes. */
|
|
23
|
+
export function decoratorCallee(decorator) {
|
|
24
|
+
const expression = decorator.expression;
|
|
25
|
+
return calleeName(isCallExpression(expression) ? expression.expression : expression);
|
|
26
|
+
}
|
|
27
|
+
/** How one decorator is spelled in a record: its expression, without a call's arguments. */
|
|
28
|
+
export function spelledDecorator(decorator) {
|
|
29
|
+
const expression = decorator.expression;
|
|
30
|
+
const named = isCallExpression(expression) ? expression.expression : expression;
|
|
31
|
+
return `@${named.getText().replace(/\s+/gu, "")}`;
|
|
32
|
+
}
|
|
33
|
+
/** The decorators written on one declaration. */
|
|
34
|
+
export function decoratorsOf(node) {
|
|
35
|
+
return (node.modifiers ?? []).filter(isDecorator);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What each of a batch of nodes resolves to, every alias followed to the declaration
|
|
39
|
+
* it stands for. A node the checker resolves to nothing, and an alias that resolves
|
|
40
|
+
* to nothing, are absent from the answer. Each alias is followed once.
|
|
41
|
+
*/
|
|
42
|
+
export function resolvedTargets(project, nodes) {
|
|
43
|
+
const followed = new Map();
|
|
44
|
+
const targets = new Map();
|
|
45
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
46
|
+
const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
|
|
47
|
+
project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
48
|
+
const node = run[index];
|
|
49
|
+
if (symbol === undefined || node === undefined) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let target = symbol;
|
|
53
|
+
if ((symbol.flags & SymbolFlags.Alias) !== 0) {
|
|
54
|
+
if (!followed.has(symbol.id)) {
|
|
55
|
+
const aliased = project.checker.getAliasedSymbol(symbol);
|
|
56
|
+
followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
|
|
57
|
+
}
|
|
58
|
+
target = followed.get(symbol.id);
|
|
59
|
+
}
|
|
60
|
+
if (target !== undefined) {
|
|
61
|
+
targets.set(node, target);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return targets;
|
|
66
|
+
}
|
|
67
|
+
/** The values one expression passes: itself, or what an array or object literal holds. */
|
|
68
|
+
function passedIn(expression, found) {
|
|
69
|
+
if (isParenthesizedExpression(expression)) {
|
|
70
|
+
passedIn(expression.expression, found);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (isArrayLiteralExpression(expression)) {
|
|
74
|
+
for (const element of expression.elements) {
|
|
75
|
+
passedIn(isSpreadElement(element) ? element.expression : element, found);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (isObjectLiteralExpression(expression)) {
|
|
80
|
+
for (const property of expression.properties) {
|
|
81
|
+
if (isPropertyAssignment(property)) {
|
|
82
|
+
passedIn(property.initializer, found);
|
|
83
|
+
}
|
|
84
|
+
else if (isShorthandPropertyAssignment(property)) {
|
|
85
|
+
found.push({ site: property.name, name: property, shorthand: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const name = calleeName(expression);
|
|
91
|
+
if (name !== undefined) {
|
|
92
|
+
found.push({ site: expression, name, shorthand: false });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The values one argument passes, a spread argument's included. */
|
|
96
|
+
export function valuesPassed(argument) {
|
|
97
|
+
const found = [];
|
|
98
|
+
passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The classes of the target each call passes, by the call. A value that names no
|
|
103
|
+
* class of the target, a class of another program included, passes nothing here.
|
|
104
|
+
*/
|
|
105
|
+
export function classesPassed(project, held, chains, calls) {
|
|
106
|
+
const passed = new Map(calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]));
|
|
107
|
+
const all = [...passed.values()].flat();
|
|
108
|
+
const targets = resolvedTargets(project, all.filter((value) => !value.shorthand).map((value) => value.name));
|
|
109
|
+
const classes = new Set(held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id));
|
|
110
|
+
const classesOf = (value) => {
|
|
111
|
+
const symbol = value.shorthand
|
|
112
|
+
? project.shorthandValueAt(project.handle(value.name))
|
|
113
|
+
: targets.get(value.name);
|
|
114
|
+
return symbol === undefined
|
|
115
|
+
? []
|
|
116
|
+
: chains.declarationsOf(symbol).filter((id) => classes.has(id));
|
|
117
|
+
};
|
|
118
|
+
return new Map([...passed].map(([call, values]) => [
|
|
119
|
+
call,
|
|
120
|
+
values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
|
|
121
|
+
]));
|
|
122
|
+
}
|
|
123
|
+
/** Every class declaration of the target that carries a decorator. */
|
|
124
|
+
export function decoratedClasses(project, held) {
|
|
125
|
+
const found = [];
|
|
126
|
+
for (const file of project.ownSourceFiles()) {
|
|
127
|
+
const visit = (node) => {
|
|
128
|
+
if (isClassDeclaration(node)) {
|
|
129
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
130
|
+
const decorators = decoratorsOf(node);
|
|
131
|
+
if (id !== undefined && decorators.length > 0) {
|
|
132
|
+
found.push({ id, decorators });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
node.forEachChild(visit);
|
|
136
|
+
};
|
|
137
|
+
file.forEachChild(visit);
|
|
138
|
+
}
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
/** Every call expression of the project's own files, decorators written as calls included. */
|
|
142
|
+
export function callsOf(project) {
|
|
143
|
+
const found = [];
|
|
144
|
+
for (const file of project.ownSourceFiles()) {
|
|
145
|
+
const visit = (node) => {
|
|
146
|
+
if (isCallExpression(node)) {
|
|
147
|
+
found.push(node);
|
|
148
|
+
}
|
|
149
|
+
node.forEachChild(visit);
|
|
150
|
+
};
|
|
151
|
+
file.forEachChild(visit);
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members a value of one of the target's classes carries, as the checker answers
|
|
3
|
+
* for the class's type, so a member a base class declares is a member of every class
|
|
4
|
+
* that extends it and a member a subclass overrides is the subclass's own.
|
|
5
|
+
*/
|
|
6
|
+
import { isClassDeclaration, isClassExpression } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { nodeKey } from "./inventory.js";
|
|
8
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
9
|
+
/** The name node of each class declaration of the target, by the class's identifier. */
|
|
10
|
+
export function classNames(project, held) {
|
|
11
|
+
const names = new Map();
|
|
12
|
+
for (const file of project.ownSourceFiles()) {
|
|
13
|
+
const visit = (node) => {
|
|
14
|
+
if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
|
|
15
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
16
|
+
if (id !== undefined) {
|
|
17
|
+
names.set(id, node.name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
node.forEachChild(visit);
|
|
21
|
+
};
|
|
22
|
+
file.forEachChild(visit);
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
/** The types of a batch of nodes, asked in capped batches. */
|
|
27
|
+
export function typesAt(project, nodes) {
|
|
28
|
+
const types = [];
|
|
29
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
30
|
+
types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
|
|
31
|
+
}
|
|
32
|
+
return types;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The type one side of each class is: the instance type at the class's name, or the
|
|
36
|
+
* type of the class's own symbol, which is its constructor's.
|
|
37
|
+
*/
|
|
38
|
+
function sideTypes(project, nodes, side) {
|
|
39
|
+
if (side === "instance") {
|
|
40
|
+
return typesAt(project, nodes);
|
|
41
|
+
}
|
|
42
|
+
const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
|
|
43
|
+
const present = symbols.filter((symbol) => symbol !== undefined);
|
|
44
|
+
const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
|
|
45
|
+
let next = 0;
|
|
46
|
+
return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The members one side of each class carries that the target declares, by the class.
|
|
50
|
+
* A member declared in a file outside the project's own, and the `prototype` a class
|
|
51
|
+
* carries on its static side, are not declarations of the target and are left out.
|
|
52
|
+
*/
|
|
53
|
+
export function classMembers(project, held, classIds, side) {
|
|
54
|
+
const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
|
|
55
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
56
|
+
const types = sideTypes(project, named.map(([, node]) => node), side);
|
|
57
|
+
const members = new Map();
|
|
58
|
+
named.forEach(([id], index) => {
|
|
59
|
+
const type = types[index];
|
|
60
|
+
if (type === undefined) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const property of project.checker.getPropertiesOfType(type)) {
|
|
65
|
+
for (const handle of property.declarations) {
|
|
66
|
+
if (!own.has(handle.path)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const node = project.declarationAt(handle)?.node;
|
|
70
|
+
const declared = node === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
73
|
+
if (node !== undefined && declared !== undefined) {
|
|
74
|
+
found.push({ id: declared, node, flags: property.flags });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
members.set(id, found);
|
|
79
|
+
});
|
|
80
|
+
return members;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The last component of one member's stable reference, `:static` included, which is
|
|
84
|
+
* how a configuration names a member; undefined for a declaration that is no member.
|
|
85
|
+
*/
|
|
86
|
+
export function memberComponent(symbol, byId) {
|
|
87
|
+
const parent = byId.get(symbol.parent);
|
|
88
|
+
if (parent === undefined || !symbol.ref.startsWith(`${parent.ref}.`)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return symbol.ref.slice(parent.ref.length + 1);
|
|
92
|
+
}
|
package/dist/src/components.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dead components of one graph
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The dead components of one graph. A component is a set of dead declarations one
|
|
3
|
+
* deletion removes together: its root members, which are where the deletion starts,
|
|
4
|
+
* and every dead declaration that falls with them, being dead only through them.
|
|
5
5
|
*/
|
|
6
6
|
import { OUTSIDE } from "./graph.js";
|
|
7
7
|
/** The name every component identifier this analyzer mints starts with. */
|
|
@@ -10,21 +10,21 @@ const ANALYZER = "deadset-ts";
|
|
|
10
10
|
const ID_DIGITS = 4;
|
|
11
11
|
/** The index of a declaration the component walk has not reached. */
|
|
12
12
|
const UNVISITED = -1;
|
|
13
|
-
/** What a report carries for one component: its roots and
|
|
13
|
+
/** What a report carries for one component: its roots and counts, and its members in full. */
|
|
14
14
|
export function listing(component, cascade) {
|
|
15
15
|
return {
|
|
16
16
|
roots: component.roots,
|
|
17
17
|
members: cascade === "full" ? component.members : [],
|
|
18
|
-
symbolCount: component.
|
|
18
|
+
symbolCount: component.members.length,
|
|
19
19
|
deletableLines: component.deletableLines,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Groups the declarations `dead` marks into components
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
24
|
+
* site; `dead` and `testOfDeadCode` hold one flag per declaration of the graph. Root
|
|
25
|
+
* members sit on the cycles no other cycle references. A cycle two roots both reach is
|
|
26
|
+
* dead only through both, so a component is a weakly connected part of the dead
|
|
27
|
+
* subgraph: no dead declaration of one references a declaration of another.
|
|
28
28
|
*/
|
|
29
29
|
export function componentsOf(graph, dead, testOfDeadCode) {
|
|
30
30
|
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
|
|
@@ -32,25 +32,21 @@ export function componentsOf(graph, dead, testOfDeadCode) {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
const { componentOf, members } = connected(adjacent);
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const outsideReference = externallyReferenced(adjacent, componentOf);
|
|
35
|
+
const referenced = referencedCycles(adjacent, componentOf, members.length);
|
|
36
|
+
const groups = joined(adjacent);
|
|
38
37
|
const idOf = (position) => graph.symbols[at[position] ?? 0]?.id ?? "";
|
|
39
|
-
return
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
// deletion starts and the member falls with it.
|
|
38
|
+
return groups.map((group, place) => {
|
|
39
|
+
const roots = group.filter((position) => {
|
|
40
|
+
// A dead member's dead container is on the member's own cycle, and the container
|
|
41
|
+
// is where the deletion starts.
|
|
44
42
|
const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
|
|
45
|
-
return
|
|
43
|
+
return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
|
|
46
44
|
});
|
|
47
|
-
const fallen = falls[group] ?? [];
|
|
48
45
|
return {
|
|
49
46
|
id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
|
|
50
|
-
members:
|
|
47
|
+
members: group.map(idOf),
|
|
51
48
|
roots: roots.map(idOf),
|
|
52
|
-
|
|
53
|
-
deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
|
|
49
|
+
deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
|
|
54
50
|
};
|
|
55
51
|
});
|
|
56
52
|
}
|
|
@@ -127,10 +123,9 @@ function deadSubgraph(graph, dead, testOfDeadCode) {
|
|
|
127
123
|
return { at, adjacent };
|
|
128
124
|
}
|
|
129
125
|
/**
|
|
130
|
-
* Each declaration's
|
|
126
|
+
* Each declaration's cycle and each cycle's members by site, by Tarjan's
|
|
131
127
|
* algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
|
|
132
|
-
* is walked without growing the call stack.
|
|
133
|
-
* each follows every component it reaches.
|
|
128
|
+
* is walked without growing the call stack.
|
|
134
129
|
*/
|
|
135
130
|
function connected(adjacent) {
|
|
136
131
|
const index = adjacent.map(() => UNVISITED);
|
|
@@ -193,97 +188,57 @@ function connected(adjacent) {
|
|
|
193
188
|
});
|
|
194
189
|
return { componentOf, members };
|
|
195
190
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* the result is acyclic.
|
|
200
|
-
*/
|
|
201
|
-
function condense(adjacent, componentOf, count) {
|
|
202
|
-
const edges = Array.from({ length: count }, () => []);
|
|
203
|
-
const into = Array.from({ length: count }, () => 0);
|
|
204
|
-
const seen = new Set();
|
|
191
|
+
/** Per cycle, whether a dead declaration outside it references one of its members. */
|
|
192
|
+
function referencedCycles(adjacent, componentOf, count) {
|
|
193
|
+
const referenced = Array.from({ length: count }, () => false);
|
|
205
194
|
adjacent.forEach((targets, from) => {
|
|
206
195
|
for (const to of targets) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (a === b || seen.has(pair)) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
seen.add(pair);
|
|
214
|
-
edges[a]?.push(b);
|
|
215
|
-
into[b] = (into[b] ?? 0) + 1;
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
return { edges, into };
|
|
219
|
-
}
|
|
220
|
-
/** Per dead declaration, whether a dead component other than its own references it. */
|
|
221
|
-
function externallyReferenced(adjacent, componentOf) {
|
|
222
|
-
const referenced = adjacent.map(() => false);
|
|
223
|
-
adjacent.forEach((targets, from) => {
|
|
224
|
-
for (const to of targets) {
|
|
225
|
-
if (componentOf[from] !== componentOf[to]) {
|
|
226
|
-
referenced[to] = true;
|
|
196
|
+
const cycle = componentOf[to] ?? 0;
|
|
197
|
+
if (componentOf[from] !== cycle) {
|
|
198
|
+
referenced[cycle] = true;
|
|
227
199
|
}
|
|
228
200
|
}
|
|
229
201
|
});
|
|
230
202
|
return referenced;
|
|
231
203
|
}
|
|
232
204
|
/**
|
|
233
|
-
* The
|
|
234
|
-
*
|
|
235
|
-
*
|
|
205
|
+
* The weakly connected parts of the dead subgraph, each its positions in ascending
|
|
206
|
+
* order, ordered by their first position. Positions follow the graph's declaration
|
|
207
|
+
* order, so a part's first position is its first member's site.
|
|
236
208
|
*/
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const owners = edges.map(() => 0);
|
|
259
|
-
edges.forEach((_targets, root) => {
|
|
260
|
-
if (into[root] !== 0) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const reached = [root];
|
|
264
|
-
stamp[root] = root;
|
|
265
|
-
// An array's iterator reads its length at every step, so what a step pushes is
|
|
266
|
-
// walked in the same loop.
|
|
267
|
-
for (const at of reached) {
|
|
268
|
-
for (const to of edges[at] ?? []) {
|
|
269
|
-
if (stamp[to] !== root) {
|
|
270
|
-
stamp[to] = root;
|
|
271
|
-
reached.push(to);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
reach.set(root, reached);
|
|
276
|
-
for (const component of reached) {
|
|
277
|
-
owners[component] = (owners[component] ?? 0) + 1;
|
|
209
|
+
function joined(adjacent) {
|
|
210
|
+
const leader = adjacent.map((_edges, position) => position);
|
|
211
|
+
const find = (position) => {
|
|
212
|
+
let top = position;
|
|
213
|
+
while ((leader[top] ?? top) !== top) {
|
|
214
|
+
top = leader[top] ?? top;
|
|
215
|
+
}
|
|
216
|
+
// Every position on the walk points at the top afterwards, so a later find is short.
|
|
217
|
+
for (let step = position; step !== top;) {
|
|
218
|
+
const next = leader[step] ?? top;
|
|
219
|
+
leader[step] = top;
|
|
220
|
+
step = next;
|
|
221
|
+
}
|
|
222
|
+
return top;
|
|
223
|
+
};
|
|
224
|
+
adjacent.forEach((targets, from) => {
|
|
225
|
+
for (const to of targets) {
|
|
226
|
+
const a = find(from);
|
|
227
|
+
const b = find(to);
|
|
228
|
+
// The smaller position leads, so a part's leader is its first position.
|
|
229
|
+
leader[Math.max(a, b)] = Math.min(a, b);
|
|
278
230
|
}
|
|
279
231
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
232
|
+
const parts = new Map();
|
|
233
|
+
adjacent.forEach((_edges, position) => {
|
|
234
|
+
const top = find(position);
|
|
235
|
+
const part = parts.get(top);
|
|
236
|
+
if (part === undefined) {
|
|
237
|
+
parts.set(top, [position]);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
part.push(position);
|
|
286
241
|
}
|
|
287
|
-
return set.sort((a, b) => a - b);
|
|
288
242
|
});
|
|
243
|
+
return [...parts.values()];
|
|
289
244
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -34,9 +34,18 @@ export function defaultConfig() {
|
|
|
34
34
|
maxFindings: 0,
|
|
35
35
|
failOn: "deny",
|
|
36
36
|
},
|
|
37
|
+
providers: {
|
|
38
|
+
analyzers: [
|
|
39
|
+
{ shape: "installed", name: "deadset-go", languages: ["go"], command: "deadset-go" },
|
|
40
|
+
{ shape: "installed", name: "deadset-ts", languages: ["ts"], command: "deadset-ts" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
37
43
|
ts: {
|
|
38
44
|
testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
|
|
39
45
|
entryFiles: [],
|
|
46
|
+
injectionRegistrations: [],
|
|
47
|
+
lifecycleContracts: [],
|
|
48
|
+
serializers: [],
|
|
40
49
|
},
|
|
41
50
|
};
|
|
42
51
|
}
|