@cplieger/deadset-ts 2.2.1 → 3.0.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 +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declarations of one graph are dead, under which liveness relation, and which
|
|
3
|
+
* dead component each belongs to. Reference counting holds a declaration live when any
|
|
4
|
+
* declaration references it, whatever that one's own liveness, and no root plays a part;
|
|
5
|
+
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
|
+
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
|
+
*/
|
|
8
|
+
import { componentsOf } from "./components.js";
|
|
9
|
+
import { namesACaller, OUTSIDE } from "./graph.js";
|
|
10
|
+
/**
|
|
11
|
+
* Answers which declarations of one graph are dead under the input's mode.
|
|
12
|
+
*
|
|
13
|
+
* The order is the order the answers depend on: the callers and the marks are live
|
|
14
|
+
* before either relation runs, the candidate set is every judged declaration at least
|
|
15
|
+
* one relation does not hold live, the tests of dead code join it, and the components
|
|
16
|
+
* are computed over the set that results.
|
|
17
|
+
*/
|
|
18
|
+
export function sweep(graph, input) {
|
|
19
|
+
const marked = graph.symbols.map(() => false);
|
|
20
|
+
for (const id of input.marked) {
|
|
21
|
+
const at = graph.at(id);
|
|
22
|
+
if (at !== OUTSIDE) {
|
|
23
|
+
marked[at] = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Every root and every mark seeds reachability. A root of a kind that names a caller,
|
|
27
|
+
// and a mark, are live under reference counting as well; a root that only supposes a
|
|
28
|
+
// caller is not.
|
|
29
|
+
const called = graph.symbols.map(() => false);
|
|
30
|
+
for (const root of graph.rooted) {
|
|
31
|
+
if (namesACaller(root.kind)) {
|
|
32
|
+
called[root.at] = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const counted = (at) => {
|
|
36
|
+
const made = graph.made[at];
|
|
37
|
+
if (made === undefined) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
return input.mode.production ? made.production : made.production + made.test;
|
|
41
|
+
};
|
|
42
|
+
const reached = reachable(graph, marked, input.mode);
|
|
43
|
+
const liveUnder = graph.symbols.map((_symbol, at) => {
|
|
44
|
+
const relations = [];
|
|
45
|
+
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
46
|
+
relations.push("reference-counting");
|
|
47
|
+
}
|
|
48
|
+
if (reached[at] === true) {
|
|
49
|
+
relations.push("reachability");
|
|
50
|
+
}
|
|
51
|
+
return relations;
|
|
52
|
+
});
|
|
53
|
+
const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2);
|
|
54
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
|
|
55
|
+
const candidates = [];
|
|
56
|
+
const live = new Map();
|
|
57
|
+
graph.symbols.forEach((symbol, at) => {
|
|
58
|
+
const relations = liveUnder[at] ?? [];
|
|
59
|
+
if (relations.length > 0) {
|
|
60
|
+
live.set(symbol.id, relations);
|
|
61
|
+
}
|
|
62
|
+
if (dead[at] !== true) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
candidates.push({
|
|
66
|
+
id: symbol.id,
|
|
67
|
+
productionRefs: graph.made[at]?.production ?? 0,
|
|
68
|
+
testRefs: graph.made[at]?.test ?? 0,
|
|
69
|
+
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
70
|
+
testOfDeadCode: testOfDeadCode[at] === true,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
liveUnder: live,
|
|
75
|
+
candidates,
|
|
76
|
+
components: componentsOf(graph, dead, testOfDeadCode),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
81
|
+
* reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
|
|
82
|
+
* no mode withholds a reference it makes, while a root reaches through the references the
|
|
83
|
+
* mode counts. The marks run first, so what they reach is expanded under the wider rule.
|
|
84
|
+
*/
|
|
85
|
+
function reachable(graph, marked, mode) {
|
|
86
|
+
const reached = graph.symbols.map(() => false);
|
|
87
|
+
const expanded = graph.symbols.map(() => false);
|
|
88
|
+
const queue = [];
|
|
89
|
+
// A reference to a file is a reference to the module's namespace, which reaches what
|
|
90
|
+
// the module exports as well as its top level. A root does not expand: a file a
|
|
91
|
+
// runtime runs without reading its exports is a root by itself.
|
|
92
|
+
const enter = (at, viaReference) => {
|
|
93
|
+
if (!reached[at]) {
|
|
94
|
+
reached[at] = true;
|
|
95
|
+
queue.push(at);
|
|
96
|
+
}
|
|
97
|
+
if (!viaReference || expanded[at] === true) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
expanded[at] = true;
|
|
101
|
+
for (const exported of graph.exportsOf[at] ?? []) {
|
|
102
|
+
enter(exported, false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const walk = (held) => {
|
|
106
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
107
|
+
for (const edge of graph.out[at] ?? []) {
|
|
108
|
+
if (!held && mode.production && edge.test) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
enter(edge.to, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
marked.forEach((mark, at) => {
|
|
116
|
+
if (mark) {
|
|
117
|
+
enter(at, false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
walk(true);
|
|
121
|
+
for (const root of graph.rooted) {
|
|
122
|
+
enter(root.at, false);
|
|
123
|
+
}
|
|
124
|
+
walk(false);
|
|
125
|
+
return reached;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Admits every test declaration whose set of referenced production declarations is
|
|
129
|
+
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
130
|
+
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
131
|
+
* which is what makes the targets dead. A test referencing one live target is never admitted.
|
|
132
|
+
*/
|
|
133
|
+
function admitTestsOfDeadCode(graph, dead, marked) {
|
|
134
|
+
const admitted = graph.symbols.map(() => false);
|
|
135
|
+
graph.symbols.forEach((_symbol, at) => {
|
|
136
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let targets = 0;
|
|
140
|
+
let live = 0;
|
|
141
|
+
for (const edge of graph.out[at] ?? []) {
|
|
142
|
+
if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
targets += 1;
|
|
146
|
+
if (dead[edge.to] !== true) {
|
|
147
|
+
live += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (targets > 0 && live === 0) {
|
|
151
|
+
dead[at] = true;
|
|
152
|
+
admitted[at] = true;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return admitted;
|
|
156
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/js": "10.0.1",
|
|
35
35
|
"@types/node": "24.13.5",
|
|
36
|
-
"eslint": "10.
|
|
36
|
+
"eslint": "10.11.0",
|
|
37
37
|
"fast-check": "4.10.1",
|
|
38
|
-
"prettier": "3.9.
|
|
38
|
+
"prettier": "3.9.8",
|
|
39
39
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
40
40
|
"typescript-eslint": "8.70.0",
|
|
41
41
|
"vitest": "5.0.1"
|
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One run's passes, composed: the projects discovery names, opened in one snapshot
|
|
3
|
+
* of one client, each read through the stages in the order they depend on each
|
|
4
|
+
* other, and the per-project answers merged into the answer for the run.
|
|
5
|
+
*
|
|
6
|
+
* The stages take a project's view, which only this composition makes: a view
|
|
7
|
+
* carries a project's identity in its type so a handle cannot cross to another
|
|
8
|
+
* project's checker, and that holds only for views the session itself hands out.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Diagnostic } from "@typescript/native/unstable/sync";
|
|
12
|
+
import type { Config, Provenance } from "./config.ts";
|
|
13
|
+
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
|
|
14
|
+
import type { Finding } from "./finding.ts";
|
|
15
|
+
import { graphOf } from "./graph.ts";
|
|
16
|
+
import type { Host } from "./host.ts";
|
|
17
|
+
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
18
|
+
import { readManifest } from "./manifest.ts";
|
|
19
|
+
import { relativePath, resolvePath } from "./paths.ts";
|
|
20
|
+
import { byPosition, type Position } from "./position.ts";
|
|
21
|
+
import { references } from "./references.ts";
|
|
22
|
+
import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
|
|
23
|
+
import type { Scope } from "./scope.ts";
|
|
24
|
+
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
25
|
+
import { sweep, type SweepInput, type SweepResult } from "./sweep.ts";
|
|
26
|
+
|
|
27
|
+
/** The setting whose source decides where a finding about a configured root sits. */
|
|
28
|
+
const ROOTS_SETTING = "roots.patterns";
|
|
29
|
+
|
|
30
|
+
/** One root of the run, merged across the projects that hold it. */
|
|
31
|
+
export interface RunRoot {
|
|
32
|
+
/** The stable reference of the declaration the root names. */
|
|
33
|
+
readonly ref: string;
|
|
34
|
+
/** Where that declaration is written. */
|
|
35
|
+
readonly position: Position;
|
|
36
|
+
readonly kind: RootKind;
|
|
37
|
+
readonly source: string;
|
|
38
|
+
/** The configurations whose project holds this root, in the run's order. */
|
|
39
|
+
readonly configurations: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The root set of one run, and the findings about the roots the configuration names. */
|
|
43
|
+
export interface RunRoots {
|
|
44
|
+
/**
|
|
45
|
+
* The configurations the run analyzed, each by the name discovery gives its
|
|
46
|
+
* project, in discovery order.
|
|
47
|
+
*/
|
|
48
|
+
readonly configurations: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Every root of the run, ordered by the site of its declaration, then by kind, then
|
|
51
|
+
* by the string that named it. A root several projects hold is one entry naming each.
|
|
52
|
+
*/
|
|
53
|
+
readonly roots: readonly RunRoot[];
|
|
54
|
+
/** One finding per configured root or pattern that named nothing in any project. */
|
|
55
|
+
readonly findings: readonly Finding[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The document that supplied one setting, below the target root, or the empty
|
|
60
|
+
* string where the setting came from no document of the target. Both paths are
|
|
61
|
+
* resolved against the working directory before one is read below the other, so a
|
|
62
|
+
* relative target and a relative document name the same place.
|
|
63
|
+
*/
|
|
64
|
+
function documentOf(
|
|
65
|
+
host: Host,
|
|
66
|
+
provenance: Provenance,
|
|
67
|
+
setting: string,
|
|
68
|
+
targetRoot: string,
|
|
69
|
+
): string {
|
|
70
|
+
const origin = provenance.get(setting);
|
|
71
|
+
if (origin === undefined || origin.source === "default" || origin.source === "flag") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
const document = resolvePath(host.workingDirectory(), origin.label);
|
|
75
|
+
return relativePath(resolvePath(host.workingDirectory(), targetRoot), document) ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Two strings ordered bytewise. */
|
|
79
|
+
function compare(a: string, b: string): number {
|
|
80
|
+
if (a === b) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
return a < b ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What every stage after the root set reads of one project. */
|
|
87
|
+
interface ProjectRead {
|
|
88
|
+
readonly configuration: string;
|
|
89
|
+
readonly held: Inventory;
|
|
90
|
+
readonly rooted: Roots;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reads every project the scope discovers, in one snapshot: each is checked for errors
|
|
95
|
+
* first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
|
|
96
|
+
* the project while its view is open. A project carrying an error fails the run with no
|
|
97
|
+
* answer, as every verb that reads declarations does.
|
|
98
|
+
*/
|
|
99
|
+
function readProjects<Answer>(
|
|
100
|
+
engine: Engine,
|
|
101
|
+
host: Host,
|
|
102
|
+
scope: Scope,
|
|
103
|
+
config: Config,
|
|
104
|
+
stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
|
|
105
|
+
): readonly Answer[] {
|
|
106
|
+
const targetRoot = scope.target.path;
|
|
107
|
+
const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
|
|
108
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
109
|
+
const options = {
|
|
110
|
+
manifest: readManifest(host, targetRoot),
|
|
111
|
+
patterns: config.rootPatterns,
|
|
112
|
+
entryFiles: config.ts.entryFiles,
|
|
113
|
+
testFiles: config.ts.testFiles,
|
|
114
|
+
publishedAPI: config.targetKind === "library",
|
|
115
|
+
};
|
|
116
|
+
const failures: Diagnostic[] = [];
|
|
117
|
+
const { projects } = runSession(engine, discovered.configFiles, (project) => {
|
|
118
|
+
const errors = diagnosticErrors(diagnosticsOf(project));
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
failures.push(...errors);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const held = inventory(project, host, targetRoot);
|
|
124
|
+
const rooted = roots(project, held, targetRoot, options);
|
|
125
|
+
const configuration =
|
|
126
|
+
ids.get(project.configFile) ??
|
|
127
|
+
relativePath(targetRoot, project.configFile) ??
|
|
128
|
+
project.configFile;
|
|
129
|
+
return stage(project, { configuration, held, rooted });
|
|
130
|
+
});
|
|
131
|
+
if (failures.length > 0) {
|
|
132
|
+
throw new DiscoveryError(
|
|
133
|
+
`${String(failures.length)} error(s); no answer was produced`,
|
|
134
|
+
failures,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return projects.filter((answer): answer is Answer => answer !== undefined);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The root set of the run the scope and the configuration describe.
|
|
142
|
+
*
|
|
143
|
+
* A configured string is a finding when it names nothing in every project, and the
|
|
144
|
+
* finding sits in the document that declared the roots.
|
|
145
|
+
*/
|
|
146
|
+
export function runRoots(
|
|
147
|
+
engine: Engine,
|
|
148
|
+
host: Host,
|
|
149
|
+
scope: Scope,
|
|
150
|
+
config: Config,
|
|
151
|
+
provenance: Provenance,
|
|
152
|
+
): RunRoots {
|
|
153
|
+
const targetRoot = scope.target.path;
|
|
154
|
+
const projects = readProjects(engine, host, scope, config, (_project, read) => read);
|
|
155
|
+
|
|
156
|
+
const configurations: string[] = [];
|
|
157
|
+
const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
|
|
158
|
+
for (const answer of projects) {
|
|
159
|
+
const configuration = answer.configuration;
|
|
160
|
+
configurations.push(configuration);
|
|
161
|
+
const symbols = new Map(answer.held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
162
|
+
for (const root of answer.rooted.liveUnderReachability) {
|
|
163
|
+
const symbol = symbols.get(root.id);
|
|
164
|
+
if (symbol === undefined) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const key = [root.id, root.kind, root.source].join("\u0000");
|
|
168
|
+
const held = merged.get(key);
|
|
169
|
+
if (held === undefined) {
|
|
170
|
+
merged.set(key, {
|
|
171
|
+
ref: symbol.ref,
|
|
172
|
+
position: symbol.position,
|
|
173
|
+
kind: root.kind,
|
|
174
|
+
source: root.source,
|
|
175
|
+
configurations: [configuration],
|
|
176
|
+
});
|
|
177
|
+
} else if (!held.configurations.includes(configuration)) {
|
|
178
|
+
held.configurations.push(configuration);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const ordered = [...merged.values()].sort(
|
|
184
|
+
(a, b) =>
|
|
185
|
+
byPosition(a.position, b.position) || compare(a.kind, b.kind) || compare(a.source, b.source),
|
|
186
|
+
);
|
|
187
|
+
const unmatched = unmatchedEverywhere(
|
|
188
|
+
config.rootPatterns,
|
|
189
|
+
projects.map((answer) => answer.rooted),
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
configurations,
|
|
193
|
+
roots: ordered,
|
|
194
|
+
findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One configuration's sweep, beside the declarations it judged. */
|
|
199
|
+
export interface ConfigurationSweep {
|
|
200
|
+
/** The name discovery gives the configuration's project. */
|
|
201
|
+
readonly configuration: string;
|
|
202
|
+
readonly symbols: readonly InventorySymbol[];
|
|
203
|
+
readonly sweep: SweepResult;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sweeps every project of the run the scope and the configuration describe, one
|
|
208
|
+
* configuration at a time, in discovery order. Each project's references are read
|
|
209
|
+
* against its own inventory with the configured test-file patterns, and its graph is
|
|
210
|
+
* swept under the input the caller decided once for the run.
|
|
211
|
+
*/
|
|
212
|
+
export function runSweeps(
|
|
213
|
+
engine: Engine,
|
|
214
|
+
host: Host,
|
|
215
|
+
scope: Scope,
|
|
216
|
+
config: Config,
|
|
217
|
+
input: SweepInput,
|
|
218
|
+
): readonly ConfigurationSweep[] {
|
|
219
|
+
const targetRoot = scope.target.path;
|
|
220
|
+
return readProjects(engine, host, scope, config, (project, read) => {
|
|
221
|
+
const resolved = references(project, read.held, targetRoot, {
|
|
222
|
+
testFiles: config.ts.testFiles,
|
|
223
|
+
});
|
|
224
|
+
const graph = graphOf(
|
|
225
|
+
read.held.symbols,
|
|
226
|
+
resolved.references,
|
|
227
|
+
read.rooted.liveUnderReachability,
|
|
228
|
+
resolved.testFilePaths,
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
configuration: read.configuration,
|
|
232
|
+
symbols: read.held.symbols,
|
|
233
|
+
sweep: sweep(graph, input),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|