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

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 (78) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +13 -14
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configured-declarations.js +15 -20
  5. package/dist/src/consumers.js +2 -2
  6. package/dist/src/contract/config.generated.js +544 -0
  7. package/dist/src/contract/report.generated.js +3 -2
  8. package/dist/src/discover.js +1 -1
  9. package/dist/src/edges.js +36 -112
  10. package/dist/src/exempt-classes.js +0 -2
  11. package/dist/src/exempt.js +1 -1
  12. package/dist/src/findings/completion.js +5 -4
  13. package/dist/src/findings/emitters.js +1 -1
  14. package/dist/src/findings/intra-function.js +1 -1
  15. package/dist/src/findings/non-code-artifacts.js +2 -2
  16. package/dist/src/findings/unused-declarations.js +1 -1
  17. package/dist/src/findings/visibility-narrowing.js +2 -2
  18. package/dist/src/findings-pass.js +1 -1
  19. package/dist/src/framework-lifecycle.js +7 -7
  20. package/dist/src/fs/document.js +59 -0
  21. package/dist/src/injection-container.js +1 -1
  22. package/dist/src/json-document.js +194 -67
  23. package/dist/src/kinds.js +0 -7
  24. package/dist/src/print.js +10 -79
  25. package/dist/src/report-schema.js +3 -224
  26. package/dist/src/resolve.js +245 -715
  27. package/dist/src/run.js +48 -55
  28. package/dist/src/schema.js +320 -344
  29. package/dist/src/scope.js +33 -101
  30. package/dist/src/serialization-contract.js +1 -1
  31. package/dist/src/suppress-file.js +15 -17
  32. package/dist/src/suppress.js +3 -3
  33. package/dist/src/template-field.js +4 -4
  34. package/dist/src/usage.js +10 -0
  35. package/dist/src/verbs/analyze.js +12 -15
  36. package/dist/src/verbs/explain.js +3 -3
  37. package/dist/src/verbs/print-projects.js +2 -2
  38. package/jsr.json +1 -1
  39. package/package.json +1 -1
  40. package/src/analysis.ts +30 -26
  41. package/src/config.ts +31 -298
  42. package/src/configured-declarations.ts +18 -23
  43. package/src/consumers.ts +4 -4
  44. package/src/contract/config.generated.ts +798 -0
  45. package/src/contract/report.generated.ts +3 -2
  46. package/src/discover.ts +1 -1
  47. package/src/edges.ts +39 -124
  48. package/src/exempt-classes.ts +0 -5
  49. package/src/exempt.ts +4 -4
  50. package/src/findings/completion.ts +5 -6
  51. package/src/findings/emitter.ts +2 -2
  52. package/src/findings/emitters.ts +1 -1
  53. package/src/findings/intra-function.ts +1 -1
  54. package/src/findings/non-code-artifacts.ts +4 -4
  55. package/src/findings/unused-declarations.ts +1 -1
  56. package/src/findings/visibility-narrowing.ts +2 -2
  57. package/src/findings-pass.ts +3 -3
  58. package/src/framework-lifecycle.ts +7 -7
  59. package/src/fs/document.ts +74 -0
  60. package/src/host.ts +12 -5
  61. package/src/injection-container.ts +1 -1
  62. package/src/json-document.ts +236 -76
  63. package/src/kinds.ts +0 -8
  64. package/src/print.ts +11 -92
  65. package/src/report-schema.ts +3 -299
  66. package/src/resolve.ts +281 -1105
  67. package/src/run.ts +50 -63
  68. package/src/schema.ts +391 -389
  69. package/src/scope.ts +39 -119
  70. package/src/serialization-contract.ts +1 -1
  71. package/src/suppress-file.ts +15 -20
  72. package/src/suppress.ts +3 -3
  73. package/src/template-field.ts +10 -7
  74. package/src/usage.ts +10 -0
  75. package/src/verbs/analyze.ts +12 -23
  76. package/src/verbs/explain.ts +3 -7
  77. package/src/verbs/print-projects.ts +2 -2
  78. package/src/verbs/verb.ts +2 -2
package/src/scope.ts CHANGED
@@ -1,13 +1,10 @@
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";
6
5
  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";
