@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/src/calls.ts ADDED
@@ -0,0 +1,224 @@
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
+
9
+ import {
10
+ isArrayLiteralExpression,
11
+ isCallExpression,
12
+ isClassDeclaration,
13
+ isDecorator,
14
+ isIdentifier,
15
+ isObjectLiteralExpression,
16
+ isParenthesizedExpression,
17
+ isPropertyAccessExpression,
18
+ isPropertyAssignment,
19
+ isShorthandPropertyAssignment,
20
+ isSpreadElement,
21
+ type CallExpression,
22
+ type Decorator,
23
+ type Expression,
24
+ type Node,
25
+ } from "@typescript/native/unstable/ast";
26
+ import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
27
+ import type { AliasChains } from "./alias-chain.ts";
28
+ import { nodeKey, type Inventory } from "./inventory.ts";
29
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
30
+ import type { ProjectView } from "./session.ts";
31
+
32
+ /** One value written where a call passes it, and the node its declaration is resolved from. */
33
+ export interface PassedValue {
34
+ /** Where the value is written, which is where a record of it sits. */
35
+ readonly site: Node;
36
+ /** The name the value is resolved through, or the shorthand assignment that holds it. */
37
+ readonly name: Node;
38
+ readonly shorthand: boolean;
39
+ }
40
+
41
+ /** One class of the target a call passes or a decorator is attached to, and where. */
42
+ export interface ClassUse {
43
+ /** The inventory's identifier of the class. */
44
+ readonly id: string;
45
+ readonly site: Node;
46
+ }
47
+
48
+ /** The name node a callee or a decorator expression resolves through, where it has one. */
49
+ export function calleeName(expression: Expression): Node | undefined {
50
+ if (isParenthesizedExpression(expression)) {
51
+ return calleeName(expression.expression);
52
+ }
53
+ if (isPropertyAccessExpression(expression)) {
54
+ return expression.name;
55
+ }
56
+ return isIdentifier(expression) ? expression : undefined;
57
+ }
58
+
59
+ /** The callee one decorator names: its expression, or the callee of the call it writes. */
60
+ export function decoratorCallee(decorator: Decorator): Node | undefined {
61
+ const expression = decorator.expression;
62
+ return calleeName(isCallExpression(expression) ? expression.expression : expression);
63
+ }
64
+
65
+ /** How one decorator is spelled in a record: its expression, without a call's arguments. */
66
+ export function spelledDecorator(decorator: Decorator): string {
67
+ const expression = decorator.expression;
68
+ const named = isCallExpression(expression) ? expression.expression : expression;
69
+ return `@${named.getText().replace(/\s+/gu, "")}`;
70
+ }
71
+
72
+ /** The decorators written on one declaration. */
73
+ export function decoratorsOf(node: Node): readonly Decorator[] {
74
+ return ((node as { readonly modifiers?: readonly Node[] }).modifiers ?? []).filter(isDecorator);
75
+ }
76
+
77
+ /**
78
+ * What each of a batch of nodes resolves to, every alias followed to the declaration
79
+ * it stands for. A node the checker resolves to nothing, and an alias that resolves
80
+ * to nothing, are absent from the answer. Each alias is followed once.
81
+ */
82
+ export function resolvedTargets<Brand>(
83
+ project: ProjectView<Brand>,
84
+ nodes: readonly Node[],
85
+ ): ReadonlyMap<Node, TSSymbol> {
86
+ const followed = new Map<number, TSSymbol | undefined>();
87
+ const targets = new Map<Node, TSSymbol>();
88
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
89
+ const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
90
+ project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
91
+ const node = run[index];
92
+ if (symbol === undefined || node === undefined) {
93
+ return;
94
+ }
95
+ let target: TSSymbol | undefined = symbol;
96
+ if ((symbol.flags & SymbolFlags.Alias) !== 0) {
97
+ if (!followed.has(symbol.id)) {
98
+ const aliased = project.checker.getAliasedSymbol(symbol);
99
+ followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
100
+ }
101
+ target = followed.get(symbol.id);
102
+ }
103
+ if (target !== undefined) {
104
+ targets.set(node, target);
105
+ }
106
+ });
107
+ }
108
+ return targets;
109
+ }
110
+
111
+ /** The values one expression passes: itself, or what an array or object literal holds. */
112
+ function passedIn(expression: Expression, found: PassedValue[]): void {
113
+ if (isParenthesizedExpression(expression)) {
114
+ passedIn(expression.expression, found);
115
+ return;
116
+ }
117
+ if (isArrayLiteralExpression(expression)) {
118
+ for (const element of expression.elements) {
119
+ passedIn(isSpreadElement(element) ? element.expression : element, found);
120
+ }
121
+ return;
122
+ }
123
+ if (isObjectLiteralExpression(expression)) {
124
+ for (const property of expression.properties) {
125
+ if (isPropertyAssignment(property)) {
126
+ passedIn(property.initializer, found);
127
+ } else if (isShorthandPropertyAssignment(property)) {
128
+ found.push({ site: property.name, name: property, shorthand: true });
129
+ }
130
+ }
131
+ return;
132
+ }
133
+ const name = calleeName(expression);
134
+ if (name !== undefined) {
135
+ found.push({ site: expression, name, shorthand: false });
136
+ }
137
+ }
138
+
139
+ /** The values one argument passes, a spread argument's included. */
140
+ export function valuesPassed(argument: Expression): readonly PassedValue[] {
141
+ const found: PassedValue[] = [];
142
+ passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
143
+ return found;
144
+ }
145
+
146
+ /**
147
+ * The classes of the target each call passes, by the call. A value that names no
148
+ * class of the target, a class of another program included, passes nothing here.
149
+ */
150
+ export function classesPassed<Brand>(
151
+ project: ProjectView<Brand>,
152
+ held: Inventory,
153
+ chains: AliasChains,
154
+ calls: readonly CallExpression[],
155
+ ): ReadonlyMap<CallExpression, readonly ClassUse[]> {
156
+ const passed = new Map(
157
+ calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]),
158
+ );
159
+ const all = [...passed.values()].flat();
160
+ const targets = resolvedTargets(
161
+ project,
162
+ all.filter((value) => !value.shorthand).map((value) => value.name),
163
+ );
164
+ const classes = new Set(
165
+ held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id),
166
+ );
167
+ const classesOf = (value: PassedValue): readonly string[] => {
168
+ const symbol = value.shorthand
169
+ ? project.shorthandValueAt(project.handle(value.name))
170
+ : targets.get(value.name);
171
+ return symbol === undefined
172
+ ? []
173
+ : chains.declarationsOf(symbol).filter((id) => classes.has(id));
174
+ };
175
+ return new Map(
176
+ [...passed].map(([call, values]) => [
177
+ call,
178
+ values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
179
+ ]),
180
+ );
181
+ }
182
+
183
+ /** One class declaration of the target and the decorators written on it. */
184
+ export interface DecoratedClass {
185
+ readonly id: string;
186
+ readonly decorators: readonly Decorator[];
187
+ }
188
+
189
+ /** Every class declaration of the target that carries a decorator. */
190
+ export function decoratedClasses<Brand>(
191
+ project: ProjectView<Brand>,
192
+ held: Inventory,
193
+ ): readonly DecoratedClass[] {
194
+ const found: DecoratedClass[] = [];
195
+ for (const file of project.ownSourceFiles()) {
196
+ const visit = (node: Node): void => {
197
+ if (isClassDeclaration(node)) {
198
+ const id = held.declarations.get(nodeKey(file, node));
199
+ const decorators = decoratorsOf(node);
200
+ if (id !== undefined && decorators.length > 0) {
201
+ found.push({ id, decorators });
202
+ }
203
+ }
204
+ node.forEachChild(visit);
205
+ };
206
+ file.forEachChild(visit);
207
+ }
208
+ return found;
209
+ }
210
+
211
+ /** Every call expression of the project's own files, decorators written as calls included. */
212
+ export function callsOf<Brand>(project: ProjectView<Brand>): readonly CallExpression[] {
213
+ const found: CallExpression[] = [];
214
+ for (const file of project.ownSourceFiles()) {
215
+ const visit = (node: Node): void => {
216
+ if (isCallExpression(node)) {
217
+ found.push(node);
218
+ }
219
+ node.forEachChild(visit);
220
+ };
221
+ file.forEachChild(visit);
222
+ }
223
+ return found;
224
+ }
@@ -0,0 +1,136 @@
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
+
7
+ import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
8
+ import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
9
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
10
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
11
+ import type { ProjectView } from "./session.ts";
12
+
13
+ /** One member of a class's value that the target declares. */
14
+ export interface ClassMember {
15
+ /** The inventory's identifier of the declaration. */
16
+ readonly id: string;
17
+ /** The declaring node, whose syntax says whether it is a property, an accessor or a method. */
18
+ readonly node: Node;
19
+ /** The member's symbol flags, as the checker reads it on this class. */
20
+ readonly flags: SymbolFlags;
21
+ }
22
+
23
+ /** Which side of a class a reading asks for: its instances, or the class itself. */
24
+ export type ClassSide = "instance" | "static";
25
+
26
+ /** The name node of each class declaration of the target, by the class's identifier. */
27
+ export function classNames<Brand>(
28
+ project: ProjectView<Brand>,
29
+ held: Inventory,
30
+ ): ReadonlyMap<string, Node> {
31
+ const names = new Map<string, Node>();
32
+ for (const file of project.ownSourceFiles()) {
33
+ const visit = (node: Node): void => {
34
+ if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
35
+ const id = held.declarations.get(nodeKey(file, node));
36
+ if (id !== undefined) {
37
+ names.set(id, node.name);
38
+ }
39
+ }
40
+ node.forEachChild(visit);
41
+ };
42
+ file.forEachChild(visit);
43
+ }
44
+ return names;
45
+ }
46
+
47
+ /** The types of a batch of nodes, asked in capped batches. */
48
+ export function typesAt<Brand>(
49
+ project: ProjectView<Brand>,
50
+ nodes: readonly Node[],
51
+ ): (Type | undefined)[] {
52
+ const types: (Type | undefined)[] = [];
53
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
54
+ types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
55
+ }
56
+ return types;
57
+ }
58
+
59
+ /**
60
+ * The type one side of each class is: the instance type at the class's name, or the
61
+ * type of the class's own symbol, which is its constructor's.
62
+ */
63
+ function sideTypes<Brand>(
64
+ project: ProjectView<Brand>,
65
+ nodes: readonly Node[],
66
+ side: ClassSide,
67
+ ): (Type | undefined)[] {
68
+ if (side === "instance") {
69
+ return typesAt(project, nodes);
70
+ }
71
+ const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
72
+ const present = symbols.filter((symbol): symbol is TSSymbol => symbol !== undefined);
73
+ const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
74
+ let next = 0;
75
+ return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
76
+ }
77
+
78
+ /**
79
+ * The members one side of each class carries that the target declares, by the class.
80
+ * A member declared in a file outside the project's own, and the `prototype` a class
81
+ * carries on its static side, are not declarations of the target and are left out.
82
+ */
83
+ export function classMembers<Brand>(
84
+ project: ProjectView<Brand>,
85
+ held: Inventory,
86
+ classIds: ReadonlySet<string>,
87
+ side: ClassSide,
88
+ ): ReadonlyMap<string, readonly ClassMember[]> {
89
+ const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
90
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
91
+ const types = sideTypes(
92
+ project,
93
+ named.map(([, node]) => node),
94
+ side,
95
+ );
96
+ const members = new Map<string, readonly ClassMember[]>();
97
+ named.forEach(([id], index) => {
98
+ const type = types[index];
99
+ if (type === undefined) {
100
+ return;
101
+ }
102
+ const found: ClassMember[] = [];
103
+ for (const property of project.checker.getPropertiesOfType(type)) {
104
+ for (const handle of property.declarations) {
105
+ if (!own.has(handle.path)) {
106
+ continue;
107
+ }
108
+ const node = project.declarationAt(handle)?.node;
109
+ const declared =
110
+ node === undefined
111
+ ? undefined
112
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
113
+ if (node !== undefined && declared !== undefined) {
114
+ found.push({ id: declared, node, flags: property.flags });
115
+ }
116
+ }
117
+ }
118
+ members.set(id, found);
119
+ });
120
+ return members;
121
+ }
122
+
123
+ /**
124
+ * The last component of one member's stable reference, `:static` included, which is
125
+ * how a configuration names a member; undefined for a declaration that is no member.
126
+ */
127
+ export function memberComponent(
128
+ symbol: InventorySymbol,
129
+ byId: ReadonlyMap<string, InventorySymbol>,
130
+ ): string | undefined {
131
+ const parent = byId.get(symbol.parent);
132
+ if (parent === undefined || !symbol.ref.startsWith(`${parent.ref}.`)) {
133
+ return undefined;
134
+ }
135
+ return symbol.ref.slice(parent.ref.length + 1);
136
+ }
package/src/components.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The dead components of one graph: the strongly connected components of the subgraph
3
- * the dead declarations induce, ordered so a report is worked from the top down, each
4
- * with the root members a deletion starts at and what falls with it.
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 and its place in the acyclic graph over the components. */
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 members no other dead component references and whose container is not dead,
28
- * which are where a deletion starts. A cycle has as many as it has members no dead
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 roots: readonly string[];
28
+ readonly members: readonly string[];
32
29
  /**
33
- * What one deletion of the component removes: its members, and every dead declaration
34
- * only this component reaches. A dead declaration two components both reach falls with
35
- * neither and is counted by its own component.
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 falls: readonly string[];
38
- /** The source lines the deletion removes: the distinct lines {@link Component.falls} spans. */
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 what falls, and its members in full. */
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.falls.length,
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 and orders them so each precedes
63
- * every component it reaches, two that neither reaches ordered by their first member's
64
- * site. `dead` and `testOfDeadCode` hold one flag per declaration of the graph; the dead
65
- * set arrives rather than being read from a sweep, so a set several sweeps intersect is
66
- * grouped over this graph's edges the same way.
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 { edges, into } = condense(adjacent, componentOf, members.length);
79
- const falls = fallSets(edges, members, into);
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 order(edges, members).map((group, place) => {
84
- const own = members[group] ?? [];
85
- const roots = own.filter((position) => {
86
- // A dead member of a dead container is never a root: the container is where the
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 outsideReference[position] !== true && dead[container] !== true;
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: own.map(idOf),
88
+ members: group.map(idOf),
95
89
  roots: roots.map(idOf),
96
- falls: fallen.map(idOf),
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 component and each component's members by site, by Tarjan'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. Components are closed in an order in which
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
- * Per component, the components it reaches in one step, and how many components reach
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 = adjacent.map(() => false);
258
+ const referenced = Array.from({ length: count }, () => false);
295
259
  adjacent.forEach((targets, from) => {
296
260
  for (const to of targets) {
297
- if (componentOf[from] !== componentOf[to]) {
298
- referenced[to] = true;
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 components in the order a report is worked in. Tarjan closes each component after
307
- * every one it reaches, so reading the closing order backwards is a topological order,
308
- * and one pass over it gives each component its depth below the roots.
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 order(
311
- edges: readonly (readonly number[])[],
312
- members: readonly (readonly number[])[],
313
- ): readonly number[] {
314
- const sequence = edges.map((_targets, component) => component).reverse();
315
- const depth = edges.map(() => 0);
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
- return sequence.sort(
322
- (a, b) => (depth[a] ?? 0) - (depth[b] ?? 0) || (members[a]?.[0] ?? 0) - (members[b]?.[0] ?? 0),
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
- const reached = [root];
347
- stamp[root] = root;
348
- // An array's iterator reads its length at every step, so what a step pushes is
349
- // walked in the same loop.
350
- for (const at of reached) {
351
- for (const to of edges[at] ?? []) {
352
- if (stamp[to] !== root) {
353
- stamp[to] = root;
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
- return members.map((own, component) => {
364
- const set = [...own];
365
- for (const other of reach.get(component) ?? []) {
366
- if (other !== component && owners[other] === 1) {
367
- set.push(...(members[other] ?? []));
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
  }