@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3

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 (113) hide show
  1. package/dist/src/analysis.js +95 -60
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/component-layer.js +1 -1
  4. package/dist/src/components.js +62 -20
  5. package/dist/src/configuration-files.js +230 -30
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +104 -13
  8. package/dist/src/convention-rows.js +10 -0
  9. package/dist/src/conventions.js +35 -1
  10. package/dist/src/dependencies.js +90 -32
  11. package/dist/src/discover.js +1 -1
  12. package/dist/src/edges.js +1 -1
  13. package/dist/src/element-access.js +341 -0
  14. package/dist/src/erased-stores.js +317 -0
  15. package/dist/src/exempt-classes.js +9 -22
  16. package/dist/src/exempt.js +3 -15
  17. package/dist/src/findings/completion.js +78 -16
  18. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  19. package/dist/src/findings/emitters.js +1 -5
  20. package/dist/src/findings/self-check.js +27 -10
  21. package/dist/src/findings-pass.js +6 -5
  22. package/dist/src/in-operands.js +30 -0
  23. package/dist/src/indexed-access.js +264 -119
  24. package/dist/src/intra-function-parts.js +25 -11
  25. package/dist/src/inventory.js +28 -12
  26. package/dist/src/keyof-comparisons.js +170 -0
  27. package/dist/src/kinds.js +17 -7
  28. package/dist/src/manifest.js +6 -16
  29. package/dist/src/matrix.js +20 -2
  30. package/dist/src/query.js +3 -0
  31. package/dist/src/references.js +14 -3
  32. package/dist/src/report.js +4 -1
  33. package/dist/src/reporters/sarif.js +4 -0
  34. package/dist/src/roots.js +0 -1
  35. package/dist/src/run.js +0 -2
  36. package/dist/src/schema.js +1 -1
  37. package/dist/src/scope.js +2 -2
  38. package/dist/src/serialization-contract.js +43 -14
  39. package/dist/src/session.js +12 -8
  40. package/dist/src/suppress-file.js +58 -6
  41. package/dist/src/suppress-inline.js +14 -5
  42. package/dist/src/suppress.js +64 -35
  43. package/dist/src/unimplemented-callees.js +312 -0
  44. package/dist/src/value-reach.js +7 -7
  45. package/dist/src/verbs/analyze.js +11 -9
  46. package/dist/src/workspace-programs.js +0 -3
  47. package/dist/src/workspace-resolution.js +0 -2
  48. package/dist/src/workspace.js +0 -3
  49. package/jsr.json +1 -1
  50. package/package.json +3 -2
  51. package/src/analysis.ts +145 -82
  52. package/src/augmentations.ts +14 -11
  53. package/src/calls.ts +3 -3
  54. package/src/component-files.ts +7 -7
  55. package/src/component-layer.ts +2 -2
  56. package/src/components.ts +104 -22
  57. package/src/config.ts +4 -4
  58. package/src/configuration-files.ts +250 -31
  59. package/src/conformance-record.ts +1 -1
  60. package/src/contextual-uses.ts +135 -17
  61. package/src/convention-rows.ts +23 -0
  62. package/src/conventions.ts +53 -1
  63. package/src/dependencies.ts +119 -38
  64. package/src/discover.ts +3 -3
  65. package/src/edges.ts +3 -3
  66. package/src/element-access.ts +446 -0
  67. package/src/entry-points.ts +1 -1
  68. package/src/erased-stores.ts +413 -0
  69. package/src/exempt-classes.ts +10 -24
  70. package/src/exempt.ts +5 -29
  71. package/src/file-facts.ts +3 -1
  72. package/src/finding.ts +3 -1
  73. package/src/findings/completion.ts +97 -21
  74. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  75. package/src/findings/emitters.ts +4 -9
  76. package/src/findings/self-check.ts +31 -14
  77. package/src/findings-pass.ts +14 -7
  78. package/src/graph.ts +2 -2
  79. package/src/html-entries.ts +1 -1
  80. package/src/in-operands.ts +45 -0
  81. package/src/indexed-access.ts +303 -137
  82. package/src/intra-function-parts.ts +41 -16
  83. package/src/inventory.ts +37 -33
  84. package/src/json-document.ts +1 -1
  85. package/src/keyof-comparisons.ts +211 -0
  86. package/src/kinds.ts +20 -7
  87. package/src/manifest.ts +7 -22
  88. package/src/matrix.ts +34 -2
  89. package/src/namespace-objects.ts +1 -1
  90. package/src/pnpm-workspace.ts +1 -1
  91. package/src/query.ts +10 -0
  92. package/src/references.ts +30 -7
  93. package/src/report.ts +11 -5
  94. package/src/reporters/sarif.ts +4 -0
  95. package/src/roots.ts +0 -3
  96. package/src/run.ts +0 -2
  97. package/src/schema.ts +2 -2
  98. package/src/scope.ts +2 -2
  99. package/src/serialization-contract.ts +53 -14
  100. package/src/session.ts +34 -31
  101. package/src/stores.ts +1 -1
  102. package/src/suppress-file.ts +68 -5
  103. package/src/suppress-inline.ts +17 -7
  104. package/src/suppress.ts +79 -47
  105. package/src/template-field.ts +1 -1
  106. package/src/unimplemented-callees.ts +411 -0
  107. package/src/value-reach.ts +12 -8
  108. package/src/verbs/analyze.ts +15 -9
  109. package/src/verbs/verb.ts +1 -4
  110. package/src/workspace-programs.ts +2 -9
  111. package/src/workspace-resolution.ts +0 -3
  112. package/src/workspace-run.ts +1 -1
  113. package/src/workspace.ts +1 -9
