@cplieger/deadset-ts 4.0.1 → 5.0.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 (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
@@ -8,7 +8,7 @@ import type { RunSweep } from "../analysis.ts";
8
8
  import { componentId, type Component } from "../components.ts";
9
9
  import type { Cascade, Confidence, Severity } from "../config.ts";
10
10
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
- import { OUTSIDE } from "../graph.ts";
11
+ import { OUTSIDE, type Graph } from "../graph.ts";
12
12
  import { KINDS, type KindRow } from "../kinds.ts";
13
13
  import { byPosition } from "../position.ts";
14
14
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
@@ -24,56 +24,47 @@ function capped(found: Confidence, ceiling: Confidence): Confidence {
24
24
  return RANK[found] <= RANK[ceiling] ? found : ceiling;
25
25
  }
26
26
 
27
- /** The kind whose default turns on what the run knows about the target's consumers. */
28
- const UNUSED_EXPORTED = "DS1001";
29
-
30
27
  /** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
31
28
  function everyDeclaredLoaded(boundary: Boundary): boolean {
32
29
  const { declared, loaded } = boundary.consumers;
33
30
  return declared.every((id) => loaded.includes(id));
34
31
  }
35
32
 
36
- /**
37
- * Whether a library's published API may have a caller the run cannot see: the target is
38
- * a library, and the run either holds no consumer information or did not load a consumer
39
- * the scope declared. A configuration declaring the consumer set complete holds consumer
40
- * information whether or not it declares a consumer, because it says no other exists.
41
- */
42
- function openWorld(input: EmitterInput): boolean {
43
- const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
44
- return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
45
- }
46
-
47
33
  /**
48
34
  * The severity the configuration gives one code: the key naming the code, else the key
49
- * naming its family, else the code's default. The unused-exported kind defaults to
50
- * `allow` while a library's published API is open to callers the run cannot see.
35
+ * naming its family, else the code's default.
51
36
  */
52
37
  function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
53
38
  const { config } = input;
54
- const named = config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH));
55
- if (named !== undefined) {
56
- return named;
57
- }
58
- return code === UNUSED_EXPORTED && openWorld(input) ? "allow" : fallback;
39
+ return (
40
+ config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
41
+ );
59
42
  }
60
43
 
61
44
  /**
62
- * The reachability class of one declaration of the run. A declaration of a library's
63
- * published API has callers outside the target: it is `certain` where the run loaded
64
- * every consumer the scope declared and declared at least one, `probable` where it
65
- * declared consumers and did not load them all, and `possible` where it declared none.
66
- * Whether the configuration declares the consumer set complete decides nothing here.
67
- * Every other declaration has every reference in the loaded program, a private member
68
- * and a `#private` name among them, and is `certain`.
45
+ * The declarations the roots name as a library's published API, each public member of a
46
+ * published type included.
69
47
  */
70
- function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
71
- const union = input.swept.matrix.union;
72
- const published = new Set(
48
+ function publishedIds(union: Graph): ReadonlySet<string> {
49
+ return new Set(
73
50
  union.rooted
74
51
  .filter((root) => root.kind === "published-api")
75
- .map((root) => union.symbols[root.at]?.id),
52
+ .flatMap((root) => union.symbols[root.at]?.id ?? []),
76
53
  );
54
+ }
55
+
56
+ /**
57
+ * The reachability class of one declaration of the run. A declaration of a library's
58
+ * published API, a public member of a published type included, has callers outside the
59
+ * target: it is `certain` where the run loaded every consumer the scope declared and
60
+ * declared at least one, `probable` where it declared consumers and did not load them
61
+ * all, and `possible` where it declared none. Whether the configuration declares the
62
+ * consumer set complete decides nothing here. Every other declaration has every reference
63
+ * in the loaded program, a private member and a `#private` name among them, and is
64
+ * `certain`.
65
+ */
66
+ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
67
+ const published = publishedIds(input.swept.matrix.union);
77
68
  const { declared } = input.boundary.consumers;
78
69
  const outside: Confidence =
79
70
  declared.length === 0
@@ -1,6 +1,8 @@
1
1
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
2
2
  import type { CompletedFinding } from "../finding.ts";
3
+ import { liveFilesAt } from "../matrix.ts";
3
4
  import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
5
+ import { insideSkipped } from "../type-errors.ts";
4
6
  import { completed, dialsOf, reportable } from "./completion.ts";
5
7
  import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
6
8
  import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
@@ -40,6 +42,28 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
40
42
  );
