@cplieger/deadset-ts 3.0.0 → 3.1.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 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 2.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, 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. 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`, `print-retained` 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.
11
+ **Status: pre-release.** This version implements contract 2.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. 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`, `print-retained` 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
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Where an import or export chain leads in one project: from one symbol to each
3
+ * declaration of the target it is, and along the chain of aliases it heads to each
4
+ * declaration every link of that chain is.
5
+ */
6
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
7
+ import { nodeKey } from "./inventory.js";
8
+ /**
9
+ * The chain walks over one project, against the declarations of `held`, the same
10
+ * project's inventory.
11
+ *
12
+ * A declaration handle carries the file it is in, so whether it is one of the
13
+ * project's own files is answered before the handle is resolved: a declaration of a
14
+ * library file is never fetched to find out that the inventory does not hold it.
15
+ */
16
+ export function aliasChains(project, held) {
17
+ const ownFiles = new Set(project.ownSourceFiles().map((file) => file.fileName));
18
+ const steps = new Map();
19
+ const chains = new Map();
20
+ let asked = 0;
21
+ const declarationsOf = (symbol) => {
22
+ const ids = [];
23
+ for (const handle of symbol.declarations) {
24
+ if (!ownFiles.has(handle.path)) {
25
+ continue;
26
+ }
27
+ const node = project.declarationAt(handle)?.node;
28
+ const id = node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
29
+ if (id !== undefined && !ids.includes(id)) {
30
+ ids.push(id);
31
+ }
32
+ }
33
+ return ids;
34
+ };
35
+ /**
36
+ * What one alias names, one link along. Only a symbol the binder flagged an alias is
37
+ * asked, which is the condition the step asserts on: a default export of an
38
+ * expression is written in an alias form and names nothing declared elsewhere, so its
39
+ * chain ends at itself.
40
+ */
41
+ const stepOf = (symbol) => {
42
+ if ((symbol.flags & SymbolFlags.Alias) === 0) {
43
+ return undefined;
44
+ }
45
+ if (steps.has(symbol.id)) {
46
+ return steps.get(symbol.id);
47
+ }
48
+ const next = project.aliasStepOf(symbol);
49
+ asked += 1;
50
+ steps.set(symbol.id, next);
51
+ return next;
52
+ };
53
+ const chainOf = (symbol) => {
54
+ const cached = chains.get(symbol.id);
55
+ if (cached !== undefined) {
56
+ return cached;
57
+ }
58
+ const targets = [];
59
+ const walked = new Set();
60
+ let at = symbol;
61
+ let stepped = false;
62
+ while (at !== undefined && !walked.has(at.id)) {
63
+ walked.add(at.id);
64
+ for (const id of declarationsOf(at)) {
65
+ if (!targets.some((target) => target.id === id)) {
66
+ targets.push({ id, stepped });
67
+ }
68
+ }
69
+ at = stepOf(at);
70
+ stepped = true;
71
+ }
72
+ chains.set(symbol.id, targets);
73
+ return targets;
74
+ };
75
+ const nextDeclared = (symbol) => {
76
+ const walked = new Set([symbol.id]);
77
+ let at = stepOf(symbol);
78
+ while (at !== undefined && !walked.has(at.id)) {
79
+ walked.add(at.id);
80
+ const ids = declarationsOf(at);
81
+ if (ids.length > 0) {
82
+ return ids;
83
+ }
84
+ at = stepOf(at);
85
+ }
86
+ return [];
87
+ };
88
+ return {
89
+ declarationsOf,
90
+ chainOf,
91
+ nextDeclared,
92
+ get steps() {
93
+ return asked;
94
+ },
95
+ };
96
+ }
@@ -8,15 +8,14 @@
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
10
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
11
- import { graphOf } from "./graph.js";
12
11
  import { inventory } from "./inventory.js";
13
12
  import { readManifest } from "./manifest.js";
13
+ import { matrixOf, sweepMatrix } from "./matrix.js";
14
14
  import { relativePath, resolvePath } from "./paths.js";
