@cplieger/deadset-ts 5.1.0 → 5.2.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +79 -37
  3. package/dist/src/config.js +3 -0
  4. package/dist/src/configuration-files.js +24 -25
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/contextual-uses.js +46 -18
  7. package/dist/src/convention-rows.js +7 -0
  8. package/dist/src/conventions.js +97 -33
  9. package/dist/src/dependencies.js +8 -0
  10. package/dist/src/entry-point-gaps.js +10 -26
  11. package/dist/src/entry-points.js +4 -0
  12. package/dist/src/exempt.js +23 -8
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -11
  15. package/dist/src/findings/interfaces.js +2 -6
  16. package/dist/src/findings/intra-function.js +15 -3
  17. package/dist/src/findings/unused-declarations.js +5 -6
  18. package/dist/src/findings/visibility-narrowing.js +6 -1
  19. package/dist/src/generated-file.js +72 -12
  20. package/dist/src/graph.js +6 -5
  21. package/dist/src/html-entries.js +137 -0
  22. package/dist/src/inventory.js +13 -3
  23. package/dist/src/kinds.js +2 -2
  24. package/dist/src/manifest.js +34 -5
  25. package/dist/src/matrix.js +9 -3
  26. package/dist/src/namespace-objects.js +158 -0
  27. package/dist/src/outside-tests.js +139 -0
  28. package/dist/src/overrides.js +104 -0
  29. package/dist/src/paths.js +17 -0
  30. package/dist/src/references.js +150 -4
  31. package/dist/src/roots.js +172 -39
  32. package/dist/src/session.js +4 -3
  33. package/dist/src/sweep.js +17 -0
  34. package/dist/src/template-field.js +60 -3
  35. package/dist/src/test-support.js +35 -23
  36. package/dist/src/version.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +122 -49
  40. package/src/config.ts +3 -0
  41. package/src/configuration-files.ts +29 -25
  42. package/src/conformance-record.ts +2 -2
  43. package/src/contextual-uses.ts +51 -20
  44. package/src/convention-rows.ts +15 -0
  45. package/src/conventions.ts +119 -30
  46. package/src/dependencies.ts +9 -0
  47. package/src/entry-point-gaps.ts +11 -28
  48. package/src/entry-points.ts +7 -1
  49. package/src/exempt.ts +29 -12
  50. package/src/file-facts.ts +25 -21
  51. package/src/findings/completion.ts +31 -11
  52. package/src/findings/interfaces.ts +2 -9
  53. package/src/findings/intra-function.ts +17 -3
  54. package/src/findings/unused-declarations.ts +5 -8
  55. package/src/findings/visibility-narrowing.ts +7 -1
  56. package/src/generated-file.ts +80 -12
  57. package/src/graph.ts +9 -8
  58. package/src/html-entries.ts +170 -0
  59. package/src/inventory.ts +15 -3
  60. package/src/kinds.ts +2 -2
  61. package/src/manifest.ts +49 -7
  62. package/src/matrix.ts +21 -3
  63. package/src/namespace-objects.ts +216 -0
  64. package/src/outside-tests.ts +191 -0
  65. package/src/overrides.ts +136 -0
  66. package/src/paths.ts +18 -0
  67. package/src/references.ts +206 -9
  68. package/src/roots.ts +210 -53
  69. package/src/session.ts +4 -2
  70. package/src/sweep.ts +19 -0
  71. package/src/template-field.ts +65 -3
  72. package/src/test-support.ts +34 -22
  73. package/src/version.ts +1 -1
package/dist/src/roots.js CHANGED
@@ -7,11 +7,13 @@
7
7
  * decides; the published API only supposes a consumer, so a published export is live
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
+ import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
10
11
  import { aliasChains } from "./alias-chain.js";
11
12
  import { conventionEntries } from "./conventions.js";
12
13
  import { entryPoints } from "./entry-points.js";
13
14
  import { globExpression } from "./glob.js";
14
15
  import { nodeKey } from "./inventory.js";
16
+ import { relativePath, resolvePath } from "./paths.js";
15
17
  import { byPosition } from "./position.js";
16
18
  import { UNANSWERED } from "./query.js";
17
19
  import { sourceFilesOf } from "./source-files.js";
@@ -22,6 +24,42 @@ const ENTRY_KINDS = {
22
24
  run: "manifest-binary",
23
25
  script: "script",
24
26
  };
