@cplieger/deadset-ts 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. package/src/version.ts +1 -1
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
87
87
  }
88
88
  return at + 1;
89
89
  }
90
- /** The identifiers one action's inner text writes outside a quoted string, with their offsets in it. */
91
- function identifiersOf(inner) {
90
+ /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
91
+ function substitutionEnd(text, from) {
92
+ let depth = 1;
93
+ let at = from;
94
+ while (at < text.length) {
95
+ const char = text[at];
96
+ if (char === '"' || char === "'") {
97
+ at = stringEnd(text, at);
98
+ continue;
99
+ }
100
+ if (char === "`") {
101
+ at = templateEnd(text, at, () => undefined);
102
+ continue;
103
+ }
104
+ if (char === "{") {
105
+ depth += 1;
106
+ }
107
+ else if (char === "}") {
108
+ depth -= 1;
109
+ if (depth === 0) {
110
+ return at + 1;
111
+ }
112
+ }
113
+ at += 1;
114
+ }
115
+ return at;
116
+ }
117
+ /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
118
+ function templateEnd(text, from, substitution) {
119
+ let at = from + 1;
120
+ while (at < text.length && text[at] !== "`") {
121
+ if (text[at] === "\\") {
122
+ at += 2;
123
+ continue;
124
+ }
125
+ if (text[at] === "$" && text[at + 1] === "{") {
126
+ const start = at + 2;
127
+ const end = substitutionEnd(text, start);
128
+ substitution(text.slice(start, end - 1), start);
129
+ at = end;
130
+ continue;
131
+ }
132
+ at += 1;
133
+ }
134
+ return at + 1;
135
+ }
136
+ /**
137
+ * The identifiers one action's inner text writes outside a quoted string, with their offsets
138
+ * in it. In `script` text, a component's markup, a template literal's substitutions are code.
139
+ */
140
+ function identifiersOf(inner, script = false) {
92
141
  const found = [];
93
142
  let at = 0;
94
143
  while (at < inner.length) {
95
144
  const char = inner[at] ?? "";
145
+ if (char === "`" && script) {
146
+ at = templateEnd(inner, at, (expression, offset) => {
147
+ for (const one of identifiersOf(expression, true)) {
148
+ found.push({ name: one.name, offset: offset + one.offset });
149
+ }
150
+ });
151
+ continue;
152
+ }
96
153
  if (char === '"' || char === "'" || char === "`") {
97
154
  at = stringEnd(inner, at);
98
155
  continue;
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
142
199
  }
143
200
  /** Every identifier the markup of one component file writes in an action. */
144
201
  function markupNamesOf(text) {
145
- return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
202
+ return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
146
203
  name,
147
204
  offset: one.start + offset,
148
205
  action: one.action,
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The values a program hands to a position the checker types from its context: the
3
+ * initializer of a declaration whose type is written, an assignment's right side, an
4
+ * argument, a returned value, an arrow function's expression body, an array element, an
5
+ * object property, and the operand of `as` or `satisfies`.
6
+ */
7
+ import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isIdentifier, isNewExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ /** The operators of an assignment whose right side is stored into the left side's type. */
9
+ const STORES = new Set([
10
+ SyntaxKind.EqualsToken,
11
+ SyntaxKind.QuestionQuestionEqualsToken,
12
+ SyntaxKind.BarBarEqualsToken,
13
+ SyntaxKind.AmpersandAmpersandEqualsToken,
14
+ ]);
15
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
16
+ function writesType(node) {
17
+ return node.type !== undefined;
18
+ }
19
+ /**
20
+ * The values one written expression hands on: the expression itself, or what it
21
+ * evaluates to through parentheses, a conditional and the operators whose result is
22
+ * one of their operands.
23
+ */
24
+ export function valuesOf(expression) {
25
+ if (isParenthesizedExpression(expression)) {
26
+ return valuesOf(expression.expression);
27
+ }
28
+ if (isConditionalExpression(expression)) {
29
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
30
+ }
31
+ if (isBinaryExpression(expression)) {
32
+ const operator = expression.operatorToken.kind;
33
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
34
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
35
+ }
36
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
37
+ return valuesOf(expression.right);
38
+ }
39
+ }
40
+ return [expression];
41
+ }
42
+ /** The expressions one node hands to a position typed by its context. */
43
+ export function handedOn(node) {
44
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
45
+ node.initializer !== undefined &&
46
+ writesType(node)) {
47
+ return [node.initializer];
48
+ }
49
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
50
+ return [node.right];
51
+ }
52
+ if (isCallExpression(node) || isNewExpression(node)) {
53
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
54
+ }
55
+ if (isReturnStatement(node)) {
56
+ return node.expression === undefined ? [] : [node.expression];
57
+ }
58
+ if (isArrowFunction(node)) {
59
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
60
+ }
61
+ if (isArrayLiteralExpression(node)) {
62
+ return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
63
+ }
64
+ if (isPropertyAssignment(node)) {
65
+ return [node.initializer];
66
+ }
67
+ if (isShorthandPropertyAssignment(node)) {
68
+ return isIdentifier(node.name) ? [node.name] : [];
69
+ }
70
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
71
+ return [node.expression];
72
+ }
73
+ return [];
74
+ }
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "5.1.0";
7
+ export const CONTRACT_VERSION = "5.2.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@eslint/js": "10.0.1",
36
- "@types/node": "24.19.0",
36
+ "@types/node": "24.19.1",
37
37
  "eslint": "10.11.0",