15
15
  import { byPosition } from "./position.js";
16
16
  import { references } from "./references.js";
17
17
  import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
18
18
  import { diagnosticsOf, runSession } from "./session.js";
19
- import { sweep } from "./sweep.js";
20
19
  /** The setting whose source decides where a finding about a configured root sits. */
21
20
  const ROOTS_SETTING = "roots.patterns";
22
21
  /**
@@ -66,9 +65,7 @@ function readProjects(engine, host, scope, config, stage) {
66
65
  }
67
66
  const held = inventory(project, host, targetRoot);
68
67
  const rooted = roots(project, held, targetRoot, options);
69
- const configuration = ids.get(project.configFile) ??
70
- relativePath(targetRoot, project.configFile) ??
71
- project.configFile;
68
+ const configuration = ids.get(project.configFile) ?? project.configFile;
72
69
  return stage(project, { configuration, held, rooted });
73
70
  });
74
71
  if (failures.length > 0) {
@@ -121,22 +118,26 @@ export function runRoots(engine, host, scope, config, provenance) {
121
118
  };
122
119
  }
123
120
  /**
124
- * Sweeps every project of the run the scope and the configuration describe, one
125
- * configuration at a time, in discovery order. Each project's references are read
126
- * against its own inventory with the configured test-file patterns, and its graph is
127
- * swept under the input the caller decided once for the run.
121
+ * Sweeps the run the scope and the configuration describe. Each project's references
122
+ * are read against its own inventory with the configured test-file patterns while its
123
+ * view is open; the projects then merge into one matrix in discovery order, which is
124
+ * swept under the input the caller decided once for the run. A project that carries
125
+ * an error ends the run before any project is swept.
128
126
  */
