@cplieger/deadset-ts 3.11.0 → 3.12.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.
@@ -3,6 +3,7 @@ import { aliasChains } from "./alias-chain.js";
3
3
  import { destructuring, propertyReadsOf } from "./destructuring.js";
4
4
  import { globExpression } from "./glob.js";
5
5
  import { nodeKey } from "./inventory.js";
6
+ import { relativePath } from "./paths.js";
6
7
  import { byPosition, renderPosition } from "./position.js";
7
8
  /**
8
9
  * The batch cap a run takes when the configuration names none. It bounds the size of
@@ -306,7 +307,9 @@ function compare(a, b) {
306
307
  *
307
308
  * `held` is the inventory of the same project, whose declaration map is what a resolved
308
309
  * symbol is looked up in; `targetRoot` is the absolute path every position is rendered
309
- * against, which is the root the inventory was read against.
310
+ * against, which is the root the inventory was read against. A pass over a consumer
311
+ * walks the consumer's files instead and renders against its root, as
312
+ * {@link ReferenceOptions.consumer} states.
310
313
  *
311
314
  * The order is by the referencing position, then by the declaration named, the
312
315
  * declaration referencing it and the use. That is a total order because one name
@@ -326,7 +329,14 @@ export function references(project, held, targetRoot, options) {
326
329
  let fileBatches = 0;
327
330
  let residueFallbacks = 0;
328
331
  let shorthandLookups = 0;
329
- const files = project.ownSourceFiles();
332
+ const consumer = options.consumer;
333
+ const root = consumer?.root ?? targetRoot;
334
+ const files = consumer === undefined
335
+ ? project.ownSourceFiles()
336
+ : project
337
+ .ownSourceFiles()
338
+ .filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
339
+ relativePath(targetRoot, file.fileName) === undefined);
330
340
  const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
331
341
  /** The identifier of the declaration one node of this program is, where it is one. */
332
342
  const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
@@ -352,7 +362,7 @@ export function references(project, held, targetRoot, options) {
352
362
  return project.resolvedSymbolAt(project.handle(site.node));
353
363
  };
