@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
@@ -1,159 +1,15 @@
1
1
  /**
2
- * What the errors a project's own files carry do to the run. An import of a module the
3
- * project expects and nothing provides is a setup failure. Any other error in an own file
4
- * is a type error: the function, method or file-level statement holding it is skipped,
5
- * which only ever withholds a finding. `projectErrors` decides, before any of these, which
6
- * errors the compiler places in no file leave the project unloaded.
2
+ * What a type error in a project's own file does: the function, method or file-level statement
3
+ * holding it is skipped, which only ever withholds a finding.
7
4
  */
8
- import { isClassDeclaration, isFunctionLikeDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
9
- import { packageOfSpecifier } from "./dependencies.js";
5
+ import { isClassDeclaration, isFunctionLikeDeclaration, isPropertyAccessExpression, } from "@typescript/native/ast";
10
6
  import { nodeKey } from "./inventory.js";
11
- import { ancestors, dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath, } from "./paths.js";
7
+ import { relativePath } from "./paths.js";
12
8
  import { isAnswered } from "./query.js";
13
- import { findWorkspace } from "./workspace.js";
14
- import { entriesOf } from "./fs/tree.js";
15
- import { INSTALLED_PACKAGES, MANIFEST, manifestAt } from "./fs/manifest.js";
16
- /** The compiler's codes for an import whose module it cannot find. */
17
- const MODULE_NOT_FOUND = new Set([2307, 2792]);
18
- /**
19
- * The compiler's codes for where the configuration places a file, which leave every type
20
- * the file's statements carry as it is: an output that would overwrite an input (5055,
21
- * 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
22
- */
23
- const CONFIGURATION_LEVEL = new Set([5055, 5056, 6059, 6307]);
24
- const MANIFEST_SECTIONS = [
25
- "dependencies",
26
- "devDependencies",
27
- "peerDependencies",
28
- "optionalDependencies",
29
- ];
30
9
  /** Whether a finding's position falls inside a skipped unit. */
31
10
  export function insideSkipped(skipped, position) {
32
11
  return skipped.some((unit) => unit.path === position.path && unit.fromLine <= position.line && position.line <= unit.toLine);
33
12
  }
34
- /** The string literal holding one offset, innermost first, a doc comment's included. */
35
- function literalAt(file, offset) {
36
- let found;
37
- const visit = (node) => {
38
- if (offset < node.pos || offset >= node.end) {
39
- return;
40
- }
41
- if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
42
- found = node.text;
43
- }
44
- // `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
45
- for (const doc of node.jsDoc ?? []) {
46
- visit(doc);
47
- }
48
- node.forEachChild(visit);
49
- };
50
- file.forEachChild(visit);
51
- return found;
52
- }
53
- /**
54
- * What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
55
- * is the specifier, or `undefined` where the key does not match it.
56
- */
57
- function mappedBy(key, specifier) {
58
- const star = key.indexOf("*");
59
- if (star < 0) {
60
- return key === specifier ? "" : undefined;
61
- }
62
- const prefix = key.slice(0, star);
63
- const suffix = key.slice(star + 1);
64
- return specifier.length >= prefix.length + suffix.length &&
65
- specifier.startsWith(prefix) &&
66
- specifier.endsWith(suffix)
67
- ? specifier.slice(prefix.length, specifier.length - suffix.length)
68
- : undefined;
69
- }
70
- /**
71
- * One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
72
- * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
73
- */
74
- function substituted(target, star) {
75
- const at = target.indexOf("*");
76
- return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
77
- }
78
- /**
79
- * Whether a path names something on disk, as written or with any extension; under
80
- * `fileOnly`, only a file counts.
81
- */
82
- function provided(host, path, fileOnly) {
83
- const counts = (at) => fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
84
- if (counts(path)) {
85
- return true;
86
- }
87
- const dir = dirnamePath(path);
88
- const stem = `${path.slice(dir.length + 1)}.`;
89
- return entriesOf(host, dir).some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
90
- }
91
- /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
92
- function declaringManifest(host, dir, targetRoot, name) {
93
- const chain = ancestors(dir, targetRoot);
94
- // A directory outside the target root reads its own manifest alone.
95
- for (const at of chain.length > 0 ? chain : [dir]) {
96
- const manifest = manifestAt(host, at);
97
- for (const section of MANIFEST_SECTIONS) {
98
- const declared = manifest?.[section];
99
- if (typeof declared === "object" && declared !== null && Object.hasOwn(declared, name)) {
100
- return joinPath(at, MANIFEST);
101
- }
102
- }
103
- }
104
- return undefined;
105
- }
106
- /**
107
- * Whether a package is installed for an importer in `dir`: in the `node_modules` of
108
- * `dir` or of any ancestor, above the target root too, as Node and the compiler look.
109
- */
110
- function installed(host, dir, name) {
111
- return ancestors(dir).some((at) => host.kindOf(joinPath(at, INSTALLED_PACKAGES, name)) === "directory");
112
- }
113
- /**
114
- * Why one import that the compiler cannot resolve names a module the project expects,
115
- * with the fix, or undefined where the module is one the project does not expect.
116
- */
117
- function expectedModule(context, importer, specifier) {
118
- const { host, targetRoot } = context;
119
- const fix = "run the generator or the build that writes it";
120
- if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
121
- return provided(host, resolvePath(dirnamePath(importer), specifier), false)
122
- ? undefined
123
- : `which names no file of the target: ${fix}`;
124
- }
125
- if (specifier.startsWith("#")) {
126
- return `which the manifest's imports map to no file: ${fix}`;
127
- }
128
- const mapped = context.paths.entries.flatMap(({ key, targets }) => {
129
- const star = mappedBy(key, specifier);
130
- return star === undefined
131
- ? []
132
- : targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
133
- });
134
- const name = packageOfSpecifier(specifier);
135
- // The compiler looks up in `node_modules` what `paths` does not resolve, so the error
136
- // of an installed package's import is the import's own, a subpath its exports omit:
137
- // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
138
- if (name !== undefined && installed(host, dirnamePath(importer), name)) {
139
- return undefined;
140
- }
141
- if (mapped.length > 0) {
142
- return mapped.some((path) => provided(host, path, true))
143
- ? undefined
144
- : `which the compiler configuration's paths map to no file: ${fix}`;
145
- }
146
- const manifest = name === undefined
147
- ? undefined
148
- : declaringManifest(host, dirnamePath(importer), targetRoot, name);
149
- // No run requires a nested package that is no workspace member to be installed.
150
- if (manifest === undefined ||
151
- (dirnamePath(manifest) !== targetRoot && !context.member(manifest))) {
152
- return undefined;
153
- }
154
- const where = relativePath(targetRoot, manifest) ?? manifest;
155
- return `a dependency ${where} declares that is not installed: run the package manager's install`;
156
- }
157
13
  /**
158
14
  * The unit a type error at one offset skips: the innermost function holding it, else the
159
15
  * class member holding it where a top-level class does, else the top-level statement.
@@ -184,55 +40,13 @@ function messageOf(text, targetRoot) {
184
40
  const line = text.split(`${targetRoot}/`).join("").split(/\s+/u).join(" ").trim();
185
41
  return line === "" ? "a type error" : line;
186
42
  }
187
- /**
188
- * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
189
- * or about where the configuration places a file is no error here, and one in a file that
190
- * is not the project's own is that file's package's to answer for.
191
- */
192
- export function readErrors(project, errors, where) {
193
- let workspace = null;
194
- const context = {
195
- ...where,
196
- member: (manifest) => {
197
- if (workspace === null) {
198
- workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
199
- }
200
- return workspace?.memberOf(manifest) !== undefined;
201
- },
202
- };
203
- const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
204
- const missing = [];
205
- const skips = [];
206
- for (const { diagnostic, at } of errors) {
207
- const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
208
- if (file === undefined ||
209
- diagnostic.reportsUnnecessary === true ||
210
- CONFIGURATION_LEVEL.has(diagnostic.code)) {
211
- continue;
212
- }
213
- const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
214
- const line = at?.line ?? 1;
215
- const specifier = MODULE_NOT_FOUND.has(diagnostic.code)
216
- ? literalAt(file, diagnostic.pos)
217
- : undefined;
218
- const expected = specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
219
- if (specifier !== undefined && expected !== undefined) {
220
- missing.push({
221
- setupClass: "missing-module",
222
- detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(specifier)}, ${expected}`,
223
- });
224
- continue;
225
- }
226
- const unit = unitAt(file, diagnostic.pos);
227
- if (unit !== undefined) {
228
- skips.push({
229
- record: { path, line, message: messageOf(diagnostic.text, context.targetRoot) },
230
- file,
231
- unit,
232
- });
233
- }
234
- }
235
- return { missing, skips };
43
+ /** The skip one type error at `offset` of an own file makes, the root spelled relative to itself. */
44
+ export function skipAt(file, offset, line, text, targetRoot) {
45
+ const unit = unitAt(file, offset);
46
+ const path = relativePath(targetRoot, file.fileName) ?? file.fileName;
47
+ return unit === undefined
48
+ ? undefined
49
+ : { record: { path, line, message: messageOf(text, targetRoot) }, file, unit };
236
50
  }
237
51
  /** The lines each skip withholds, below the target root. */
238
52
  export function skippedUnits(skips, targetRoot) {
@@ -297,22 +111,3 @@ export function skippedRoots(project, held, skips) {
297
111
  }
298
112
  return [...kept];
299
113
  }
300
- /** One compiler configuration's `paths` mapping, empty where it cannot be read. */
301
- export function pathsOf(engine, configFile) {
302
- const base = dirnamePath(configFile);
303
- try {
304
- const { options } = engine.parseConfigFile(configFile);
305
- const { paths } = options;
306
- // The parse answers `pathsBasePath`, the directory of the configuration that writes
307
- // `paths` (an extended one included), though the client's option type omits it:
308
- // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
309
- const declaredAt = options.pathsBasePath;
310
- return {
311
- base: typeof declaredAt === "string" ? declaredAt : base,
312
- entries: Object.entries(paths ?? {}).map(([key, targets]) => ({ key, targets })),
313
- };
314
- }
315
- catch {
316
- return { base, entries: [] };
317
- }
318
- }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A refusal of the invocation: the fix is to the command line or to a document it
3
+ * names, so the run ends with the usage code before any analysis.
4
+ */
5
+ export class UsageError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "UsageError";
9
+ }
10
+ }
@@ -1,10 +1,10 @@
1
- import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, } from "../contract/contract.generated.js";
1
+ import { CONTRACT_VERSION, EXIT_CLEAN } from "../contract/contract.generated.js";
2
2
  import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
3
- import { ConfigError, kindOf } from "../config.js";
3
+ import { kindOf } from "../config.js";
4
4
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
5
- import { moduleIdentity } from "../consumers.js";
6
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
7
6
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
+ import { moduleIdentity } from "../load/discover.js";
8
8
  import { partialNotes } from "../partial.js";
9
9
  import { buildReport, capFindings, sortFindings, } from "../report.js";
10
10
  import { reportRefusals } from "../report-schema.js";
@@ -14,8 +14,11 @@ import { parseTemplate, TemplateError } from "../reporters/template.js";
14
14
  import { resolve } from "../resolve.js";
15
15
  import { otherSchemeRows, rowReason, writeBaseline } from "../suppress-file.js";
16
16
  import {} from "./verb.js";
17
+ import { UsageError } from "../usage.js";
17
18
  import { bytewise } from "../text/compare.js";
18
- import { diagnose, LANGUAGE, message } from "../product.js";
19
+ import { productFault, failureExit } from "../failure.js";
20
+ import { diagnose, message } from "../product.js";
21
+ import { ownVersion } from "./version.js";
19
22
  /** The options `analyze` takes beside the ones every verb takes. */
20
23
  export const ANALYZE_OPTIONS = {
21
24
  plain: ["report", "baseline-write", "exit-code", "template"],
@@ -24,13 +27,6 @@ export const ANALYZE_OPTIONS = {
24
27
  /** The two values `--exit-code` takes; on, the default, puts the run's verdict in the exit code. */
25
28
  const EXIT_CODE_ON = "on";
26
29
  const EXIT_CODE_OFF = "off";
27
- /** A path of the run no report can name, which ends the run with the failure code. */
28
- class ReportPathError extends Error {
29
- constructor(message) {
30
- super(message);
31
- this.name = "ReportPathError";
32
- }
33
- }
34
30
  /** The formats this analyzer renders, for a message that names them. */
35
31
  function formatNames() {
36
32
  return [...RENDERINGS.keys()].sort(bytewise).join(", ");
@@ -41,10 +37,10 @@ function formatsOf(named, configured) {
41
37
  const seen = new Set();
42
38
  for (const format of formats) {
43
39
  if (!RENDERINGS.has(format)) {
44
- throw new ConfigError("malformed", "", `${JSON.stringify(format)} is not a format this analyzer renders: the formats are ${formatNames()}`);
40
+ throw new UsageError(`${JSON.stringify(format)} is not a format this analyzer renders: the formats are ${formatNames()}`);
45
41
  }
46
42
  if (seen.has(format)) {
47
- throw new ConfigError("malformed", "", `the format ${JSON.stringify(format)} is named twice`);
43
+ throw new UsageError(`the format ${JSON.stringify(format)} is named twice`);
48
44
  }
49
45
  seen.add(format);
50
46
  }
@@ -61,7 +57,7 @@ function runRelative(invoked, path) {
61
57
  }
62
58
  const below = relativePath(invoked, path);
63
59
  if (below === undefined) {
64
- throw new ReportPathError(`a report names its target relative to the directory the run was invoked from, and ${invoked} does not contain ${path}: run from a directory that does`);
60
+ throw failureExit("unreportable-path", `a report names its target relative to the directory the run was invoked from, and ${invoked} does not contain ${path}: run from a directory that does`);
65
61
  }
66
62
  return below;
67
63
  }
@@ -103,7 +99,7 @@ function notBuiltOf(dropped, targetRoot) {
103
99
  return dropped.map((one) => {
104
100
  const project = relativePath(targetRoot, one.configFile);
105
101
  if (project === undefined) {
106
- throw new ReportPathError(`the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`);
102
+ throw failureExit("unreportable-path", `the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it: analyze a target that holds it`);
107
103
  }
108
104
  return { id: one.id, project, error: one.error };
109
105
  });
@@ -113,11 +109,24 @@ function configurationsOf(projects, targetRoot) {
113
109
  return projects.map((one) => {
114
110
  const project = relativePath(targetRoot, one.configFile);
115
111
  if (project === undefined) {
116
- throw new ReportPathError(`the project ${one.id} is opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`);
112
+ throw failureExit("unreportable-path", `the project ${one.id} is opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it: analyze a target that holds it`);
117
113
  }
118
114
  return { id: one.id, project };
119
115
  });
120
116
  }
117
+ /**
118
+ * Writes one document the run was asked for. A document that cannot be produced or written
119
+ * ends the run with the run failure that names it and `fix`, the documents written before it
120
+ * kept.
121
+ */
122
+ function writeDocument(host, path, fix, produce) {
123
+ try {
124
+ host.writeDocument(path, produce());
125
+ }
126
+ catch (error) {
127
+ throw failureExit("unwritten-output", `${path}: ${message(error)}: ${fix}`);
128
+ }
129
+ }
121
130
  /** A count with its noun, so a message reads for one record as well as for several. */
122
131
  function counted(n, noun) {
123
132
  return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
@@ -130,7 +139,7 @@ function counted(n, noun) {
130
139
  function templateOf(host, invoked, named, formats) {
131
140
  if (named === undefined || named === "") {
132
141
  if (formats.includes("template")) {
133
- throw new ConfigError("malformed", "", "the template format renders the template --template names, and none was named");
142
+ throw new UsageError("the template format renders the template --template names, and none was named");
134
143
  }
135
144
  return undefined;
136
145
  }
@@ -139,7 +148,7 @@ function templateOf(host, invoked, named, formats) {
139
148
  source = host.readFile(resolvePath(invoked, named));
140
149
  }
141
150
  catch (error) {
142
- throw new ConfigError("malformed", "", `--template=${named}: ${message(error)}`);
151
+ throw new UsageError(`--template=${named}: ${message(error)}`);
143
152
  }
144
153
  try {
145
154
  return parseTemplate(source);
@@ -148,7 +157,7 @@ function templateOf(host, invoked, named, formats) {
148
157
  if (!(error instanceof TemplateError)) {
149
158
  throw error;
150
159
  }
151
- throw new ConfigError("malformed", "", `--template=${named}: ${error.message}`);
160
+ throw new UsageError(`--template=${named}: ${error.message}`);
152
161
  }
153
162
  }
154
163
  /**
@@ -162,17 +171,13 @@ function templateOf(host, invoked, named, formats) {
162
171
  export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
163
172
  const reportPath = option("report");
164
173
  if (reportPath === undefined || reportPath === "") {
165
- throw new ConfigError("malformed", "", "analyze writes its report to the path --report names, and no path was named");
174
+ throw new UsageError("analyze writes its report to the path --report names, and no path was named");
166
175
  }
167
176
  const exitCode = option("exit-code") ?? EXIT_CODE_ON;
168
177
  if (exitCode !== EXIT_CODE_ON && exitCode !== EXIT_CODE_OFF) {
169
- throw new ConfigError("malformed", "", `--exit-code=${JSON.stringify(exitCode)}: the values are ${EXIT_CODE_ON} and ${EXIT_CODE_OFF}`);
178
+ throw new UsageError(`--exit-code=${JSON.stringify(exitCode)}: the values are ${EXIT_CODE_ON} and ${EXIT_CODE_OFF}`);
170
179
  }
171
180
  const { config, provenance } = resolve(inputs());
172
- const kind = config.targetKind;
173
- if (kind === "") {
174
- throw new ConfigError("missing-target-kind", "target.kind", "target.kind is not set");
175
- }
176
181
  const invoked = host.workingDirectory();
177
182
  const baseline = option("baseline-write");
178
183
  const documents = {
@@ -183,108 +188,81 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
183
188
  const template = templateOf(host, invoked, option("template"), documents.formats);
184
189
  const scoped = scope();
185
190
  const targetRoot = scoped.target.path;
186
- let version;
187
- try {
188
- version = host.analyzerVersion();
189
- }
190
- catch (error) {
191
- diagnose(err, message(error));
192
- return EXIT_FAILURE;
193
- }
194
- let report;
191
+ const version = ownVersion(host);
195
192
  let recorded;
196
- let verdict;
197
- let partial;
198
- let warnings;
199
- let unread;
200
- let unfound;
201
- try {
202
- const root = runRelative(invoked, targetRoot);
203
- const mode = { production: true };
204
- let analysis;
205
- if (documents.baseline === undefined) {
206
- analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
207
- recorded = [];
208
- }
209
- else {
210
- const reason = rowReason(analyzerProvenance(version));
211
- const write = runBaselineWrite(openClient(config), host, scoped, config, provenance, mode, reason);
212
- analysis = write.first;
213
- recorded = write.rows;
214
- }
215
- const { result, run } = analysis;
216
- warnings = run.componentWarnings;
217
- unread = run.componentsUnread.map(({ configFile, reason }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`);
218
- unfound = run.librariesUnfound.map(({ configFile, error }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: ${error}: the configuration is analyzed without that library`);
219
- partial = partialNotes({
220
- notBuilt: run.notBuilt,
221
- unanswered: run.unanswered,
222
- targetRoot: resolvePath(invoked, targetRoot),
223
- });
224
- const built = buildReport({
225
- contractVersion: CONTRACT_VERSION,
226
- version,
227
- conformance: CONFORMANCE,
228
- declaredGaps: DECLARED_GAPS,
229
- target: {
230
- kind: kindOf(kind, LANGUAGE),
231
- root,
232
- identity: moduleIdentity(host, scoped.target),
233
- },
234
- configurations: configurationsOf(run.projects, targetRoot),
235
- notBuilt: notBuiltOf(run.notBuilt, targetRoot),
236
- loaded: run.consumers.map((consumer) => ({
237
- id: consumer.id,
238
- path: runRelative(invoked, consumer.path),
239
- })),
240
- unavailable: [],
241
- result,
242
- testFileRules: run.testFileRules,
243
- typeErrorSkips: run.typeErrorSkips,
244
- unanswered: unansweredCounts(analysis),
245
- conventionsApplied: run.conventionsApplied,
246
- listsComponents: config.reporters.cascade === "full",
247
- notes: [
248
- ...unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
249
- ...unseenLoadNotes(run.unseenLoads),
250
- ],
251
- });
252
- report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
253
- verdict = verdictOf(result, config);
193
+ const root = runRelative(invoked, targetRoot);
194
+ const mode = { production: true };
195
+ let analysis;
196
+ if (documents.baseline === undefined) {
197
+ analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
198
+ recorded = [];
254
199
  }
255
- catch (error) {
256
- if (!(error instanceof ReportPathError)) {
257
- throw error;
258
- }
259
- diagnose(err, error.message);
260
- return EXIT_FAILURE;
200
+ else {
201
+ const reason = rowReason(analyzerProvenance(version));
202
+ const write = runBaselineWrite(openClient(config), host, scoped, config, provenance, mode, reason);
203
+ analysis = write.first;
204
+ recorded = write.rows;
261
205
  }
262
- try {
263
- const [refused] = reportRefusals(report);
264
- if (refused !== undefined) {
265
- throw new Error(`the report breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`);
266
- }
267
- host.writeDocument(documents.report, json(report));
268
- const options = {
269
- readSource: (path) => host.readFile(joinPath(targetRoot, path)),
270
- template,
271
- };
272
- for (const format of documents.formats) {
273
- const rendering = RENDERINGS.get(format);
274
- if (rendering !== undefined) {
275
- host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
276
- }
277
- }
278
- if (documents.baseline !== undefined) {
279
- const kept = host.kindOf(documents.baseline) === "file"
280
- ? otherSchemeRows(host.readFile(documents.baseline))
281
- : [];
282
- host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version), kept));
206
+ const { result, run } = analysis;
207
+ const warnings = run.componentWarnings;
208
+ const unread = run.componentsUnread.map(({ configFile, reason }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`);
209
+ const unfound = run.librariesUnfound.map(({ configFile, error }) => `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: ${error}: the configuration is analyzed without that library`);
210
+ const partial = partialNotes({
211
+ notBuilt: run.notBuilt,
212
+ unanswered: run.unanswered,
213
+ targetRoot: resolvePath(invoked, targetRoot),
214
+ });
215
+ const built = buildReport({
216
+ contractVersion: CONTRACT_VERSION,
217
+ version,
218
+ conformance: CONFORMANCE,
219
+ declaredGaps: DECLARED_GAPS,
220
+ target: {
221
+ kind: kindOf(config.target.kind),
222
+ root,
223
+ identity: moduleIdentity(host, scoped.target),
224
+ },
225
+ configurations: configurationsOf(run.projects, targetRoot),
226
+ notBuilt: notBuiltOf(run.notBuilt, targetRoot),
227
+ loaded: run.consumers.map((consumer) => ({
228
+ id: consumer.id,
229
+ path: runRelative(invoked, consumer.path),
230
+ })),
231
+ unavailable: [],
232
+ result,
233
+ testFileRules: run.testFileRules,
234
+ typeErrorSkips: run.typeErrorSkips,
235
+ unanswered: unansweredCounts(analysis),
236
+ conventionsApplied: run.conventionsApplied,
237
+ listsComponents: config.reporters.cascade === "full",
238
+ notes: [
239
+ ...unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
240
+ ...unseenLoadNotes(run.unseenLoads),
241
+ ],
242
+ });
243
+ const report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.max_findings);
244
+ const verdict = verdictOf(result, config);
245
+ const [refused] = reportRefusals(report);
246
+ if (refused !== undefined) {
247
+ throw productFault(`built a report that breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`);
248
+ }
249
+ writeDocument(host, documents.report, "name a path the run can write", () => json(report));
250
+ const options = {
251
+ readSource: (path) => host.readFile(joinPath(targetRoot, path)),
252
+ template,
253
+ };
254
+ for (const format of documents.formats) {
255
+ const rendering = RENDERINGS.get(format);
256
+ if (rendering !== undefined) {
257
+ writeDocument(host, documents.report + rendering.suffix, "name a path the run can write, or correct the template that renders it", () => rendering.render(report, options));
283
258
  }
284
259
  }
285
- catch (error) {
286
- diagnose(err, message(error));
287
- return EXIT_FAILURE;
260
+ const baselinePath = documents.baseline;
261
+ if (baselinePath !== undefined) {
262
+ writeDocument(host, baselinePath, "name a path the run can write, or repair the rows of the baseline the run would keep", () => {
263
+ const kept = host.kindOf(baselinePath) === "file" ? otherSchemeRows(host.readFile(baselinePath)) : [];
264
+ return writeBaseline(recorded, analyzerProvenance(version), kept);
265
+ });
288
266
  }
289
267
  for (const note of partial) {
290
268
  diagnose(err, note);
@@ -1,7 +1,8 @@
1
- import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
1
+ import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_USAGE, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
2
2
  import { CONFORMANCE } from "../conformance.js";
3
3
  import {} from "./verb.js";
4
- import { diagnose, LANGUAGE, message, PRODUCT } from "../product.js";
4
+ import { diagnose, LANGUAGE, PRODUCT } from "../product.js";
5
+ import { ownVersion } from "./version.js";
5
6
  /**
6
7
  * The document `describe` writes: the analyzer, the Contract version it implements, the
7
8
  * report schema versions it reads, the languages it claims, and its recorded result over
@@ -26,20 +27,12 @@ export function describeDocument(version, recorded) {
26
27
  };
27
28
  return `${JSON.stringify(document, null, 2)}\n`;
28
29
  }
29
- /** Writes this analyzer's description to the output stream, and nothing to the error stream. */
30
+ /** Writes this analyzer's description to the output stream. */
30
31
  export const describeVerb = ({ out, err, host, args }) => {
31
32
  if (args.length > 0) {
32
33
  diagnose(err, `describe takes no argument, got ${JSON.stringify(args[0])}`);
33
34
  return EXIT_USAGE;
34
35
  }
35
- let version;
36
- try {
37
- version = host.analyzerVersion();
38
- }
39
- catch (error) {
40
- diagnose(err, `describe: ${message(error)}`);
41
- return EXIT_FAILURE;
42
- }
43
- out.write(describeDocument(version, CONFORMANCE));
36
+ out.write(describeDocument(ownVersion(host), CONFORMANCE));
44
37
  return EXIT_CLEAN;
45
38
  };
@@ -1,7 +1,6 @@
1
1
  import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
2
2
  import { runAnalysis } from "../analysis.js";
3
3
  import { listing } from "../components.js";
4
- import { ConfigError } from "../config.js";
5
4
  import { OUTSIDE, swept as judged } from "../graph.js";
6
5
  import { referenceKey } from "../matrix.js";
7
6
  import { partialNotes } from "../partial.js";
@@ -9,6 +8,7 @@ import { resolvePath } from "../paths.js";
9
8
  import { positionKey } from "../position.js";
10
9
  import { resolve } from "../resolve.js";
11
10
  import {} from "./verb.js";
11
+ import { UsageError } from "../usage.js";
12
12
  import { bytewise } from "../text/compare.js";
13
13
  import { diagnose } from "../product.js";
14
14
  /**
@@ -36,10 +36,10 @@ function namedSymbol(option) {
36
36
  });
37
37
  const [only, ...more] = named;
38
38
  if (only === undefined) {
39
- throw new ConfigError("malformed", "", "explain explains the symbol --why, --why-live or --why-not names, and none was named");
39
+ throw new UsageError("explain explains the symbol --why, --why-live or --why-not names, and none was named");
40
40
  }
41
41
  if (more.length > 0) {
42
- throw new ConfigError("malformed", "", `explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`);
42
+ throw new UsageError(`explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`);
43
43
  }
44
44
  return only.value;
45
45
  }
@@ -2,7 +2,7 @@ import { EXIT_CLEAN } from "../contract/contract.generated.js";
2
2
  import { runProjects } from "../analysis.js";
3
3
  import { partialNotes } from "../partial.js";
4
4
  import { resolvePath } from "../paths.js";
5
- import { resolveUnkinded } from "../resolve.js";
5
+ import { resolvePartial } from "../resolve.js";
6
6
  import {} from "./verb.js";
7
7
  import { diagnose } from "../product.js";
8
8
  /**
@@ -13,7 +13,7 @@ import { diagnose } from "../product.js";
13
13
  * instead, and one that fails the analysis fails this command the same way.
14
14
  */
15
15
  export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient }) => {
16
- const config = resolveUnkinded(inputs());
16
+ const config = resolvePartial(inputs());
17
17
  const scoped = scope();
18
18
  const answer = runProjects(openClient(config), host, scoped, config);
19
19
  for (const id of answer.configurations) {