@cplieger/deadset-ts 3.4.0 → 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/components.js +61 -106
- package/dist/src/config.js +6 -0
- package/dist/src/kinds.js +40 -0
- package/dist/src/print.js +10 -1
- package/dist/src/resolve.js +90 -9
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +37 -1
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +109 -19
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/components.ts +67 -130
- package/src/config.ts +37 -2
- package/src/kinds.ts +41 -0
- package/src/print.ts +17 -1
- package/src/resolve.ts +123 -9
- package/src/run.ts +10 -3
- package/src/schema.ts +47 -2
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +138 -20
- package/src/version.ts +1 -1
package/dist/src/scope.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ConfigError } from "./config.js";
|
|
1
2
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
3
|
+
import { checkDocument } from "./schema.js";
|
|
2
4
|
/** The roles a scope document declares for a module. */
|
|
3
5
|
export const ROLE_TARGET = "target";
|
|
4
6
|
export const ROLE_CONSUMER = "consumer";
|
|
@@ -17,18 +19,37 @@ export class ScopeError extends Error {
|
|
|
17
19
|
this.name = "ScopeError";
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const MODULE = {
|
|
23
|
+
kind: "section",
|
|
24
|
+
members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
|
|
25
|
+
};
|
|
26
|
+
/** The closed key list of the scope document, compared as bytes. */
|
|
27
|
+
const SCOPE_ROOT = {
|
|
28
|
+
kind: "section",
|
|
29
|
+
members: {
|
|
30
|
+
target: MODULE,
|
|
31
|
+
workspace: { kind: "leaf" },
|
|
32
|
+
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
22
35
|
function isRecord(value) {
|
|
23
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* One string member, or undefined where the document omits it. A member the
|
|
40
|
+
* document writes is a string holding at least one character, so `null` and `""`
|
|
41
|
+
* are refused rather than read as absent.
|
|
42
|
+
*/
|
|
25
43
|
function stringMember(parent, name, at) {
|
|
26
44
|
if (!Object.hasOwn(parent, name)) {
|
|
27
|
-
return
|
|
45
|
+
return undefined;
|
|
28
46
|
}
|
|
29
47
|
const value = parent[name];
|
|
30
48
|
if (typeof value !== "string") {
|
|
31
|
-
throw new ScopeError(`scope: ${at}.${name} is not a string`);
|
|
49
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
50
|
+
}
|
|
51
|
+
if (value === "") {
|
|
52
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
32
53
|
}
|
|
33
54
|
return value;
|
|
34
55
|
}
|
|
@@ -36,30 +57,46 @@ function readModule(value, at, role, documentDir) {
|
|
|
36
57
|
if (!isRecord(value)) {
|
|
37
58
|
throw new ScopeError(`scope: ${at} is not an object`);
|
|
38
59
|
}
|
|
39
|
-
for (const name of Object.keys(value)) {
|
|
40
|
-
if (!MODULE_KEYS.has(name)) {
|
|
41
|
-
throw new ScopeError(`scope: ${at}.${name} is not a declared key`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
60
|
const declared = stringMember(value, "role", at);
|
|
45
|
-
if (declared !==
|
|
61
|
+
if (declared !== undefined && declared !== role) {
|
|
46
62
|
throw new ScopeError(`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`);
|
|
47
63
|
}
|
|
48
64
|
const path = stringMember(value, "path", at);
|
|
49
|
-
if (path ===
|
|
65
|
+
if (path === undefined) {
|
|
50
66
|
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
51
67
|
}
|
|
52
68
|
return {
|
|
53
|
-
id: stringMember(value, "id", at),
|
|
69
|
+
id: stringMember(value, "id", at) ?? "",
|
|
54
70
|
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
74
|
+
* Walks the document's text against the closed key list, refusing a key the list
|
|
75
|
+
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
76
|
+
* parse cannot report because it keeps the last value.
|
|
77
|
+
*/
|
|
78
|
+
function checkKeys(text, file) {
|
|
79
|
+
try {
|
|
80
|
+
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof ConfigError) {
|
|
84
|
+
// The configuration's refusal names the nearest configuration key, which a
|
|
85
|
+
// scope document does not have.
|
|
86
|
+
throw new ScopeError(error.kind === "unimplemented-key"
|
|
87
|
+
? `scope: ${file}: ${error.key} is not a declared key`
|
|
88
|
+
: error.message);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
|
|
95
|
+
* key written twice in one object, and a value of a type the document does not
|
|
96
|
+
* admit, `null` included, are errors, and a relative path inside the document
|
|
97
|
+
* resolves against the document's own directory.
|
|
61
98
|
*
|
|
62
|
-
* The `workspace` key the format declares is
|
|
99
|
+
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
63
100
|
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
64
101
|
* project resolves one through its own compiler configuration.
|
|
65
102
|
*/
|
|
@@ -85,11 +122,8 @@ export function readScope(host, file) {
|
|
|
85
122
|
if (!isRecord(value)) {
|
|
86
123
|
throw new ScopeError(`scope: ${file} is not one JSON object`);
|
|
87
124
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new ScopeError(`scope: ${file}: ${name} is not a declared key`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
125
|
+
checkKeys(text, file);
|
|
126
|
+
stringMember(value, "workspace", "");
|
|
93
127
|
const documentDir = dirnamePath(absolute);
|
|
94
128
|
if (!Object.hasOwn(value, "target")) {
|
|
95
129
|
throw new ScopeError(`scope: ${file} names no target`);
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
|
|
4
4
|
* `toString` where a value is passed to an `unknown` or `any` parameter of a function
|
|
5
5
|
* outside the analysis. A function of the project handing such a parameter of its own
|
|
6
|
-
* to a destination is one, holding back what that destination does, to a fixpoint.
|
|
6
|
+
* to a destination is one, holding back what that destination does, to a fixpoint. A
|
|
7
|
+
* function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
|
|
8
|
+
* given the arguments those methods pass it.
|
|
7
9
|
*/
|
|
8
|
-
import { isArrowFunction, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
-
import { SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
11
|
+
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
10
12
|
import { aliasChains } from "./alias-chain.js";
|
|
11
13
|
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
12
14
|
import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
@@ -71,6 +73,55 @@ function evaluated(argument) {
|
|
|
71
73
|
const value = isSpreadElement(argument) ? argument.expression : argument;
|
|
72
74
|
return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
|
|
73
75
|
}
|
|
76
|
+
/** The methods of `Function.prototype` that invoke a function with arguments of their own. */
|
|
77
|
+
const FUNCTION_METHODS = new Set(["call", "apply", "bind"]);
|
|
78
|
+
/** The interfaces of the standard library that declare those methods for a function value. */
|
|
79
|
+
const FUNCTION_INTERFACES = new Set([
|
|
80
|
+
"Function",
|
|
81
|
+
"CallableFunction",
|
|
82
|
+
"NewableFunction",
|
|
83
|
+
]);
|
|
84
|
+
function unparenthesized(expression) {
|
|
85
|
+
return isParenthesizedExpression(expression)
|
|
86
|
+
? unparenthesized(expression.expression)
|
|
87
|
+
: expression;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The invocation a call writes through a method of `Function.prototype`, read from
|
|
91
|
+
* the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
|
|
92
|
+
* binds `a`, and `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
|
|
93
|
+
* that is no array literal is the value of the first parameter, whose elements it
|
|
94
|
+
* carries. Whether the method is the prototype's is resolution's to say.
|
|
95
|
+
*/
|
|
96
|
+
function throughMethod(call) {
|
|
97
|
+
const expression = unparenthesized(call.expression);
|
|
98
|
+
if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
|
|
99
|
+
const [, ...rest] = call.arguments;
|
|
100
|
+
let passed = rest;
|
|
101
|
+
if (expression.name.text === "apply") {
|
|
102
|
+
const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
|
|
103
|
+
passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
method: expression.name,
|
|
107
|
+
invocation: { callee: expression.expression, arguments: passed, indirect: true },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (isCallExpression(expression)) {
|
|
111
|
+
const bound = unparenthesized(expression.expression);
|
|
112
|
+
if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
|
|
113
|
+
return {
|
|
114
|
+
method: bound.name,
|
|
115
|
+
invocation: {
|
|
116
|
+
callee: bound.expression,
|
|
117
|
+
arguments: [...expression.arguments.slice(1), ...call.arguments],
|
|
118
|
+
indirect: true,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
74
125
|
/**
|
|
75
126
|
* Every call of the project's own files with the class `this` stands for there, and
|
|
76
127
|
* every identifier-named parameter of every function, method and function-valued
|
|
@@ -146,9 +197,9 @@ function erasedParameters(project, parameters) {
|
|
|
146
197
|
return bySymbol;
|
|
147
198
|
}
|
|
148
199
|
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
149
|
-
function handedOn(project, sites, erased) {
|
|
200
|
+
function handedOn(project, sites, erased, invocationOf) {
|
|
150
201
|
const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
|
|
151
|
-
const candidates = sites.flatMap((site) => site.call.arguments.flatMap((argument, index) => valuesPassed(argument)
|
|
202
|
+
const candidates = sites.flatMap((site) => invocationOf(site.call).arguments.flatMap((argument, index) => valuesPassed(argument)
|
|
152
203
|
.filter((value) => spellings.has(value.name.getText()))
|
|
153
204
|
.map((value) => ({ site, argument: index, value }))));
|
|
154
205
|
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
@@ -175,11 +226,20 @@ function destinations(input, chains, walked) {
|
|
|
175
226
|
const checker = project.checker;
|
|
176
227
|
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
177
228
|
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
229
|
+
const forms = new Map();
|
|
230
|
+
for (const site of walked.sites) {
|
|
231
|
+
const form = throughMethod(site.call);
|
|
232
|
+
if (form !== undefined) {
|
|
233
|
+
forms.set(site.call, form);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const callees = resolvedTargets(project, [
|
|
237
|
+
...walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
|
|
238
|
+
...[...forms.values()].flatMap((form) => {
|
|
239
|
+
const name = calleeName(form.invocation.callee);
|
|
240
|
+
return name === undefined ? [form.method] : [form.method, name];
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
183
243
|
const erased = erasedParameters(project, walked.parameters);
|
|
184
244
|
const byFunction = new Map();
|
|
185
245
|
for (const parameter of erased.values()) {
|
|
@@ -188,15 +248,42 @@ function destinations(input, chains, walked) {
|
|
|
188
248
|
const outside = (target) => target.declarations.length > 0 &&
|
|
189
249
|
target.declarations.every((handle) => !own.has(handle.path) &&
|
|
190
250
|
!input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)));
|
|
251
|
+
const invocation = (call) => {
|
|
252
|
+
const form = forms.get(call);
|
|
253
|
+
const method = form === undefined ? undefined : callees.get(form.method);
|
|
254
|
+
// Only the standard library's function methods pass their arguments on: the
|
|
255
|
+
// program's own `call` and `Reflect.apply` invoke nothing here.
|
|
256
|
+
if (form !== undefined &&
|
|
257
|
+
method !== undefined &&
|
|
258
|
+
outside(method) &&
|
|
259
|
+
FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")) {
|
|
260
|
+
return form.invocation;
|
|
261
|
+
}
|
|
262
|
+
return { callee: call.expression, arguments: call.arguments, indirect: false };
|
|
263
|
+
};
|
|
264
|
+
const calleeOf = (call) => {
|
|
265
|
+
const name = calleeName(invocation(call).callee);
|
|
266
|
+
return name === undefined ? undefined : callees.get(name);
|
|
267
|
+
};
|
|
191
268
|
const erasedArguments = new Map();
|
|
192
269
|
const erasedAt = (call, argument) => {
|
|
193
270
|
let indexes = erasedArguments.get(call);
|
|
194
271
|
if (indexes === undefined) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
272
|
+
const invoked = invocation(call);
|
|
273
|
+
// Through a method of the prototype the resolved signature is the method's, so
|
|
274
|
+
// the parameters are read off the callee's own signatures, any one erasing.
|
|
275
|
+
const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
|
|
276
|
+
const signatures = invoked.indirect
|
|
277
|
+
? type === undefined
|
|
278
|
+
? []
|
|
279
|
+
: checker.getSignaturesOfType(type, SignatureKind.Call)
|
|
280
|
+
: [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
|
|
281
|
+
indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
|
|
282
|
+
const parameter = checker.getParameterType(signature, index);
|
|
283
|
+
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
|
284
|
+
})
|
|
285
|
+
? [index]
|
|
286
|
+
: []));
|
|
200
287
|
erasedArguments.set(call, indexes);
|
|
201
288
|
}
|
|
202
289
|
return indexes.has(argument);
|
|
@@ -215,7 +302,10 @@ function destinations(input, chains, walked) {
|
|
|
215
302
|
const declared = chains.declarationsOf(target);
|
|
216
303
|
if (declared.length === 0) {
|
|
217
304
|
return outside(target) && erasedAt(call, argument)
|
|
218
|
-
? {
|
|
305
|
+
? {
|
|
306
|
+
reach: "conversions",
|
|
307
|
+
callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
|
|
308
|
+
}
|
|
219
309
|
: undefined;
|
|
220
310
|
}
|
|
221
311
|
for (const fn of declared) {
|
|
@@ -227,7 +317,7 @@ function destinations(input, chains, walked) {
|
|
|
227
317
|
}
|
|
228
318
|
return undefined;
|
|
229
319
|
};
|
|
230
|
-
const forwarded = handedOn(project, walked.sites, erased);
|
|
320
|
+
const forwarded = handedOn(project, walked.sites, erased, invocation);
|
|
231
321
|
for (let moved = true; moved;) {
|
|
232
322
|
moved = false;
|
|
233
323
|
for (const one of forwarded) {
|
|
@@ -254,13 +344,13 @@ function destinations(input, chains, walked) {
|
|
|
254
344
|
}
|
|
255
345
|
return declared.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
256
346
|
};
|
|
257
|
-
return { at, mayReceive };
|
|
347
|
+
return { invocation, at, mayReceive };
|
|
258
348
|
}
|
|
259
349
|
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
260
350
|
function flowsOf(project, held, sites, to) {
|
|
261
351
|
const reach = valueReach(project, held);
|
|
262
352
|
const arguments_ = sites.flatMap((site) => to.mayReceive(site.call)
|
|
263
|
-
? site.call.arguments.flatMap((argument, index) => {
|
|
353
|
+
? to.invocation(site.call).arguments.flatMap((argument, index) => {
|
|
264
354
|
const value = evaluated(argument);
|
|
265
355
|
return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
|
|
266
356
|
})
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/components.ts
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
|
|
|
7
7
|
import type { Cascade } from "./config.ts";
|
|
@@ -17,25 +17,22 @@ const ID_DIGITS = 4;
|
|
|
17
17
|
/** The index of a declaration the component walk has not reached. */
|
|
18
18
|
const UNVISITED = -1;
|
|
19
19
|
|
|
20
|
-
/** One dead component
|
|
20
|
+
/** One dead component. */
|
|
21
21
|
export interface Component {
|
|
22
22
|
/** The analyzer's name, a solidus, and `c-` followed by the component's place in the order. */
|
|
23
23
|
readonly id: string;
|
|
24
|
-
/** The component's declarations, by site. A dead member of a dead container is one of them. */
|
|
25
|
-
readonly members: readonly string[];
|
|
26
24
|
/**
|
|
27
|
-
* The
|
|
28
|
-
*
|
|
29
|
-
* component outside it references.
|
|
25
|
+
* The component's declarations by site: its root members and every dead declaration
|
|
26
|
+
* that falls with them. A dead member of a dead container is one of them.
|
|
30
27
|
*/
|
|
31
|
-
readonly
|
|
28
|
+
readonly members: readonly string[];
|
|
32
29
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* The root members, by site. A root member is a member of a cycle no dead declaration
|
|
31
|
+
* outside the cycle references, whose container is not dead; every member of such a
|
|
32
|
+
* cycle with a live container is one, so a component has at least one.
|
|
36
33
|
*/
|
|
37
|
-
readonly
|
|
38
|
-
/** The source lines
|
|
34
|
+
readonly roots: readonly string[];
|
|
35
|
+
/** The source lines one deletion of the component removes: the distinct lines its members span. */
|
|
39
36
|
readonly deletableLines: number;
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -48,22 +45,22 @@ export interface Listing {
|
|
|
48
45
|
readonly deletableLines: number;
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
/** What a report carries for one component: its roots and
|
|
48
|
+
/** What a report carries for one component: its roots and counts, and its members in full. */
|
|
52
49
|
export function listing(component: Component, cascade: Cascade): Listing {
|
|
53
50
|
return {
|
|
54
51
|
roots: component.roots,
|
|
55
52
|
members: cascade === "full" ? component.members : [],
|
|
56
|
-
symbolCount: component.
|
|
53
|
+
symbolCount: component.members.length,
|
|
57
54
|
deletableLines: component.deletableLines,
|
|
58
55
|
};
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
/**
|
|
62
|
-
* Groups the declarations `dead` marks into components
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
59
|
+
* Groups the declarations `dead` marks into components, ordered by their first member's
|
|
60
|
+
* site; `dead` and `testOfDeadCode` hold one flag per declaration of the graph. Root
|
|
61
|
+
* members sit on the cycles no other cycle references. A cycle two roots both reach is
|
|
62
|
+
* dead only through both, so a component is a weakly connected part of the dead
|
|
63
|
+
* subgraph: no dead declaration of one references a declaration of another.
|
|
67
64
|
*/
|
|
68
65
|
export function componentsOf(
|
|
69
66
|
graph: Graph,
|
|
@@ -75,26 +72,22 @@ export function componentsOf(
|
|
|
75
72
|
return [];
|
|
76
73
|
}
|
|
77
74
|
const { componentOf, members } = connected(adjacent);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const outsideReference = externallyReferenced(adjacent, componentOf);
|
|
75
|
+
const referenced = referencedCycles(adjacent, componentOf, members.length);
|
|
76
|
+
const groups = joined(adjacent);
|
|
81
77
|
const idOf = (position: number): string => graph.symbols[at[position] ?? 0]?.id ?? "";
|
|
82
78
|
|
|
83
|
-
return
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
// deletion starts and the member falls with it.
|
|
79
|
+
return groups.map((group, place) => {
|
|
80
|
+
const roots = group.filter((position) => {
|
|
81
|
+
// A dead member's dead container is on the member's own cycle, and the container
|
|
82
|
+
// is where the deletion starts.
|
|
88
83
|
const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
|
|
89
|
-
return
|
|
84
|
+
return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
|
|
90
85
|
});
|
|
91
|
-
const fallen = falls[group] ?? [];
|
|
92
86
|
return {
|
|
93
87
|
id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
|
|
94
|
-
members:
|
|
88
|
+
members: group.map(idOf),
|
|
95
89
|
roots: roots.map(idOf),
|
|
96
|
-
|
|
97
|
-
deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
|
|
90
|
+
deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
|
|
98
91
|
};
|
|
99
92
|
});
|
|
100
93
|
}
|
|
@@ -185,10 +178,9 @@ interface Frame {
|
|
|
185
178
|
}
|
|
186
179
|
|
|
187
180
|
/**
|
|
188
|
-
* Each declaration's
|
|
181
|
+
* Each declaration's cycle and each cycle's members by site, by Tarjan's
|
|
189
182
|
* algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
|
|
190
|
-
* is walked without growing the call stack.
|
|
191
|
-
* each follows every component it reaches.
|
|
183
|
+
* is walked without growing the call stack.
|
|
192
184
|
*/
|
|
193
185
|
function connected(adjacent: readonly (readonly number[])[]): {
|
|
194
186
|
readonly componentOf: readonly number[];
|
|
@@ -257,45 +249,18 @@ function connected(adjacent: readonly (readonly number[])[]): {
|
|
|
257
249
|
return { componentOf, members };
|
|
258
250
|
}
|
|
259
251
|
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
* it, each pair counted once. An edge inside a component is not an edge between two, so
|
|
263
|
-
* the result is acyclic.
|
|
264
|
-
*/
|
|
265
|
-
function condense(
|
|
252
|
+
/** Per cycle, whether a dead declaration outside it references one of its members. */
|
|
253
|
+
function referencedCycles(
|
|
266
254
|
adjacent: readonly (readonly number[])[],
|
|
267
255
|
componentOf: readonly number[],
|
|
268
256
|
count: number,
|
|
269
|
-
): { readonly edges: readonly (readonly number[])[]; readonly into: readonly number[] } {
|
|
270
|
-
const edges: number[][] = Array.from({ length: count }, () => []);
|
|
271
|
-
const into = Array.from({ length: count }, () => 0);
|
|
272
|
-
const seen = new Set<string>();
|
|
273
|
-
adjacent.forEach((targets, from) => {
|
|
274
|
-
for (const to of targets) {
|
|
275
|
-
const a = componentOf[from] ?? 0;
|
|
276
|
-
const b = componentOf[to] ?? 0;
|
|
277
|
-
const pair = `${String(a)}>${String(b)}`;
|
|
278
|
-
if (a === b || seen.has(pair)) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
seen.add(pair);
|
|
282
|
-
edges[a]?.push(b);
|
|
283
|
-
into[b] = (into[b] ?? 0) + 1;
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
return { edges, into };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Per dead declaration, whether a dead component other than its own references it. */
|
|
290
|
-
function externallyReferenced(
|
|
291
|
-
adjacent: readonly (readonly number[])[],
|
|
292
|
-
componentOf: readonly number[],
|
|
293
257
|
): readonly boolean[] {
|
|
294
|
-
const referenced =
|
|
258
|
+
const referenced = Array.from({ length: count }, () => false);
|
|
295
259
|
adjacent.forEach((targets, from) => {
|
|
296
260
|
for (const to of targets) {
|
|
297
|
-
|
|
298
|
-
|
|
261
|
+
const cycle = componentOf[to] ?? 0;
|
|
262
|
+
if (componentOf[from] !== cycle) {
|
|
263
|
+
referenced[cycle] = true;
|
|
299
264
|
}
|
|
300
265
|
}
|
|
301
266
|
});
|
|
@@ -303,70 +268,42 @@ function externallyReferenced(
|
|
|
303
268
|
}
|
|
304
269
|
|
|
305
270
|
/**
|
|
306
|
-
* The
|
|
307
|
-
*
|
|
308
|
-
*
|
|
271
|
+
* The weakly connected parts of the dead subgraph, each its positions in ascending
|
|
272
|
+
* order, ordered by their first position. Positions follow the graph's declaration
|
|
273
|
+
* order, so a part's first position is its first member's site.
|
|
309
274
|
*/
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const from of sequence) {
|
|
317
|
-
for (const to of edges[from] ?? []) {
|
|
318
|
-
depth[to] = Math.max(depth[to] ?? 0, (depth[from] ?? 0) + 1);
|
|
275
|
+
function joined(adjacent: readonly (readonly number[])[]): readonly (readonly number[])[] {
|
|
276
|
+
const leader = adjacent.map((_edges, position) => position);
|
|
277
|
+
const find = (position: number): number => {
|
|
278
|
+
let top = position;
|
|
279
|
+
while ((leader[top] ?? top) !== top) {
|
|
280
|
+
top = leader[top] ?? top;
|
|
319
281
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Per component, the dead declarations one deletion of it removes. A root of the
|
|
328
|
-
* condensation carries its members and every component only it reaches; any other
|
|
329
|
-
* component carries its own members alone, because what it reaches is reached through a
|
|
330
|
-
* root above it as well.
|
|
331
|
-
*/
|
|
332
|
-
function fallSets(
|
|
333
|
-
edges: readonly (readonly number[])[],
|
|
334
|
-
members: readonly (readonly number[])[],
|
|
335
|
-
into: readonly number[],
|
|
336
|
-
): readonly (readonly number[])[] {
|
|
337
|
-
// Each root's walk stamps what it reaches with the root's own number, so the walks
|
|
338
|
-
// share one array rather than allocating one per root.
|
|
339
|
-
const stamp = edges.map(() => UNVISITED);
|
|
340
|
-
const reach = new Map<number, readonly number[]>();
|
|
341
|
-
const owners = edges.map(() => 0);
|
|
342
|
-
edges.forEach((_targets, root) => {
|
|
343
|
-
if (into[root] !== 0) {
|
|
344
|
-
return;
|
|
282
|
+
// Every position on the walk points at the top afterwards, so a later find is short.
|
|
283
|
+
for (let step = position; step !== top;) {
|
|
284
|
+
const next = leader[step] ?? top;
|
|
285
|
+
leader[step] = top;
|
|
286
|
+
step = next;
|
|
345
287
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
reached.push(to);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
reach.set(root, reached);
|
|
359
|
-
for (const component of reached) {
|
|
360
|
-
owners[component] = (owners[component] ?? 0) + 1;
|
|
288
|
+
return top;
|
|
289
|
+
};
|
|
290
|
+
adjacent.forEach((targets, from) => {
|
|
291
|
+
for (const to of targets) {
|
|
292
|
+
const a = find(from);
|
|
293
|
+
const b = find(to);
|
|
294
|
+
// The smaller position leads, so a part's leader is its first position.
|
|
295
|
+
leader[Math.max(a, b)] = Math.min(a, b);
|
|
361
296
|
}
|
|
362
297
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
298
|
+
const parts = new Map<number, number[]>();
|
|
299
|
+
adjacent.forEach((_edges, position) => {
|
|
300
|
+
const top = find(position);
|
|
301
|
+
const part = parts.get(top);
|
|
302
|
+
if (part === undefined) {
|
|
303
|
+
parts.set(top, [position]);
|
|
304
|
+
} else {
|
|
305
|
+
part.push(position);
|
|
369
306
|
}
|
|
370
|
-
return set.sort((a, b) => a - b);
|
|
371
307
|
});
|
|
308
|
+
return [...parts.values()];
|
|
372
309
|
}
|