@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.
package/src/sweep.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { TSExemptionClass } from "./exempt-classes.ts";
10
- import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
10
+ import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
11
11
  import type { Position } from "./position.ts";
12
12
 
13
13
  /** The liveness relation that decided a declaration, as a finding spells it. */
@@ -156,6 +156,14 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
156
156
  return { liveUnder: live, candidates };
157
157
  }
158
158
 
159
+ /**
160
+ * The references a loaded consumer makes that the mode counts. A production mode counts
161
+ * none a consumer's test file made, as it counts none of the target's.
162
+ */
163
+ function callsOf(graph: Graph, mode: Mode): readonly Edge[] {
164
+ return graph.consumed.filter((edge) => !mode.production || !edge.test);
165
+ }
166
+
159
167
  /** Per declaration of the graph, whether one of the identifiers names it. */
160
168
  function flagged(graph: Graph, ids: readonly string[]): boolean[] {
161
169
  const flags = graph.symbols.map(() => false);
@@ -218,6 +226,10 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
218
226
  for (const root of graph.rooted) {
219
227
  enter(root.at, false);
220
228
  }
229
+ // A consumer's reference reaches what it names as one of the target's own does.
230
+ for (const edge of callsOf(graph, mode)) {
231
+ enter(edge.to, !edge.evaluation);
232
+ }
221
233
  walk(false);
222
234
  return reached;
223
235
  }
@@ -1,8 +1,8 @@
1
1
  import { runAnalysis, type RunProject } from "../analysis.ts";
2
2
  import { ConfigError, type Format } from "../config.ts";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
4
+ import { moduleIdentity } from "../consumers.ts";
4
5
  import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.ts";
5
- import { packageScope } from "../inventory.ts";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
7
7
  import type { Host } from "../host.ts";
