@cplieger/deadset-ts 8.0.0-dev.7 → 8.0.0-dev.9

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 (137) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +94 -299
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configuration-files.js +2 -9
  5. package/dist/src/configured-declarations.js +15 -20
  6. package/dist/src/consumers.js +3 -30
  7. package/dist/src/contract/config.generated.js +544 -0
  8. package/dist/src/contract/contract.generated.js +20 -10
  9. package/dist/src/contract/report.generated.js +3 -2
  10. package/dist/src/conventions.js +3 -8
  11. package/dist/src/edges.js +36 -112
  12. package/dist/src/exempt-classes.js +0 -2
  13. package/dist/src/exempt.js +1 -1
  14. package/dist/src/failure.js +36 -0
  15. package/dist/src/findings/completion.js +5 -4
  16. package/dist/src/findings/emitters.js +1 -1
  17. package/dist/src/findings/intra-function.js +1 -1
  18. package/dist/src/findings/non-code-artifacts.js +2 -2
  19. package/dist/src/findings/unused-declarations.js +1 -1
  20. package/dist/src/findings/visibility-narrowing.js +2 -2
  21. package/dist/src/findings-pass.js +1 -1
  22. package/dist/src/framework-lifecycle.js +7 -7
  23. package/dist/src/fs/document.js +52 -0
  24. package/dist/src/fs/manifest.js +7 -0
  25. package/dist/src/injection-container.js +1 -1
  26. package/dist/src/json-document.js +194 -67
  27. package/dist/src/kinds.js +0 -7
  28. package/dist/src/load/classify.js +236 -0
  29. package/dist/src/load/diagnostics.js +79 -0
  30. package/dist/src/load/discover.js +230 -0
  31. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  32. package/dist/src/load/expected-module.js +136 -0
  33. package/dist/src/load/load.js +22 -0
  34. package/dist/src/load/module-lookup.js +37 -0
  35. package/dist/src/load/plan.js +155 -0
  36. package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
  37. package/dist/src/load/test-joins.js +82 -0
  38. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
  39. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
  40. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
  41. package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
  42. package/dist/src/{workspace.js → load/workspace.js} +21 -17
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/print.js +10 -79
  45. package/dist/src/report-schema.js +3 -224
  46. package/dist/src/resolve.js +245 -715
  47. package/dist/src/run.js +59 -72
  48. package/dist/src/schema.js +320 -344
  49. package/dist/src/scope.js +37 -113
  50. package/dist/src/serialization-contract.js +1 -1
  51. package/dist/src/session.js +22 -28
  52. package/dist/src/source-files.js +1 -1
  53. package/dist/src/suppress-file.js +15 -17
  54. package/dist/src/suppress.js +3 -3
  55. package/dist/src/template-field.js +4 -4
  56. package/dist/src/type-errors.js +11 -216
  57. package/dist/src/usage.js +10 -0
  58. package/dist/src/verbs/analyze.js +100 -122
  59. package/dist/src/verbs/describe.js +5 -12
  60. package/dist/src/verbs/explain.js +3 -3
  61. package/dist/src/verbs/print-projects.js +2 -2
  62. package/dist/src/verbs/version.js +12 -2
  63. package/jsr.json +1 -1
  64. package/package.json +1 -1
  65. package/src/analysis.ts +95 -460
  66. package/src/config.ts +31 -298
  67. package/src/configuration-files.ts +8 -10
  68. package/src/configured-declarations.ts +18 -23
  69. package/src/consumers.ts +6 -57
  70. package/src/contract/config.generated.ts +798 -0
  71. package/src/contract/contract.generated.ts +20 -10
  72. package/src/contract/report.generated.ts +3 -2
  73. package/src/conventions.ts +6 -12
  74. package/src/edges.ts +39 -124
  75. package/src/exempt-classes.ts +0 -5
  76. package/src/exempt.ts +4 -4
  77. package/src/failure.ts +63 -0
  78. package/src/findings/completion.ts +5 -6
  79. package/src/findings/emitter.ts +2 -2
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +1 -1
  82. package/src/findings/non-code-artifacts.ts +4 -4
  83. package/src/findings/unused-declarations.ts +1 -1
  84. package/src/findings/visibility-narrowing.ts +2 -2
  85. package/src/findings-pass.ts +3 -3
  86. package/src/framework-lifecycle.ts +7 -7
  87. package/src/fs/document.ts +70 -0
  88. package/src/fs/manifest.ts +8 -0
  89. package/src/host.ts +12 -5
  90. package/src/injection-container.ts +1 -1
  91. package/src/json-document.ts +236 -76
  92. package/src/kinds.ts +0 -8
  93. package/src/load/classify.ts +352 -0
  94. package/src/load/diagnostics.ts +141 -0
  95. package/src/load/discover.ts +378 -0
  96. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  97. package/src/load/expected-module.ts +188 -0
  98. package/src/load/load.ts +61 -0
  99. package/src/load/module-lookup.ts +50 -0
  100. package/src/load/plan.ts +258 -0
  101. package/src/{test-globals.ts → load/test-globals.ts} +59 -65
  102. package/src/load/test-joins.ts +98 -0
  103. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
  104. package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
  105. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
  106. package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
  107. package/src/{workspace.ts → load/workspace.ts} +28 -21
  108. package/src/outside-tests.ts +1 -1
  109. package/src/partial.ts +1 -1
  110. package/src/print.ts +11 -92
  111. package/src/report-schema.ts +3 -299
  112. package/src/resolve.ts +281 -1105
  113. package/src/run.ts +68 -80
  114. package/src/schema.ts +391 -389
  115. package/src/scope.ts +45 -131
  116. package/src/serialization-contract.ts +1 -1
  117. package/src/session.ts +52 -53
  118. package/src/source-files.ts +1 -1
  119. package/src/suppress-file.ts +15 -20
  120. package/src/suppress.ts +3 -3
  121. package/src/template-field.ts +10 -7
  122. package/src/type-errors.ts +18 -288
  123. package/src/usage.ts +10 -0
  124. package/src/verbs/analyze.ts +139 -157
  125. package/src/verbs/describe.ts +4 -11
  126. package/src/verbs/explain.ts +3 -7
  127. package/src/verbs/print-projects.ts +2 -2
  128. package/src/verbs/verb.ts +2 -2
  129. package/src/verbs/version.ts +13 -2
  130. package/dist/src/base-url.js +0 -18
  131. package/dist/src/discover.js +0 -374
  132. package/dist/src/setup-failure.js +0 -19
  133. package/dist/src/test-joins.js +0 -101
  134. package/src/base-url.ts +0 -24
  135. package/src/discover.ts +0 -548
  136. package/src/setup-failure.ts +0 -37
  137. package/src/test-joins.ts +0 -118