27
+ /** The rules whose rooted files a manifest target is read back to by name. */
28
+ const READ_BACK_RULES = new Set([
29
+ "configuration-file",
30
+ "configuration-string",
31
+ "html-entry",
32
+ ]);
33
+ /** The extensions a manifest target's name drops: the ones a build writes. */
34
+ const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
35
+ /** The extension a source file's name drops. */
36
+ const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
37
+ /**
38
+ * The rooted files one manifest target reads back to by name: its path below the
39
+ * manifest's directory, past its first directory where it names one, without its output
40
+ * extension, is the name, and a rooted file is one whose path below the same directory,
41
+ * without its source extension, ends with that name at a directory boundary.
42
+ */
43
+ function readBackByName(host, entry, rooted) {
44
+ const dir = resolvePath(host.workingDirectory(), entry.dir);
45
+ const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
46
+ if (below === undefined || below === "") {
47
+ return [];
48
+ }
49
+ const cut = below.indexOf("/");
50
+ const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
51
+ if (name === "") {
52
+ return [];
53
+ }
54
+ const found = new Map();
55
+ for (const file of rooted) {
56
+ const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
57
+ if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
58
+ found.set(file.fileName, file);
59
+ }
60
+ }
61
+ return [...found.values()];
62
+ }
25
63
  /** Whether a configured string carries either special character. */