354
364
  for (const file of files) {
355
- const path = renderPosition(file, targetRoot, 0).path;
365
+ const path = renderPosition(file, root, 0).path;
356
366
  const test = patterns.some((pattern) => pattern.test(path));
357
367
  if (test) {
358
368
  tests.push(path);
@@ -365,7 +375,7 @@ export function references(project, held, targetRoot, options) {
365
375
  found.push({
366
376
  from: declaration,
367
377
  to: declaration,
368
- position: renderPosition(file, targetRoot, decorator.getStart()),
378
+ position: renderPosition(file, root, decorator.getStart()),
369
379
  use: "decorator",
370
380
  resolution: "syntax",
371
381
  test,
@@ -401,7 +411,7 @@ export function references(project, held, targetRoot, options) {
401
411
  if (symbol === undefined) {
402
412
  continue;
403
413
  }
404
- const position = renderPosition(file, targetRoot, link.at.getStart());
414
+ const position = renderPosition(file, root, link.at.getStart());
405
415
  for (const id of carriedBy(link, symbol)) {
406
416
  found.push({
407
417
  from: link.from,
@@ -418,7 +428,7 @@ export function references(project, held, targetRoot, options) {
418
428
  if (module === undefined) {
419
429
  continue;
420
430
  }
421
- const position = renderPosition(file, targetRoot, evaluation.node.getStart());
431
+ const position = renderPosition(file, root, evaluation.node.getStart());
422
432
  for (const id of chains.declarationsOf(module)) {
423
433
  if (fileIds.has(id)) {
424
434
  found.push({
@@ -439,7 +449,7 @@ export function references(project, held, targetRoot, options) {
439
449
  if (symbol === undefined || read === undefined) {
440
450
  return;
441
451
  }
442
- const position = renderPosition(file, targetRoot, read.key.getStart());
452
+ const position = renderPosition(file, root, read.key.getStart());
443
453
  for (const target of chains.chainOf(symbol)) {
444
454
  found.push({
445
455
  from: read.from,
@@ -461,7 +471,7 @@ export function references(project, held, targetRoot, options) {
461
471
  if (symbol === undefined) {
462
472
  continue;
463
473
  }
464
- const position = renderPosition(file, targetRoot, site.node.getStart());
474
+ const position = renderPosition(file, root, site.node.getStart());
465
475
  // A re-export something imports is used and so is the declaration behind it, so
466
476
  // the use names every link of the chain its name stands at the head of.
467
477
  for (const target of chains.chainOf(symbol)) {
@@ -482,7 +492,9 @@ export function references(project, held, targetRoot, options) {
482
492
  compare(a.use, b.use));
483
493
  return {
484
494
  configFile: project.configFile,
485
- references: found,
495
+ references: consumer === undefined
496
+ ? found
497
+ : found.map((reference) => ({ ...reference, consumer: consumer.id })),
486
498
  testFilePaths: tests.sort(compare),
487
499
  cost: {
488
500
  batched,
@@ -188,6 +188,9 @@ export function buildReport(input) {
188
188
  const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
189
189
  .sort((a, b) => compare(a.id, b.id))
190
190
  .map((one) => ({ id: one.id, role: "consumer", reason: one.reason }));
191
+ const loaded = [...input.loaded]
192
+ .sort((a, b) => compare(a.id, b.id) || compare(a.path, b.path))
193
+ .map((one) => ({ id: one.id, role: "consumer", path: one.path }));
191
194
  return {
192
195
  schema_version: SCHEMA_VERSION,
193
196
  contract_version: input.contractVersion,
@@ -207,7 +210,7 @@ export function buildReport(input) {
207
210
  .sort((a, b) => compare(a.id, b.id))
208
211
  .map((one) => ({ id: one.id, project: one.project })),
209
212
  configurations_not_built: [],
210
- consumers: { declared: unavailable.length, loaded: [], unavailable },
213
+ consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
211
214
  findings,
212
215
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
213
216
  stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
@@ -71,6 +71,21 @@ function viewOf(project) {
71
71
  },
72
72
  };
73
73
  }
74
+ /**
75
+ * The same project, its own source files narrowed to the ones `keep` admits. A consumer's
76
+ * program holds the target's files it imports beside its own, and a pass that enumerates
77
+ * the target's declarations in that program reads those files alone.
78
+ */
79
+ export function narrowedTo(project, keep) {
80
+ let kept;
81
+ return {
82
+ ...project,
83
+ ownSourceFiles: () => {
84
+ kept ??= project.ownSourceFiles().filter(keep);
85
+ return kept;
86
+ },
87
+ };
88
+ }
74
89
  /** The three diagnostic sets one project carries, read before any analysis. */
75
90
  export function diagnosticsOf(project) {
76
91
  return {
package/dist/src/sweep.js CHANGED
@@ -67,6 +67,13 @@ export function sweep(graph, input) {
67
67
  });
68
68
  return { liveUnder: live, candidates };
69
69
  }
70
+ /**
71
+ * The references a loaded consumer makes that the mode counts. A production mode counts
72
+ * none a consumer's test file made, as it counts none of the target's.
73
+ */
74
+ function callsOf(graph, mode) {
75
+ return graph.consumed.filter((edge) => !mode.production || !edge.test);
76
+ }
70
77
  /** Per declaration of the graph, whether one of the identifiers names it. */
71
78
  function flagged(graph, ids) {
72
79
  const flags = graph.symbols.map(() => false);
@@ -126,6 +133,10 @@ function reachable(graph, held, mode) {
126
133
  for (const root of graph.rooted) {
127
134
  enter(root.at, false);
128
135
  }
136
+ // A consumer's reference reaches what it names as one of the target's own does.
137
+ for (const edge of callsOf(graph, mode)) {
138
+ enter(edge.to, !edge.evaluation);
139
+ }
129
140
  walk(false);
130
141
  return reached;
131
142
  }
@@ -1,8 +1,8 @@
1
1
  import { runAnalysis } from "../analysis.js";
2
2
  import { ConfigError } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
+ import { moduleIdentity } from "../consumers.js";
4
5
  import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.js";
5
- import { packageScope } from "../inventory.js";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
7
  import { buildReport, capFindings, sortFindings, } from "../report.js";
8
8
  import { json } from "../reporters/json.js";
@@ -20,8 +20,6 @@ export const ANALYZE_OPTIONS = {
20
20
  /** The two values `--exit-code` takes; on, the default, puts the run's verdict in the exit code. */
21
21
  const EXIT_CODE_ON = "on";
22
22
  const EXIT_CODE_OFF = "off";
23
- /** Why a declared consumer is not loaded, which every report names it with. */
24
- const NOT_LOADED = "this analyzer loads no consumer beside the target";
25
23
  /** A path of the run no report can name, which ends the run with the failure code. */
26
24
  class ReportPathError extends Error {
27
25
  constructor(message) {
@@ -73,10 +71,6 @@ function configurationsOf(projects, targetRoot) {
73
71
  return { id: one.id, project };
74
72
  });
75
73
  }
76
- /** The name one module publishes itself under: the scope's, else its manifest's. */
77
- function identityOf(module, host) {
78
- return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
79
- }
80
74
  /** A count with its noun, so a message reads for one record as well as for several. */
81
75
  function counted(n, noun) {
82
76
  return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
@@ -163,12 +157,13 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
163
157
  version,
164
158
  conformance: CONFORMANCE,
165
159
  declaredGaps: DECLARED_GAPS,
166
- target: { kind, root, identity: identityOf(scoped.target, host) },
160
+ target: { kind, root, identity: moduleIdentity(host, scoped.target) },
167
161
  configurations: configurationsOf(run.projects, targetRoot),
168
- unavailable: scoped.consumers.map((consumer) => ({
169
- id: identityOf(consumer, host),
170
- reason: NOT_LOADED,
162
+ loaded: run.consumers.map((consumer) => ({
163
+ id: consumer.id,
164
+ path: runRelative(invoked, consumer.path),
171
165
  })),
166
+ unavailable: [],
172
167
  result,
173
168
  testFileRules: run.testFileRules,
174
169
  });
@@ -2,6 +2,7 @@ import { runAnalysis } from "../analysis.js";
2
2
  import { listing } from "../components.js";
3
3
  import { ConfigError } from "../config.js";
4
4
  import { OUTSIDE, swept as judged } from "../graph.js";
5
+ import { referenceKey } from "../matrix.js";
5
6
  import { positionKey } from "../position.js";
6
7
  import { resolve } from "../resolve.js";
7
8
  import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
@@ -78,7 +79,7 @@ function referencesOf(configured) {
78
79
  const seen = new Set();
79
80
  const held = [];
80
81
  for (const reference of configured.flatMap((one) => one.references)) {
81
- const key = [reference.from, reference.to, positionKey(reference.position), reference.use].join("\u0000");
82
+ const key = referenceKey(reference);
82
83
  if (!seen.has(key)) {
83
84
  seen.add(key);
84
85
  held.push(reference);
@@ -101,8 +102,9 @@ function rootsOf(configured) {
101
102
  });
102
103
  }
103
104
  /**
104
- * The shortest path from a root to one declaration, walked breadth first from every root at
105
- * once in the order the run names them, following the references the sweep follows: a
105
+ * The shortest path from a root or a loaded consumer to one declaration, walked breadth first
106
+ * from every root at once in the order the run names them, a consumer's reference being the
107
+ * first hop of a path of its own, and following the references the sweep follows: a
106
108
  * reference that reads a module reaches what the module exports, and an evaluation or a root
107
109
  * reaches the module alone. Under `production` no reference a test file made is followed,
108
110
  * which is the set the report's sweep counts.
@@ -110,10 +112,16 @@ function rootsOf(configured) {
110
112
  function shortestPath(analysis, to, production) {
111
113
  const graph = analysis.swept.matrix.union;
112
114
  const out = new Map();
115
+ const consumed = [];
113
116
  for (const reference of referencesOf(analysis.configured)) {
114
117
  if (production && reference.test) {
115
118
  continue;
116
119
  }
120
+ const { consumer } = reference;
121
+ if (consumer !== undefined) {
122
+ consumed.push({ ...reference, consumer });
123
+ continue;
124
+ }
117
125
  const held = out.get(reference.from);
118
126
  if (held === undefined) {
119
127
  out.set(reference.from, [reference]);
@@ -131,10 +139,32 @@ function shortestPath(analysis, to, production) {
131
139
  steps.push(step);
132
140
  }
133
141
  };
142
+ const follow = (reference, previous, origin) => {
143
+ enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
144
+ const target = graph.at(reference.to);
145
+ if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
146
+ return;
147
+ }
148
+ expanded.add(reference.to);
149
+ for (const exported of graph.exportsOf[target] ?? []) {
150
+ const id = graph.symbols[exported]?.id;
151
+ if (id !== undefined) {
152
+ enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
153
+ }
154
+ }
155
+ };
134
156
  for (const root of rootsOf(analysis.configured)) {
135
- enter({ at: root.id, hop: undefined, previous: -1, root });
157
+ enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
136
158
  }
137
- for (let head = 0; head < steps.length; head += 1) {
159
+ // A consumer's reference is one hop long, so it joins the walk where the roots' first
160
+ // hops do.
161
+ const firstHops = steps.length;
162
+ for (let head = 0;; head += 1) {
163
+ if (head === firstHops) {
164
+ for (const reference of consumed) {
165
+ follow(reference, -1, { k: "consumer", consumer: reference.consumer });
166
+ }
167
+ }
138
168
  const step = steps[head];
139
169
  if (step === undefined) {
140
170
  break;
@@ -144,35 +174,18 @@ function shortestPath(analysis, to, production) {
144
174
  for (let at = step; at?.hop !== undefined; at = steps[at.previous]) {
145
175
  path.unshift(at.hop);
146
176
  }
147
- return { root: step.root, path };
177
+ return { origin: step.origin, path };
148
178
  }
149
179
  for (const reference of out.get(step.at) ?? []) {
150
- enter({
151
- at: reference.to,
152
- hop: { k: "reference", reference },
153
- previous: head,
154
- root: step.root,
155
- });
156
- const target = graph.at(reference.to);
157
- if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
158
- continue;
159
- }
160
- expanded.add(reference.to);
161
- for (const exported of graph.exportsOf[target] ?? []) {
162
- const id = graph.symbols[exported]?.id;
163
- if (id !== undefined) {
164
- enter({
165
- at: id,
166
- hop: { k: "export", reference, to: id },
167
- previous: head,
168
- root: step.root,
169
- });
170
- }
171
- }
180
+ follow(reference, head, step.origin);
172
181
  }
173
182
  }
174
183
  return undefined;
175
184
  }
185
+ /** What made a reference: a declaration of the target, or a loaded consumer. */
186
+ function madeBy(reference) {
187
+ return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
188
+ }
176
189
  function referenceLine(reference, tail) {
177
190
  return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
178
191
  }
@@ -190,9 +203,10 @@ function liveness(analysis, subject, cascade) {
190
203
  }
191
204
  const found = shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
192
205
  if (found !== undefined && found.path.length > 0) {
193
- text += ` reached from: root ${found.root.kind} ${found.root.id}\n`;
206
+ const { origin } = found;
207
+ text += ` reached from: ${origin.k === "root" ? `root ${origin.root.kind} ${origin.root.id}` : `consumer ${origin.consumer}`}\n`;
194
208
  for (const hop of found.path) {
195
- text += referenceLine(hop.reference, `${hop.reference.from} -> ${hop.reference.to}`);
209
+ text += referenceLine(hop.reference, `${madeBy(hop.reference)} -> ${hop.reference.to}`);
196
210
  if (hop.k === "export") {
197
211
  text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
198
212
  }
@@ -200,10 +214,10 @@ function liveness(analysis, subject, cascade) {
200
214
  }
201
215
  else if (found === undefined) {
202
216
  const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
203
- text += " unreachable: no path of references reaches it from a root\n";
217
+ text += " unreachable: no path of references reaches it from a root or a loaded consumer\n";
204
218
  text += ` references: ${String(references.length)}\n`;
205
219
  for (const reference of references) {
206
- text += referenceLine(reference, `from ${reference.from}`);
220
+ text += referenceLine(reference, `from ${madeBy(reference)}`);
207
221
  }
208
222
  }
209
223
  if (candidate !== undefined) {
@@ -277,9 +291,9 @@ function explanation(analysis, subject, cascade) {
277
291
  * Explains one symbol from the analysis the report is built from, so an explanation cannot
278
292
  * disagree with a finding: why a reported symbol is reported, which exemption classes held a
279
293
  * retained one back, and for any other declaration the relations that hold it live and the
280
- * shortest path of references from a root, or the references it has where no path reaches it.
281
- * A request naming no one declaration exits with the usage code and names what partially
282
- * matches it.
294
+ * shortest path of references from a root or a loaded consumer, or the references it has where
295
+ * no path reaches it. A request naming no one declaration exits with the usage code and names
296
+ * what partially matches it.
283
297
  */
284
298
  export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
285
299
  const named = namedSymbol(option);
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -10,6 +10,12 @@
10
10
 
11
11
  import type { Diagnostic } from "@typescript/native/unstable/sync";
12
12
  import type { Config, Provenance } from "./config.ts";
13
+ import {
14
+ consumerLoads,
15
+ consumerReferences,
16
+ moduleIdentity,
17
+ type LoadedConsumer,
18
+ } from "./consumers.ts";
13
19
  import { decorator } from "./decorator.ts";
14
20
  import { dependenciesOf, projectNeeds } from "./dependencies.ts";
15
21
  import { deprecatedDeclarations } from "./deprecation.ts";
@@ -41,11 +47,18 @@ import { injectionContainer } from "./injection-container.ts";
41
47
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
42
48
  import { inventory, type Inventory } from "./inventory.ts";
43
49
  import { readManifest } from "./manifest.ts";
44
- import { matrixOf, sweepMatrix, type Configured, type Matrix, type SweepResult } from "./matrix.ts";
50
+ import {
51
+ matrixOf,
52
+ referenceKey,
53
+ sweepMatrix,
54
+ type Configured,
55
+ type Matrix,
56
+ type SweepResult,
57
+ } from "./matrix.ts";
45
58
  import { relativePath, resolvePath } from "./paths.ts";
46
59
  import { byPosition, type Position } from "./position.ts";
47
60
  import { reflectiveLookup } from "./reflective-lookup.ts";
48
- import { references, testFileRulesOf, type TestFileRule } from "./references.ts";
61
+ import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
49
62
  import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
50
63
  import type { Scope } from "./scope.ts";
51
64
  import { serializationContract } from "./serialization-contract.ts";
@@ -125,11 +138,27 @@ interface ProjectRead {
125
138
  readonly rooted: Roots;
126
139
  }
127
140
 
141
+ /** What a run read of the consumers its scope declares. */
142
+ interface ConsumerReads {
143
+ /** Every declared consumer, each loaded, in the scope's order. */
144
+ readonly loaded: readonly LoadedConsumer[];
145
+ /** Every reference a consumer makes to a declaration of the target, each once. */
146
+ readonly references: readonly Reference[];
147
+ }
148
+
149
+ /** What a run read of its projects: each target project's answer, and its consumers. */
150
+ interface ReadProjects<Answer> {
151
+ readonly answers: readonly Answer[];
152
+ readonly consumers: ConsumerReads;
153
+ }
154
+
128
155
  /**
129
- * Reads every project the scope discovers, in one snapshot: each is checked for errors
130
- * first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
131
- * the project while its view is open. A project carrying an error fails the run with no
132
- * answer, as every verb that reads declarations does.
156
+ * Reads every project the scope discovers, and every project of every consumer it
157
+ * declares, in one snapshot. Each is checked for errors first. A target project is then
158
+ * enumerated and rooted, and `stage` reads the rest of what it needs from the project
159
+ * while its view is open; a consumer project is read for its references to the target's
160
+ * declarations. A project carrying an error fails the run with no answer, as every verb
161
+ * that reads declarations does, and so does a declared consumer that cannot be loaded.
133
162
  */
134
163
  function readProjects<Answer>(
135
164
  engine: Engine,
@@ -137,10 +166,16 @@ function readProjects<Answer>(
137
166
  scope: Scope,
138
167
  config: Config,
139
168
  stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
140
- ): readonly Answer[] {
169
+ ): ReadProjects<Answer> {
141
170
  const targetRoot = scope.target.path;
142
171
  const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
143
172
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
173
+ const loads = consumerLoads(engine, host, scope, discovered.configFiles);
174
+ const consumerOf = new Map(
175
+ loads.flatMap((load) =>
176
+ load.configFiles.map((configFile) => [configFile, load.consumer] as const),
177
+ ),
178
+ );
144
179
  const options = {
145
180
  manifest: readManifest(host, targetRoot),
146
181
  patterns: config.rootPatterns,
@@ -149,24 +184,51 @@ function readProjects<Answer>(
149
184
  publishedAPI: config.targetKind === "library",
150
185
  };
151
186
  const failures: Diagnostic[] = [];
152
- const { projects } = runSession(engine, discovered.configFiles, (project) => {
153
- const errors = diagnosticErrors(diagnosticsOf(project));
154
- if (errors.length > 0) {
155
- failures.push(...errors);
156
- return undefined;
157
- }
158
- const held = inventory(project, host, targetRoot);
159
- const rooted = roots(project, held, targetRoot, options);
160
- const configuration = ids.get(project.configFile) ?? project.configFile;
161
- return stage(project, { configuration, configFile: project.configFile, held, rooted });
162
- });
187
+ const consumed = new Map<string, Reference>();
188
+ const { projects } = runSession(
189
+ engine,
190
+ [...discovered.configFiles, ...consumerOf.keys()],
191
+ (project) => {
192
+ const errors = diagnosticErrors(diagnosticsOf(project));
193
+ if (errors.length > 0) {
194
+ failures.push(...errors);
195
+ return undefined;
196
+ }
197
+ const consumer = consumerOf.get(project.configFile);
198
+ if (consumer !== undefined) {
199
+ for (const reference of consumerReferences(
200
+ project,
201
+ host,
202
+ targetRoot,
203
+ {
204
+ id: consumer.id,
205
+ root: consumer.path,
206
+ },
207
+ config,
208
+ )) {
209
+ consumed.set(referenceKey(reference), reference);
210
+ }
211
+ return undefined;
212
+ }
213
+ const held = inventory(project, host, targetRoot);
214
+ const rooted = roots(project, held, targetRoot, options);
215
+ const configuration = ids.get(project.configFile) ?? project.configFile;
216
+ return stage(project, { configuration, configFile: project.configFile, held, rooted });
217
+ },
218
+ );
163
219
  if (failures.length > 0) {
164
220
  throw new DiscoveryError(
165
221
  `${String(failures.length)} error(s); no answer was produced`,
166
222
  failures,
167
223
  );
168
224
  }
169
- return projects.filter((answer): answer is Answer => answer !== undefined);
225
+ return {
226
+ answers: projects.filter((answer): answer is Answer => answer !== undefined),
227
+ consumers: {
228
+ loaded: loads.map((load) => load.consumer),
229
+ references: [...consumed.values()],
230
+ },
231
+ };
170
232
  }
171
233
 
172
234
  /**
@@ -183,7 +245,7 @@ export function runRoots(
183
245
  provenance: Provenance,
184
246
  ): RunRoots {
185
247
  const targetRoot = scope.target.path;
186
- const projects = readProjects(engine, host, scope, config, (_project, read) => read);
248
+ const projects = readProjects(engine, host, scope, config, (_project, read) => read).answers;
187
249
 
188
250
  const configurations: string[] = [];
189
251
  const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
@@ -266,12 +328,15 @@ interface ReadRun<Extra> {
266
328
  readonly matrix: Matrix;
267
329
  readonly exempt: readonly Exemption[];
268
330
  readonly projects: readonly SweptProject<Extra>[];
331
+ /** Every declared consumer, each loaded, in the scope's order. */
332
+ readonly consumers: readonly LoadedConsumer[];
269
333
  }
270
334
 
271
335
  /**
272
336
  * Reads every project of the run, its references and exemptions, and `extra`'s answer,
273
337
  * while the project's view is open, and merges the projects into one matrix in the run's
274
- * order.
338
+ * order. Every configuration holds every consumer's references beside its own, because a
339
+ * consumer calls the target whichever configuration the target is built under.
275
340
  */
276
341
  function readRun<Extra>(
277
342
  engine: Engine,
@@ -288,7 +353,7 @@ function readRun<Extra>(
288
353
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
289
354
  : readTemplates(host, targetRoot, config.analysis);
290
355
  const consumers = scope.consumers.map((consumer) => consumer.path);
291
- const projects = readProjects(engine, host, scope, config, (project, projectRead) => {
356
+ const read = readProjects(engine, host, scope, config, (project, projectRead) => {
292
357
  const resolved = references(project, projectRead.held, targetRoot, {
293
358
  testFiles: config.ts.testFiles,
294
359
  });
@@ -315,10 +380,23 @@ function readRun<Extra>(
315
380
  extra: extra(project, projectRead),
316
381
  };
317
382
  });
383
+ const consumed = read.consumers.references;
384
+ const projects = read.answers.map((one) =>
385
+ consumed.length === 0
386
+ ? one
387
+ : {
388
+ ...one,
389
+ configured: {
390
+ ...one.configured,
391
+ references: [...one.configured.references, ...consumed],
392
+ },
393
+ },
394
+ );
318
395
  return {
319
396
  matrix: matrixOf(projects.map((one) => one.configured)),
320
397
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
321
398
  projects,
399
+ consumers: read.consumers.loaded,
322
400
  };
323
401
  }
324
402
 
@@ -430,7 +508,10 @@ function emitterInputOver(
430
508
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
431
509
  },
432
510
  boundary: {
433
- consumers: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
511
+ consumers: {
512
+ declared: scope.consumers.map((consumer) => moduleIdentity(host, consumer)),
513
+ loaded: read.consumers.map((consumer) => consumer.id),
514
+ },
434
515
  encapsulated: readManifest(host, targetRoot).declaresExports,
435
516
  edges: readEdgeSides(host, targetRoot),
436
517
  },
@@ -442,8 +523,8 @@ function emitterInputOver(
442
523
  * What every emitter reads of the run the scope and the configuration describe, swept
443
524
  * under the caller's marks: the run's sweep, the facts beside it each kind family reads,
444
525
  * and the sides of the target's declared cross-language edges that name a symbol of this
445
- * language. The run loads no consumer, and it reads no suppression document, so the
446
- * self-check family has nothing to report.
526
+ * language. The run reads no suppression document, so the self-check family has nothing
527
+ * to report.
447
528
  */
448
529
  export function runEmitterInput(
449
530
  engine: Engine,
@@ -526,6 +607,7 @@ function runAnalysisInputs(
526
607
  config.ts.testFiles,
527
608
  read.projects.flatMap((one) => one.configured.testFiles),
528
609
  ),
610
+ consumers: read.consumers,
529
611
  },
530
612
  configured: read.projects.map((one) => one.configured),
531
613
  };
@@ -544,6 +626,8 @@ export interface RunFacts {
544
626
  readonly projects: readonly RunProject[];
545
627
  /** The rules that classified files as test files, each counted over every project at once. */
546
628
  readonly testFileRules: readonly TestFileRule[];
629
+ /** Every consumer the run loaded beside the target, in the scope's order. */
630
+ readonly consumers: readonly LoadedConsumer[];
547
631
  }
548
632
 
549
633
  /** The findings of one run, beside what a report states about the run itself. */
@@ -3,8 +3,8 @@
3
3
  export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "1.9.0",
6
- result: "fail",
7
- digest: "sha256:8c29f19fc189f4f5d9ea210d556b62d12c3701c221eb7a8adde499b7060bec0d",
6
+ result: "pass",
7
+ digest: "sha256:3e9e40876551faa2c196311bf06d47d220ab5127a9fb8921969aae552c42e824",
8
8
  },
9
9
  gaps: [
10
10
  {
@@ -13,17 +13,5 @@ export const RECORD = {
13
13
  reason:
14
14
  "The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
15
15
  },
16
- {
17
- fixture: "redundant-export-keyword",
18
- capability: "DS1104",
19
- reason:
20
- "No consumer is loaded beside the target, so the published surface is unknown and no export is narrowed; review an export only the target uses by hand.",
21
- },
22
- {
23
- fixture: "unused-exported-consumer",
24
- capability: "DS1001",
25
- reason:
26
- "No consumer is loaded beside the target, so an export nothing in the target references is reported at the possible class rather than the certain one.",
27
- },
28
16
  ],
29
17
  } as const;