6
+ import { firstRefusal } from "./schema.ts";
7
+ import { UsageError } from "./usage.ts";
11
8
 
12
9
  /**
13
10
  * A scope document above this length is refused rather than decoded. It names one
@@ -16,8 +13,14 @@ const ROLE_CONSUMER = "consumer";
16
13
  const MAX_DOCUMENT_LENGTH = 1 << 20;
17
14
 
18
15
  /**
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.
16
+ * The bytes read of a scope document. A UTF-16 unit decodes from at most three bytes,
17
+ * so a document past the bound is past it within these.
18
+ */
19
+ const MAX_DOCUMENT_BYTES = 3 * (MAX_DOCUMENT_LENGTH + 1);
20
+
21
+ /**
22
+ * A scope that cannot be read as one. It ends the run with the failure code: the run
23
+ * could not learn what to analyze, so it produced no answer.
21
24
  */
22
25
  export class ScopeError extends Error {
23
26
  constructor(message: string) {
@@ -27,12 +30,8 @@ export class ScopeError extends Error {
27
30
  }
28
31
 
29
32
  /**
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.
33
+ * One module the run reads: the target or a declared consumer. The role a document
34
+ * declares is not carried, because a module's role is where the scope holds it.
36
35
  */
37
36
  export interface Module {
38
37
  /** The package name. A document may state it; otherwise it is empty. */
@@ -47,131 +46,52 @@ export interface Scope {
47
46
  readonly consumers: readonly Module[];
48
47
  }
49
48
 
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
- };
49
+ interface DeclaredModule {
50
+ readonly id?: string;
51
+ readonly path: string;
106
52
  }
107
53
 
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
- }
54
+ function refusal(file: string, place: string, reason: string): ScopeError {
55
+ return new ScopeError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
128
56
  }
129
57
 
130
58
  /**
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.
59
+ * Decodes the scope document at `file`, held to the Contract's scope schema, a relative
60
+ * path resolved against the document's own directory. A document that cannot be read is
61
+ * a usage error, one that is refused a scope error. `workspace` is read by Go consumers
62
+ * alone: a TypeScript project resolves the target through its own compiler configuration.
139
63
  */
140
64
  export function readScope(host: Host, file: string): Scope {
141
65
  const absolute = resolvePath(host.workingDirectory(), file);
142
66
  let text: string;
143
67
  try {
144
- text = host.readFile(absolute);
68
+ text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
145
69
  } catch (error: unknown) {
146
- throw new ScopeError(`scope: ${file}: ${message(error)}`);
70
+ throw new UsageError(`scope: ${file}: ${message(error)}`);
147
71
  }
148
72
  if (text.length > MAX_DOCUMENT_LENGTH) {
149
73
  throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
150
74
  }
151
75
  let value: unknown;
152
76
  try {
153
- value = JSON.parse(text);
77
+ value = readStrict(text).value;
154
78
  } catch (error: unknown) {
155
- throw new ScopeError(`scope: ${file}: ${message(error)}`);
79
+ if (!(error instanceof StrictJSONError)) {
80
+ throw error;
81
+ }
82
+ throw refusal(file, dotted(error.place), locatedFault(text, error));
156
83
  }
157
- if (!isRecord(value)) {
158
- throw new ScopeError(`scope: ${file} is not one JSON object`);
84
+ const refused = firstRefusal(SCOPE_SCHEMA, value);
85
+ if (refused !== undefined) {
86
+ throw refusal(file, dotted(placeIn(value, refused.pointer, refused.member)), refused.reason);
159
87
  }
160
- checkKeys(text, file);
161
- stringMember(value, "workspace", "");
88
+ const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
162
89
  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 };
90
+ const moduleOf = (module: DeclaredModule): Module => ({
91
+ id: module.id ?? "",
92
+ path: isAbsolutePath(module.path) ? module.path : joinPath(documentDir, module.path),
93
+ });
94
+ return { target: moduleOf(declared.target), consumers: (declared.consumers ?? []).map(moduleOf) };
175
95
  }
176
96
 
177
97
  /** The single-package scope one directory is, when no scope document is given. */
@@ -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"]);
@@ -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)),
package/src/usage.ts ADDED
@@ -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: string) {
7
+ super(message);
8
+ this.name = "UsageError";
9
+ }
10
+ }
@@ -12,7 +12,7 @@ import {
12
12
  type RunAnalysis,
13
13
  type RunProject,
14
14
  } from "../analysis.ts";