26
64
  function isPattern(text) {
27
65
  return text.includes("*") || text.includes("?");
@@ -124,6 +162,7 @@ export function roots(project, held, targetRoot, options) {
124
162
  const enter = (file, kind, source, exports) => {
125
163
  entered.push({ file, kind, source, exports });
126
164
  };
165
+ const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse);
127
166
  const published = [];
128
167
  const entries = [
129
168
  ...options.manifest.entries.map((entry) => ({
@@ -131,16 +170,27 @@ export function roots(project, held, targetRoot, options) {
131
170
  publishes: entry.published &&
132
171
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
133
172
  })),
134
- ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
173
+ ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
174
  ];
175
+ const scripted = entries.flatMap(({ entry }) => entry.role === "script" ? files.named(entry.path) : []);
176
+ const readBackTo = [
177
+ ...scripted,
178
+ ...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
179
+ ];
180
+ const entryFiles = (entry) => {
181
+ const named = files.named(entry.path);
182
+ return named.length > 0 || entry.role === "script"
183
+ ? named
184
+ : readBackByName(options.host, entry, readBackTo);
185
+ };
136
186
  for (const { entry, publishes } of entries) {
137
187
  if (options.publishedAPI && publishes) {
138
- published.push(...files.named(entry.path));
188
+ published.push(...entryFiles(entry));
139
189
  }
140
190
  }
141
191
  const publishedNames = new Set(published.map((file) => file.fileName));
142
192
  for (const { entry } of entries) {
143
- for (const file of files.named(entry.path)) {
193
+ for (const file of entryFiles(entry)) {
144
194
  const kind = ENTRY_KINDS[entry.role];
145
195
  enter(file, kind, entry.member, !publishedNames.has(file.fileName));
146
196
  }
@@ -158,7 +208,7 @@ export function roots(project, held, targetRoot, options) {
158
208
  : conventionEntries(options.conventions, files)) {
159
209
  enter(file, "convention", row, true);
160
210
  }
161
- for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse)) {
211
+ for (const point of points) {
162
212
  enter(point.file, point.rule, point.source, point.exports);
163
213
  }
164
214
  for (const file of files.byPath.values()) {
@@ -188,10 +238,11 @@ export function roots(project, held, targetRoot, options) {
188
238
  }
189
239
  }
190
240
  }
191
- if (options.publishedAPI) {
192
- for (const id of publishedAPI(published, exportsOf, held)) {
193
- add(id, "published-api", "");
194
- }
241
+ const api = options.publishedAPI
242
+ ? publishedAPI(project, published, exportsOf, held, files.byPath)
243
+ : { ids: [], guessed: [] };
244
+ for (const id of api.ids) {
245
+ add(id, "published-api", "");
195
246
  }
196
247
  const unmatched = configured(held, options.patterns, add);
197
248
  const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
@@ -205,7 +256,7 @@ export function roots(project, held, targetRoot, options) {
205
256
  configFile: project.configFile,
206
257
  liveUnderReachability: ordered,
207
258
  unmatched,
208
- unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
259
+ unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
209
260
  };
210
261
  }
211
262
  /**
@@ -255,52 +306,134 @@ function exportTables(project, held, wanted) {
255
306
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
256
307
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
257
308
  /**
258
- * Every declaration a consumer of the library can name: what the published files
259
- * export, and below each, the members a consumer reaches through it.
260
- *
261
- * The published files are the ones `exports` reaches where the manifest declares
262
- * one, because a package that declares `exports` has said exactly what a consumer may
263
- * import; where it declares none, they are the ones the members that preceded it
264
- * name. Below a published module or namespace the walk follows what its export table
265
- * names; below any other published declaration it follows every member a consumer
266
- * can write: a public one, and a protected one, which a consumer's subclass names. A
267
- * private member is not published, nor is anything below one, and a type parameter
268
- * is not published either, because a consumer supplies a type argument by position.
269
- *
270
- * A published file is not itself published. A consumer imports a module and names
271
- * what it exports; the file is a root because the manifest names it, which is a
272
- * different reason.
309
+ * Every declaration a consumer of the library can name: what the published files export
310
+ * and, below each, the members a consumer writes: a namespace's exported ones, any other
311
+ * declaration's public and protected ones, never a private one or a type parameter. A
312
+ * declaration a published one's type names is published too, exported or not. A type name
313
+ * the checker leaves unanswered publishes every declaration spelled as it is, and those are
314
+ * `guessed`. A published file is not itself published.
273
315
  */
274
- function publishedAPI(published, exportsOf, held) {
316
+ function publishedAPI(project, published, exportsOf, held, byPath) {
275
317
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
276
318
  const children = childrenOf(held);
319
+ const nodes = declarationNodes(held, byPath);
320
+ const chains = aliasChains(project, held);
277
321
  const reached = new Set();
322
+ const guessed = new Set();
278
323
  const pending = [];
324
+ const reach = (id) => {
325
+ if (!reached.has(id) && byId.get(id)?.kind !== "file") {
326
+ reached.add(id);
327
+ pending.push(id);
328
+ }
329
+ };
279
330
  for (const file of published) {
280
331
  for (const id of exportsOf.get(file.fileName) ?? []) {
281
332
  if (byId.get(id)?.kind === "file") {
282
333
  pending.push(id);
283
334
  }
284
- else if (!reached.has(id)) {
285
- reached.add(id);
286
- pending.push(id);
335
+ else {
336
+ reach(id);
287
337
  }
288
338
  }
289
339
  }
290
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
291
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
292
- for (const member of children.get(next) ?? []) {
293
- const reachable = exporting
294
- ? member.exported
295
- : member.visibility !== "private" && member.visibility !== "private-name";
296
- if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
297
- continue;
340
+ while (pending.length > 0) {
341
+ const round = [];
342
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
343
+ round.push(next);
344
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
345
+ for (const member of children.get(next) ?? []) {
346
+ const reachable = exporting
347
+ ? member.exported
348
+ : member.visibility !== "private" && member.visibility !== "private-name";
349
+ if (member.kind !== "type-parameter" && reachable) {
350
+ reach(member.id);
351
+ }
298
352
  }
299
- reached.add(member.id);
300
- pending.push(member.id);
301
353
  }
354
+ const named = round.flatMap((id) => {
355
+ const symbol = byId.get(id);
356
+ return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
357
+ });
358
+ project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
359
+ if (symbol === UNANSWERED) {
360
+ for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
361
+ guessed.add(id);
362
+ reach(id);
363
+ }
364
+ }
365
+ else if (symbol !== undefined) {
366
+ for (const target of chains.chainOf(symbol)) {
367
+ reach(target.id);
368
+ }
369
+ }
370
+ });
302
371
  }
303
- return [...reached];
372
+ return { ids: [...reached], guessed: [...guessed] };
373
+ }
374
+ /** The type names declarations of the inventory write, each file walked once, when first asked. */
375
+ function declarationNodes(held, byPath) {
376
+ const walked = new Map();
377
+ const nodesIn = (path) => {
378
+ const known = walked.get(path);
379
+ if (known !== undefined) {
380
+ return known;
381
+ }
382
+ const found = { byId: new Map(), nodes: new Set() };
383
+ const file = byPath.get(path);
384
+ if (file !== undefined) {
385
+ const visit = (node) => {
386
+ const id = held.declarations.get(nodeKey(file, node));
387
+ if (id !== undefined) {
388
+ found.nodes.add(node);
389
+ if (!found.byId.has(id)) {
390
+ found.byId.set(id, node);
391
+ }
392
+ }
393
+ node.forEachChild(visit);
394
+ };
395
+ visit(file);
396
+ }
397
+ walked.set(path, found);
398
+ return found;
399
+ };
400
+ return {
401
+ typeNamesOf: (symbol) => {
402
+ const { byId, nodes } = nodesIn(symbol.position.path);
403
+ const declaration = byId.get(symbol.id);
404
+ return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
405
+ },
406
+ };
407
+ }
408
+ /**
409
+ * The names of types one declaration's own syntax writes: in a type reference, a
410
+ * heritage clause or a type query. A function body is not read, and neither is a
411
+ * declaration nested in this one, which `nested` holds and which is read on its own.
412
+ */
413
+ function typeNamesOf(declaration, nested) {
414
+ const found = [];
415
+ const visit = (node) => {
416
+ if (node !== declaration && nested.has(node)) {
417
+ return;
418
+ }
419
+ if (isTypeReferenceNode(node)) {
420
+ found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
421
+ }
422
+ else if (isExpressionWithTypeArguments(node)) {
423
+ found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
424
+ }
425
+ else if (isTypeQueryNode(node)) {
426
+ found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
427
+ }
428
+ const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
429
+ node.forEachChild((child) => {
430
+ if (child !== body) {
431
+ visit(child);
432
+ }
433
+ });
434
+ };
435
+ visit(declaration);
436
+ return found;
304
437
  }
305
438
  /**
306
439
  * Keeps every declaration the configuration names and returns every string that
@@ -225,10 +225,10 @@ function projectsOf(snapshot, built, written) {
225
225
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
226
226
  * built as that program, or opened from the configuration it writes, instead of as a project.
227
227
  * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
228
- * under `"record"`. The snapshot is disposed after the last visit and the client released
229
- * after it, whether the visit completed or threw.
228
+ * under `"record"`. `last` runs after the last visit while the client still answers; the
229
+ * snapshot is disposed and the client released after it, whether the visit completed or threw.
230
230
  */
231
- export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
231
+ export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
232
232
  try {
233
233
  const built = [];
234
234
  const params = [];
@@ -262,6 +262,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
262
262
  }
263
263
  projects.push(visit(viewOf(project, configFile, engine, log)));
264
264
  }
