@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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 (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -1,7 +1,6 @@
1
1
  import type { Confidence } from "../contract/contract.generated.ts";
2
2
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
3
3
  import type { CompletedFinding } from "../finding.ts";
4
- import { liveFilesAt } from "../matrix.ts";
5
4
  import { declarationKey } from "../ref.ts";
6
5
  import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
7
6
  import { insideSkipped } from "../type-errors.ts";
@@ -11,7 +10,7 @@ import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
11
10
  import { interfaces } from "./interfaces.ts";
12
11
  import { intraFunction } from "./intra-function.ts";
13
12
  import { nonCodeArtifacts } from "./non-code-artifacts.ts";
14
- import { readsAndWrites } from "./reads-and-writes.ts";
13
+ import { fallingWithWrites, readsAndWrites } from "./reads-and-writes.ts";
15
14
  import { selfCheck } from "./self-check.ts";
16
15
  import { fallenMembers, unusedDeclarations } from "./unused-declarations.ts";
17
16
  import { visibilityNarrowing } from "./visibility-narrowing.ts";
@@ -28,42 +27,19 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
28
27
  ["intra-function", intraFunction],
29
28
  ]);
30
29
 
31
- /**
32
- * The keys of every declaration an unanswered question could have kept live, which the run
33
- * reports nothing about: a finding about one would be computed from the gap.
34
- */
35
- function heldKeys(input: EmitterInput): ReadonlySet<string> {
36
- const held = new Set(input.swept.heldByUnanswered);
37
- if (held.size === 0) {
38
- return held;
39
- }
30
+ /** The keys of the declarations the identifiers name, as {@link findingKey} keys a finding. */
31
+ function keysOf(input: EmitterInput, ids: Iterable<string>): ReadonlySet<string> {
32
+ const named = new Set(ids);
40
33
  return new Set(
41
34
  input.swept.matrix.union.symbols
42
- .filter((symbol) => held.has(symbol.id))
35
+ .filter((symbol) => named.has(symbol.id))
43
36
  .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
44
37
  );
45
38
  }
46
39
 
47
- /**
48
- * The keys of every top-level declaration of a live component file. The file's
49
- * markup may use each of them, which no analysis of the markup can rule out, so the run
50
- * reports nothing about one while the file is live.
51
- */
52
- function markupKeys(input: EmitterInput): ReadonlySet<string> {
53
- const { swept } = input;
54
- if (swept.componentFiles.length === 0) {
55
- return new Set();
56
- }
57
- const live = new Set(
58
- liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map(
59
- (symbol) => symbol.id,
60
- ),
61
- );
62
- return new Set(
63
- swept.matrix.union.symbols
64
- .filter((symbol) => live.has(symbol.parent))
65
- .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
66
- );
40
+ /** The key of the declaration a finding is about, or of the one holding the part it is about. */
41
+ function findingKey(finding: CompletedFinding): string {
42
+ return declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
67
43
  }
68
44
 
69
45
  /** What the suppressions of a run swept under their marks are decided against. */
@@ -91,15 +67,20 @@ interface DecidedFindings {
91
67
  /** How many findings the minimum confidence withheld at each confidence. */
92
68
  export type Withheld = Readonly<Record<Confidence, number>>;
93
69
 
94
- /** Every emitter's findings over one sweep, and those of any further emitter, completed by one step. */
70
+ /**
71
+ * Every emitter's findings over one sweep, and those of any further emitter, completed by
72
+ * one step, but none about a declaration that falls with a write-only subject: that
73
+ * subject's finding names it, so no finding about it exists for a record to match.
74
+ */
95
75
  function completedOver(
96
76
  input: EmitterInput,
97
77
  ...more: readonly Emitter[]
98
78
  ): readonly CompletedFinding[] {
79
+ const falls = keysOf(input, fallingWithWrites(input));
99
80
  return completed(
100
81
  input,
101
82
  [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)),
102
- );
83
+ ).filter((finding) => !falls.has(findingKey(finding)));
103
84
  }
104
85
 
105
86
  /**
@@ -161,21 +142,22 @@ function dialledOver(
161
142
  unmarked: readonly CompletedFinding[],
162
143
  ): Omit<DecidedFindings, "would"> {
163
144
  const would = [...unmarked, ...found];
164
- const unanswered = heldKeys(input);
165
- const heldIds = new Set(input.swept.heldByUnanswered);
166
- const keyOf = (finding: CompletedFinding): string =>
167
- declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
145
+ const { sweep } = input.swept;
146
+ // A hold is a gap of the analysis, which leaves a record it touches dormant; a live
147
+ // component file's markup is no gap, so a record on one of its bindings stays in effect.
148
+ const unanswered = keysOf(input, sweep.held);
149
+ const markup = keysOf(input, sweep.bindings);
150
+ const heldIds = new Set(sweep.held);
168
151
  const held = (finding: CompletedFinding): boolean =>
169
- unanswered.has(keyOf(finding)) || insideSkipped(input.swept.skipped, finding.position);
152
+ unanswered.has(findingKey(finding)) || insideSkipped(input.swept.skipped, finding.position);
170
153
  const ledger = ledgerOf(
171
154
  suppressed.records,
172
155
  would,
173
156
  dialsOver(input, found, suppressed, unmarked),
174
157
  { withholds: held, holdsRecord: (record) => heldIds.has(record.bound) },
175
158
  );
176
- const markup = markupKeys(input);
177
159
  const candidates = found.filter(
178
- (finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(keyOf(finding)),
160
+ (finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(findingKey(finding)),
179
161
  );
180
162
  const kept = reportable(input, candidates);
181
163
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
@@ -2,7 +2,7 @@ import { CODE } from "../contract/contract.generated.ts";
2
2
  import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
3
3
  import type { InventorySymbol } from "../inventory.ts";
4
4
  import { byPosition } from "../position.ts";
5
- import type { MatrixCandidate } from "../matrix.ts";
5
+ import type { Candidate } from "../liveness/sweep.ts";
6
6
  import type { Emitter, EmitterInput } from "./emitter.ts";
7
7
 
8
8
  /** What each finding says, by what the sweep found about the subject's references. */
@@ -21,7 +21,7 @@ function positionOf(symbol: InventorySymbol): FindingPosition {
21
21
  function indexed(input: EmitterInput): {
22
22
  readonly symbolOf: (id: string) => InventorySymbol | undefined;
23
23
  readonly inTest: (id: string) => boolean;
24
- readonly candidateOf: (id: string) => MatrixCandidate | undefined;
24
+ readonly candidateOf: (id: string) => Candidate | undefined;
25
25
  readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
26
26
  } {
27
27
  const union = input.swept.matrix.union;
@@ -113,7 +113,7 @@ export const interfaces: Emitter = (input) => {
113
113
  };
114
114
 
115
115
  /** What one unused interface's finding says, by the references the sweep counted. */
116
- function unusedMessage(candidate: MatrixCandidate): string {
116
+ function unusedMessage(candidate: Candidate): string {
117
117
  if (candidate.relation === "reachability") {
118
118
  return UNUSED_DEAD_MESSAGE;
119
119
  }
@@ -3,7 +3,7 @@ import type { Finding } from "../finding.ts";
3
3
  import { KINDS } from "../kinds.ts";
4
4
  import { parameterRef } from "../ref.ts";
5
5
  import { type PartFact } from "../intra-function-parts.ts";
6
- import { reachedWithTheTests } from "../sweep.ts";
6
+ import { reachedWithTheTests } from "../liveness/sweep.ts";
7
7
  import type { Emitter, EmitterInput } from "./emitter.ts";
8
8
 
9
9
  /** A list of rules as a sentence names them. */
@@ -109,8 +109,8 @@ export const intraFunction: Emitter = (input) => {
109
109
  const union = input.swept.matrix.union;
110
110
  const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
111
111
  const reached = reachedWithTheTests(union, [
112
- ...input.swept.exempt.map((record) => record.id),
113
- ...input.swept.heldByUnanswered,
112
+ ...input.swept.sweep.exempt.map((record) => record.id),
113
+ ...input.swept.sweep.held,
114
114
  ]);
115
115
  const testsOfDeadCode = new Set(
116
116
  input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
@@ -164,8 +164,19 @@ function fileNeverImported(input: EmitterInput): readonly Finding[] {
164
164
  });
165
165
  }
166
166
 
167
- /** The findings of the files family, `DS1600` to `DS1699`. */
168
- export const nonCodeArtifacts: Emitter = (input) => [
169
- ...fileNeverBuilt(input),
170
- ...fileNeverImported(input),
171
- ];
167
+ /**
168
+ * The findings of the files family, `DS1600` to `DS1699`, each at the class `possible` in a
169
+ * package a command of which may run a file whose path no word of it writes, since that
170
+ * command may run the file (confidence.35).
171
+ */
172
+ export const nonCodeArtifacts: Emitter = (input) => {
173
+ const unwritten = [...(input.files.unwrittenRuns ?? [])];
174
+ return [...fileNeverBuilt(input), ...fileNeverImported(input)].map((finding) =>
175
+ unwritten.some(
176
+ (dir) =>
177
+ dir === "." || finding.position.path === dir || finding.position.path.startsWith(`${dir}/`),
178
+ )
179
+ ? { ...finding, reachabilityClass: "possible" as const }
180
+ : finding,
181
+ );
182
+ };
@@ -1,10 +1,12 @@
1
1
  import { CODE } from "../contract/contract.generated.ts";
2
2
  import type { Finding, FindingDetails, FindingPosition } from "../finding.ts";
3
- import type { InventorySymbol } from "../inventory.ts";
4
- import type { MatrixCandidate } from "../matrix.ts";
3
+ import { WORDS, type InventorySymbol } from "../inventory.ts";
4
+ import type { Candidate } from "../liveness/sweep.ts";
5
5
  import { positionKey } from "../position.ts";
6
6
  import { declarationKey } from "../ref.ts";
7
+ import { reachabilityClasses } from "./completion.ts";
7
8
  import type { Emitter, EmitterInput } from "./emitter.ts";
9
+
8
10
  import { bytewise } from "../text/compare.ts";
9
11
 
10
12
  /** The codes this emitter reports. */
@@ -18,7 +20,7 @@ const SIGNATURES: ReadonlySet<string> = new Set(["function", "method", "type"]);
18
20
  interface Run {
19
21
  readonly input: EmitterInput;
20
22
  readonly symbols: ReadonlyMap<string, InventorySymbol>;
21
- readonly candidates: ReadonlyMap<string, MatrixCandidate>;
23
+ readonly candidates: ReadonlyMap<string, Candidate>;
22
24
  }
23
25
 
24
26
  function indexed(input: EmitterInput): Run {
@@ -112,19 +114,16 @@ function parametersOnlyNamedBy(
112
114
  }
113
115
 
114
116
  /** What one write-only finding says, the declarations its deletion takes with it included. */
115
- function writeOnlyMessage(
116
- run: Run,
117
- namedBy: ReadonlyMap<string, ReadonlySet<string>>,
118
- symbol: InventorySymbol,
119
- held: Uses,
120
- ): string {
117
+ function writeOnlyMessage(run: Run, written: Written, subject: Subject): string {
118
+ const { symbol, held } = subject;
121
119
  const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
122
120
  const read = run.input.stores.readInTests.has(symbol.id)
123
121
  ? "read only from test files"
124
122
  : "never read";
125
123
  const cascade = [
126
- ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
124
+ ...parametersOnlyNamedBy(run, written.namedBy, symbol).map((name) => `type parameter ${name}`),
127
125
  ...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
126
+ ...(written.named.get(symbol.id) ?? []),
128
127
  ];
129
128
  const word = SUBJECT_WORDS[symbol.kind] ?? "member";
130
129
  const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
@@ -133,17 +132,55 @@ function writeOnlyMessage(
133
132
  : `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
134
133
  }
135
134
 
135
+ /** One declaration the write-only kind reports, with its uses. */
136
+ interface Subject {
137
+ readonly symbol: InventorySymbol;
138
+ readonly held: Uses;
139
+ }
140
+
136
141
  /**
137
- * `DS1301`: a module-level variable, a type member or a class member the counted
138
- * references store into and never read, named with every position written.
142
+ * What the write-only kind answers of one run: its subjects in site order, and the
143
+ * declarations that fall with them, each named under the innermost subject of every chain
144
+ * of carriers its references hold.
145
+ */
146
+ interface Written {
147
+ readonly subjects: readonly Subject[];
148
+ readonly namedBy: ReadonlyMap<string, ReadonlySet<string>>;
149
+ readonly named: ReadonlyMap<string, readonly string[]>;
150
+ readonly falls: ReadonlySet<string>;
151
+ }
152
+
153
+ /**
154
+ * The kinds of declaration that fall with a write-only subject: a declaration of its own.
155
+ * A member falls with its container, and a type parameter is the type-parameter clause's.
156
+ */
157
+ const FALLING: ReadonlySet<InventorySymbol["kind"]> = new Set([
158
+ "function",
159
+ "class",
160
+ "interface",
161
+ "type",
162
+ "enum",
163
+ "namespace",
164
+ "variable",
165
+ ]);
166
+
167
+ const WRITTEN = new WeakMap<EmitterInput, Written>();
168
+
169
+ /**
170
+ * `DS1301`'s subjects: a module-level variable, a type member or a class member the
171
+ * counted references store into and never read.
139
172
  *
140
173
  * The subject is a live declaration: a store is a reference, so the sweep holds a
141
174
  * written one live and its candidates are never this kind's. Every reference that is
142
175
  * not a store reads, and so does an exemption record naming the declaration.
143
176
  */
144
- function writeOnlySymbols(run: Run): readonly Finding[] {
177
+ function writtenOf(run: Run): Written {
178
+ const known = WRITTEN.get(run.input);
179
+ if (known !== undefined) {
180
+ return known;
181
+ }
145
182
  const { stores } = run.input;
146
- const { matrix } = run.input.swept;
183
+ const { matrix, sweep } = run.input.swept;
147
184
  const uses = new Map<string, Uses>();
148
185
  for (const reference of stores.references) {
149
186
  let held = uses.get(reference.to);
@@ -174,26 +211,54 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
174
211
  }
175
212
  }
176
213
 
177
- const found: Finding[] = [];
178
- for (const symbol of matrix.union.symbols) {
214
+ const subjects = matrix.union.symbols.flatMap((symbol): Subject[] => {
179
215
  const held = uses.get(symbol.id);
180
- if (
181
- held === undefined ||
216
+ return held === undefined ||
182
217
  held.writes.length === 0 ||
183
218
  held.reads > 0 ||
184
219
  !writable(run, symbol) ||
185
220
  stores.exempt.has(symbol.id) ||
186
221
  run.candidates.has(symbol.id)
222
+ ? []
223
+ : [{ symbol, held }];
224
+ });
225
+ const subjectIds = new Set(subjects.map((subject) => subject.symbol.id));
226
+ const classOf = reachabilityClasses(run.input);
227
+ const named = new Map<string, string[]>();
228
+ const falls = new Set<string>();
229
+ for (const symbol of matrix.union.symbols) {
230
+ const chains = sweep.carried.get(symbol.id);
231
+ if (
232
+ chains === undefined ||
233
+ subjectIds.has(symbol.id) ||
234
+ !FALLING.has(symbol.kind) ||
235
+ classOf(symbol.id) !== "certain"
187
236
  ) {
188
237
  continue;
189
238
  }
190
- found.push(
191
- findingAbout(symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, namedBy, symbol, held), {
192
- writePositions: held.writes,
193
- }),
194
- );
239
+ const innermost = chains.map((chain) => chain.find((id) => subjectIds.has(id)));
240
+ if (innermost.includes(undefined)) {
241
+ continue;
242
+ }
243
+ falls.add(symbol.id);
244
+ for (const subject of new Set(innermost)) {
245
+ const name = `${WORDS[symbol.kind]} ${symbol.name}`;
246
+ named.set(subject ?? "", [...(named.get(subject ?? "") ?? []), name]);
247
+ }
195
248
  }
196
- return found;
249
+ const written = { subjects, namedBy, named, falls };
250
+ WRITTEN.set(run.input, written);
251
+ return written;
252
+ }
253
+
254
+ /** `DS1301`: each subject, named with every position written and what falls with it. */
255
+ function writeOnlySymbols(run: Run): readonly Finding[] {
256
+ const written = writtenOf(run);
257
+ return written.subjects.map((subject) =>
258
+ findingAbout(subject.symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, written, subject), {
259
+ writePositions: subject.held.writes,
260
+ }),
261
+ );
197
262
  }
198
263
 
199
264
  /**
@@ -253,12 +318,20 @@ function unusedTypeParameters(run: Run): readonly Finding[] {
253
318
  */
254
319
  export function writeOnlyDeclarations(input: EmitterInput): ReadonlySet<string> {
255
320
  return new Set(
256
- writeOnlySymbols(indexed(input)).map((finding) =>
257
- declarationKey(finding.symbol.ref, finding.position.path),
321
+ writtenOf(indexed(input)).subjects.map(({ symbol }) =>
322
+ declarationKey(symbol.ref, symbol.position.path),
258
323
  ),
259
324
  );
260
325
  }
261
326
 
327
+ /**
328
+ * The declarations that fall with a write-only subject, whatever the severity: a finding
329
+ * about one would name code its subject's deletion already deletes.
330
+ */
331
+ export function fallingWithWrites(input: EmitterInput): ReadonlySet<string> {
332
+ return writtenOf(indexed(input)).falls;
333
+ }
334
+
262
335
  /**
263
336
  * The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
264
337
  * order and each code's in site order.
@@ -1,9 +1,8 @@
1
1
  import { CODE } from "../contract/contract.generated.ts";
2
2
  import type { Finding } from "../finding.ts";
3
- import type { InventorySymbol, SymbolKind } from "../inventory.ts";
4
- import type { MatrixCandidate } from "../matrix.ts";
3
+ import { WORDS, type InventorySymbol, type SymbolKind } from "../inventory.ts";
4
+ import type { Candidate } from "../liveness/sweep.ts";
5
5
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
- import { testsUnseenAPI } from "./completion.ts";
7
6
  import { unreachableExports } from "./visibility-narrowing.ts";
8
7
 
9
8
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
@@ -26,39 +25,18 @@ const NOT_REPORTED_HERE: ReadonlySet<SymbolKind> = new Set([
26
25
  "type-parameter",
27
26
  ]);
28
27
 
29
- /** The reader's word for each kind of declaration a finding of this emitter names. */
30
- const WORDS: Readonly<Record<SymbolKind, string>> = {
31
- file: "file",
32
- function: "function",
33
- class: "class",
34
- interface: "interface",
35
- type: "type alias",
36
- enum: "enum",
37
- namespace: "namespace",
38
- variable: "variable",
39
- "export-alias": "re-export",
40
- method: "method",
41
- "class-member": "class member",
42
- "interface-method": "interface method",
43
- "type-member": "type member",
44
- "enum-member": "enum member",
45
- "type-parameter": "type parameter",
46
- };
47
-
48
28
  /** What the test-of-dead-code kind says, which states the rule it applies. */
49
29
  const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
50
30
 
51
31
  /** What the run holds about one candidate that decides its code. */
52
32
  interface Judged {
53
- readonly candidate: MatrixCandidate;
33
+ readonly candidate: Candidate;
54
34
  readonly symbol: InventorySymbol;
55
35
  /** Whether a test file declares it. */
56
36
  readonly test: boolean;
57
37
  readonly deprecated: boolean;
58
38
  /** Whether the unreachable-export kind reports it. */
59
39
  readonly unreachable: boolean;
60
- /** Whether it is a test with a target that is published API no declared consumer judges. */
61
- readonly testsUnseenAPI: boolean;
62
40
  }
63
41
 
64
42
  /**
@@ -72,7 +50,7 @@ interface Judged {
72
50
  function codeOf(judged: Judged): string | undefined {
73
51
  const { candidate, symbol } = judged;
74
52
  if (candidate.testOfDeadCode) {
75
- return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
53
+ return CODE.testOfDeadCode;
76
54
  }
77
55
  if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
78
56
  return undefined;
@@ -245,7 +223,6 @@ function emitterFindings(input: EmitterInput, fallen: boolean): Finding[] {
245
223
  const union = input.swept.matrix.union;
246
224
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
247
225
  const unreachable = unreachableExports(input);
248
- const unseenAPI = testsUnseenAPI(input);
249
226
  let brands: ReadonlySet<string> | undefined;
250
227
 
251
228
  const found: Finding[] = [];
@@ -265,7 +242,6 @@ function emitterFindings(input: EmitterInput, fallen: boolean): Finding[] {
265
242
  test: union.test[at] === true,
266
243
  deprecated: input.deprecated.has(symbol.id),
267
244
  unreachable: unreachable.has(symbol.id),
268
- testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
269
245
  };
270
246
  const code = codeOf(judged);
271
247
  if (
@@ -1,11 +1,12 @@
1
1
  import { CODE } from "../contract/contract.generated.ts";
2
2
  import type { Finding } from "../finding.ts";
3
- import { OUTSIDE, type Graph } from "../graph.ts";
4
- import type { InventorySymbol, SymbolKind } from "../inventory.ts";
5
- import type { MatrixCandidate } from "../matrix.ts";
3
+ import { OUTSIDE, type Graph } from "../liveness/graph.ts";
4
+ import { WORDS, type InventorySymbol } from "../inventory.ts";
5
+ import type { Candidate } from "../liveness/sweep.ts";
6
6
  import { declarationKey } from "../ref.ts";
7
7
  import type { RootKind } from "../roots/root-set.ts";
8
- import { runnerCalls } from "../sweep.ts";
8
+ import { runnerCalls } from "../liveness/sweep.ts";
9
+ import { exposedTypes, exposes } from "../exposure.ts";
9
10
  import type { Emitter, EmitterInput } from "./emitter.ts";
10
11
  import { writeOnlyDeclarations } from "./reads-and-writes.ts";
11
12
 
@@ -19,18 +20,6 @@ type Reach = "file" | "package" | "outside";
19
20
  */
20
21
  const PUBLISHING: ReadonlySet<RootKind> = new Set<RootKind>(["manifest-entry", "published-api"]);
21
22
 
22
- /** The reader's word for each kind of declaration this emitter reports. */
23
- const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
24
- function: "function",
25
- class: "class",
26
- interface: "interface",
27
- type: "type alias",
28
- enum: "enum",
29
- namespace: "namespace",
30
- variable: "variable",
31
- "export-alias": "re-export",
32
- };
33
-
34
23
  /** Per declaration of the run, the facts both rules read. */
35
24
  interface Facts {
36
25
  readonly graph: Graph;
@@ -142,10 +131,6 @@ function topLevelExport(facts: Facts, at: number, symbol: InventorySymbol): bool
142
131
  return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
143
132
  }
144
133
 
145
- function wordOf(symbol: InventorySymbol): string {
146
- return WORDS[symbol.kind] ?? symbol.kind;
147
- }
148
-
149
134
  /** The visibility each narrowing code names, which only the narrowing codes carry. */
150
135
  const NARROWER: Readonly<Record<string, "file" | "package">> = {
151
136
  [CODE.redundantExportKeyword]: "file",
@@ -181,7 +166,11 @@ function narrowing(
181
166
  facts: Facts,
182
167
  at: number,
183
168
  symbol: InventorySymbol,
184
- world: { readonly closed: boolean; readonly writeOnly: ReadonlySet<string> },
169
+ world: {
170
+ readonly closed: boolean;
171
+ readonly writeOnly: ReadonlySet<string>;
172
+ readonly exposed: ReadonlySet<string> | "every";
173
+ },
185
174
  ): Finding | undefined {
186
175
  const { closed } = world;
187
176
  const file = facts.fileOf[at] ?? OUTSIDE;
@@ -195,7 +184,7 @@ function narrowing(
195
184
  ) {
196
185
  return undefined;
197
186
  }
198
- const word = wordOf(symbol);
187
+ const word = WORDS[symbol.kind];
199
188
  switch (facts.reach[at]) {
200
189
  case "file":
201
190
  if (facts.published[file] === true && !closed) {
@@ -207,7 +196,7 @@ function narrowing(
207
196
  `exported ${word} is referenced only inside the file that declares it`,
208
197
  );
209
198
  case "package":
210
- if (!closed || !publishes(facts.rootedBy[at])) {
199
+ if (!closed || !publishes(facts.rootedBy[at]) || exposes(world.exposed, symbol)) {
211
200
  return undefined;
212
201
  }
213
202
  return findingOf(
@@ -231,7 +220,7 @@ function unreachable(
231
220
  facts: Facts,
232
221
  at: number,
233
222
  symbol: InventorySymbol,
234
- candidate: MatrixCandidate,
223
+ candidate: Candidate,
235
224
  input: EmitterInput,
236
225
  ): Finding | undefined {
237
226
  if (
@@ -252,7 +241,7 @@ function unreachable(
252
241
  return findingOf(
253
242
  symbol,
254
243
  CODE.unreachableExport,
255
- `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`,
244
+ `exported ${WORDS[symbol.kind]} ${found}, and nothing outside can import the file that declares it`,
256
245
  );
257
246
  }
258
247
 
@@ -267,7 +256,17 @@ export function narrowings(input: EmitterInput): readonly Finding[] {
267
256
  const facts = factsOf(input.swept.matrix.union);
268
257
  const { declared, loaded } = input.boundary.consumers;
269
258
  const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
270
- const world = { closed, writeOnly: writeOnlyDeclarations(input) };
259
+ const world = {
260
+ closed,
261
+ writeOnly: writeOnlyDeclarations(input),
262
+ exposed: closed
263
+ ? exposedTypes(
264
+ facts.graph,
265
+ facts.reach.map((reach) => reach === "outside"),
266
+ input.boundary.carriers,
267
+ )
268
+ : new Set<string>(),
269
+ };
271
270
  const candidates = new Map(
272
271
  input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
273
272
  );
@@ -32,7 +32,7 @@ import { aliasChains } from "./alias-chain.ts";
32
32
  import { emptyImplementation } from "./findings/implementations.ts";
33
33
  import type { DetectorInput, Evidence } from "./exempt.ts";
34
34
  import type { Reference } from "./references.ts";
35
- import type { Exemption } from "./sweep.ts";
35
+ import type { Exemption } from "./exempt.ts";
36
36
  import { nodeKey, type Inventory } from "./inventory.ts";
37
37
  import { renderPosition } from "./position.ts";
38
38
  import { must } from "./query.ts";
package/src/inventory.ts CHANGED
@@ -50,6 +50,25 @@ export type SymbolKind =
50
50
  | "enum-member"
51
51
  | "type-parameter";
52
52
 
53
+ /** The reader's word for each kind of declaration, as a finding names it. */
54
+ export const WORDS: Readonly<Record<SymbolKind, string>> = {
55
+ file: "file",
56
+ function: "function",
57
+ class: "class",
58
+ interface: "interface",
59
+ type: "type alias",
60
+ enum: "enum",
61
+ namespace: "namespace",
62
+ variable: "variable",
63
+ "export-alias": "re-export",
64
+ method: "method",
65
+ "class-member": "class member",
66
+ "interface-method": "interface method",
67
+ "type-member": "type member",
68
+ "enum-member": "enum member",
69
+ "type-parameter": "type parameter",
70
+ };
71
+
53
72
  /**
54
73
  * How a member may be named from outside its container. A `private` member is still
55
74
  * reachable at run time by its name; a private name does not exist outside the class body.
@@ -4,10 +4,10 @@
4
4
  * and every dead declaration that falls with them, being dead only through them.
5
5
  */
6
6
 
7
- import type { Cascade } from "./config.ts";
7
+ import type { Cascade } from "../config.ts";
8
8
  import { OUTSIDE, type Graph } from "./graph.ts";
9
- import type { InventorySymbol } from "./inventory.ts";
10
- import { PRODUCT } from "./product.ts";
9
+ import type { InventorySymbol } from "../inventory.ts";
10
+ import { PRODUCT } from "../product.ts";
11
11
 
12
12
  /** The fewest digits a component identifier's number is written with. */
13
13
  const ID_DIGITS = 4;