package/src/scope.ts CHANGED
@@ -1,13 +1,12 @@
1
- import { ConfigError } from "./config.ts";
1
+ import { SCOPE_SCHEMA } from "./contract/config.generated.ts";
2
2
  import type { Host } from "./host.ts";
3
+ import { dotted, locatedFault, placeIn, readStrict, StrictJSONError } from "./json-document.ts";
3
4
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
4
- import { checkDocument, type KeyNode } from "./schema.ts";
5
- import { isRecord } from "./json-document.ts";
5
+ import { MARKERS } from "./load/discover.ts";
6
+ import { failureExit } from "./failure.ts";
6
7
  import { message } from "./product.ts";
7
-
8
- /** The roles a scope document declares for a module. */
9
- const ROLE_TARGET = "target";
10
- const ROLE_CONSUMER = "consumer";
8
+ import { firstRefusal } from "./schema.ts";
9
+ import { UsageError } from "./usage.ts";
11
10
 
12
11
  /**
13
12
  * A scope document above this length is refused rather than decoded. It names one
@@ -16,23 +15,14 @@ const ROLE_CONSUMER = "consumer";
16
15
  const MAX_DOCUMENT_LENGTH = 1 << 20;
17
16
 
18
17
  /**
19
- * A scope document that cannot be read as one. It ends the run with the failure
20
- * code: the run could not learn what to analyze, so it produced no answer.
18
+ * The bytes read of a scope document. A UTF-16 unit decodes from at most three bytes,
19
+ * so a document past the bound is past it within these.
21
20
  */