265
+ last();
265
266
  return {
266
267
  projects,
267
268
  unopened: missing,
package/dist/src/sweep.js CHANGED
@@ -70,6 +70,23 @@ export function sweep(graph, input) {
70
70
  });
71
71
  return { liveUnder: live, candidates };
72
72
  }
73
+ /**
74
+ * Per declaration of the graph, whether the run that counts test references reaches it.
75
+ * That run's roots are the graph's own and each test file, being the file's top level
76
+ * and every declaration it exports, which a test runner may call; `held` seeds it as a
77
+ * mark or an exemption seeds a sweep.
78
+ */
79
+ export function reachedWithTheTests(graph, held) {
80
+ const tests = graph.symbols.flatMap((symbol, at) => {
81
+ const top = symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
82
+ return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
83
+ ? [{ at, kind: "test-runner" }]
84
+ : [];
85
+ });
86
+ return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
87
+ production: false,
88
+ });
89
+ }
73
90
  /**
74
91
  * The references a loaded consumer makes that the mode counts. A production mode counts
75
92
  * none a consumer's test file made, as it counts none of the target's.
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
87
87
  }
88
88
  return at + 1;
89
89
  }
90
- /** The identifiers one action's inner text writes outside a quoted string, with their offsets in it. */
91
- function identifiersOf(inner) {
90
+ /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
91
+ function substitutionEnd(text, from) {
92
+ let depth = 1;
93
+ let at = from;
94
+ while (at < text.length) {
95
+ const char = text[at];
96
+ if (char === '"' || char === "'") {
97
+ at = stringEnd(text, at);
98
+ continue;
99
+ }
100
+ if (char === "`") {
101
+ at = templateEnd(text, at, () => undefined);
102
+ continue;
103
+ }
104
+ if (char === "{") {
105
+ depth += 1;
106
+ }
107
+ else if (char === "}") {
108
+ depth -= 1;
109
+ if (depth === 0) {
110
+ return at + 1;
111
+ }
112
+ }
113
+ at += 1;
114
+ }
115
+ return at;
116
+ }
117
+ /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
118
+ function templateEnd(text, from, substitution) {
119
+ let at = from + 1;
120
+ while (at < text.length && text[at] !== "`") {
121
+ if (text[at] === "\\") {
122
+ at += 2;
123
+ continue;
124
+ }
125
+ if (text[at] === "$" && text[at + 1] === "{") {
126
+ const start = at + 2;
127
+ const end = substitutionEnd(text, start);
128
+ substitution(text.slice(start, end - 1), start);
129
+ at = end;
130
+ continue;
131
+ }
132
+ at += 1;
133
+ }
134
+ return at + 1;
135
+ }
136
+ /**
137
+ * The identifiers one action's inner text writes outside a quoted string, with their offsets
138
+ * in it. In `script` text, a component's markup, a template literal's substitutions are code.
139
+ */
140
+ function identifiersOf(inner, script = false) {
92
141
  const found = [];
93
142
  let at = 0;
94
143
  while (at < inner.length) {
95
144
  const char = inner[at] ?? "";
145
+ if (char === "`" && script) {
146
+ at = templateEnd(inner, at, (expression, offset) => {
147
+ for (const one of identifiersOf(expression, true)) {
148
+ found.push({ name: one.name, offset: offset + one.offset });
149
+ }
150
+ });
151
+ continue;
152
+ }
96
153
  if (char === '"' || char === "'" || char === "`") {
97
154
  at = stringEnd(inner, at);
98
155
  continue;
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
142
199
  }
143
200
  /** Every identifier the markup of one component file writes in an action. */
144
201
  function markupNamesOf(text) {
145
- return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
202
+ return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
146
203
  name,
147
204
  offset: one.start + offset,
148
205
  action: one.action,
@@ -1,47 +1,59 @@
1
1
  /**
2
- * Test-support code: a file that is not a test file by name and that only test code
3
- * imports. At least one test file reaches it, directly or through other such files, and
4
- * nothing else does, so it is judged with the tests: what it references is referenced by
5
- * test code, and a declaration of it is live while test code references it.
2
+ * Test-support code: the files only tests reach. A test reaches each one through a chain
3
+ * of references, and every reference to a declaration of one, an import for its effects
4
+ * included, is written in a test file or in another such file, so it is judged with the
5
+ * tests: what it references is referenced by test code.
6
6
  */
7
7
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
8
8
  const SKIP_ROOT = "type-error";
9
9
  /**
10
- * The files of one project that are test-support code, below the target root. A file
11
- * holding a root is not one, because a root has a caller outside the import graph: a
12
- * runtime, a tool, a configuration, or a consumer of a library's published API.
13
- * `testFiles` are the files the test-file rules classified.
10
+ * The files of one project that are test-support code, below the target root: the
11
+ * largest set of files a test file reaches, none of them a test file or holding a root,
12
+ * whose every user outside the set is a test file. A root has a caller outside the
13
+ * reference graph: a runtime, a tool, a configuration, or a consumer of a library's
14
+ * published API. `testFiles` are the files the test-file rules classified.
14
15
  */
15
16
  export function testSupportFiles(symbols, references, roots, testFiles) {
16
17
  const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
17
18
  const users = new Map();
19
+ const uses = new Map();
20
+ const add = (map, key, value) => {
21
+ const held = map.get(key);
22
+ if (held === undefined) {
23
+ map.set(key, new Set([value]));
24
+ }
25
+ else {
26
+ held.add(value);
27
+ }
28
+ };
18
29
  for (const reference of references) {
19
30
  const used = fileOf.get(reference.to);
20
31
  const from = reference.position.path;
21
32
  if (reference.consumer !== undefined || used === undefined || used === from) {
22
33
  continue;
23
34
  }
24
- const held = users.get(used);
25
- if (held === undefined) {
26
- users.set(used, new Set([from]));
27
- }
28
- else {
29
- held.add(from);
30
- }
35
+ add(users, used, from);
36
+ add(uses, from, used);
31
37
  }
32
38
  const tests = new Set(testFiles);
33
39
  const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
34
- const candidates = [...users.keys()].filter((path) => !tests.has(path) && !rooted.has(path));
35
- const support = new Set();
40
+ const reached = new Set();
41
+ const queue = [...tests];
42
+ for (let path = queue.pop(); path !== undefined; path = queue.pop()) {
43
+ for (const next of uses.get(path) ?? []) {
44
+ if (!reached.has(next) && !tests.has(next)) {
45
+ reached.add(next);
46
+ queue.push(next);
47
+ }
48
+ }
49
+ }
50
+ const support = new Set([...reached].filter((path) => !rooted.has(path)));
36
51
  for (let changed = true; changed;) {
37
52
  changed = false;
38
- for (const path of candidates) {
39
- if (support.has(path)) {
40
- continue;
41
- }
53
+ for (const path of support) {
42
54
  const from = users.get(path) ?? new Set();
43
- if ([...from].every((one) => tests.has(one) || support.has(one))) {
44
- support.add(path);
55
+ if (![...from].every((one) => tests.has(one) || support.has(one))) {
56
+ support.delete(path);
45
57
  changed = true;
46
58
  }
47
59
  }
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "5.2.0";
7
+ export const CONTRACT_VERSION = "5.3.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.1.0",
3
+ "version": "5.2.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": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",