38
38
  "fast-check": "4.10.2",
39
39
  "prettier": "3.9.9",
package/src/analysis.ts CHANGED
@@ -36,14 +36,18 @@ import {
36
36
  } from "./discover.ts";
37
37
  import {
38
38
  conventionFailuresFor,
39
+ generatedFiles,
39
40
  readConventions,
40
41
  type AppliedConvention,
41
42
  type ConventionManifest,
42
43
  } from "./conventions.ts";
43
44
  import { readEdgeSides } from "./edges.ts";
44
45
  import { enumGroup } from "./enum-group.ts";
46
+ import { generatedFile } from "./generated-file.ts";
47
+ import { sourceFilesOf } from "./source-files.ts";
45
48
  import {
46
- computeExemptions,
49
+ evidenceRecords,
50
+ heldRecords,
47
51
  disabledClasses,
48
52
  exemptionsOf,
49
53
  holdingWhile,
@@ -53,7 +57,8 @@ import {
53
57
  type Detectors,
54
58
  } from "./exempt.ts";
55
59
  import type { TSExemptionClass } from "./exempt-classes.ts";
56
- import { heldByInclusion, sourceTree } from "./file-facts.ts";
60
+ import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.ts";
61
+ import { globExpression } from "./glob.ts";
57
62
  import type { Finding } from "./finding.ts";
58
63
  import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
59
64
  import type { EmitterInput } from "./findings/emitter.ts";
@@ -69,7 +74,7 @@ import { injectionContainer } from "./injection-container.ts";
69
74
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
70
75
  import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
71
76
  import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
72
- import { readManifest } from "./manifest.ts";
77
+ import { packageEntries, readManifest } from "./manifest.ts";
73
78
  import {
74
79
  liveFilesAt,
75
80
  matrixOf,
@@ -79,7 +84,7 @@ import {
79
84
  type Matrix,
80
85
  type SweepResult,
81
86
  } from "./matrix.ts";
82
- import { joinPath, relativePath, resolvePath } from "./paths.ts";
87
+ import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
83
88
  import type { UnansweredQuestion } from "./query.ts";
84
89
  import { byPosition, isComponentFile, type Position } from "./position.ts";
85
90
  import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
@@ -96,6 +101,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
96
101
  import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
97
102
  import type { Scope } from "./scope.ts";
98
103
  import { serializationContract } from "./serialization-contract.ts";
104
+ import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
99
105
  import { supportReferences, testSupportFiles } from "./test-support.ts";
100
106
  import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
101
107
  import { accessorsOf, storesOf } from "./stores.ts";
@@ -185,6 +191,8 @@ interface ProjectRead {
185
191
  readonly typeErrorSkips: readonly TypeErrorSkip[];
186
192
  /** The lines each of those skips withholds every finding on. */
187
193
  readonly skipped: readonly SkippedUnit[];
194
+ /** The project's generated files, by path below the target root, each with the row naming it. */
195
+ readonly generated: ReadonlyMap<string, string>;
188
196
  }
189
197
 
190
198
  /** One target project the run analyzes, as its stage is handed it. */
@@ -214,6 +222,8 @@ interface ReadProjects<Answer> {
214
222
  readonly componentsUnread: readonly ComponentsUnread[];
215
223
  /** The convention rows the run applied. */
216
224
  readonly conventionsApplied: readonly AppliedConvention[];
225
+ /** The test files below the target root no project the run reads holds, parsed. */
226
+ readonly outsideTests: readonly OutsideTest[];
217
227
  }
218
228
 
219
229
  /**
@@ -279,11 +289,20 @@ function readProjects<Answer>(
279
289
  const options = {
280
290
  host,
281
291
  manifest: workspace?.manifest ?? readManifest(host, targetRoot),
282
- workspaceEntries: workspace?.entries,
292
+ otherPackages: [
293
+ ...(workspace?.entries ?? []),
294
+ ...packageEntries(
295
+ host,
296
+ normalizePath(absoluteRoot),
297
+ discovered.configFiles,
298
+ new Set(workspace?.memberDirs ?? []),
299
+ ),
300
+ ],
283
301
  patterns: config.rootPatterns,
284
302
  entryFiles: config.ts.entryFiles,
285
303
  conventions,
286
304
  testFiles: config.ts.testFiles,
305
+ parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
287
306
  publishedAPI: config.targetKind === "library",
288
307
  };
289
308
  const notBuilt: NotBuilt[] = [...discovered.notBuilt];
@@ -298,6 +317,8 @@ function readProjects<Answer>(
298
317
  const unbuildable: SetupFailure[] = [];
299
318
  const setupDropped: SetupFailure[] = [];
300
319
  const consumed = new Map<string, Reference>();
320
+ const heldFiles = new Set<string>();
321
+ let outsideTests: readonly OutsideTest[] = [];
301
322
  const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
302
323
  const [first] = met;
303
324
  if (first !== undefined && isGuess(discovered, configFile)) {
@@ -425,12 +446,27 @@ function readProjects<Answer>(
425
446
  .ownSourceFiles()
426
447
  .filter((file) => referenceOnly(file.fileName))
427
448
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
449
+ generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
428
450
  };
429
451
  };
452
+ for (const file of project.ownSourceFiles()) {
453
+ heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
454
+ }
430
455
  return stage(project, { configuration, read });
431
456
  },
432
457
  discovered.derived ? "record" : "refuse",
433
458
  workspace?.programs,
459
+ () => {
460
+ const patterns = config.ts.testFiles.map(globExpression);
461
+ outsideTests = testFilesAt(
462
+ host,
463
+ targetRoot,
464
+ sourcePaths(host, targetRoot).filter(
465
+ (path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path)),
466
+ ),
467
+ options.parse,
468
+ );
469
+ },
434
470
  );
435
471
  for (const configFile of session.unopened) {
436
472
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -468,6 +504,7 @@ function readProjects<Answer>(
468
504
  unanswered: session.unanswered,
469
505
  componentsUnread: workspace?.componentsUnread ?? [],
470
506
  conventionsApplied: conventions.applied,
507
+ outsideTests,
471
508
  };
472
509
  }
473
510
 
@@ -599,6 +636,7 @@ export interface ComponentWarning {
599
636
  const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
600
637
  ["interface-satisfaction", interfaceSatisfaction],
601
638
  ["enum-group", enumGroup],
639
+ ["generated-file", generatedFile],
602
640
  ["template-field", templateField],
603
641
  ["reflective-lookup", reflectiveLookup],
604
642
  ["decorator", decorator],
@@ -622,6 +660,17 @@ interface SweptProject<Extra> {
622
660
  readonly extra: Extra;
623
661
  }
624
662
 
663
+ /** What one project's stage reads while its view is open, before the run's test-support code is known. */
664
+ interface ReadProject<Extra> {
665
+ readonly projectRead: ProjectRead;
666
+ readonly references: readonly Reference[];
667
+ readonly testFiles: readonly string[];
668
+ readonly evidence: readonly Exemption[];
669
+ readonly componentFiles: readonly string[];
670
+ readonly componentWarnings: readonly ComponentWarning[];
671
+ readonly extra: Extra;
672
+ }
673
+
625
674
  /** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
626
675
  interface ReadRun<Extra> {
627
676
  readonly matrix: Matrix;
@@ -666,56 +715,34 @@ function readRun<Extra>(
666
715
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
667
716
  : readTemplates(host, targetRoot, config.analysis);
668
717
  const consumers = scope.consumers.map((consumer) => consumer.path);
669
- const read = readProjects<SweptProject<Extra>>(engine, host, scope, config, (project, opened) => {
718
+ const read = readProjects<ReadProject<Extra>>(engine, host, scope, config, (project, opened) => {
670
719
  const projectRead = opened.read();
671
720
  const resolved = references(project, projectRead.held, targetRoot, {
672
721
  testFiles: config.ts.testFiles,
673
722
  });
674
- const support = testSupportFiles(
675
- projectRead.held.symbols,
676
- resolved.references,
677
- projectRead.rooted.liveUnderReachability,
678
- resolved.testFilePaths,
679
- );
680
- const made = supportReferences(resolved.references, support);
681
- const configured: Configured = {
682
- configuration: projectRead.configuration,
683
- symbols: projectRead.held.symbols,
684
- references: made,
685
- roots: projectRead.rooted.liveUnderReachability,
686
- testFiles: resolved.testFilePaths,
687
- ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
688
- ...(projectRead.referenceOnly.length === 0
689
- ? {}
690
- : { referenceOnly: projectRead.referenceOnly }),
691
- };
692
- const exempt = computeExemptions(
693
- { project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
694
- detectors,
723
+ const evidence = evidenceRecords(
695
724
  {
696
- disabled,
697
- mode,
698
- testFiles: new Set([...resolved.testFilePaths, ...support]),
725
+ project,
726
+ held: projectRead.held,
727
+ targetRoot,
728
+ templates,
729
+ ts: config.ts,
730
+ consumers,
731
+ ...(config.analysis.generatedFiles === "include"
732
+ ? {}
733
+ : { generated: projectRead.generated }),
699
734
  },
735
+ detectors,
736
+ disabled,
700
737
  );
701
- const unanswered = [
702
- ...projectRead.held.unanswered,
703
- ...projectRead.rooted.unanswered,
704
- ...made
705
- .filter((reference) => reference.resolution === "by-name")
706
- .map((reference) => reference.to),
707
- ...exempt.filter(isUnansweredRecord).map((record) => record.id),
708
- ];
709
738
  const components = project.ownSourceFiles().filter(isComponentFile);
710
739
  const pathOf = (file: (typeof components)[number]): string =>
711
740
  relativePath(targetRoot, file.fileName) ?? file.fileName;
712
741
  return {
713
- configFile: projectRead.configFile,
714
- configured,
715
- exempt,
716
- unanswered,
717
- typeErrorSkips: projectRead.typeErrorSkips,
718
- skipped: projectRead.skipped,
742
+ projectRead,
743
+ references: resolved.references,
744
+ testFiles: resolved.testFilePaths,
745
+ evidence,
719
746
  componentFiles: components.map(pathOf),
720
747
  componentWarnings: components.flatMap((file) =>
721
748
  readComponent(file.originalText).warnings.map((warning) => ({
@@ -724,11 +751,69 @@ function readRun<Extra>(
724
751
  reason: warning.reason,
725
752
  })),
726
753
  ),
727
- extra: extra(project, projectRead, made),
754
+ extra: extra(project, projectRead, resolved.references),
755
+ };
756
+ });
757
+ const outsidePaths = read.outsideTests.map((one) => one.path);
758
+ const withOutside = read.answers.map((one) => ({
759
+ ...one,
760
+ references: [
761
+ ...one.references,
762
+ ...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
763
+ ],
764
+ testFiles: [...one.testFiles, ...outsidePaths],
765
+ }));
766
+ // Test-support code is code only test code imports in any configuration of the run: a
767
+ // configuration that holds no test file says nothing about what the tests import.
768
+ const support = testSupportFiles(
769
+ withOutside.flatMap((one) => one.projectRead.held.symbols),
770
+ withOutside.flatMap((one) => one.references),
771
+ withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
772
+ withOutside.flatMap((one) => one.testFiles),
773
+ );
774
+ const answers = withOutside.map((one): SweptProject<Extra> => {
775
+ const { projectRead } = one;
776
+ const detected = heldRecords(one.evidence, {
777
+ disabled,
778
+ mode,
779
+ testFiles: new Set([...one.testFiles, ...support]),
780
+ });
781
+ const exempt = detected.records;
782
+ const made = supportReferences(one.references, support);
783
+ const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
784
+ const supportFiles = [...support].filter((path) => held.has(path)).sort();
785
+ const configured: Configured = {
786
+ configuration: projectRead.configuration,
787
+ symbols: projectRead.held.symbols,
788
+ references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
789
+ roots: projectRead.rooted.liveUnderReachability,
790
+ testFiles: one.testFiles,
791
+ ...(supportFiles.length === 0 ? {} : { supportFiles }),
792
+ ...(projectRead.referenceOnly.length === 0
793
+ ? {}
794
+ : { referenceOnly: projectRead.referenceOnly }),
795
+ };
796
+ return {
797
+ configFile: projectRead.configFile,
798
+ configured,
799
+ exempt,
800
+ unanswered: [
801
+ ...projectRead.held.unanswered,
802
+ ...projectRead.rooted.unanswered,
803
+ ...made
804
+ .filter((reference) => reference.resolution === "by-name")
805
+ .map((reference) => reference.to),
806
+ ...exempt.filter(isUnansweredRecord).map((record) => record.id),
807
+ ],
808
+ typeErrorSkips: projectRead.typeErrorSkips,
809
+ skipped: projectRead.skipped,
810
+ componentFiles: one.componentFiles,
811
+ componentWarnings: one.componentWarnings,
812
+ extra: one.extra,
728
813
  };
729
814
  });
730
815
  const consumed = read.consumers.references;
731
- const projects = read.answers.map((one) =>
816
+ const projects = answers.map((one) =>
732
817
  consumed.length === 0
733
818
  ? one
734
819
  : {
@@ -744,7 +829,7 @@ function readRun<Extra>(
744
829
  exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
745
830
  unansweredHeld: [
746
831
  ...new Set([
747
- ...read.answers.flatMap((one) => one.unanswered),
832
+ ...answers.flatMap((one) => one.unanswered),
748
833
  ...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
749
834
  ]),
750
835
  ],
@@ -784,6 +869,61 @@ function holdingSweep<Extra>(
784
869
  }
785
870
  }
786
871
 
872
+ /**
873
+ * The test references exemption evidence written in a test file makes: each is a read of
874
+ * the declaration it would retain, made by the test file at the evidence's site.
875
+ */
876
+ function evidenceReferences(
877
+ symbols: readonly InventorySymbol[],
878
+ inTestFiles: readonly Exemption[],
879
+ ): readonly Reference[] {
880
+ if (inTestFiles.length === 0) {
881
+ return [];
882
+ }
883
+ const files = new Map(
884
+ symbols
885
+ .filter((symbol) => symbol.kind === "file")
886
+ .map((symbol) => [symbol.position.path, symbol.id]),
887
+ );
888
+ return inTestFiles.flatMap((record) => {
889
+ const from = files.get(record.site.path);
890
+ return from === undefined
891
+ ? []
892
+ : [
893
+ {
894
+ from,
895
+ to: record.id,
896
+ position: record.site,
897
+ use: "read",
898
+ resolution: "syntax",
899
+ test: true,
900
+ },
901
+ ];
902
+ });
903
+ }
904
+
905
+ /**
906
+ * The declarations every reference the mode counts stores into, a loaded consumer's
907
+ * reference counting as a read: what the write-only kind would report but for a record.
908
+ */
909
+ function writtenOnly<Extra>(read: ReadRun<Extra>, mode: Mode): ReadonlySet<string> {
910
+ const written = new Set<string>();
911
+ const readSome = new Set<string>();
912
+ for (const { configured } of read.projects) {
913
+ for (const reference of configured.references) {
914
+ if (mode.production && reference.test) {
915
+ continue;
916
+ }
917
+ if (reference.use === "write" && reference.consumer === undefined) {
918
+ written.add(reference.to);
919
+ } else {
920
+ readSome.add(reference.to);
921
+ }
922
+ }
923
+ }
924
+ return new Set([...written].filter((id) => !readSome.has(id)));
925
+ }
926
+
787
927
  /**
788
928
  * The sweep of a run read once, under one set of marks. Where a question went unanswered,
789
929
  * the run is swept once more without what that question could have kept live and without
@@ -834,7 +974,7 @@ function sweptOf<Extra>(
834
974
  matrix: read.matrix,
835
975
  sweep: result,
836
976
  exempt,
837
- retained: retainedIn(read.matrix, swept),
977
+ retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
838
978
  notBuilt: read.notBuilt,
839
979
  unanswered: read.unanswered,
840
980
  heldByUnanswered: [...held],
@@ -874,6 +1014,7 @@ interface EmitterExtra {
874
1014
  readonly needs: ReturnType<typeof projectNeeds>;
875
1015
  readonly implementations: ReturnType<typeof implementations>;
876
1016
  readonly heldByInclusion: readonly string[];
1017
+ readonly generated: readonly string[];
877
1018
  readonly rooted: Roots;
878
1019
  readonly directives: readonly InlineDirective[];
879
1020
  readonly declarationsNamed: readonly boolean[];
@@ -900,9 +1041,12 @@ function readEmitterRun(
900
1041
  (project, read, references) => ({
901
1042
  deprecation: deprecatedDeclarations(project, read.held),
902
1043
  accessors: accessorsOf(project, read.held),
903
- needs: projectNeeds(project, read.held, host, targetRoot),
1044
+ needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) =>
1045
+ engine.parseSourceFile(fileName, text),
1046
+ ),
904
1047
  implementations: implementations(project, read.held),
905
1048
  heldByInclusion: heldByInclusion(project, targetRoot),
1049
+ generated: [...read.generated.keys()],
906
1050
  rooted: read.rooted,
907
1051
  directives: inlineDirectives(project, read.held, targetRoot).filter(
908
1052
  (directive) => !read.referenceOnly.includes(directive.site.path),
@@ -956,6 +1100,7 @@ function emitterInputOver(
956
1100
  files: {
957
1101
  tree: sourceTree(host, targetRoot),
958
1102
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
1103
+ generated: new Set(projects.flatMap((one) => one.extra.generated)),
959
1104
  },
960
1105
  boundary: {
961
1106
  consumers: {