@@ -9,6 +9,7 @@ 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
11
  import { OUTSIDE, type Graph } from "../graph.ts";
12
+ import type { SymbolKind } from "../inventory.ts";
12
13
  import { KINDS, type KindRow } from "../kinds.ts";
13
14
  import { byPosition, positionKey } from "../position.ts";
14
15
  import { declarationKey } from "../ref.ts";
@@ -17,6 +18,17 @@ import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
17
18
  import type { Boundary } from "./boundary.ts";
18
19
  import type { EmitterInput } from "./emitter.ts";
19
20
 
21
+ /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
22
+ const MANUAL = "manual";
23
+
24
+ /** The member kinds an object property can declare, which an `in` expression tests by name. */
25
+ const IN_TESTED: ReadonlySet<SymbolKind> = new Set([
26
+ "class-member",
27
+ "method",
28
+ "type-member",
29
+ "interface-method",
30
+ ]);
31
+
20
32
  /** The classes ranked by the claim each makes, the strongest highest. */
21
33
  const RANK: Readonly<Record<Confidence, number>> = { certain: 3, probable: 2, possible: 1 };
22
34
 
@@ -54,19 +66,43 @@ function publishedIds(union: Graph): ReadonlySet<string> {
54
66
  );
55
67
  }
56
68
 
69
+ /**
70
+ * The members whose name an `in` expression of a production file tests by a literal: a
71
+ * value narrowed that way is read back by the name, which no type links to the member.
72
+ */
73
+ function testedByIn(input: EmitterInput): ReadonlySet<string> {
74
+ const names = input.files.inOperands ?? new Set<string>();
75
+ const union = input.swept.matrix.union;
76
+ if (names.size === 0) {
77
+ return names;
78
+ }
79
+ return new Set(
80
+ union.symbols.flatMap((symbol, at) => {
81
+ const parent = union.symbols[union.parent[at] ?? OUTSIDE];
82
+ return IN_TESTED.has(symbol.kind) &&
83
+ parent !== undefined &&
84
+ symbol.name.startsWith(`${parent.name}.`) &&
85
+ names.has(symbol.name.slice(parent.name.length + 1))
86
+ ? [symbol.id]
87
+ : [];
88
+ }),
89
+ );
90
+ }
91
+
57
92
  /**
58
93
  * The reachability class of one declaration of the run. A declaration of a library's
59
94
  * published API, a public member of a published type included, is `certain` where the
60
95
  * run loaded every consumer the scope declared and declared one, `probable` where one did
61
96
  * not load, and `possible` where it declared none, whatever the configuration says of the
62
- * consumer set. A declaration of test-support code that test code references, outside the
63
- * test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
97
+ * consumer set. Test-support code test code references, outside the test-of-dead-code
98
+ * rule, what it contains, and a member an `in` expression tests by name are `possible`.
64
99
  */
65
100
  function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
