@cplieger/deadset-ts 3.11.0 → 3.12.1

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),
package/dist/src/roots.js CHANGED
@@ -4,9 +4,8 @@
4
4
  *
5
5
  * Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
6
6
  * that names a caller is live under reference counting as well, which the sweep
7
- * decides; a root that only supposes a consumer is not, so an exported declaration
8
- * something outside the program reaches is live under reachability and still a
9
- * candidate under reference counting.
7
+ * decides; the published API only supposes a consumer, so a published export is live
8
+ * under reachability and still a candidate under reference counting.
10
9
  */
11
10
  import { aliasChains } from "./alias-chain.js";
12
11
  import { entryPoints } from "./entry-points.js";
@@ -94,6 +93,7 @@ function childrenOf(held) {
94
93
  * is a root with everything its module exports: each name of the module's export
95
94
  * table, and where a name is a re-export, every link of the chain to the declaration
96
95
  * it carries, so a re-exported declaration is a root of the file that publishes it.
96
+ * In a library, what a published file exports is rooted by the published API alone.
97
97
  * A file a runtime executes without reading its exports, a test file or a worker, is
98
98
  * a root by itself.
99
99
  */
@@ -116,14 +116,19 @@ export function roots(project, held, targetRoot, options) {
116
116
  entered.push({ file, kind, source, exports });
117
117
  };
118
118
  const published = [];
119
- const publishes = (entry) => entry.published &&
119
+ const publishes = (entry) => options.publishedAPI &&
120
+ entry.published &&
120
121
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
122
+ for (const entry of options.manifest.entries) {
123
+ if (publishes(entry)) {
124
+ published.push(...files.named(entry.path));
125
+ }
126
+ }
127
+ const publishedNames = new Set(published.map((file) => file.fileName));
121
128
  for (const entry of options.manifest.entries) {
122
129
  for (const file of files.named(entry.path)) {
123
- enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
124
- if (options.publishedAPI && publishes(entry)) {
125
- published.push(file);
126
- }
130
+ const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
131
+ enter(file, kind, entry.member, !publishedNames.has(file.fileName));
127
132
  }
128
133
  }
129
134
  for (const pattern of options.entryFiles) {
@@ -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.1",
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.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",