129
- export function runSweeps(engine, host, scope, config, input) {
127
+ export function runSweep(engine, host, scope, config, input) {
130
128
  const targetRoot = scope.target.path;
131
- return readProjects(engine, host, scope, config, (project, read) => {
129
+ const configured = readProjects(engine, host, scope, config, (project, read) => {
132
130
  const resolved = references(project, read.held, targetRoot, {
133
131
  testFiles: config.ts.testFiles,
134
132
  });
135
- const graph = graphOf(read.held.symbols, resolved.references, read.rooted.liveUnderReachability, resolved.testFilePaths);
136
133
  return {
137
134
  configuration: read.configuration,
138
135
  symbols: read.held.symbols,
139
- sweep: sweep(graph, input),
136
+ references: resolved.references,
137
+ roots: read.rooted.liveUnderReachability,
138
+ testFiles: resolved.testFilePaths,
140
139
  };
141
140
  });
141
+ const matrix = matrixOf(configured);
142
+ return { matrix, sweep: sweepMatrix(matrix, input) };
142
143
  }
package/dist/src/graph.js CHANGED
@@ -85,7 +85,11 @@ export function graphOf(symbols, references, roots, testFiles) {
85
85
  counts.production += 1;
86
86
  }
87
87
  }
88
- out[at(reference.from)]?.push({ to, test: reference.test });
88
+ out[at(reference.from)]?.push({
89
+ to,
90
+ test: reference.test,
91
+ evaluation: reference.use === "evaluation",
92
+ });
89
93
  }
90
94
  const parent = symbols.map((symbol) => at(symbol.parent));
91
95
  const exportsOf = symbols.map(() => []);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * One run's projects as one matrix: every declaration any project holds, keyed by its
3
+ * position and carrying the configurations that hold it, swept once per configuration
4
+ * and intersected, with the dead components computed once over every configuration.
5
+ */
6
+ import { componentsOf } from "./components.js";
7
+ import { graphOf } from "./graph.js";
8
+ import { byPosition } from "./position.js";
9
+ import { sweep } from "./sweep.js";
10
+ /** Two strings ordered bytewise. */
11
+ function compare(a, b) {
12
+ if (a === b) {
13
+ return 0;
14
+ }
15
+ return a < b ? -1 : 1;
16
+ }
17
+ /** What makes one reference one reference, whichever configuration made it. */
18
+ function referenceKey(reference) {
19
+ const { path, line, column } = reference.position;
20
+ return [reference.from, reference.to, path, String(line), String(column), reference.use].join("\u0000");
21
+ }
22
+ /**
23
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
24
+ *
25
+ * A declaration is keyed across configurations by its identifier, which is its rendered
26
+ * position and so is the same in every configuration that compiles its file; the fields
27
+ * of the first configuration that holds it are the ones kept. A declaration one
28
+ * configuration alone holds is in the union with that one configuration, so it is
29
+ * judged where it exists rather than skipped.
30
+ *
31
+ * One reference written at one position of a file several configurations compile is seen
32
+ * once per configuration and is one reference, so the union holds it once; two references
33
+ * one configuration makes at one position are two.
34
+ */
35
+ export function matrixOf(per) {
36
+ const symbols = [];
37
+ const holders = new Map();
38
+ const references = [];
39
+ const firstSeen = new Map();
40
+ per.forEach((one, index) => {
41
+ for (const symbol of one.symbols) {
42
+ const held = holders.get(symbol.id);
43
+ if (held === undefined) {
44
+ holders.set(symbol.id, [one.configuration]);
45
+ symbols.push(symbol);
46
+ }
47
+ else if (!held.includes(one.configuration)) {
48
+ held.push(one.configuration);
49
+ }
50
+ }
51
+ for (const reference of one.references) {
52
+ const key = referenceKey(reference);
53
+ const seen = firstSeen.get(key);
54
+ if (seen === undefined) {
55
+ firstSeen.set(key, index);
56
+ }
57
+ else if (seen !== index) {
58
+ continue;
59
+ }
60
+ references.push(reference);
61
+ }
62
+ });
63
+ symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
64
+ return {
65
+ configurations: per.map((one) => one.configuration),
66
+ graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
67
+ union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))]),
68
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
69
+ };
70
+ }
71
+ /**
72
+ * Answers which declarations of the run are dead in every configuration that holds them.
73
+ *
74
+ * Liveness is a question about one configuration, because a file another configuration
75
+ * does not compile makes no reference there and a root it does not hold seeds nothing
76
+ * there, so each configuration is swept on its own graph under the run's input. A
77
+ * declaration is a candidate when every configuration holding it judged it one: a
78
+ * reference under one configuration is a reference, so what one project uses is never
79
+ * reported, and a configuration that does not hold a declaration says nothing about it.
80
+ * The candidate's relation is reference counting where every configuration found it so,
81
+ * and reachability otherwise; its counts are the union's, one per reference.
82
+ *
83
+ * The components are computed once, over the union and the intersected dead set, so one
84
+ * dead component is one entry of the run with one identifier.
85
+ */
86
+ export function sweepMatrix(matrix, input) {
87
+ const per = matrix.graphs.map((graph) => sweep(graph, input));
88
+ const byConfiguration = new Map(matrix.configurations.map((configuration, index) => [
89
+ configuration,
90
+ new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
91
+ ]));
92
+ const union = matrix.union;
93
+ const dead = union.symbols.map(() => false);
94
+ const testOfDeadCode = union.symbols.map(() => false);
95
+ const candidates = [];
96
+ union.symbols.forEach((symbol, at) => {
97
+ const held = matrix.heldIn[at] ?? [];
98
+ const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
99
+ if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
100
+ return;
101
+ }
102
+ const all = found.filter((candidate) => candidate !== undefined);
103
+ const admitted = all.every((candidate) => candidate.testOfDeadCode);
104
+ dead[at] = true;
105
+ testOfDeadCode[at] = admitted;
106
+ candidates.push({
107
+ id: symbol.id,
108
+ productionRefs: union.made[at]?.production ?? 0,
109
+ testRefs: union.made[at]?.test ?? 0,
110
+ relation: all.every((candidate) => candidate.relation === "reference-counting")
111
+ ? "reference-counting"
112
+ : "reachability",
113
+ testOfDeadCode: admitted,
114
+ configurations: held,
115
+ });
116
+ });
117
+ return {
118
+ liveUnder: liveAnywhere(union.symbols, per),
119
+ candidates,
120
+ components: componentsOf(union, dead, testOfDeadCode),
121
+ };
122
+ }
123
+ /** The relations in the order a declaration's entry lists them. */
124
+ const RELATIONS = ["reference-counting", "reachability"];
125
+ /**
126
+ * Per declaration, in site order, every relation that holds it live in at least one
127
+ * configuration.
128
+ */
129
+ function liveAnywhere(symbols, per) {
130
+ const live = new Map();
131
+ for (const symbol of symbols) {
132
+ const relations = RELATIONS.filter((relation) => per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)));
133
+ if (relations.length > 0) {
134
+ live.set(symbol.id, relations);
135
+ }
136
+ }
137
+ return live;
138
+ }
@@ -1,5 +1,5 @@
1
- import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isForInStatement, isForOfStatement, isNamespaceExport, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
2
- import { SymbolFlags } from "@typescript/native/unstable/sync";
1
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
2
+ import { aliasChains } from "./alias-chain.js";
3
3
  import { globExpression } from "./glob.js";