8
8
  import {
@@ -16,7 +16,6 @@ import { json } from "../reporters/json.ts";
16
16
  import { RENDERINGS } from "../reporters/reporters.ts";
17
17
  import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
18
18
  import { resolve } from "../resolve.ts";
19
- import type { Module } from "../scope.ts";
20
19
  import { writeBaseline, type Recorded } from "../suppress-file.ts";
21
20
  import { CONTRACT_VERSION } from "../version.ts";
22
21
  import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts";
@@ -31,9 +30,6 @@ export const ANALYZE_OPTIONS: VerbOptions = {
31
30
  const EXIT_CODE_ON = "on";
32
31
  const EXIT_CODE_OFF = "off";
33
32
 
34
- /** Why a declared consumer is not loaded, which every report names it with. */
35
- const NOT_LOADED = "this analyzer loads no consumer beside the target";
36
-
37
33
  /** A path of the run no report can name, which ends the run with the failure code. */
38
34
  class ReportPathError extends Error {
39
35
  constructor(message: string) {
@@ -101,11 +97,6 @@ function configurationsOf(
101
97
  });
102
98
  }
103
99
 
104
- /** The name one module publishes itself under: the scope's, else its manifest's. */
105
- function identityOf(module: Module, host: Host): string {
106
- return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
107
- }
108
-
109
100
  /** A count with its noun, so a message reads for one record as well as for several. */
110
101
  function counted(n: number, noun: string): string {
111
102
  return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
@@ -221,12 +212,13 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
221
212
  version,
222
213
  conformance: CONFORMANCE,
223
214
  declaredGaps: DECLARED_GAPS,
224
- target: { kind, root, identity: identityOf(scoped.target, host) },
215
+ target: { kind, root, identity: moduleIdentity(host, scoped.target) },
225
216
  configurations: configurationsOf(run.projects, targetRoot),
226
- unavailable: scoped.consumers.map((consumer) => ({
227
- id: identityOf(consumer, host),
228
- reason: NOT_LOADED,
217
+ loaded: run.consumers.map((consumer) => ({
218
+ id: consumer.id,
219
+ path: runRelative(invoked, consumer.path),
229
220
  })),
221
+ unavailable: [],
230
222
  result,
231
223
  testFileRules: run.testFileRules,
232
224
  });
@@ -5,7 +5,7 @@ import { ConfigError } from "../config.ts";
5
5
  import type { CompletedFinding } from "../finding.ts";
6
6
  import { OUTSIDE, swept as judged } from "../graph.ts";
7
7
  import type { InventorySymbol } from "../inventory.ts";
8
- import type { Configured } from "../matrix.ts";
8
+ import { referenceKey, type Configured } from "../matrix.ts";
9
9
  import { positionKey } from "../position.ts";
10
10
  import type { Reference } from "../references.ts";
11
11
  import { resolve } from "../resolve.ts";
@@ -108,9 +108,7 @@ function referencesOf(configured: readonly Configured[]): readonly Reference[] {
108
108
  const seen = new Set<string>();
109
109
  const held: Reference[] = [];
110
110
  for (const reference of configured.flatMap((one) => one.references)) {
111
- const key = [reference.from, reference.to, positionKey(reference.position), reference.use].join(
112
- "\u0000",
113
- );
111
+ const key = referenceKey(reference);
114
112
  if (!seen.has(key)) {
115
113
  seen.add(key);
116
114
  held.push(reference);
@@ -142,9 +140,15 @@ type Hop =
142
140
  | { readonly k: "reference"; readonly reference: Reference }
143
141
  | { readonly k: "export"; readonly reference: Reference; readonly to: string };
144
142
 
143
+ /** Where a path starts: a root of the run, or a reference a loaded consumer makes. */
144
+ type Origin =
145
+ | { readonly k: "root"; readonly root: Root }
146
+ | { readonly k: "consumer"; readonly consumer: string };
147
+
145
148
  /**
146
- * The shortest path from a root to one declaration, walked breadth first from every root at
147
- * once in the order the run names them, following the references the sweep follows: a
149
+ * The shortest path from a root or a loaded consumer to one declaration, walked breadth first
150
+ * from every root at once in the order the run names them, a consumer's reference being the
151
+ * first hop of a path of its own, and following the references the sweep follows: a
148
152
  * reference that reads a module reaches what the module exports, and an evaluation or a root
149
153
  * reaches the module alone. Under `production` no reference a test file made is followed,
150
154
  * which is the set the report's sweep counts.
@@ -153,13 +157,19 @@ function shortestPath(
153
157
  analysis: RunAnalysis,
154
158
  to: string,
155
159
  production: boolean,
156
- ): { readonly root: Root; readonly path: readonly Hop[] } | undefined {
160
+ ): { readonly origin: Origin; readonly path: readonly Hop[] } | undefined {
157
161
  const graph = analysis.swept.matrix.union;
158
162
  const out = new Map<string, Reference[]>();
163
+ const consumed: (Reference & { readonly consumer: string })[] = [];
159
164
  for (const reference of referencesOf(analysis.configured)) {
160
165
  if (production && reference.test) {
161
166
  continue;
162
167
  }
168
+ const { consumer } = reference;
169
+ if (consumer !== undefined) {
170
+ consumed.push({ ...reference, consumer });
171
+ continue;
172
+ }
163
173
  const held = out.get(reference.from);
164
174
  if (held === undefined) {
165
175
  out.set(reference.from, [reference]);
@@ -171,7 +181,7 @@ function shortestPath(
171
181
  readonly at: string;
172
182
  readonly hop: Hop | undefined;
173
183
  readonly previous: number;
174
- readonly root: Root;
184
+ readonly origin: Origin;
175
185
  }
176
186
  const steps: Step[] = [];
177
187
  const seen = new Set<string>();
@@ -182,10 +192,32 @@ function shortestPath(
182
192
  steps.push(step);
183
193
  }
184
194
  };
195
+ const follow = (reference: Reference, previous: number, origin: Origin): void => {
196
+ enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
197
+ const target = graph.at(reference.to);
198
+ if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
199
+ return;
200
+ }
201
+ expanded.add(reference.to);
202
+ for (const exported of graph.exportsOf[target] ?? []) {
203
+ const id = graph.symbols[exported]?.id;
204
+ if (id !== undefined) {
205
+ enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
206
+ }
207
+ }
208
+ };
185
209
  for (const root of rootsOf(analysis.configured)) {
186
- enter({ at: root.id, hop: undefined, previous: -1, root });
210
+ enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
187
211
  }
188
- for (let head = 0; head < steps.length; head += 1) {
212
+ // A consumer's reference is one hop long, so it joins the walk where the roots' first
213
+ // hops do.
214
+ const firstHops = steps.length;
215
+ for (let head = 0; ; head += 1) {
216
+ if (head === firstHops) {
217
+ for (const reference of consumed) {
218
+ follow(reference, -1, { k: "consumer", consumer: reference.consumer });
219
+ }
220
+ }
189
221
  const step = steps[head];
190
222
  if (step === undefined) {
191
223
  break;
@@ -195,36 +227,20 @@ function shortestPath(
195
227
  for (let at: Step | undefined = step; at?.hop !== undefined; at = steps[at.previous]) {
196
228
  path.unshift(at.hop);
197
229
  }
198
- return { root: step.root, path };
230
+ return { origin: step.origin, path };
199
231
  }
200
232
  for (const reference of out.get(step.at) ?? []) {
201
- enter({
202
- at: reference.to,
203
- hop: { k: "reference", reference },
204
- previous: head,
205
- root: step.root,
206
- });
207
- const target = graph.at(reference.to);
208
- if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
209
- continue;
210
- }
211
- expanded.add(reference.to);
212
- for (const exported of graph.exportsOf[target] ?? []) {
213
- const id = graph.symbols[exported]?.id;
214
- if (id !== undefined) {
215
- enter({
216
- at: id,
217
- hop: { k: "export", reference, to: id },
218
- previous: head,
219
- root: step.root,
220
- });
221
- }
222
- }
233
+ follow(reference, head, step.origin);
223
234
  }
224
235
  }
225
236
  return undefined;
226
237
  }
227
238
 
239
+ /** What made a reference: a declaration of the target, or a loaded consumer. */
240
+ function madeBy(reference: Reference): string {
241
+ return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
242
+ }
243
+
228
244
  function referenceLine(reference: Reference, tail: string): string {
229
245
  return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
230
246
  }
@@ -244,19 +260,20 @@ function liveness(analysis: RunAnalysis, subject: InventorySymbol, cascade: Casc
244
260
  const found =
245
261
  shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
246
262
  if (found !== undefined && found.path.length > 0) {
247
- text += ` reached from: root ${found.root.kind} ${found.root.id}\n`;
263
+ const { origin } = found;
264
+ text += ` reached from: ${origin.k === "root" ? `root ${origin.root.kind} ${origin.root.id}` : `consumer ${origin.consumer}`}\n`;
248
265
  for (const hop of found.path) {
249
- text += referenceLine(hop.reference, `${hop.reference.from} -> ${hop.reference.to}`);
266
+ text += referenceLine(hop.reference, `${madeBy(hop.reference)} -> ${hop.reference.to}`);
250
267
  if (hop.k === "export") {
251
268
  text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
252
269
  }
253
270
  }
254
271
  } else if (found === undefined) {
255
272
  const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
256
- text += " unreachable: no path of references reaches it from a root\n";
273
+ text += " unreachable: no path of references reaches it from a root or a loaded consumer\n";
257
274
  text += ` references: ${String(references.length)}\n`;
258
275
  for (const reference of references) {
259
- text += referenceLine(reference, `from ${reference.from}`);
276
+ text += referenceLine(reference, `from ${madeBy(reference)}`);
260
277
  }
261
278
  }
262
279
  if (candidate !== undefined) {
@@ -337,9 +354,9 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
337
354
  * Explains one symbol from the analysis the report is built from, so an explanation cannot
338
355
  * disagree with a finding: why a reported symbol is reported, which exemption classes held a
339
356
  * retained one back, and for any other declaration the relations that hold it live and the
340
- * shortest path of references from a root, or the references it has where no path reaches it.
341
- * A request naming no one declaration exits with the usage code and names what partially
342
- * matches it.
357
+ * shortest path of references from a root or a loaded consumer, or the references it has where
358
+ * no path reaches it. A request naming no one declaration exits with the usage code and names
359
+ * what partially matches it.
343
360
  */
344
361
  export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
345
362
  const named = namedSymbol(option);