41
43
  }
42
44
 
45
+ /**
46
+ * The references of every top-level declaration of a live component file. The file's
47
+ * markup may use each of them, which no analysis of the markup can rule out, so the run
48
+ * reports nothing about one while the file is live.
49
+ */
50
+ function markupRefs(input: EmitterInput): ReadonlySet<string> {
51
+ const { swept } = input;
52
+ if (swept.componentFiles.length === 0) {
53
+ return new Set();
54
+ }
55
+ const live = new Set(
56
+ liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map(
57
+ (symbol) => symbol.id,
58
+ ),
59
+ );
60
+ return new Set(
61
+ swept.matrix.union.symbols
62
+ .filter((symbol) => live.has(symbol.parent))
63
+ .map((symbol) => symbol.ref),
64
+ );
65
+ }
66
+
43
67
  /** What the suppressions of a run swept under their marks are decided against. */
44
68
  export interface Suppressed {
45
69
  /** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
@@ -109,9 +133,16 @@ export function decidedFindings(
109
133
  const would = [...unmarked, ...found];
110
134
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
111
135
  const unanswered = heldRefs(input);
136
+ const markup = markupRefs(input);
112
137
  const kept = reportable(
113
138
  input,
114
- found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)),
139
+ found.filter(
140
+ (finding) =>
141
+ !ledger.withheld(finding) &&
142
+ !unanswered.has(finding.symbol.ref) &&
143
+ !markup.has(finding.symbol.ref) &&
144
+ !insideSkipped(input.swept.skipped, finding.position),
145
+ ),
115
146
  );
116
147
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
117
148
  return {
@@ -52,6 +52,8 @@ interface Judged {
52
52
  readonly symbol: InventorySymbol;
53
53
  /** Whether a test file declares it. */
54
54
  readonly test: boolean;
55
+ /** Whether a test-support file declares it, which test code keeps live. */
56
+ readonly support: boolean;
55
57
  /** Whether its container is itself dead, so it falls with the container. */
56
58
  readonly containerDead: boolean;
57
59
  readonly deprecated: boolean;
@@ -62,8 +64,9 @@ interface Judged {
62
64
  /**
63
65
  * The code one candidate is reported under, or none, each symbol once under the most
64
66
  * specific code: a test of dead code first; no other test-file declaration, member of a
65
- * dead container or enum member nothing names; then deprecation, a test reference, and
66
- * the unreferenced codes, an unimportable unused export being the unreachable kind's.
67
+ * dead container or enum member nothing names, nor a test-support declaration test code
68
+ * references; then deprecation, a test reference, and the unreferenced codes, an
69
+ * unimportable unused export being the unreachable kind's.
67
70
  */
68
71
  function codeOf(judged: Judged): string | undefined {
69
72
  const { candidate, symbol } = judged;
@@ -77,6 +80,9 @@ function codeOf(judged: Judged): string | undefined {
77
80
  if (symbol.kind === "enum-member" && !referenced) {
78
81
  return undefined;
79
82
  }
83
+ if (judged.support && candidate.testRefs > 0) {
84
+ return undefined;
85
+ }
80
86
  if (candidate.productionRefs === 0 && judged.deprecated) {
81
87
  return "DS1006";
82
88
  }
@@ -167,6 +173,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
167
173
  candidate,
168
174
  symbol,
169
175
  test: union.test[at] === true,
176
+ support: union.support[at] === true,
170
177
  containerDead: containerDead && !fallen,
171
178
  deprecated: input.deprecated.has(symbol.id),
172
179
  unreachable: unreachable.has(symbol.id),
package/src/graph.ts CHANGED
@@ -39,20 +39,26 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
39
39
 
40
40
  /**
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
- * tool reads the configuration it loads, a runtime runs a test file, a setup file, a
43
- * worker and a command, the manifest names what enters the package, and an entry file, a
44
- * configured root and a pattern assert a caller. A library's published API only supposes
42
+ * tool reads the configuration it loads and the files its strings name, a runtime runs a
43
+ * test file, a setup file, a worker, a command and a script, the manifest names what
44
+ * enters the package, an alias global stands for the export it names, and an entry file,
45
+ * a configured root and a pattern assert a caller. A library's published API only supposes
45
46
  * a consumer the run may not see, so it is live under reachability alone.
46
47
  */
47
48
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
48
49
  "entry-file": true,
50
+ convention: true,
49
51
  "manifest-entry": true,
50
52
  "manifest-binary": true,
53
+ script: true,
51
54
  "published-api": false,
52
55
  configured: true,
53
56
  pattern: true,
57
+ "type-error": true,
58
+ "type-query-alias": true,
59
+ "configuration-file": true,
60
+ "configuration-string": true,
54
61
  "test-runner": true,
55
- "lint-configuration": true,
56
62
  "mutation-testing": true,
57
63
  "browser-tests": true,
58
64
  worker: true,
@@ -103,6 +109,8 @@ export interface Graph {
103
109
  readonly parent: readonly number[];
104
110
  /** Per declaration, whether a test file holds it. */
105
111
  readonly test: readonly boolean[];
112
+ /** Per declaration, whether a test-support file holds it. */
113
+ readonly support: readonly boolean[];
106
114
  /** Per declaration, whether the sweep judges its liveness. */
107
115
  readonly subject: readonly boolean[];
108
116
  /**
@@ -135,7 +143,9 @@ export function graphOf(
135
143
  references: readonly Reference[],
136
144
  roots: readonly Root[],
137
145
  testFiles: readonly string[],
146
+ supportFiles: readonly string[] = [],
138
147
  ): Graph {
148
+ const supports = new Set(supportFiles);
139
149
  const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
140
150
  const at = (id: string): number => index.get(id) ?? OUTSIDE;
141
151
  const tests = new Set(testFiles);
@@ -187,6 +197,7 @@ export function graphOf(
187
197
  made,
188
198
  parent,
189
199
  test: symbols.map((symbol) => tests.has(symbol.position.path)),
200
+ support: symbols.map((symbol) => supports.has(symbol.position.path)),
190
201
  subject: symbols.map((symbol) => swept(symbol.kind)),
191
202
  exportsOf,
192
203
  rooted,
package/src/host.ts CHANGED
@@ -27,6 +27,12 @@ export interface Host {
27
27
  readDirectory(path: string): readonly DirectoryEntry[];
28
28
  /** What one path names, `"absent"` where nothing is there. */
29
29
  kindOf(path: string): PathKind;
30
+ /**
31
+ * The path one path names once every symbolic link along it is followed, or a throw
32
+ * naming why it cannot be: a package manager links an installed workspace package
33
+ * into `node_modules`, and the link's target is what says which package it is.
34
+ */
35
+ realPath(path: string): string;
30
36
  /**
31
37
  * The version of the package this analyzer was installed from, or a throw
32
38
  * naming why it cannot be known. It is a method rather than a value because
@@ -41,4 +47,21 @@ export interface Host {
41
47
  * never a part of it.
42
48
  */
43
49
  writeDocument(path: string, pieces: Iterable<string>): void;
50
+ /**
51
+ * A new empty directory of its own, outside every tree the run reads, or a throw naming
52
+ * why it cannot be made. The run removes it before it ends.
53
+ */
54
+ temporaryDirectory(): TemporaryDirectory;
55
+ /**
56
+ * The command that runs this analyzer's mapper for component files, under the runtime
57
+ * the analyzer itself runs under. The compiler starts it to read a component file.
58
+ */
59
+ componentMapperCommand(): readonly string[];
60
+ }
61
+
62
+ /** One directory a run made for itself. */
63
+ export interface TemporaryDirectory {
64
+ readonly path: string;
65
+ /** Removes the directory and everything in it; removing it twice removes it once. */
66
+ remove(): void;
44
67
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
2
- export type { DirectoryEntry, Host, PathKind } from "./host.ts";
2
+ export type { DirectoryEntry, Host, PathKind, TemporaryDirectory } from "./host.ts";
3
3
  export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
4
4
  export {
5
5
  byPosition,
package/src/inventory.ts CHANGED
@@ -31,7 +31,13 @@ import {
31
31
  import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
32
32
  import type { Host } from "./host.ts";
33
33
  import { dirnamePath, joinPath, relativePath } from "./paths.ts";
34
- import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
34
+ import {
35
+ byPosition,
36
+ positionKey,
37
+ renderPosition,
38
+ writtenLength,
39
+ type Position,
40
+ } from "./position.ts";
35
41
  import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
36
42
  import { isAnswered, UNANSWERED } from "./query.ts";
37
43
  import type { Handle, ProjectView } from "./session.ts";
@@ -534,7 +540,12 @@ export function inventory<Brand>(
534
540
  typeParameter: undefined,
535
541
  });
536
542
  exportScopes.push({ owner: fileRecord, nameNode: file });
537
- walkStatements(file.statements, fileRecord);
543
+ // A statement a component file's module appends is not written in the file.
544
+ const written = writtenLength(file);
545
+ walkStatements(
546
+ file.statements.filter((statement) => statement.end <= written),
547
+ fileRecord,
548
+ );
538
549
  }
539
550
 
540
551
  /** One declaration of a module, a namespace or a container body. */
package/src/kinds.ts CHANGED
@@ -69,7 +69,7 @@ const ROWS: readonly KindRow[] = [
69
69
  code: "DS1004",
70
70
  name: "test-only-use",
71
71
  languages: ["go", "ts"],
72
- rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production.",
72
+ rule: "A symbol with zero production references and at least one test reference: an unused-exported, unused-unexported or unused-member candidate whose test reference count is not zero, reported once under this code. A reference from a consumer's test files is a test reference unless the configuration counts consumer tests as production. A reference from test-support code, a package or file that only test code imports, is a test reference, and a declaration of test-support code is never reported under this code.",
73
73
  defaultSeverity: "deny",
74
74
  maxClass: "certain",
75
75
  fixability: "deletable",
@@ -317,7 +317,7 @@ const ROWS: readonly KindRow[] = [
317
317
  languages: ["go", "ts"],
318
318
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
319
319
  precondition:
320
- "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
320
+ "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
321
321
  defaultSeverity: "warn",
322
322
  maxClass: "certain",
323
323
  fixability: "manual",
package/src/manifest.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * The entry points a package manifest names, which are roots because something
3
3
  * outside the analyzed program reaches them: a consumer importing the package, or a
4
- * shell running its command.
4
+ * shell running its command or one of its scripts.
5
5
  *
6
- * Only the target's own manifest is read. A manifest below the target root belongs
7
- * to another package, whose entry points are that package's roots and not this
8
- * target's.
6
+ * One call reads one manifest, the one in the directory it is given.
9
7
  */
10
8
 
11
9
  import type { Host } from "./host.ts";
@@ -22,7 +20,9 @@ export type EntryRole =
22
20
  /** A consumer importing the package, which is a caller the analysis cannot see. */
23
21
  | "import"
24
22
  /** A shell running the package's command, which is a caller that exists. */
25
- | "run";
23
+ | "run"
24
+ /** A token of one of the package's scripts, which the package manager runs. */
25
+ | "script";
26
26
 
27
27
  /** One entry point the manifest names. */
28
28
  export interface ManifestEntry {
@@ -59,6 +59,24 @@ function isTarget(value: string, requireDot: boolean): boolean {
59
59
  return !requireDot || value.startsWith("./");
60
60
  }
61
61
 
62
+ /** Where a script's command line splits into tokens: white space and the shell's operators. */
63
+ const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
64
+
65
+ /** A token written inside one pair of quotes. */
66
+ const QUOTED = /^(["'])(.*)\1$/su;
67
+
68
+ /**
69
+ * Every token one script's command line holds, each with one pair of enclosing quotes
70
+ * removed. A token holding a wildcard is a pattern rather than a path, and names no
71
+ * entry point.
72
+ */
73
+ export function scriptTokens(command: string): readonly string[] {
74
+ return command
75
+ .split(SCRIPT_SEPARATORS)
76
+ .map((token) => token.replace(QUOTED, "$2"))
77
+ .filter((token) => token !== "" && !token.includes("*"));
78
+ }
79
+
62
80
  /** One member's spelling below another: `exports` then `exports["."]`. */
63
81
  function below(member: string, key: string): string {
64
82
  return `${member}[${JSON.stringify(key)}]`;
@@ -67,12 +85,9 @@ function below(member: string, key: string): string {
67
85
  /**
68
86
  * Every target one `exports` value carries, at the member each is written under.
69
87
  *
70
- * The value is a string, an object whose keys are subpaths or conditions, or an
71
- * array of alternatives, nested to any depth, and a `null` blocks a subpath rather
72
- * than naming a target. Every level is walked because a condition names a different
73
- * file of the same package and each is an entry point of its own; a key's meaning
74
- * is not read, because a subpath and a condition both lead to a target and the
75
- * walk needs no more than that.
88
+ * The value nests strings, subpath or condition objects and alternative arrays to
89
+ * any depth; a `null` blocks a subpath. Every level is walked, because each
90
+ * condition names its own entry file, and a key's meaning is never read.
76
91
  */
77
92
  function exportTargets(value: unknown, member: string, found: ManifestEntry[]): void {
78
93
  if (typeof value === "string") {
@@ -116,11 +131,12 @@ function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | u
116
131
  return isRecord(value) ? value : undefined;
117
132
  }
118
133
 
119
- /** What the target's own manifest declares about where the package is entered. */
134
+ /** What one manifest declares about where its package is entered. */
120
135
  export interface Manifest {
121
136
  /**
122
137
  * Every entry point the manifest names, in the order the members are read: `main`,
123
- * `module`, `types`, then each `bin` command, then every target of `exports`. Each
138
+ * `module`, `types`, then each `bin` command, then every target of `exports`, then
139
+ * every token of each script, which names a file only where one is at its path. Each
124
140
  * path is absolute.
125
141
  */
126
142
  readonly entries: readonly ManifestEntry[];
@@ -134,7 +150,7 @@ export interface Manifest {
134
150
  }
135
151
 
136
152
  /**
137
- * What the target's own manifest declares about where the package is entered.
153
+ * What the manifest in `targetRoot` declares about where its package is entered.
138
154
  *
139
155
  * A target is taken as written. It is not resolved to a source file here, because
140
156
  * which file a target names is a fact about the compiler configuration that emits
@@ -167,6 +183,23 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
167
183
 
168
184
  exportTargets(manifest["exports"], "exports", found);
169
185
 
186
+ const scripts = manifest["scripts"];
187
+ if (isRecord(scripts)) {
188
+ for (const [name, command] of Object.entries(scripts)) {
189
+ if (typeof command !== "string") {
190
+ continue;
191
+ }
192
+ for (const token of new Set(scriptTokens(command))) {
193
+ found.push({
194
+ member: below("scripts", name),
195
+ path: token,
196
+ role: "script",
197
+ published: false,
198
+ });
199
+ }
200
+ }
201
+ }
202
+
170
203
  return {
171
204
  entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
172
205
  declaresExports: manifest["exports"] !== undefined,
package/src/matrix.ts CHANGED
@@ -21,6 +21,14 @@ export interface Configured {
21
21
  readonly roots: readonly Root[];
22
22
  /** The paths the reference pass classified as test files. */
23
23
  readonly testFiles: readonly string[];
24
+ /** The paths of the test-support files, which only test code imports. */
25
+ readonly supportFiles?: readonly string[] | undefined;
26
+ /**
27
+ * The paths of the workspace-package files the configuration holds but does not
28
+ * compile itself. Their declarations count in its graph and its verdict on them is no
29
+ * finding.
30
+ */
31
+ readonly referenceOnly?: readonly string[] | undefined;
24
32
  }
25
33
 
26
34
  /** The run's configurations, indexed for the sweep. */
@@ -30,9 +38,10 @@ export interface Matrix {
30
38
  /** Per configuration, in the same order, the graph of what that configuration holds. */
31
39
  readonly graphs: readonly Graph[];
32
40
  /**
33
- * The graph over every configuration at once: every declaration any configuration
34
- * holds, once per position and in site order, and every reference any configuration
35
- * makes, a reference several configurations make at one position counted once.
41
+ * The graph over every configuration at once: every declaration a configuration
42
+ * holds in a file it does not hold only for its references, once per position and in
43
+ * site order, and every reference any configuration makes, a reference several
44
+ * configurations make at one position counted once.
36
45
  */
37
46
  readonly union: Graph;
38
47
  /**
@@ -88,31 +97,30 @@ export function referenceKey(reference: Reference): string {
88
97
 
89
98
  /**
90
99
  * Indexes what the passes answered for each configuration of one run, in the run's order.
91
- *
92
- * A declaration is keyed across configurations by its identifier, which is its rendered
93
- * position and so is the same in every configuration that compiles its file; the fields
94
- * of the first configuration that holds it are the ones kept. A declaration one
95
- * configuration alone holds is in the union with that one configuration, so it is
96
- * judged where it exists rather than skipped.
97
- *
98
- * One reference written at one position of a file several configurations compile is seen
99
- * once per configuration and is one reference, so the union holds it once; two references
100
- * one configuration makes at one position are two.
100
+ * A declaration is keyed by its rendered position, with the fields of the first
101
+ * configuration that judges it; one in a file every holder holds only for its references
102
+ * is judged by none and is not in the union. One reference at one position of a file
103
+ * several configurations compile is one reference; two one configuration makes are two.
101
104
  */
102
105
  export function matrixOf(per: readonly Configured[]): Matrix {
103
106
  const symbols: InventorySymbol[] = [];
104
107
  const holders = new Map<string, string[]>();
105
108
  const references: Reference[] = [];
106
109
  const firstSeen = new Map<string, number>();
110
+ const judged = new Set<string>();
107
111
  per.forEach((one, index) => {
112
+ const referenceOnly = new Set(one.referenceOnly);
108
113
  for (const symbol of one.symbols) {
109
114
  const held = holders.get(symbol.id);
110
115
  if (held === undefined) {
111
116
  holders.set(symbol.id, [one.configuration]);
112
- symbols.push(symbol);
113
117
  } else if (!held.includes(one.configuration)) {
114
118
  held.push(one.configuration);
115
119
  }
120
+ if (!referenceOnly.has(symbol.position.path) && !judged.has(symbol.id)) {
121
+ judged.add(symbol.id);
122
+ symbols.push(symbol);
123
+ }
116
124
  }
117
125
  for (const reference of one.references) {
118
126
  const key = referenceKey(reference);
@@ -129,12 +137,15 @@ export function matrixOf(per: readonly Configured[]): Matrix {
129
137
 
130
138
  return {
131
139
  configurations: per.map((one) => one.configuration),
132
- graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
140
+ graphs: per.map((one) =>
141
+ graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles),
142
+ ),
133
143
  union: graphOf(
134
144
  symbols,
135
145
  references,
136
146
  per.flatMap((one) => one.roots),
137
147
  [...new Set(per.flatMap((one) => one.testFiles))],
148
+ [...new Set(per.flatMap((one) => one.supportFiles ?? []))],
138
149
  ),
139
150
  heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
140
151
  };
@@ -197,6 +208,23 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
197
208
  };
198
209
  }
199
210
 
211
+ /**
212
+ * The file declarations at `paths`, below the target root, that one sweep holds live
213
+ * under both relations, the rule a judged declaration is live by.
214
+ */
215
+ export function liveFilesAt(
216
+ matrix: Matrix,
217
+ result: SweepResult,
218
+ paths: ReadonlySet<string>,
219
+ ): readonly InventorySymbol[] {
220
+ return matrix.union.symbols.filter(
221
+ (symbol) =>
222
+ symbol.kind === "file" &&
223
+ paths.has(symbol.position.path) &&
224
+ (result.liveUnder.get(symbol.id)?.length ?? 0) === RELATIONS.length,
225
+ );
226
+ }
227
+
200
228
  /** The relations in the order a declaration's entry lists them. */
201
229
  const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
202
230
 
package/src/partial.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { NotBuilt } from "./discover.ts";
2
+ import { relativePath } from "./paths.ts";
2
3
  import type { UnansweredQuestion } from "./query.ts";
3
4
 
4
5
  /** What a run read only in part. */
@@ -7,6 +8,8 @@ interface Partial {
7
8
  readonly notBuilt: readonly NotBuilt[];
8
9
  /** Every question the checker could not answer, each once. */
9
10
  readonly unanswered: readonly UnansweredQuestion[];
11
+ /** The target root, absolute, which every configuration file is named below. */
12
+ readonly targetRoot: string;
10
13
  }
11
14
 
12
15
  /** A count with its noun, so a line reads for one as well as for several. */
@@ -29,7 +32,9 @@ export function partialNotes(partial: Partial): readonly string[] {
29
32
  if (partial.unanswered.length > 0) {
30
33
  const byProject = new Map<string, number>();
31
34
  for (const question of partial.unanswered) {
32
- byProject.set(question.configFile, (byProject.get(question.configFile) ?? 0) + 1);
35
+ const configFile =
36
+ relativePath(partial.targetRoot, question.configFile) ?? question.configFile;
37
+ byProject.set(configFile, (byProject.get(configFile) ?? 0) + 1);
33
38
  }
34
39
  const where = [...byProject].map(([configFile, n]) => `${configFile}: ${String(n)}`).join(", ");
35
40
  notes.push(