4
4
  import { nodeKey } from "./inventory.js";
5
5
  import { byPosition, renderPosition } from "./position.js";
@@ -126,6 +126,38 @@ function markStores(node, writes) {
126
126
  }
127
127
  }
128
128
  }
129
+ /** Whether one node is a string literal or a template literal with no substitution. */
130
+ function isLiteralText(node) {
131
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
132
+ }
133
+ /**
134
+ * The module specifier of the module one node evaluates, if it evaluates one: an import
135
+ * declaration, an import-equals declaration of an external module, an export
136
+ * declaration that names a module, and an `import()` call whose argument is literal
137
+ * text. A type-only form evaluates nothing. An `import()` of a computed argument names
138
+ * no module this pass can know.
139
+ */
140
+ function evaluatedBy(node) {
141
+ if (isImportDeclaration(node)) {
142
+ return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword
143
+ ? undefined
144
+ : node.moduleSpecifier;
145
+ }
146
+ if (isExportDeclaration(node)) {
147
+ return node.isTypeOnly ? undefined : node.moduleSpecifier;
148
+ }
149
+ if (isImportEqualsDeclaration(node)) {
150
+ const reference = node.moduleReference;
151
+ return !node.isTypeOnly && isExternalModuleReference(reference)
152
+ ? reference.expression
153
+ : undefined;
154
+ }
155
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
156
+ const [argument] = node.arguments;
157
+ return isLiteralText(argument) ? argument : undefined;
158
+ }
159
+ return undefined;
160
+ }
129
161
  /**
130
162
  * The re-exports one node writes, if it is an export declaration. A specifier and a
131
163
  * namespace export are declarations whose names are where they are written, so no use is
@@ -174,6 +206,7 @@ function linksOf(file, node, declarations, enclosing) {
174
206
  function sitesOf(file, declarations, fileId) {
175
207
  const found = [];
176
208
  const links = [];
209
+ const evaluations = [];
177
210
  const writes = new Set();
178
211
  const declared = new Set();
179
212
  const shorthands = new Map();
@@ -210,6 +243,10 @@ function sitesOf(file, declarations, fileId) {
210
243
  markNames(node);
211
244
  markStores(node, writes);
212
245
  links.push(...linksOf(file, node, declarations, enclosing));
246
+ const evaluated = evaluatedBy(node);
247
+ if (evaluated !== undefined) {
248
+ evaluations.push({ node: evaluated, from: enclosing });
249
+ }
213
250
  const held = declarations.get(nodeKey(file, node));
214
251
  const outer = enclosing;
215
252
  if (held !== undefined) {
@@ -219,7 +256,7 @@ function sitesOf(file, declarations, fileId) {
219
256
  enclosing = outer;
220
257
  };
221
258
  file.forEachChild(visit);
222
- return { uses: found, links };
259
+ return { uses: found, links, evaluations };
223
260
  }
224
261
  /**
225
262
  * The name one test-file rule takes. A rule's name is a word list the report's own
@@ -247,7 +284,8 @@ function compare(a, b) {
247
284
  * The order is by the referencing position, then by the declaration named, the
248
285
  * declaration referencing it and the use. That is a total order because one name
249
286
  * references one declaration once from one declaration, while a chain of aliases reaches
250
- * several declarations at one position.
287
+ * several declarations at one position and a bare star re-export's specifier both reads
288
+ * and evaluates the module it names.
251
289
  */