101
+ const tested = testedByIn(input);
66
102
  const published = publishedIds(input.swept.matrix.union);
67
103
  const usedByTests = new Set(
68
104
  input.swept.sweep.candidates
69
- .filter((candidate) => candidate.heldByTests === candidate.id)
105
+ .filter((candidate) => candidate.heldByTests !== undefined)
70
106
  .map((candidate) => candidate.id),
71
107
  );
72
108
  const { declared } = input.boundary.consumers;
@@ -77,13 +113,42 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
77
113
  ? "certain"
78
114
  : "probable";
79
115
  return (id) => {
80
- if (usedByTests.has(id)) {
116
+ if (usedByTests.has(id) || tested.has(id)) {
81
117
  return "possible";
82
118
  }
83
119
  return published.has(id) ? outside : "certain";
84
120
  };
85
121
  }
86
122
 
123
+ /**
124
+ * The confidence ceiling of a test of dead code: the lowest reachability class among the
125
+ * target symbols it references, because its deadness is theirs, whether or not they share
126
+ * its component. Every other declaration has none below `certain`.
127
+ */
128
+ function testCeilings(
129
+ input: EmitterInput,
130
+ classOf: (id: string) => Confidence,
131
+ ): (id: string) => Confidence {
132
+ const union = input.swept.matrix.union;
133
+ const tests = new Set(
134
+ input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
135
+ );
136
+ return (id) => {
137
+ let lowest: Confidence = "certain";
138
+ if (!tests.has(id)) {
139
+ return lowest;
140
+ }
141
+ for (const edge of union.out[union.at(id)] ?? []) {
142
+ const target = union.symbols[edge.to];
143
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
144
+ if (target !== undefined && !testCode && union.subject[edge.to] === true) {
145
+ lowest = capped(lowest, classOf(target.id));
146
+ }
147
+ }
148
+ return lowest;
149
+ };
150
+ }
151
+
87
152
  /**
88
153
  * The component each dead declaration of the run falls in, and none for a live one, with
89
154
  * its members listed in the canonical order where the run lists a component in full.
@@ -154,7 +219,11 @@ export function completed(
154
219
  );
155
220
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
156
221
  const classOf = reachabilityClasses(input);
222
+ const ceilingOf = testCeilings(input, classOf);
157
223
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
224
+ const held = new Set(
225
+ swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members),
226
+ );
158
227
  let minted = swept.sweep.components.length;
159
228
  const dead = new Set(swept.sweep.components.map((component) => component.id));
160
229
  const fallingWith = new Map<string, number>();
@@ -212,6 +281,18 @@ export function completed(
212
281
  const reachabilityClass =
213
282
  judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
214
283
  const generated = input.files.generated?.has(finding.position.path) === true;
284
+ const fixability = generated
285
+ ? "none"
286
+ : found !== undefined &&
287
+ symbol !== undefined &&
288
+ held.has(symbol.id) &&
289
+ row.findingFixabilities?.includes(MANUAL) === true
290
+ ? MANUAL
291
+ : row.fixability;
292
+ const lastUses =
293
+ fixability === "deletable" && candidate !== undefined
294
+ ? input.dependencies.lastUses.get(candidate.id)
295
+ : undefined;
215
296
  return {
216
297
  code: finding.code,
217
298
  kind: row.name,
@@ -219,7 +300,10 @@ export function completed(
219
300
  position: finding.position,
220
301
  symbol: finding.symbol,
221
302
  reachabilityClass,
222
- confidence: capped(reachabilityClass, row.maxClass),
303
+ confidence: capped(
304
+ capped(reachabilityClass, row.maxClass),
305
+ judged === undefined ? "certain" : ceilingOf(judged.id),
306
+ ),
223
307
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
224
308
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
225
309
  generated,
@@ -231,10 +315,13 @@ export function completed(
231
315
  ? swept.matrix.configurations
232
316
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
233
317
  consumersLoaded: input.boundary.consumers.loaded,
234
- fixability: generated ? "none" : row.fixability,
318
+ fixability,
235
319
  severity: severityOf(input, finding.code, row.defaultSeverity),
236
320
  message: finding.message,
237
- details: finding.details ?? {},
321
+ details:
322
+ lastUses === undefined
323
+ ? (finding.details ?? {})
324
+ : { ...finding.details, removesLastUseOf: lastUses },
238
325
  };
239
326
  });
240
327
  return componentCapped(done, dead);
@@ -278,33 +365,22 @@ function dialed(
278
365
  * below the minimum confidence is withheld, and so is every finding of the component of a
279
366
  * root the dials withhold, because a symbol that falls with a root is dead only through it
280
367
  * and its deletion does not compile while the root stays. A withheld finding that is no
281
- * root withholds itself alone. A code is withheld from a declaration when the finding it
282
- * would report there, completed, is withheld by its own severity or confidence.
368
+ * root withholds itself alone.
283
369
  */
