@cplieger/deadset-ts 4.0.1 → 5.1.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 (138) 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 +301 -41
  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 +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
package/src/session.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import type {
3
3
  APIRequestGenerator,
4
+ CreateSnapshotProgramParams,
4
5
  Diagnostic,
5
6
  DocumentIdentifier,
7
+ ModuleResolutionEntry,
8
+ ModuleResolver,
6
9
  NodeHandle,
7
10
  Project,
8
11
  Program,
@@ -11,7 +14,11 @@ import type {
11
14
  TimingInfo,
12
15
  } from "@typescript/native/unstable/sync";
13
16
  import type { Node, SourceFile } from "@typescript/native/unstable/ast";
17
+ import { componentLayer, mapperManifest } from "./component-layer.ts";
14
18
  import { DiscoveryError } from "./discover.ts";
19
+ import type { Host, TemporaryDirectory } from "./host.ts";
20
+ import type { WorkspaceProgram } from "./workspace-programs.ts";
21
+ import { joinPath } from "./paths.ts";
15
22
  import {
16
23
  queriesOf,
17
24
  UnansweredLog,
@@ -36,8 +43,34 @@ export interface Engine {
36
43
  * the compiler package returns here is not one of its exported names.
37
44
  */
38
45
  parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
39
- /** Opens one snapshot holding every project named. */
40
- createSnapshot(openProjects: readonly string[]): Snapshot;
46
+ /** Resolves one configuration's JSON as if `configFile` held it, which no file need. */
47
+ parseConfigJson(json: unknown, configFile: string): ReturnType<API["parseConfigFile"]>;
48
+ /**
49
+ * Opens one snapshot holding every project named and every program described, the
50
+ * programs in the order given in the snapshot's operation. A project named in
51
+ * `configurations` is opened from that configuration text, which the snapshot alone
52
+ * holds.
53
+ */
54
+ createSnapshot(
55
+ openProjects: readonly string[],
56
+ createPrograms?: readonly CreateSnapshotProgramParams[],
57
+ configurations?: ReadonlyMap<string, string>,
58
+ ): Snapshot;
59
+ /**
60
+ * Whether the snapshots read component files. A described program reads none, because
61
+ * the compiler gives a program no content mapper.
62
+ */
63
+ readonly readsComponents: boolean;
64
+ /**
65
+ * A module resolver answering each entry's import with the entry's file, and every
66
+ * other import as the compiler resolves it.
67
+ */
68
+ createModuleResolver(
69
+ options: CreateSnapshotProgramParams["compilerOptions"],
70
+ entries: readonly ModuleResolutionEntry[],
71
+ ): ModuleResolver;
72
+ /** Parses one file's text outside every program. */
73
+ parseSourceFile(fileName: string, text: string): SourceFile;
41
74
  /** Sends many questions in one round trip and answers each, in order. */
42
75
  batch<T>(questions: readonly APIRequestGenerator<T>[]): T[];
43
76
  /**
@@ -56,6 +89,20 @@ export interface Engine {
56
89
  export interface EngineOptions {
57
90
  /** Collect per-request timing, which the calibration record reads. */
58
91
  readonly collectTiming: boolean;
92
+ /**
93
+ * The component files every snapshot reads. Present, every configuration a snapshot opens
94
+ * takes this analyzer's mapper in place of its own, or no mapper where no extension is
95
+ * listed, and with an extension the client runs that mapper from a temporary directory the
96
+ * close removes. Absent, the configurations are opened as they are and no mapper runs.
97
+ */
98
+ readonly components?: ComponentReading;
99
+ }
100
+
101
+ /** Which files a run reads as component files, and the host that runs their mapper. */
102
+ export interface ComponentReading {
103
+ /** The file name extensions of component files, `ts.component_extensions`. */
104
+ readonly extensions: readonly string[];
105
+ readonly host: Host;
59
106
  }
60
107
 
61
108
  /**
@@ -66,11 +113,61 @@ export interface EngineOptions {
66
113
  * second call has nothing left to do.
67
114
  */
68
115
  export function openEngine(options: EngineOptions): Engine {
69
- const api = new API({ collectTiming: options.collectTiming });
116
+ const { components } = options;
117
+ let mapper: TemporaryDirectory | undefined;
118
+ if (components !== undefined && components.extensions.length > 0) {
119
+ const made = components.host.temporaryDirectory();
120
+ try {
121
+ components.host.writeDocument(joinPath(made.path, "package.json"), [
122
+ mapperManifest(components.host.componentMapperCommand()),
123
+ ]);
124
+ } catch (error: unknown) {
125
+ made.remove();
126
+ throw error;
127
+ }
128
+ mapper = made;
129
+ }
130
+ const api = new API({
131
+ collectTiming: options.collectTiming,
132
+ ...(mapper === undefined ? {} : { runExternalCode: true }),
133
+ });
70
134
  let closed = false;
71
135
  return {
72
136
  parseConfigFile: (file) => api.parseConfigFile(file),
73
- createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
137
+ parseConfigJson: (json, configFile) =>
138
+ api.parseJsonConfigFileContent(json, { configFileName: configFile }),
139
+ createSnapshot: (openProjects, createPrograms, configurations = new Map()) =>
140
+ api.createSnapshot({
141
+ openProjects: [...openProjects],
142
+ ...(createPrograms === undefined ? {} : { createPrograms: [...createPrograms] }),
143
+ ...(components === undefined
144
+ ? configurations.size === 0
145
+ ? {}
146
+ : { fileSystem: { kind: "layer", files: Object.fromEntries(configurations) } }
147
+ : {
148
+ fileSystem: componentLayer(
149
+ components.host,
150
+ openProjects,
151
+ mapper === undefined
152
+ ? undefined
153
+ : { extensions: components.extensions, packageDirectory: mapper.path },
154
+ configurations,
155
+ ),
156
+ }),
157
+ }),
158
+ readsComponents: mapper !== undefined,
159
+ createModuleResolver: (options, entries) =>
160
+ api.createModuleResolver(options, {
161
+ moduleResolutions: { fallback: "resolve", entries: [...entries] },
162
+ }),
163
+ parseSourceFile: (fileName, text) => {
164
+ const retained = api.createSourceFile(fileName, text);
165
+ try {
166
+ return retained.sourceFile;
167
+ } finally {
168
+ retained.dispose();
169
+ }
170
+ },
74
171
  batch: <T>(questions: readonly APIRequestGenerator<T>[]) =>
75
172
  api.batch<readonly APIRequestGenerator<T>[]>(...questions),
76
173
  ask: (_accessor, _locations, question) => question(),
@@ -80,7 +177,11 @@ export function openEngine(options: EngineOptions): Engine {
80
177
  return;
81
178
  }
82
179
  closed = true;
83
- api.close();
180
+ try {
181
+ api.close();
182
+ } finally {
183
+ mapper?.remove();
184
+ }
84
185
  },
85
186
  };
86
187
  }
@@ -112,6 +213,11 @@ export interface ProjectView<Brand> {
112
213
  * trip, and one may go unanswered.
113
214
  */
114
215
  readonly queries: Queries;
216
+ /**
217
+ * The files the project's configuration names, as the project reads them, so a
218
+ * component file a mapper reads is among them.
219
+ */
220
+ readonly rootFiles: readonly string[];
115
221
  /** The source files the program holds that are the target's own. */
116
222
  ownSourceFiles(): readonly SourceFile[];
117
223
  /**
@@ -166,6 +272,34 @@ export interface ProjectView<Brand> {
166
272
  */
167
273
  export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
168
274
 
275
+ /**
276
+ * The source files of one program that `own` admits, in the program's order, none of
277
+ * them one of the compiler's default libraries.
278
+ */
279
+ function filesOwned(
280
+ program: Program,
281
+ own: (fileName: string, fromExternalLibrary: boolean) => boolean,
282
+ ): readonly SourceFile[] {
283
+ return (
284
+ program
285
+ .getSourceFileNames()
286
+ // The metadata decides which files belong to the target, and it is read by
287
+ // name. Fetching each file first and filtering afterwards costs one round
288
+ // trip per library file the answer then discards, and a program holds far
289
+ // more library files than target files.
290
+ .filter((name) => {
291
+ const metadata = program.getSourceFileMetadata(name);
292
+ return (
293
+ metadata !== undefined &&
294
+ !metadata.isDefaultLibrary &&
295
+ own(name, metadata.isFromExternalLibrary)
296
+ );
297
+ })
298
+ .map((name) => program.getSourceFile(name))
299
+ .filter((file): file is SourceFile => file !== undefined)
300
+ );
301
+ }
302
+
169
303
  function viewOf<Brand>(
170
304
  project: Project,
171
305
  configFile: string,
@@ -186,25 +320,13 @@ function viewOf<Brand>(
186
320
  let ownFiles: readonly SourceFile[] | undefined;
187
321
  let ownKeys: ReadonlySet<string> | undefined;
188
322
  const ownSourceFiles = (): readonly SourceFile[] => {
189
- ownFiles ??= project.program
190
- .getSourceFileNames()
191
- // The metadata decides which files belong to the target, and it is read by
192
- // name. Fetching each file first and filtering afterwards costs one round
193
- // trip per library file the answer then discards, and a program holds far
194
- // more library files than target files.
195
- .filter((name) => {
196
- const metadata = project.program.getSourceFileMetadata(name);
197
- return (
198
- metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary
199
- );
200
- })
201
- .map((name) => project.program.getSourceFile(name))
202
- .filter((file): file is SourceFile => file !== undefined);
323
+ ownFiles ??= filesOwned(project.program, (_name, fromExternalLibrary) => !fromExternalLibrary);
203
324
  return ownFiles;
204
325
  };
205
326
  return {
206
327
  configFile,
207
328
  program: project.program,
329
+ rootFiles: project.parsedCommandLine.fileNames,
208
330
  queries,
209
331
  ownSourceFiles,
210
332
  ownPaths: () => {
@@ -263,6 +385,30 @@ export function narrowedTo<Brand>(
263
385
  };
264
386
  }
265
387
 
388
+ /**
389
+ * The same project, its own source files the ones `own` admits among every file its
390
+ * program holds, given whether the compiler reached the file as a library's.
391
+ */
392
+ export function ownedAs<Brand>(
393
+ project: ProjectView<Brand>,
394
+ own: (fileName: string, fromExternalLibrary: boolean) => boolean,
395
+ ): ProjectView<Brand> {
396
+ let owned: readonly SourceFile[] | undefined;
397
+ let keys: ReadonlySet<string> | undefined;
398
+ const ownSourceFiles = (): readonly SourceFile[] => {
399
+ owned ??= filesOwned(project.program, own);
400
+ return owned;
401
+ };
402
+ return {
403
+ ...project,
404
+ ownSourceFiles,
405
+ ownPaths: () => {
406
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
407
+ return keys;
408
+ },
409
+ };
410
+ }
411
+
266
412
  /** Every diagnostic set a project is refused for carrying an error in. */
267
413
  export interface ProjectDiagnostics {
268
414
  readonly configFile: string;
@@ -309,33 +455,68 @@ export interface SessionResult<Result> {
309
455
  type Unopened = "refuse" | "record";
310
456
 
311
457
  /**
312
- * Runs `visit` over every project the compiler configurations name, inside one
313
- * snapshot of one client, in the order they are named, and answers in that order.
314
- *
315
- * The snapshot is opened once with every configuration, so each becomes a project
316
- * with its own program and checker; it is never updated, because the analysis
317
- * reads and the tree does not change under it. The order the snapshot lists its
318
- * projects in is the compiler's own, so each named configuration is looked up in it
319
- * instead. A configuration the snapshot opened no project for ends the run under
320
- * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
321
- * the last project has been visited, and the client is released after it, whether the
322
- * visit completed or threw.
458
+ * The projects one snapshot holds for the configurations named, by configuration file. A
459
+ * project opened from a written configuration is the project of the configuration
460
+ * `written` maps it to.
461
+ */
462
+ function projectsOf(
463
+ snapshot: Snapshot,
464
+ built: readonly string[],
465
+ written: ReadonlyMap<string, string>,
466
+ ): ReadonlyMap<string, Project> {
467
+ const opened = new Map<string, Project>();
468
+ for (const project of snapshot.getProjects()) {
469
+ if (project.configFileName !== undefined) {
470
+ opened.set(written.get(project.configFileName) ?? project.configFileName, project);
471
+ }
472
+ }
473
+ (snapshot.operation.createdPrograms ?? []).forEach((program, at) => {
474
+ const project = snapshot.getProject(program.id);
475
+ const configFile = built[at];
476
+ if (project !== undefined && configFile !== undefined) {
477
+ opened.set(configFile, project);
478
+ }
479
+ });
480
+ return opened;
481
+ }
482
+
483
+ /**
484
+ * Runs `visit` over every project the compiler configurations name, inside one never updated
485
+ * snapshot of one client, and answers in their order. A configuration `programs` describes is
486
+ * built as that program, or opened from the configuration it writes, instead of as a project.
487
+ * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
488
+ * under `"record"`. The snapshot is disposed after the last visit and the client released
489
+ * after it, whether the visit completed or threw.
323
490
  */
324
491
  export function runSession<Result>(
325
492
  engine: Engine,
326
493
  configFiles: readonly string[],
327
494
  visit: ProjectVisitor<Result>,
328
495
  unopened: Unopened = "refuse",
496
+ programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
329
497
  ): SessionResult<Result> {
330
498
  try {
331
- const snapshot = engine.createSnapshot(configFiles);
332
- try {
333
- const opened = new Map<string, Project>();
334
- for (const project of snapshot.getProjects()) {
335
- if (project.configFileName !== undefined) {
336
- opened.set(project.configFileName, project);
337
- }
499
+ const built: string[] = [];
500
+ const params: CreateSnapshotProgramParams[] = [];
501
+ const written = new Map<string, string>();
502
+ const texts = new Map<string, string>();
503
+ for (const configFile of configFiles) {
504
+ const program = programs.get(configFile);
505
+ if (program?.kind === "program") {
506
+ built.push(configFile);
507
+ params.push(program.params);
508
+ } else if (program?.kind === "configuration") {
509
+ written.set(program.file, configFile);
510
+ texts.set(program.file, program.text);
338
511
  }
512
+ }
513
+ const snapshot = engine.createSnapshot(
514
+ [...configFiles.filter((configFile) => !programs.has(configFile)), ...written.keys()],
515
+ params.length === 0 ? undefined : params,
516
+ texts,
517
+ );
518
+ try {
519
+ const opened = projectsOf(snapshot, built, written);
339
520
  const log = new UnansweredLog();
340
521
  const projects: Result[] = [];
341
522
  const missing: string[] = [];
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A setup failure: a file or a component the analysis needs and the project does not
3
+ * provide. Each one names what is missing and the fix, on one line of the error stream
4
+ * that starts with its class.
5
+ */
6
+
7
+ /** The classes of setup failure this analyzer meets. */
8
+ type SetupClass =
9
+ | "missing-module"
10
+ | "missing-consumer"
11
+ | "workspace-member-without-source"
12
+ | "convention-not-literal";
13
+
14
+ export interface SetupFailure {
15
+ readonly setupClass: SetupClass;
16
+ /** What is missing and the fix, on one line. */
17
+ readonly detail: string;
18
+ }
19
+
20
+ /** The failure as the one line of the error stream that names it. */
21
+ export function setupLine(failure: SetupFailure): string {
22
+ return `setup failure: ${failure.setupClass}: ${failure.detail}`;
23
+ }
24
+
25
+ /**
26
+ * A run that met a setup failure where none may be dropped: in a configuration the
27
+ * configuration document or the scope declares, in a declared consumer, or in every
28
+ * configuration discovery derived. It ends the run with the failure code and no finding
29
+ * list.
30
+ */
31
+ export class SetupError extends Error {
32
+ readonly failures: readonly SetupFailure[];
33
+
34
+ constructor(failures: readonly SetupFailure[]) {
35
+ super(failures.map(setupLine).join("\n"));
36
+ this.name = "SetupError";
37
+ this.failures = failures;
38
+ }
39
+ }
@@ -5,53 +5,13 @@
5
5
  */
6
6
 
7
7
  import type { SourceFile } from "@typescript/native/unstable/ast";
8
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
8
+ import { commonDirectory, emittedFrom } from "./emit-map.ts";
9
+ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
9
10
  import type { ProjectView } from "./session.ts";
10
11
 
11
- /** The source extensions the compiler emits each output extension from. */
12
- const EMITTED: readonly { readonly emitted: string; readonly sources: readonly string[] }[] = [
13
- { emitted: ".d.ts", sources: [".ts", ".tsx"] },
14
- { emitted: ".d.mts", sources: [".mts"] },
15
- { emitted: ".d.cts", sources: [".cts"] },
16
- { emitted: ".js", sources: [".ts", ".tsx"] },
17
- { emitted: ".jsx", sources: [".tsx"] },
18
- { emitted: ".mjs", sources: [".mts"] },
19
- { emitted: ".cjs", sources: [".cts"] },
20
- ];
21
-
22
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
23
13
  const SUBPATH_WILDCARD = "*";
24
14
 
25
- /** One path with its extension replaced, where it carries the one named. */
26
- function reExtended(path: string, emitted: string, source: string): string | undefined {
27
- return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
28
- }
29
-
30
- /**
31
- * The longest directory every path shares, which is the directory the compiler
32
- * emits from where the configuration declares no root directory.
33
- */
34
- function commonDirectory(paths: readonly string[]): string {
35
- const first = paths[0];
36
- if (first === undefined) {
37
- return "";
38
- }
39
- let common = dirnamePath(first).split("/");
40
- for (const path of paths.slice(1)) {
41
- const segments = dirnamePath(path).split("/");
42
- let shared = 0;
43
- while (
44
- shared < common.length &&
45
- shared < segments.length &&
46
- common[shared] === segments[shared]
47
- ) {
48
- shared += 1;
49
- }
50
- common = common.slice(0, shared);
51
- }
52
- return common.join("/");
53
- }
54
-
55
15
  /**
56
16
  * Whether one path is what a subpath pattern stands for: the text before the
57
17
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -108,32 +68,10 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
108
68
  const declarationDir = at(options.declarationDir);
109
69
  const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
110
70
 
111
- /** One path read below the directory the compiler emits from, where it is below the one it emits to. */
112
- const fromEmit = (path: string, emitted: string): string | undefined => {
113
- const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
114
- if (emitDir === undefined) {
115
- return undefined;
116
- }
117
- const below = relativePath(emitDir, path);
118
- return below === undefined ? undefined : joinPath(sourceDir, below);
119
- };
71
+ const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
120
72
 
121
73
  /** The source paths one written path may stand for, the path itself first. */
122
- const candidates = (path: string): readonly string[] => {
123
- const found = [path];
124
- for (const { emitted, sources } of EMITTED) {
125
- const mapped = fromEmit(path, emitted);
126
- for (const source of sources) {
127
- for (const from of [path, mapped]) {
128
- const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
129
- if (candidate !== undefined) {
130
- found.push(candidate);
131
- }
132
- }
133
- }
134
- }
135
- return found;
136
- };
74
+ const candidates = (path: string): readonly string[] => [path, ...emittedFrom(path, layout)];
137
75
 
138
76
  return {
139
77
  byPath,
package/src/stores.ts CHANGED
@@ -22,6 +22,8 @@ export interface Stores {
22
22
  * configuration. A production mode counts none a test file made.
23
23
  */
24
24
  readonly references: readonly Reference[];
25
+ /** The declarations a test file reads, which a production mode counts no read of. */
26
+ readonly readInTests: ReadonlySet<string>;
25
27
  /**
26
28
  * The class members declared by a getter or a setter. A store into one runs the
27
29
  * setter, which is code rather than state.
@@ -71,6 +73,13 @@ export function storesOf(
71
73
  references: per.flatMap((one) =>
72
74
  one.references.filter((reference) => !mode.production || !reference.test),
73
75
  ),
76
+ readInTests: new Set(
77
+ per.flatMap((one) =>
78
+ one.references
79
+ .filter((reference) => reference.test && reference.use !== "write")
80
+ .map((reference) => reference.to),
81
+ ),
82
+ ),
74
83
  accessors: new Set(per.flatMap((one) => one.accessors)),
75
84
  exempt: new Set(exempt.map((record) => record.id)),
76
85
  };
package/src/sweep.ts CHANGED
@@ -22,6 +22,11 @@ export interface Exemption {
22
22
  readonly detail: string;
23
23
  /** Where the evidence is written. */
24
24
  readonly site: Position;
25
+ /**
26
+ * The component file, below the target root, whose markup holds the evidence. The
27
+ * record holds only while that file is live.
28
+ */
29
+ readonly whileLive?: string;
25
30
  }
26
31
 
27
32
  /**
@@ -253,13 +258,15 @@ function admitTestsOfDeadCode(
253
258
  ): readonly boolean[] {
254
259
  const admitted = graph.symbols.map(() => false);
255
260
  graph.symbols.forEach((_symbol, at) => {
256
- if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
261
+ const testCode = graph.test[at] === true || graph.support[at] === true;
262
+ if (!testCode || graph.subject[at] !== true || held[at] === true) {
257
263
  return;
258
264
  }
259
265
  let targets = 0;
260
266
  let live = 0;
261
267
  for (const edge of graph.out[at] ?? []) {
262
- if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
268
+ const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
269
+ if (target || graph.subject[edge.to] !== true) {
263
270
  continue;
264
271
  }
265
272
  targets += 1;
@@ -12,13 +12,14 @@
12
12
  * type, because nothing ties the name to a type.
13
13
  */
14
14
 
15
+ import { readComponent } from "./component-files.ts";
15
16
  import { ConfigError, type Analysis, type TemplateDelimiters } from "./config.ts";
16
17
  import type { DetectorInput, Evidence } from "./exempt.ts";
17
18
  import type { Host } from "./host.ts";
18
19
  import type { SymbolKind } from "./inventory.ts";
19
20
  import { memberNames } from "./member-names.ts";
20
- import { isAbsolutePath, joinPath, normalizePath } from "./paths.ts";
21
- import type { Position } from "./position.ts";
21
+ import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
+ import { isComponentFile, type Position } from "./position.ts";
22
23
 
23
24
  /** One template, by its path below the target root. */
24
25
  export interface TemplateFile {
@@ -40,6 +41,12 @@ const READ_BY_A_TEMPLATE: ReadonlySet<SymbolKind> = new Set([
40
41
  "type-member",
41
42
  ]);
42
43
 
44
+ /**
45
+ * The kinds of declaration a component file's markup can name. Its expressions are the
46
+ * language's own, so a static member and an enum member are reachable by name too.
47
+ */
48
+ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE, "enum-member"]);
49
+
43
50
  /** One identifier, as the grammar of the analyzed language spells one. */
44
51
  const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
45
52
 
@@ -173,6 +180,17 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
173
180
  }
174
181
  }
175
182
 
183
+ /** Every identifier the markup of one component file writes in an action. */
184
+ function markupNamesOf(text: string): readonly Named[] {
185
+ return readComponent(text).markup.flatMap((one) =>
186
+ identifiersOf(one.text).map(({ name, offset }) => ({
187
+ name,
188
+ offset: one.start + offset,
189
+ action: one.action,
190
+ })),
191
+ );
192
+ }
193
+
176
194
  /** Where one offset of a template is, its column counted in UTF-16 code units. */
177
195
  function positionIn(file: TemplateFile, offset: number): Position {
178
196
  const before = file.text.slice(0, offset);
@@ -187,25 +205,42 @@ function positionIn(file: TemplateFile, offset: number): Position {
187
205
  /**
188
206
  * The template-field detector: every instance member of a class and every member of
189
207
  * an interface or an object type whose name an action of a configured template
190
- * writes, recorded at the identifier.
208
+ * writes, and every member of those and of an enum whose name a component file's
209
+ * markup writes, recorded at the identifier and held while that file is live.
191
210
  */
192
211
  export function templateField<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
193
212
  const { templates } = input;
194
- if (templates.files.length === 0) {
213
+ const components = input.project.ownSourceFiles().flatMap((file) => {
214
+ const path = isComponentFile(file) ? relativePath(input.targetRoot, file.fileName) : undefined;
215
+ return path === undefined ? [] : [{ path, text: file.originalText }];
216
+ });
217
+ if (templates.files.length === 0 && components.length === 0) {
195
218
  return [];
196
219
  }
197
220
  const members = memberNames(input.project.ownSourceFiles(), input.held);
198
221
  const found: Evidence[] = [];
199
- for (const file of templates.files) {
200
- for (const named of namesOf(file.text, templates.delimiters)) {
222
+ const scanned = [
223
+ ...templates.files.map((file) => ({
224
+ file,
225
+ names: namesOf(file.text, templates.delimiters),
226
+ markup: false,
227
+ })),
228
+ ...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
229
+ ];
230
+ for (const { file, names, markup } of scanned) {
231
+ for (const named of names) {
201
232
  for (const member of members.get(named.name) ?? []) {
202
- if (!READ_BY_A_TEMPLATE.has(member.kind) || member.static) {
233
+ const readable = markup
234
+ ? READ_BY_MARKUP.has(member.kind)
235
+ : READ_BY_A_TEMPLATE.has(member.kind) && !member.static;
236
+ if (!readable) {
203
237
  continue;
204
238
  }
205
239
  found.push({
206
240
  id: member.id,
207
241
  detail: `named by ${named.action}`,
208
242
  site: positionIn(file, named.offset),
243
+ ...(markup ? { whileLive: file.path } : {}),
209
244
  });
210
245
  }
211
246
  }