22
- export class ScopeError extends Error {
23
- constructor(message: string) {
24
- super(message);
25
- this.name = "ScopeError";
26
- }
27
- }
21
+ const MAX_DOCUMENT_BYTES = 3 * (MAX_DOCUMENT_LENGTH + 1);
28
22
 
29
23
  /**
30
- * One module the run reads: the target or a declared consumer.
31
- *
32
- * The role a document declares is not carried here, because the resolved scope
33
- * holds exactly one target and a list of consumers, so a module's role is where it
34
- * sits. What a document declares is read and refused where it contradicts that
35
- * position, which is the only place the declaration can be wrong.
24
+ * One module the run reads: the target or a declared consumer. The role a document
25
+ * declares is not carried, because a module's role is where the scope holds it.
36
26
  */
37
27
  export interface Module {
38
28
  /** The package name. A document may state it; otherwise it is empty. */
@@ -47,142 +37,66 @@ export interface Scope {
47
37
  readonly consumers: readonly Module[];
48
38
  }
49
39
 
50
- const MODULE: KeyNode = {
51
- kind: "section",
52
- members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
53
- };
54
-
55
- /** The closed key list of the scope document, compared as bytes. */
56
- const SCOPE_ROOT: KeyNode = {
57
- kind: "section",
58
- members: {
59
- target: MODULE,
60
- workspace: { kind: "leaf" },
61
- consumers: { kind: "list", members: MODULE.members ?? {} },
62
- },
63
- };
64
-
65
- /**
66
- * One string member, or undefined where the document omits it. A member the
67
- * document writes is a string holding at least one character, so `null` and `""`
68
- * are refused rather than read as absent.
69
- */
70
- function stringMember(
71
- parent: Record<string, unknown>,
72
- name: string,
73
- at: string,
74
- ): string | undefined {
75
- if (!Object.hasOwn(parent, name)) {
76
- return undefined;
77
- }
78
- const value = parent[name];
79
- if (typeof value !== "string") {
80
- throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
81
- }
82
- if (value === "") {
83
- throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
84
- }
85
- return value;
86
- }
87
-
88
- function readModule(value: unknown, at: string, role: string, documentDir: string): Module {
89
- if (!isRecord(value)) {
90
- throw new ScopeError(`scope: ${at} is not an object`);
91
- }
92
- const declared = stringMember(value, "role", at);
93
- if (declared !== undefined && declared !== role) {
94
- throw new ScopeError(
95
- `scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`,
96
- );
97
- }
98
- const path = stringMember(value, "path", at);
99
- if (path === undefined) {
100
- throw new ScopeError(`scope: ${at}.path names nothing`);
101
- }
102
- return {
103
- id: stringMember(value, "id", at) ?? "",
104
- path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
105
- };
40
+ interface DeclaredModule {
41
+ readonly id?: string;
42
+ readonly path: string;
106
43
  }
107
44
 
108
- /**
109
- * Walks the document's text against the closed key list, refusing a key the list
110
- * does not declare, compared as bytes, and a key one object writes twice, which a
111
- * parse cannot report because it keeps the last value.
112
- */
113
- function checkKeys(text: string, file: string): void {
114
- try {
115
- checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
116
- } catch (error: unknown) {
117
- if (error instanceof ConfigError) {
118
- // The configuration's refusal names the nearest configuration key, which a
119
- // scope document does not have.
120
- throw new ScopeError(
121
- error.kind === "unimplemented-key"
122
- ? `scope: ${file}: ${error.key} is not a declared key`
123
- : error.message,
124
- );
125
- }
126
- throw error;
127
- }
45
+ function refusal(file: string, place: string, reason: string): UsageError {
46
+ return new UsageError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
128
47
  }
129
48
 
130
49
  /**
131
- * Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
132
- * key written twice in one object, and a value of a type the document does not
133
- * admit, `null` included, are errors, and a relative path inside the document
134
- * resolves against the document's own directory.
135
- *
136
- * The `workspace` key the format declares is checked and carries nothing here: it
137
- * names the file a Go consumer resolves the target through, and a TypeScript
138
- * project resolves one through its own compiler configuration.
50
+ * Decodes the scope document at `file`, held to the Contract's scope schema, a relative
51
+ * path resolved against the document's own directory. A document that cannot be read or is
52
+ * refused is a usage error, as any document the invocation names. `workspace` is read by Go consumers
53
+ * alone: a TypeScript project resolves the target through its own compiler configuration.
139
54
  */
140
55
  export function readScope(host: Host, file: string): Scope {
141
56
  const absolute = resolvePath(host.workingDirectory(), file);
142
57
  let text: string;
143
58
  try {
144
- text = host.readFile(absolute);
59
+ text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
145
60
  } catch (error: unknown) {
146
- throw new ScopeError(`scope: ${file}: ${message(error)}`);
61
+ throw new UsageError(`scope: ${file}: ${message(error)}`);
147
62
  }
148
63
  if (text.length > MAX_DOCUMENT_LENGTH) {
149
- throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
64
+ throw new UsageError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
150
65
  }
151
66
  let value: unknown;
152
67
  try {
153
- value = JSON.parse(text);
68
+ value = readStrict(text).value;
154
69
  } catch (error: unknown) {
155
- throw new ScopeError(`scope: ${file}: ${message(error)}`);
70
+ if (!(error instanceof StrictJSONError)) {
71
+ throw error;
72
+ }
73
+ throw refusal(file, dotted(error.place), locatedFault(text, error));
156
74
  }
157
- if (!isRecord(value)) {
158
- throw new ScopeError(`scope: ${file} is not one JSON object`);
75
+ const refused = firstRefusal(SCOPE_SCHEMA, value);
76
+ if (refused !== undefined) {
77
+ throw refusal(file, dotted(placeIn(value, refused.pointer, refused.member)), refused.reason);
159
78
  }
160
- checkKeys(text, file);
161
- stringMember(value, "workspace", "");
79
+ const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
162
80
  const documentDir = dirnamePath(absolute);
163
- if (!Object.hasOwn(value, "target")) {
164
- throw new ScopeError(`scope: ${file} names no target`);
165
- }
166
- const target = readModule(value["target"], "target", ROLE_TARGET, documentDir);
167
- const listed = Object.hasOwn(value, "consumers") ? value["consumers"] : [];
168
- if (!Array.isArray(listed)) {
169
- throw new ScopeError(`scope: ${file}: consumers is not an array`);
170
- }
171
- const consumers = (listed as unknown[]).map((entry, index) =>
172
- readModule(entry, `consumers[${String(index)}]`, ROLE_CONSUMER, documentDir),
173
- );
174
- return { target, consumers };
81
+ const moduleOf = (module: DeclaredModule): Module => ({
82
+ id: module.id ?? "",
83
+ path: isAbsolutePath(module.path) ? module.path : joinPath(documentDir, module.path),
84
+ });
85
+ return { target: moduleOf(declared.target), consumers: (declared.consumers ?? []).map(moduleOf) };
175
86
  }
176
87
 
177
- /** The single-package scope one directory is, when no scope document is given. */
88
+ /**
89
+ * The single-package scope one directory is, when no scope document is given. A target that is
90
+ * no directory holds no program.
91
+ */
178
92
  export function scopeForDir(host: Host, dir: string): Scope {
179
93
  const path = resolvePath(host.workingDirectory(), dir);
180
94
  const kind = host.kindOf(path);
181
- if (kind === "absent") {
182
- throw new ScopeError(`scope: ${path} does not exist`);
183
- }
184
95
  if (kind !== "directory") {
185
- throw new ScopeError(`scope: ${path} is not a directory`);
96
+ throw failureExit(
97
+ "no-program",
98
+ `the target ${path} ${kind === "absent" ? "does not exist" : "is not a directory"}, so it holds no compiler configuration, no file named ${MARKERS.join(", ")}: name the directory that holds one`,
99
+ );
186
100
  }
187
101
  return { target: { id: "", path }, consumers: [] };
188
102
  }
@@ -82,7 +82,7 @@ import { erasedStores, type ErasedStores } from "./erased-stores.ts";
82
82
  import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
83
83
 
84
84
  /** The serializer every run reads, whatever the configuration names. */
85
- const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
85
+ const JSON_STRINGIFY: DeclarationEntry = { global: "JSON.stringify" };
86
86
 
87
87
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
88
88
  const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
package/src/session.ts CHANGED
@@ -15,9 +15,7 @@ import type {
15
15
  } from "@typescript/native/sync";
16
16
  import type { Node, SourceFile } from "@typescript/native/ast";
17
17
  import { componentLayer, mapperManifest } from "./component-layer.ts";
18
- import { DiscoveryError } from "./discover.ts";
19
18
  import type { Host, TemporaryDirectory } from "./host.ts";
20
- import type { WorkspaceProgram } from "./workspace-programs.ts";
21
19
  import { joinPath } from "./paths.ts";
22
20
  import {
23
21
  queriesOf,
@@ -37,8 +35,8 @@ import { MANIFEST } from "./fs/manifest.ts";
37
35
  export interface Engine {
38
36
  /**
39
37
  * Resolves one compiler configuration file, or throws with the reason it cannot
40
- * be read. The result is spelled through the client's own signature: the type
41
- * the compiler package returns here is not one of its exported names.
38
+ * be read. The result is spelled through the client's own signature: the type the
39
+ * compiler package returns here is not one of its exported names.
42
40
  */
43
41
  parseConfigFile(file: DocumentIdentifier): ReturnType<API["parseConfigFile"]>;
44
42
  /** Resolves one configuration's JSON as if `configFile` held it, which no file need. */
@@ -272,13 +270,6 @@ export interface ProjectView<Brand> {
272
270
  */
273
271
  export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
274
272
 
275
- /** One project a session opened: its configuration and the files of its own program. */
276
- export interface OpenedFiles {
277
- readonly configFile: string;
278
- /** The program's source files no default library and no external library holds. */
279
- readonly fileNames: () => readonly string[];
280
- }
281
-
282
273
  /**
283
274
  * The source files of one program that `own` admits, in the program's order, none of
284
275
  * them one of the compiler's default libraries.
@@ -472,19 +463,30 @@ export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagno
472
463
  };
473
464
  }
474
465
 
466
+ /** How a snapshot builds one configuration other than by opening the project it names. */
467
+ export type ProgramBuild =
468
+ | { readonly kind: "program"; readonly params: CreateSnapshotProgramParams }
469
+ /** A configuration only the snapshot holds, at a path no file has. */
470
+ | { readonly kind: "configuration"; readonly file: string; readonly text: string };
471
+
475
472
  /** One run's result: what the visitor answered for each project, and what it cost. */
476
473
  export interface SessionResult<Result> {
477
- /** One answer per configuration the snapshot opened a project for, in the order named. */
474
+ /** One answer per configuration, in the order named. */
478
475
  readonly projects: readonly Result[];
479
- /** The configurations the snapshot opened no project for, in the order named. */
480
- readonly unopened: readonly string[];
481
476
  readonly timing: TimingInfo;
482
- /** Every question the checker could not answer, each once, in the order first asked. */
483
- readonly unanswered: readonly UnansweredQuestion[];
484
477
  }
485
478
 
486
- /** What a run does about a configuration the snapshot opened no project for. */
487
- type Unopened = "refuse" | "record";
479
+ /** One open snapshot's projects, valid only while the session that handed it out runs. */
480
+ export interface OpenedSession {
481
+ /** One view per configuration the snapshot opened a project for, in the order named. */
482
+ readonly views: readonly ProjectView<unknown>[];
483
+ }
484
+
485
+ /** How one session opens its configurations. */
486
+ export interface SessionOptions {
487
+ /** How the snapshot builds each configuration it does not open as its project. */
488
+ readonly programs?: ReadonlyMap<string, ProgramBuild> | undefined;
489
+ }
488
490
 
489
491
  /**
490
492
  * The projects one snapshot holds for the configurations named, by configuration file. A
@@ -513,22 +515,18 @@ function projectsOf(
513
515
  }
514
516
 
515
517
  /**
516
- * Runs `visit` over every project the compiler configurations name, inside one never updated
517
- * snapshot of one client, and answers in their order. A configuration `programs` describes is
518
- * built as that program, or opened from the configuration it writes. One the snapshot holds
519
- * nothing for ends the run under `"refuse"` and is named in the result under `"record"`.
520
- * `first` runs over every opened project before the first visit and `last` after the last,
521
- * both while the client answers; the snapshot and the client are then released.
518
+ * Opens every project the compiler configurations name in one never updated snapshot of one
519
+ * client, and hands them to `use` while the client answers; the snapshot and the client are
520
+ * then released. A configuration `options.programs` describes is built as that program, or
521
+ * opened from the configuration it writes. One the snapshot holds nothing for has no view.
522
522
  */
523
- export function runSession<Result>(
523
+ export function openSession<Result>(
524
524
  engine: Engine,
525
525
  configFiles: readonly string[],
526
- visit: ProjectVisitor<Result>,
527
- unopened: Unopened = "refuse",
528
- programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
529
- last: () => void = () => undefined,
530
- first: (opened: readonly OpenedFiles[]) => void = () => undefined,
531
- ): SessionResult<Result> {
526
+ options: SessionOptions,
527
+ use: (opened: OpenedSession, unanswered: () => readonly UnansweredQuestion[]) => Result,
528
+ ): Result {
529
+ const programs = options.programs ?? new Map<string, ProgramBuild>();
532
530
  try {
533
531
  const built: string[] = [];
534
532
  const params: CreateSnapshotProgramParams[] = [];
@@ -553,32 +551,13 @@ export function runSession<Result>(
553
551
  const opened = projectsOf(snapshot, built, written);
554
552
  const log = new UnansweredLog();
555
553
  const views: ProjectView<unknown>[] = [];
556
- const missing: string[] = [];
557
554
  for (const configFile of configFiles) {
558
555
  const project = opened.get(configFile);
559
- if (project === undefined) {
560
- if (unopened === "refuse") {
561
- throw new DiscoveryError(`${configFile} opened no project`, []);
562
- }
563
- missing.push(configFile);
564
- continue;
556
+ if (project !== undefined) {
557
+ views.push(viewOf(project, configFile, engine, log));
565
558
  }
566
- views.push(viewOf(project, configFile, engine, log));
567
559
  }
568
- first(
569
- views.map((view) => ({
570
- configFile: view.configFile,
571
- fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
572
- })),
573
- );
574
- const projects = views.map((view) => visit(view));
575
- last();
576
- return {
577
- projects,
578
- unopened: missing,
579
- timing: engine.getTimingInfo(),
580
- unanswered: log.questions,
581
- };
560
+ return use({ views }, () => log.questions);
582
561
  } finally {
583
562
  snapshot.dispose();
584
563
  }
@@ -586,3 +565,23 @@ export function runSession<Result>(
586
565
  engine.close();
587
566
  }
588
567
  }
568
+
569
+ /**
570
+ * Runs `visit` over the project {@link openSession} opens for each configuration, and answers
571
+ * in their order; a configuration the snapshot opens nothing for throws before any visit.
572
+ */
573
+ export function runSession<Result>(
574
+ engine: Engine,
575
+ configFiles: readonly string[],
576
+ visit: ProjectVisitor<Result>,
577
+ ): SessionResult<Result> {
578
+ return openSession(engine, configFiles, {}, (opened) => {
579
+ const unopened = configFiles.find(
580
+ (configFile) => !opened.views.some((view) => view.configFile === configFile),
581
+ );
582
+ if (unopened !== undefined) {
583
+ throw new Error(`${unopened} opened no project`);
584
+ }
585
+ return { projects: opened.views.map((view) => visit(view)), timing: engine.getTimingInfo() };
586
+ });
587
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { SourceFile } from "@typescript/native/ast";
8
- import { commonDirectory, emittedFrom } from "./emit-map.ts";
8
+ import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
9
9
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
11
 
@@ -8,9 +8,9 @@
8
8
  import { CODE } from "./contract/contract.generated.ts";
9
9
  import type { Host } from "./host.ts";
10
10
  import type { InventorySymbol } from "./inventory.ts";
11
+ import { readTargetDocument } from "./fs/document.ts";
11
12
  import { jsonChunks } from "./json-chunks.ts";
12
- import { joinPath } from "./paths.ts";
13
- import { isRecord, positionAt, walkDocument } from "./json-document.ts";
13
+ import { isRecord, positionAt, readStrict, StrictJSONError } from "./json-document.ts";
14
14
  import type { Position } from "./position.ts";
15
15
  import { declarationKey } from "./ref.ts";
16
16
  import {
@@ -185,30 +185,26 @@ function readDocument(
185
185
  symbols: readonly InventorySymbol[],
186
186
  shape: Shape,
187
187
  ): Suppressions {
188
- const file = joinPath(targetRoot, shape.file);
189
- if (host.kindOf(file) === "absent") {
188
+ const text = readTargetDocument(host, targetRoot, shape.file);
189
+ if (text === undefined) {
190
190
  return { records: [], refusals: [] };
191
191
  }
192
- const text = host.readFile(file);
193
192
  const malformed = (detail: string, site = documentSite(shape)): never => {
194
193
  throw new SuppressionError(shape.mechanism, siteText(site), detail, shape.documentWant);
195
194
  };
196
- let parsed: unknown;
195
+ let document;
197
196
  try {
198
- parsed = JSON.parse(text);
197
+ document = readStrict(text, shape.array);
199
198
  } catch (error: unknown) {
200
- return malformed(`${shape.file}: ${message(error)}`);
199
+ if (!(error instanceof StrictJSONError)) {
200
+ throw error;
201
+ }
202
+ return malformed(error.message, positionAt(text, shape.file, error.offset));
201
203
  }
204
+ const parsed = document.value;
202
205
  if (!isRecord(parsed)) {
203
206
  return malformed(shape.file);
204
207
  }
205
- const walk = walkDocument(text, shape.array);
206
- if (walk.twice !== undefined) {
207
- malformed(
208
- `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
209
- positionAt(text, shape.file, walk.twice.offset),
210
- );
211
- }
212
208
  for (const key of Object.keys(parsed)) {
213
209
  if (key !== "description" && key !== shape.array) {
214
210
  malformed(`the member ${JSON.stringify(key)} is not one the grammar declares`);
@@ -224,7 +220,7 @@ function readDocument(
224
220
  const records: SuppressionRecord[] = [];
225
221
  const refusals: Refusal[] = [];
226
222
  (held as readonly unknown[]).forEach((value, index) => {
227
- const offset = walk.starts[index];
223
+ const offset = document.starts.get(index);
228
224
  const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
229
225
  const read = readRecord(value, site, byReference, shape);
230
226
  records.push(...read.records);
@@ -382,10 +378,9 @@ function* separatorsEscaped(pieces: Iterable<string>): Generator<string, void, u
382
378
  * A baseline holding one row per finding, in the order given, which is the report's, and
383
379
  * the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
384
380
  * the order given, so two analyzers writing one document in either order write the same
385
- * bytes, in the form grammar/suppression.md fixes. Every recorded row carries the
386
- * provenance as its reason. A finding
387
- * missing a value a row needs, and an identity missing its name or version, are refused
388
- * rather than written, because the grammar requires a reason on every row.
381
+ * bytes, in the form grammar/suppression.md fixes. Every row carries the provenance as
382
+ * its reason, so a finding missing a value a row needs, or an identity missing its name
383
+ * or version, is refused rather than written.
389
384
  */
390
385
  export function writeBaseline(
391
386
  findings: readonly Recorded[],
package/src/suppress.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  SYMBOL_SUBJECTS,
14
14
  type Subject,
15
15
  } from "./contract/contract.generated.ts";
16
- import { ConfigError } from "./config.ts";
16
+ import { UsageError } from "./usage.ts";
17
17
  import type { Finding } from "./finding.ts";
18
18
  import { positionKey, type Position } from "./position.ts";
19
19
  import { isRef } from "./ref.ts";
@@ -68,9 +68,9 @@ export interface Suppressions {
68
68
  * instruction the analysis cannot carry out. It is a malformed input, so the command line
69
69
  * answers it with the usage code.
70
70
  */
71
- export class SuppressionError extends ConfigError {
71
+ export class SuppressionError extends UsageError {
72
72
  constructor(mechanism: Mechanism, at: string, text: string, want: string) {
73
- super("malformed", at, `${mechanism} ${at}: ${text}: want ${want}`);
73
+ super(`${mechanism} ${at}: ${text}: want ${want}`);
74
74
  this.name = "SuppressionError";
75
75
  }
76
76
  }
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { readComponent } from "./component-files.ts";
16
- import { ConfigError, type Analysis, type TemplateDelimiters } from "./config.ts";
16
+ import type { ResolvedConfig, TemplateDelimiters } from "./config.ts";
17
17
  import type { DetectorInput, Evidence } from "./exempt.ts";
18
18
  import type { Host } from "./host.ts";
19
19
  import type { SymbolKind } from "./inventory.ts";
@@ -22,6 +22,7 @@ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } fr
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
23
  import { bytewise } from "./text/compare.ts";
24
24
  import { filesBelow } from "./fs/tree.ts";
25
+ import { UsageError } from "./usage.ts";
25
26
 
26
27
  /** One template, by its path below the target root. */
27
28
  interface TemplateFile {
@@ -60,7 +61,11 @@ const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
60
61
  * exemption it was configured for off. A symbolic link to a directory is not
61
62
  * followed.
62
63
  */
63
- export function readTemplates(host: Host, targetRoot: string, analysis: Analysis): Templates {
64
+ export function readTemplates(
65
+ host: Host,
66
+ targetRoot: string,
67
+ analysis: ResolvedConfig["analysis"],
68
+ ): Templates {
64
69
  const files = new Map<string, string>();
65
70
  const read = (dir: string): void => {
66
71
  for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
@@ -71,7 +76,7 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
71
76
  }
72
77
  }
73
78
  };
74
- for (const configured of analysis.templateDirs) {
79
+ for (const configured of analysis.template_dirs) {
75
80
  const dir = normalizePath(configured);
76
81
  if (
77
82
  isAbsolutePath(dir) ||
@@ -79,16 +84,14 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
79
84
  dir.startsWith("../") ||
80
85
  host.kindOf(joinPath(targetRoot, dir)) !== "directory"
81
86
  ) {
82
- throw new ConfigError(
83
- "malformed",
84
- "analysis.template_dirs",
87
+ throw new UsageError(
85
88
  `analysis.template_dirs: ${JSON.stringify(configured)} is not a directory below the target root`,
86
89
  );
87
90
  }
88
91
  read(dir);
89
92
  }
90
93
  return {
91
- delimiters: analysis.templateDelimiters,
94
+ delimiters: analysis.template_delimiters,
92
95
  files: [...files]
93
96
  .map(([path, text]) => ({ path, text }))
94
97
  .sort((a, b) => bytewise(a.path, b.path)),