284
370
  export function dialsOf(
285
371
  input: EmitterInput,
286
372
  findings: readonly CompletedFinding[],
287
373
  ): Dials<CompletedFinding> {
288
374
  const minConfidence = input.config.analysis.minConfidence;
289
- const classOf = reachabilityClasses(input);
290
375
  const components = new Set(
291
376
  findings
292
377
  .filter((finding) => finding.component.root && dialed(finding, minConfidence))
293
378
  .map((finding) => finding.component.id),
294
379
  );
295
380
  return {
296
- withholdsCode: (code, id) => {
381
+ allows: (code) => {
297
382
  const row = KINDS.get(code);
298
- return (
299
- row !== undefined &&
300
- dialed(
301
- {
302
- severity: severityOf(input, code, row.defaultSeverity),
303
- confidence: capped(classOf(id), row.maxClass),
304
- },
305
- minConfidence,
306
- )
307
- );
383
+ return row !== undefined && severityOf(input, code, row.defaultSeverity) === "allow";
308
384
  },
309
385
  withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
310
386
  };
@@ -19,7 +19,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
19
19
  return dependencies.declared
20
20
  .filter(
21
21
  (dependency) =>
22
- !dependencies.needed.has(dependency.name) &&
22
+ !dependencies.needed.has(declarationKey(dependency)) &&
23
23
  !dependencies.ran.has(declarationKey(dependency)),
24
24
  )
25
25
  .map((dependency) => ({
@@ -67,7 +67,7 @@ function markupKeys(input: EmitterInput): ReadonlySet<string> {
67
67
  }
68
68
 
69
69
  /** What the suppressions of a run swept under their marks are decided against. */
70
- export interface Suppressed {
70
+ interface Suppressed {
71
71
  /** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
72
72
  readonly unmarked: EmitterInput;
73
73
  /** The run's suppression records, in reading order. */
@@ -75,13 +75,13 @@ export interface Suppressed {
75
75
  }
76
76
 
77
77
  /** What the emitter table answers about one run. */
78
- export interface DecidedFindings {
78
+ interface DecidedFindings {
79
79
  /** Every finding the run reports, each family's in table order. */
80
80
  readonly findings: readonly CompletedFinding[];
81
81
  /** One evaluation per declared edge side of this language, ordered by edge, then by side. */
82
82
  readonly evaluations: readonly EdgeEvaluation<CompletedFinding>[];
83
83
  /** What every suppression record did, in reading order. */
84
- readonly ledger: Ledger<CompletedFinding>;
84
+ readonly ledger: Ledger;
85
85
  /** Every finding a record could name: the run's with no mark, then the run's as swept. */
86
86
  readonly would: readonly CompletedFinding[];
87
87
  /** The findings the minimum confidence withheld, by confidence. */
@@ -116,7 +116,7 @@ function dialsOver(
116
116
  const before = dialsOf(suppressed.unmarked, unmarked);
117
117
  const fromUnmarked = new Set(unmarked);
118
118
  return {
119
- withholdsCode: marked.withholdsCode,
119
+ allows: marked.allows,
120
120
  withholds: (finding) =>
121
121
  fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
122
122
  };
@@ -198,8 +198,3 @@ function withheldOf(input: EmitterInput, candidates: readonly CompletedFinding[]
198
198
  }
199
199
  return counts;
200
200
  }
201
-
202
- /** Every finding the run reports, each family's in table order. */
203
- export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
204
- return decidedFindings(input).findings;
205
- }
@@ -1,8 +1,11 @@
1
1
  import { REPOSITORY_DOCUMENT } from "../config.ts";
2
2
  import type { Finding, FindingDetails, SuppressionEntry } from "../finding.ts";
3
3
  import type { InventorySymbol } from "../inventory.ts";
4
+ import { FIXED_SEVERITY_CODES } from "../kinds.ts";
4
5
  import type { Position } from "../position.ts";
5
6
  import {
7
+ isDependencyCode,
8
+ isUnsuppressibleCode,
6
9
  STALE_SUPPRESSION,
7
10
  SUPPRESSION_WITHOUT_REASON,
8
11
  type Mechanism,
@@ -58,14 +61,6 @@ interface UnmatchedDeclaration {
58
61
  readonly document: string;
59
62
  }
60
63
 
61
- /** A run with nothing for the self-check family to report. */
62
- export const NO_SELF_CHECK: SelfCheckFacts = {
63
- refusals: [],
64
- unmatchedRoots: [],
65
- rootsDocument: "",
66
- unmatchedDeclarations: [],
67
- };
68
-
69
64
  /** The words a message names a mechanism by. */
70
65
  const WRITTEN: Readonly<Record<Mechanism, string>> = {
71
66
  inline: "inline directive",
@@ -122,7 +117,7 @@ function writtenEntry(refused: Refusal): SuppressionEntry {
122
117
  }
123
118
 
124
119
  /** Each file of the run by its path, as the reference of the file it is. */
125
- export type FileRefs = (path: string) => string | undefined;
120
+ type FileRefs = (path: string) => string | undefined;
126
121
 
127
122
  /**
128
123
  * The reference a finding about a suppression names: the symbol it names or bound, else
@@ -133,10 +128,7 @@ function suppressionRef(symbol: string, path: string, files: FileRefs): string {
133
128
  }
134
129
 
135
130
  /** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
136
- export function refusedSuppressions(
137
- refusals: readonly Refusal[],
138
- files: FileRefs,
139
- ): readonly Finding[] {
131
+ function refusedSuppressions(refusals: readonly Refusal[], files: FileRefs): readonly Finding[] {
140
132
  return refusals.map((refused) =>
141
133
  aboutSuppression(
142
134
  refused.reported,
@@ -190,7 +182,32 @@ export function staleSuppressions(
190
182
  const reports = [...new Set(records.flatMap((one) => one.reports))]
191
183
  .filter((code) => !codes.includes(code))
192
184
  .sort();
193
- const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
185
+ const rows = codes.filter(isUnsuppressibleCode);
186
+ const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
187
+ const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
188
+ const remedy =
189
+ settable.length === 0
190
+ ? "fix what the finding names instead"
191
+ : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
192
+ const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
193
+ const because = [
194
+ ...(rows.length > 0
195
+ ? [
196
+ `${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}`,
197
+ ]
198
+ : []),
199
+ ...(dependencies.length > 0
200
+ ? [
201
+ `${spelled(dependencies)} ${dependencies.length === 1 ? "reports a dependency row" : "report dependency rows"}, which only an ignore entry or a baseline row can name`,
202
+ ]
203
+ : []),
204
+ ];
205
+ const instead =
206
+ because.length > 0
207
+ ? `, because ${because.join("; ")}${declared === "" ? "" : `; ${declared}`}`
208
+ : declared === ""
209
+ ? ""
210
+ : `, and ${declared}`;
194
211
  return {
195
212
  code: STALE_SUPPRESSION,
196
213
  mechanism: first.mechanism,
@@ -15,12 +15,18 @@ import {
15
15
  type StaleRecord,
16
16
  type StaleSuppression,
17
17
  } from "./findings/self-check.ts";
18
- import { isRowSubject, pairs, suppressionCounts, type Ledger } from "./suppress.ts";
18
+ import {
19
+ isDependencySubject,
20
+ isRowSubject,
21
+ pairs,
22
+ suppressionCounts,
23
+ type Ledger,
24
+ } from "./suppress.ts";
19
25
  import type { Provenance, Recorded } from "./suppress-file.ts";
20
26
  import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
21
27
 
22
28
  /** The counts a report's totals carry beside its findings. */
23
- export interface PassTotals {
29
+ interface PassTotals {
24
30
  readonly suppressionsInEffect: number;
25
31
  readonly reasonsRecorded: number;
26
32
  readonly staleSuppressions: number;
@@ -40,7 +46,7 @@ export interface PassResult {
40
46
  readonly edgeEvaluations: readonly EdgeEvaluation<CompletedFinding>[];
41
47
  readonly totals: PassTotals;
42
48
  /** What every suppression record did, in reading order. */
43
- readonly ledger: Ledger<CompletedFinding>;
49
+ readonly ledger: Ledger;
44
50
  }
45
51
 
46
52
  /** The exit code of a report holding at least one pending finding. */
@@ -134,14 +140,15 @@ export interface RecordableFinding {
134
140
 
135
141
  /**
136
142
  * The rows one round of a baseline write records from its findings, in their order. A
137
- * finding about a row of a document is not recorded: no record withholds one, so a row
138
- * recording it would be stale on the first read back. Two findings that would record the
139
- * same row, two parts of one declaration under one code, record it once.
143
+ * finding about a row of a document other than a dependency row is not recorded: no record
144
+ * withholds one, so a row recording it would be stale on the first read back. Two findings
145
+ * that would record the same row, two parts of one declaration under one code, record it
146
+ * once.
140
147
  */
141
148
  export function recordedFindings(findings: readonly RecordableFinding[]): readonly Recorded[] {
142
149
  const rows = new Map<string, Recorded>();
143
150
  for (const finding of findings) {
144
- if (isRowSubject(finding.symbol.kind)) {
151
+ if (isRowSubject(finding.symbol.kind) && !isDependencySubject(finding.symbol.kind)) {
145
152
  continue;
146
153
  }
147
154
  const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
package/src/graph.ts CHANGED
@@ -87,13 +87,13 @@ export interface Edge {
87
87
  }
88
88
 
89
89
  /** How many references one declaration carries, split by the classification of the file that made each. */
90
- export interface Counts {
90
+ interface Counts {
91
91
  readonly production: number;
92
92
  readonly test: number;
93
93
  }
94
94
 
95
95
  /** One root the inventory holds, at the position of the declaration it names. */
96
- export interface Rooted {
96
+ interface Rooted {
97
97
  readonly at: number;
98
98
  readonly kind: RootKind;
99
99
  }
@@ -16,7 +16,7 @@ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
16
16
  import type { SourceFiles } from "./source-files.ts";
17
17
 
18
18
  /** One file a script loads, and the value or specifier that named it. */
19
- export interface HTMLEntry {
19
+ interface HTMLEntry {
20
20
  readonly file: SourceFile;
21
21
  readonly source: string;
22
22
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The property names `in` tests in a project's production files. A value narrowed by
3
+ * `"name" in value` is read back by that name, often through `unknown`, where no type
4
+ * links the read to a declaration, so only a text match can tell which member it reads.
5
+ */
6
+
7
+ import {
8
+ isBinaryExpression,
9
+ isNoSubstitutionTemplateLiteral,
10
+ isStringLiteral,
11
+ SyntaxKind,
12
+ type Node,
13
+ } from "@typescript/native/unstable/ast";
14
+ import type { ProjectView } from "./session.ts";
15
+ import { sourceFilesOf } from "./source-files.ts";
16
+
17
+ /**
18
+ * The text of each string literal, or template literal with no substitution, written as
19
+ * the left operand of an `in` expression in a file of the project that is no test file.
20
+ * `testFiles` holds the test files by their path below the target root.
21
+ */
22
+ export function inOperandNames<Brand>(
23
+ project: ProjectView<Brand>,
24
+ targetRoot: string,
25
+ testFiles: readonly string[],
26
+ ): readonly string[] {
27
+ const tests = new Set(testFiles);
28
+ const found = new Set<string>();
29
+ const visit = (node: Node): void => {
30
+ if (
31
+ isBinaryExpression(node) &&
32
+ node.operatorToken.kind === SyntaxKind.InKeyword &&
33
+ (isStringLiteral(node.left) || isNoSubstitutionTemplateLiteral(node.left))
34
+ ) {
35
+ found.add(node.left.text);
36
+ }
37
+ node.forEachChild(visit);
38
+ };
39
+ for (const [path, file] of sourceFilesOf(project, targetRoot).byPath) {
40
+ if (!tests.has(path)) {
41
+ file.forEachChild(visit);
42
+ }
43
+ }
44
+ return [...found].sort();
45
+ }