252
290
  export function references(project, held, targetRoot, options) {
253
291
  const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
@@ -256,85 +294,15 @@ export function references(project, held, targetRoot, options) {
256
294
  const matched = options.testFiles.map(() => 0);
257
295
  const found = [];
258
296
  const tests = [];
259
- const reached = new Map();
260
- const steps = new Map();
297
+ const chains = aliasChains(project, held);
261
298
  let batched = 0;
262
299
  let fileBatches = 0;
263
300
  let residueFallbacks = 0;
264
301
  let shorthandLookups = 0;
265
- let aliasSteps = 0;
266
302
  const files = project.ownSourceFiles();
267
- const ownFiles = new Set(files.map((file) => file.fileName));
303
+ const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
268
304
  /** The identifier of the declaration one node of this program is, where it is one. */
269
305
  const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
270
- /**
271
- * The declarations of the inventory one symbol is.
272
- *
273
- * A declaration handle carries the file it is in, so the question is answered before
274
- * the handle is resolved: a declaration outside the project's own files belongs to
275
- * another program and is never read, which is what keeps the pass from fetching a
276
- * library file to discover that the inventory does not hold what is in it.
277
- */
278
- const declarationsOf = (symbol) => {
279
- const ids = [];
280
- for (const handle of symbol.declarations) {
281
- if (!ownFiles.has(handle.path)) {
282
- continue;
283
- }
284
- const node = project.declarationAt(handle)?.node;
285
- const id = node === undefined ? undefined : declaredAt(node);
286
- if (id !== undefined && !ids.includes(id)) {
287
- ids.push(id);
288
- }
289
- }
290
- return ids;
291
- };
292
- /**
293
- * What one alias names, one link along, asked once per alias whatever the number of
294
- * chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
295
- * the condition the step asserts on: a default export of an expression is written in
296
- * an alias form and names nothing declared elsewhere, so its chain ends at itself.
297
- */
298
- const stepOf = (symbol) => {
299
- if ((symbol.flags & SymbolFlags.Alias) === 0) {
300
- return undefined;
301
- }
302
- if (steps.has(symbol.id)) {
303
- return steps.get(symbol.id);
304
- }
305
- const next = project.aliasStepOf(symbol);
306
- aliasSteps += 1;
307
- steps.set(symbol.id, next);
308
- return next;
309
- };
310
- /**
311
- * The declarations one symbol names: the declaration it is, and every link of the
312
- * alias chain it stands at the head of. A re-export something imports is used and so
313
- * is the declaration behind it, so both are named; a link this project does not
314
- * declare is stepped over rather than recorded.
315
- */
316
- const targetsOf = (symbol) => {
317
- const cached = reached.get(symbol.id);
318
- if (cached !== undefined) {
319
- return cached;
320
- }
321
- const targets = [];
322
- const walked = new Set();
323
- let at = symbol;
324
- let stepped = false;
325
- while (at !== undefined && !walked.has(at.id)) {
326
- walked.add(at.id);
327
- for (const id of declarationsOf(at)) {
328
- if (!targets.some((target) => target.id === id)) {
329
- targets.push({ id, stepped });
330
- }
331
- }
332
- at = stepOf(at);
333
- stepped = true;
334
- }
335
- reached.set(symbol.id, targets);
336
- return targets;
337
- };
338
306
  /**
339
307
  * The declarations one re-export carries forward: for an alias, the first link along
340
308
  * its chain that the inventory declares, stepping over the links it does not, such as
@@ -342,22 +310,7 @@ export function references(project, held, targetRoot, options) {
342
310
  * Each later link is a declaration of its own that references the next, so one link
343
311
  * per re-export is what lets a chain be followed one declaration at a time.
344
312
  */
345
- const carriedBy = (link, symbol) => {
346
- if (!link.alias) {
347
- return declarationsOf(symbol);
348
- }
349
- const walked = new Set([symbol.id]);
350
- let at = stepOf(symbol);
351
- while (at !== undefined && !walked.has(at.id)) {
352
- walked.add(at.id);
353
- const ids = declarationsOf(at);
354
- if (ids.length > 0) {
355
- return ids;
356
- }
357
- at = stepOf(at);
358
- }
359
- return [];
360
- };
313
+ const carriedBy = (link, symbol) => link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
361
314
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
362
315
  const symbolFor = (site, answered) => {
363
316
  if (site.shorthand !== undefined) {
@@ -383,14 +336,18 @@ export function references(project, held, targetRoot, options) {
383
336
  if (test) {
384
337
  tests.push(path);
385
338
  }
386
- const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
339
+ const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
387
340
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
388
- // the uses. A shorthand is left out of the batch: its name resolves to the property
389
- // the literal declares, so the batch's answer for it would name something this
390
- // project's inventory never holds.
341
+ // the uses and the evaluated modules' specifiers last, each node once: a bare star
342
+ // re-export's specifier is a link and an evaluation both. A shorthand is left out
343
+ // of the batch: its name resolves to the property the literal declares, so the
344
+ // batch's answer for it would name something this project's inventory never holds.
391
345
  const batching = [
392
- ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
393
- ...links.map((link) => link.node),
346
+ ...new Set([
347
+ ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
348
+ ...links.map((link) => link.node),
349
+ ...evaluations.map((evaluation) => evaluation.node),
350
+ ]),
394
351
  ];
395
352
  const answered = new Map();
396
353
  batched += batching.length;
@@ -422,6 +379,25 @@ export function references(project, held, targetRoot, options) {
422
379
  });
423
380
  }
424
381
  }
382
+ for (const evaluation of evaluations) {
383
+ const module = answered.get(evaluation.node);
384
+ if (module === undefined) {
385
+ continue;
386
+ }
387
+ const position = renderPosition(file, targetRoot, evaluation.node.getStart());
388
+ for (const id of chains.declarationsOf(module)) {
389
+ if (fileIds.has(id)) {
390
+ found.push({
391
+ from: evaluation.from,
392
+ to: id,
393
+ position,
394
+ use: "evaluation",
395
+ resolution: "batch",
396
+ test,
397
+ });
398
+ }
399
+ }
400
+ }
425
401
  for (const site of uses) {
426
402
  const direct = site.shorthand !== undefined
427
403
  ? "shorthand"
@@ -433,7 +409,9 @@ export function references(project, held, targetRoot, options) {
433
409
  continue;
434
410
  }
435
411
  const position = renderPosition(file, targetRoot, site.node.getStart());
436
- for (const target of targetsOf(symbol)) {
412
+ // A re-export something imports is used and so is the declaration behind it, so
413
+ // the use names every link of the chain its name stands at the head of.
414
+ for (const target of chains.chainOf(symbol)) {
437
415
  found.push({
438
416
  from: site.from,
439
417
  to: target.id,
@@ -456,6 +434,6 @@ export function references(project, held, targetRoot, options) {
456
434
  .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
457
435
  .sort((a, b) => compare(a.rule, b.rule)),
458
436
  testFilePaths: tests.sort(compare),
459
- cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
437
+ cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
460
438
  };
461
439
  }