15
- import { ConfigError, kindOf } from "../config.ts";
15
+ import { kindOf } from "../config.ts";
16
16
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
17
17
  import { moduleIdentity } from "../consumers.ts";
18
18
  import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
@@ -38,8 +38,9 @@ import { resolve } from "../resolve.ts";
38
38
  import { otherSchemeRows, rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
39
39
  import type { UnseenLoad } from "../unseen-loads.ts";
40
40
  import { type Verb, type VerbOptions } from "./verb.ts";
41
+ import { UsageError } from "../usage.ts";
41
42
  import { bytewise } from "../text/compare.ts";
42
- import { diagnose, LANGUAGE, message } from "../product.ts";
43
+ import { diagnose, message } from "../product.ts";
43
44
 
44
45
  /** The options `analyze` takes beside the ones every verb takes. */
45
46
  export const ANALYZE_OPTIONS: VerbOptions = {
@@ -70,14 +71,12 @@ function formatsOf(named: readonly string[], configured: readonly Format[]): rea
70
71
  const seen = new Set<string>();
71
72
  for (const format of formats) {
72
73
  if (!RENDERINGS.has(format as Format)) {
73
- throw new ConfigError(
74
- "malformed",
75
- "",
74
+ throw new UsageError(
76
75
  `${JSON.stringify(format)} is not a format this analyzer renders: the formats are ${formatNames()}`,
77
76
  );
78
77
  }
79
78
  if (seen.has(format)) {
80
- throw new ConfigError("malformed", "", `the format ${JSON.stringify(format)} is named twice`);
79
+ throw new UsageError(`the format ${JSON.stringify(format)} is named twice`);
81
80
  }
82
81
  seen.add(format);
83
82
  }
@@ -189,9 +188,7 @@ function templateOf(
189
188
  ): Template | undefined {
190
189
  if (named === undefined || named === "") {
191
190
  if (formats.includes("template")) {
192
- throw new ConfigError(
193
- "malformed",
194
- "",
191
+ throw new UsageError(
195
192
  "the template format renders the template --template names, and none was named",
196
193
  );
197
194
  }
@@ -201,7 +198,7 @@ function templateOf(
201
198
  try {
202
199
  source = host.readFile(resolvePath(invoked, named));
203
200
  } catch (error: unknown) {
204
- throw new ConfigError("malformed", "", `--template=${named}: ${message(error)}`);
201
+ throw new UsageError(`--template=${named}: ${message(error)}`);
205
202
  }
206
203
  try {
207
204
  return parseTemplate(source);
@@ -209,7 +206,7 @@ function templateOf(
209
206
  if (!(error instanceof TemplateError)) {
210
207
  throw error;
211
208
  }
212
- throw new ConfigError("malformed", "", `--template=${named}: ${error.message}`);
209
+ throw new UsageError(`--template=${named}: ${error.message}`);
213
210
  }
214
211
  }
215
212
 
@@ -231,25 +228,17 @@ interface Documents {
231
228
  export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
232
229
  const reportPath = option("report");
233
230
  if (reportPath === undefined || reportPath === "") {
234
- throw new ConfigError(
235
- "malformed",
236
- "",
231
+ throw new UsageError(
237
232
  "analyze writes its report to the path --report names, and no path was named",
238
233
  );
239
234
  }
240
235
  const exitCode = option("exit-code") ?? EXIT_CODE_ON;
241
236
  if (exitCode !== EXIT_CODE_ON && exitCode !== EXIT_CODE_OFF) {
242
- throw new ConfigError(
243
- "malformed",
244
- "",
237
+ throw new UsageError(
245
238
  `--exit-code=${JSON.stringify(exitCode)}: the values are ${EXIT_CODE_ON} and ${EXIT_CODE_OFF}`,
246
239
  );
247
240
  }
248
241
  const { config, provenance } = resolve(inputs());
249
- const kind = config.targetKind;
250
- if (kind === "") {
251
- throw new ConfigError("missing-target-kind", "target.kind", "target.kind is not set");
252
- }
253
242
  const invoked = host.workingDirectory();
254
243
  const baseline = option("baseline-write");
255
244
  const documents: Documents = {
@@ -318,7 +307,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
318
307
  conformance: CONFORMANCE,
319
308
  declaredGaps: DECLARED_GAPS,
320
309
  target: {
321
- kind: kindOf(kind, LANGUAGE),
310
+ kind: kindOf(config.target.kind),
322
311
  root,
323
312
  identity: moduleIdentity(host, scoped.target),
324
313
  },
@@ -340,7 +329,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
340
329
  ...unseenLoadNotes(run.unseenLoads),
341
330
  ],
342
331
  });
343
- report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
332
+ report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.max_findings);
344
333
  verdict = verdictOf(result, config);
345
334
  } catch (error: unknown) {
346
335
  if (!(error instanceof ReportPathError)) {
@@ -2,7 +2,6 @@ import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.ts";
2
2
  import { runAnalysis, type RunAnalysis } from "../analysis.ts";
3
3
  import { listing } from "../components.ts";
4
4
  import type { Cascade } from "../config.ts";
5
- import { ConfigError } from "../config.ts";
6
5
  import type { CompletedFinding } from "../finding.ts";
7
6
  import { OUTSIDE, swept as judged } from "../graph.ts";
8
7
  import type { InventorySymbol } from "../inventory.ts";
@@ -14,6 +13,7 @@ import type { Reference } from "../references.ts";
14
13
  import { resolve } from "../resolve.ts";
15
14
  import type { Root } from "../roots.ts";
16
15
  import { type Verb, type VerbOptions } from "./verb.ts";
16
+ import { UsageError } from "../usage.ts";
17
17
  import { bytewise } from "../text/compare.ts";
18
18
  import { diagnose } from "../product.ts";
19
19
 
@@ -47,16 +47,12 @@ function namedSymbol(option: (name: string) => string | undefined): string {
47
47
  });
48
48
  const [only, ...more] = named;
49
49
  if (only === undefined) {
50
- throw new ConfigError(
51
- "malformed",
52
- "",
50
+ throw new UsageError(
53
51
  "explain explains the symbol --why, --why-live or --why-not names, and none was named",
54
52
  );
55
53
  }
56
54
  if (more.length > 0) {
57
- throw new ConfigError(
58
- "malformed",
59
- "",
55
+ throw new UsageError(
60
56
  `explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`,
61
57
  );
62
58
  }
@@ -2,7 +2,7 @@ import { EXIT_CLEAN } from "../contract/contract.generated.ts";
2
2
  import { runProjects } from "../analysis.ts";
3
3
  import { partialNotes } from "../partial.ts";
4
4
  import { resolvePath } from "../paths.ts";
5
- import { resolveUnkinded } from "../resolve.ts";
5
+ import { resolvePartial } from "../resolve.ts";
6
6
  import { type Verb } from "./verb.ts";
7
7
  import { diagnose } from "../product.ts";
8
8
 
@@ -14,7 +14,7 @@ import { diagnose } from "../product.ts";
14
14
  * instead, and one that fails the analysis fails this command the same way.
15
15
  */
16
16
  export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
17
- const config = resolveUnkinded(inputs());
17
+ const config = resolvePartial(inputs());
18
18
  const scoped = scope();
19
19
  const answer = runProjects(openClient(config), host, scoped, config);
20
20
  for (const id of answer.configurations) {
package/src/verbs/verb.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Config, Inputs } from "../config.ts";
1
+ import type { Inputs, PartialConfig } from "../config.ts";
2
2
  import type { Host } from "../host.ts";
3
3
  import type { Writer } from "../run.ts";
4
4
  import type { Scope } from "../scope.ts";
@@ -21,7 +21,7 @@ interface Invocation {
21
21
  * Opens the compiler client for a run under `config`. A client the verb opened is
22
22
  * closed if the verb throws.
23
23
  */
24
- readonly openClient: (config: Config) => Engine;
24
+ readonly openClient: (config: PartialConfig) => Engine;
25
25
  /** The arguments after the verb's name, as written. */
26
26
  readonly args: readonly string[];
27
27
  /** The last value one of the verb's own options was given, undefined where none was. */