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

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 (158) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +3 -2
  3. package/dist/src/analysis.js +25 -43
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +2 -1
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/contract/contract.generated.js +13 -0
  11. package/dist/src/conventions.js +20 -60
  12. package/dist/src/dependencies.js +25 -43
  13. package/dist/src/discover.js +13 -39
  14. package/dist/src/edges.js +7 -15
  15. package/dist/src/element-access.js +4 -6
  16. package/dist/src/entry-point-gaps.js +2 -8
  17. package/dist/src/entry-points.js +3 -10
  18. package/dist/src/enum-group.js +3 -10
  19. package/dist/src/erased-stores.js +4 -6
  20. package/dist/src/exempt-classes.js +2 -4
  21. package/dist/src/exempt.js +6 -22
  22. package/dist/src/file-facts.js +2 -25
  23. package/dist/src/findings/reads-and-writes.js +2 -1
  24. package/dist/src/findings/self-check.js +3 -2
  25. package/dist/src/findings-pass.js +6 -11
  26. package/dist/src/fs/manifest.js +43 -0
  27. package/dist/src/fs/tree.js +88 -0
  28. package/dist/src/git-ignore.js +34 -25
  29. package/dist/src/html-entries.js +6 -33
  30. package/dist/src/in-operands.js +2 -1
  31. package/dist/src/intra-function-parts.js +10 -13
  32. package/dist/src/inventory.js +2 -21
  33. package/dist/src/json-document.js +4 -0
  34. package/dist/src/keyof-comparisons.js +4 -6
  35. package/dist/src/manifest.js +5 -43
  36. package/dist/src/mdx-entries.js +3 -26
  37. package/dist/src/partial.js +2 -1
  38. package/dist/src/paths.js +26 -15
  39. package/dist/src/position.js +2 -1
  40. package/dist/src/print.js +3 -2
  41. package/dist/src/product.js +13 -0
  42. package/dist/src/references.js +14 -27
  43. package/dist/src/report-schema.js +15 -16
  44. package/dist/src/report.js +22 -31
  45. package/dist/src/reporters/sarif.js +4 -2
  46. package/dist/src/reporters/template.js +4 -19
  47. package/dist/src/resolve.js +11 -20
  48. package/dist/src/roots.js +7 -17
  49. package/dist/src/run.js +8 -7
  50. package/dist/src/schema.js +4 -3
  51. package/dist/src/scope.js +4 -5
  52. package/dist/src/serialization-contract.js +4 -8
  53. package/dist/src/session.js +2 -1
  54. package/dist/src/suppress-file.js +8 -10
  55. package/dist/src/symbol-keys.js +3 -5
  56. package/dist/src/template-field.js +5 -15
  57. package/dist/src/test-globals.js +6 -28
  58. package/dist/src/test-joins.js +6 -6
  59. package/dist/src/text/compare.js +26 -0
  60. package/dist/src/text/glob.js +194 -0
  61. package/dist/src/text/shell.js +159 -0
  62. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  63. package/dist/src/type-errors.js +14 -38
  64. package/dist/src/unseen-loads.js +2 -9
  65. package/dist/src/unwrap.js +23 -0
  66. package/dist/src/verbs/analyze.js +15 -13
  67. package/dist/src/verbs/describe.js +4 -4
  68. package/dist/src/verbs/explain.js +5 -3
  69. package/dist/src/verbs/print-projects.js +2 -1
  70. package/dist/src/verbs/print-retained.js +2 -1
  71. package/dist/src/verbs/print-roots.js +2 -1
  72. package/dist/src/verbs/version.js +2 -1
  73. package/dist/src/workflow-steps.js +9 -46
  74. package/dist/src/workspace-manifests.js +2 -2
  75. package/dist/src/workspace-programs.js +5 -3
  76. package/dist/src/workspace-resolution.js +13 -17
  77. package/dist/src/workspace.js +28 -116
  78. package/jsr.json +1 -1
  79. package/package.json +1 -1
  80. package/src/analysis.ts +26 -51
  81. package/src/automatic-types.ts +11 -29
  82. package/src/build-scripts.ts +17 -38
  83. package/src/component-layer.ts +4 -2
  84. package/src/components.ts +2 -4
  85. package/src/config.ts +4 -2
  86. package/src/configuration-files.ts +27 -170
  87. package/src/contract/contract.generated.ts +25 -0
  88. package/src/conventions.ts +26 -74
  89. package/src/dependencies.ts +32 -50
  90. package/src/discover.ts +14 -39
  91. package/src/edges.ts +7 -21
  92. package/src/element-access.ts +4 -7
  93. package/src/entry-point-gaps.ts +3 -9
  94. package/src/entry-points.ts +2 -13
  95. package/src/enum-group.ts +2 -11
  96. package/src/erased-stores.ts +3 -7
  97. package/src/exempt-classes.ts +4 -4
  98. package/src/exempt.ts +5 -27
  99. package/src/file-facts.ts +6 -24
  100. package/src/findings/reads-and-writes.ts +2 -1
  101. package/src/findings/self-check.ts +3 -2
  102. package/src/findings-pass.ts +6 -12
  103. package/src/fs/manifest.ts +52 -0
  104. package/src/fs/tree.ts +110 -0
  105. package/src/git-ignore.ts +38 -25
  106. package/src/html-entries.ts +6 -33
  107. package/src/in-operands.ts +2 -1
  108. package/src/intra-function-parts.ts +10 -14
  109. package/src/inventory.ts +2 -20
  110. package/src/json-document.ts +5 -0
  111. package/src/keyof-comparisons.ts +3 -7
  112. package/src/manifest.ts +5 -46
  113. package/src/mdx-entries.ts +3 -26
  114. package/src/partial.ts +2 -1
  115. package/src/paths.ts +27 -15
  116. package/src/position.ts +2 -1
  117. package/src/print.ts +3 -2
  118. package/src/product.ts +18 -0
  119. package/src/references.ts +14 -29
  120. package/src/report-schema.ts +15 -17
  121. package/src/report.ts +24 -34
  122. package/src/reporters/sarif.ts +4 -4
  123. package/src/reporters/template.ts +4 -20
  124. package/src/resolve.ts +20 -27
  125. package/src/roots.ts +7 -20
  126. package/src/run.ts +8 -13
  127. package/src/schema.ts +4 -3
  128. package/src/scope.ts +4 -10
  129. package/src/serialization-contract.ts +4 -9
  130. package/src/session.ts +2 -1
  131. package/src/suppress-file.ts +8 -11
  132. package/src/symbol-keys.ts +2 -5
  133. package/src/template-field.ts +5 -18
  134. package/src/test-globals.ts +9 -27
  135. package/src/test-joins.ts +6 -8
  136. package/src/text/compare.ts +27 -0
  137. package/src/text/glob.ts +210 -0
  138. package/src/text/shell.ts +161 -0
  139. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  140. package/src/type-errors.ts +25 -36
  141. package/src/unseen-loads.ts +1 -12
  142. package/src/unwrap.ts +34 -0
  143. package/src/verbs/analyze.ts +22 -21
  144. package/src/verbs/describe.ts +4 -4
  145. package/src/verbs/explain.ts +5 -3
  146. package/src/verbs/print-projects.ts +2 -1
  147. package/src/verbs/print-retained.ts +2 -1
  148. package/src/verbs/print-roots.ts +2 -1
  149. package/src/verbs/version.ts +2 -1
  150. package/src/workflow-steps.ts +9 -44
  151. package/src/workspace-manifests.ts +2 -2
  152. package/src/workspace-programs.ts +5 -3
  153. package/src/workspace-resolution.ts +21 -17
  154. package/src/workspace.ts +35 -121
  155. package/dist/src/glob.js +0 -65
  156. package/dist/src/pnpm-workspace.js +0 -120
  157. package/src/glob.ts +0 -67
  158. package/src/pnpm-workspace.ts +0 -135
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  import { REPORT_SCHEMAS, type Report } from "./contract/report.generated.ts";
10
+ import { bytewise } from "./text/compare.ts";
11
+ import { isRecord } from "./json-document.ts";
10
12
 
11
13
  /** One value a schema refuses: its JSON Pointer, the keyword, and why. */
12
14
  interface SchemaRefusal {
@@ -41,10 +43,6 @@ function pattern(source: string): RegExp {
41
43
  return held;
42
44
  }
43
45
 
44
- function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
45
- return value !== null && typeof value === "object" && !Array.isArray(value);
46
- }
47
-
48
46
  function typeOf(value: unknown): string {
49
47
  if (value === null) {
50
48
  return "null";
@@ -68,9 +66,9 @@ function canonical(value: unknown): string {
68
66
  if (Array.isArray(value)) {
69
67
  return `[${value.map(canonical).join(",")}]`;
70
68
  }
71
- if (isObject(value)) {
69
+ if (isRecord(value)) {
72
70
  return `{${Object.keys(value)
73
- .sort()
71
+ .sort(bytewise)
74
72
  .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
75
73
  .join(",")}}`;
76
74
  }
@@ -101,11 +99,11 @@ class Checker {
101
99
  const target = file === "" ? doc : file;
102
100
  let schema: unknown = this.documents[target];
103
101
  for (const token of pointer.split("/").filter((one) => one !== "")) {
104
- schema = isObject(schema)
102
+ schema = isRecord(schema)
105
103
  ? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
106
104
  : undefined;
107
105
  }
108
- if (!isObject(schema)) {
106
+ if (!isRecord(schema)) {
109
107
  throw new Error(`report schema: ${ref} names no schema`);
110
108
  }
111
109
  return [target, schema];
@@ -141,7 +139,7 @@ class Checker {
141
139
  this.checkValue(schema, value, refuse);
142
140
  if (Array.isArray(value)) {
143
141
  this.checkArray(doc, schema, value, at, found, refuse);
144
- } else if (isObject(value)) {
142
+ } else if (isRecord(value)) {
145
143
  this.checkObject(doc, schema, value, at, found, refuse);
146
144
  }
147
145
  this.checkApplicators(doc, schema, value, at, found, refuse);
@@ -188,19 +186,19 @@ class Checker {
188
186
  found: SchemaRefusal[],
189
187
  refuse: (keyword: string, reason: string) => void,
190
188
  ): void {
191
- const properties = isObject(schema["properties"]) ? schema["properties"] : {};
189
+ const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
192
190
  const additional = schema["additionalProperties"];
193
- for (const name of Object.keys(value).sort()) {
191
+ for (const name of Object.keys(value).sort(bytewise)) {
194
192
  const member = `${at}/${escape(name)}`;
195
193
  const declared = properties[name];
196
- if (isObject(declared)) {
194
+ if (isRecord(declared)) {
197
195
  this.check(doc, declared, value[name], member, found);
198
196
  } else if (additional === false) {
199
197
  refuse(
200
198
  "additionalProperties",
201
199
  `the member ${JSON.stringify(name)} is not one the schema declares`,
202
200
  );
203
- } else if (isObject(additional)) {
201
+ } else if (isRecord(additional)) {
204
202
  this.check(doc, additional, value[name], member, found);
205
203
  }
206
204
  }
@@ -227,7 +225,7 @@ class Checker {
227
225
  refuse: (keyword: string, reason: string) => void,
228
226
  ): void {
229
227
  const items = schema["items"];
230
- if (isObject(items)) {
228
+ if (isRecord(items)) {
231
229
  value.forEach((item, i) => {
232
230
  this.check(doc, items, item, `${at}/${String(i)}`, found);
233
231
  });
@@ -273,12 +271,12 @@ class Checker {
273
271
  `the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
274
272
  );
275
273
  }
276
- if (isObject(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
274
+ if (isRecord(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
277
275
  refuse("not", "the value matches a schema it must not match");
278
276
  }
279
- if (isObject(schema["if"])) {
277
+ if (isRecord(schema["if"])) {
280
278
  const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
281
- if (isObject(branch)) {
279
+ if (isRecord(branch)) {
282
280
  this.check(doc, branch, value, at, found);
283
281
  }
284
282
  }
package/src/report.ts CHANGED
@@ -19,12 +19,8 @@ import type { AppliedConvention } from "./conventions.ts";
19
19
  import type { PassResult } from "./findings-pass.ts";
20
20
  import type { StaleSuppression } from "./findings/self-check.ts";
21
21
  import type { TestFileRule } from "./references.ts";
22
-
23
- /** The name this analyzer writes every document under and mints every identifier with. */
24
- export const ANALYZER_NAME = "deadset-ts";
25
-
26
- /** The language this analyzer claims. */
27
- export const LANGUAGE = "ts";
22
+ import { bytewise } from "./text/compare.ts";
23
+ import { LANGUAGE, PRODUCT } from "./product.ts";
28
24
 
29
25
  /** One project configuration of the matrix a run analyzed. */
30
26
  export interface ReportConfiguration {
@@ -131,14 +127,6 @@ function componentsOf(
131
127
  return [...listed.values()];
132
128
  }
133
129
 
134
- /** Two strings ordered bytewise. */
135
- function compare(a: string, b: string): number {
136
- if (a === b) {
137
- return 0;
138
- }
139
- return a < b ? -1 : 1;
140
- }
141
-
142
130
  function wirePosition(position: FindingPosition): wire.Position {
143
131
  return {
144
132
  path: position.path,
@@ -257,22 +245,22 @@ function wireStale(stale: StaleSuppression): wire.StaleSuppressionsEntry {
257
245
  /** The canonical key over two findings: path, line, column, code, then the symbol reference. */
258
246
  function canonical(a: wire.Finding, b: wire.Finding): number {
259
247
  return (
260
- compare(a.position.path, b.position.path) ||
248
+ bytewise(a.position.path, b.position.path) ||
261
249
  a.position.line - b.position.line ||
262
250
  a.position.column - b.position.column ||
263
- compare(a.code, b.code) ||
264
- compare(a.symbol.ref, b.symbol.ref)
251
+ bytewise(a.code, b.code) ||
252
+ bytewise(a.symbol.ref, b.symbol.ref)
265
253
  );
266
254
  }
267
255
 
268
256
  /** The canonical key over two stale suppressions, each component from the record's own member. */
269
257
  function canonicalStale(a: wire.StaleSuppressionsEntry, b: wire.StaleSuppressionsEntry): number {
270
258
  return (
271
- compare(a.position.path, b.position.path) ||
259
+ bytewise(a.position.path, b.position.path) ||
272
260
  a.position.line - b.position.line ||
273
261
  a.position.column - b.position.column ||
274
- compare(a.code, b.code) ||
275
- compare(a.symbol, b.symbol)
262
+ bytewise(a.code, b.code) ||
263
+ bytewise(a.symbol, b.symbol)
276
264
  );
277
265
  }
278
266
 
@@ -289,9 +277,9 @@ function wireGap(gap: DeclaredGap): wire.DeclaredGapsEntry {
289
277
  /** Edge evaluations by edge, then side, then their compact encodings. */
290
278
  function byEdge(a: wire.EdgeEvaluationsEntry, b: wire.EdgeEvaluationsEntry): number {
291
279
  return (
292
- compare(a.edge, b.edge) ||
293
- compare(a.side, b.side) ||
294
- compare(JSON.stringify(a), JSON.stringify(b))
280
+ bytewise(a.edge, b.edge) ||
281
+ bytewise(a.side, b.side) ||
282
+ bytewise(JSON.stringify(a), JSON.stringify(b))
295
283
  );
296
284
  }
297
285
 
@@ -346,16 +334,16 @@ export function buildReport(input: ReportInput): wire.Report {
346
334
  one.finding === undefined ? [] : [[wireFinding(one.finding), one.finding] as const],
347
335
  );
348
336
  const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
349
- .sort((a, b) => compare(a.id, b.id))
337
+ .sort((a, b) => bytewise(a.id, b.id))
350
338
  .map((one) => ({ id: one.id, role: "consumer" as const, reason: one.reason }));
351
339
  const loaded = [...input.loaded]
352
- .sort((a, b) => compare(a.id, b.id) || compare(a.path, b.path))
340
+ .sort((a, b) => bytewise(a.id, b.id) || bytewise(a.path, b.path))
353
341
  .map((one) => ({ id: one.id, role: "consumer" as const, path: one.path }));
354
342
  return {
355
343
  schema_version: SCHEMA_VERSION,
356
344
  contract_version: input.contractVersion,
357
345
  analyzer: {
358
- name: ANALYZER_NAME,
346
+ name: PRODUCT,
359
347
  version: input.version,
360
348
  languages: [LANGUAGE],
361
349
  schema_versions_accepted: SCHEMA_VERSIONS,
@@ -367,10 +355,10 @@ export function buildReport(input: ReportInput): wire.Report {
367
355
  },
368
356
  target: { kind: input.target.kind, root: input.target.root, identity: input.target.identity },
369
357
  configurations: [...input.configurations]
370
- .sort((a, b) => compare(a.id, b.id))
358
+ .sort((a, b) => bytewise(a.id, b.id))
371
359
  .map((one) => ({ id: one.id, project: one.project })),
372
360
  configurations_not_built: [...input.notBuilt]
373
- .sort((a, b) => compare(a.id, b.id))
361
+ .sort((a, b) => bytewise(a.id, b.id))
374
362
  .map((one) => ({ id: one.id, project: one.project, error: one.error })),
375
363
  consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
376
364
  findings,
@@ -383,10 +371,10 @@ export function buildReport(input: ReportInput): wire.Report {
383
371
  // name, so the compact encodings alone order them.
384
372
  declared_gaps: input.declaredGaps
385
373
  .map(wireGap)
386
- .sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b))),
374
+ .sort((a, b) => bytewise(JSON.stringify(a), JSON.stringify(b))),
387
375
  excluded_by_cgo: [],
388
376
  test_file_rules: [...input.testFileRules]
389
- .sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
377
+ .sort((a, b) => bytewise(a.rule, b.rule) || a.matched - b.matched)
390
378
  .map((one) => ({ rule: one.rule, matched: one.matched })),
391
379
  type_error_skips: [
392
380
  ...new Map(
@@ -396,16 +384,18 @@ export function buildReport(input: ReportInput): wire.Report {
396
384
  }),
397
385
  ),
398
386
  ]
399
- .sort(([a, x], [b, y]) => compare(x.path, y.path) || x.line - y.line || compare(a, b))
387
+ .sort(([a, x], [b, y]) => bytewise(x.path, y.path) || x.line - y.line || bytewise(a, b))
400
388
  .map(([, skip]) => skip),
401
389
  notes: [...new Map((input.notes ?? []).map((one) => [JSON.stringify(one), one])).entries()]
402
- .sort(([a, x], [b, y]) => compare(x.kind, y.kind) || compare(x.path, y.path) || compare(a, b))
390
+ .sort(
391
+ ([a, x], [b, y]) => bytewise(x.kind, y.kind) || bytewise(x.path, y.path) || bytewise(a, b),
392
+ )
403
393
  .map(([, note]) => note),
404
394
  unanswered_questions: [...input.unanswered]
405
395
  .sort(
406
396
  (a, b) =>
407
- compare(a.configuration, b.configuration) ||
408
- compare(JSON.stringify(a), JSON.stringify(b)),
397
+ bytewise(a.configuration, b.configuration) ||
398
+ bytewise(JSON.stringify(a), JSON.stringify(b)),
409
399
  )
410
400
  .map((one) => ({
411
401
  configuration: one.configuration,
@@ -12,6 +12,8 @@ import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
12
12
  import { RenderError, type RenderOptions } from "./reporter.ts";
13
13
  import { utf8Bytes } from "./sha256.ts";
14
14
  import { withheldLine } from "./text.ts";
15
+ import { bytewise } from "../text/compare.ts";
16
+ import { message } from "../product.ts";
15
17
 
16
18
  const SCHEMA =
17
19
  "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
@@ -133,9 +135,7 @@ function lineHashCache(read: (path: string) => string): (at: Located) => string
133
135
  try {
134
136
  text = read(at.path);
135
137
  } catch (error: unknown) {
136
- throw new RenderError(
137
- `read ${at.path} for its line fingerprint: ${error instanceof Error ? error.message : String(error)}`,
138
- );
138
+ throw new RenderError(`read ${at.path} for its line fingerprint: ${message(error)}`);
139
139
  }
140
140
  hashes = lineHashes(text);
141
141
  held.set(at.path, hashes);
@@ -292,7 +292,7 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
292
292
  ...report.findings.map((found) => findingResult(found, index, hashOf, members)),
293
293
  ...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
294
294
  ];
295
- const languages = [...report.analyzer.languages].sort().join("+");
295
+ const languages = [...report.analyzer.languages].sort(bytewise).join("+");
296
296
  const withheld = withheldLine(report.totals);
297
297
  const log = {
298
298
  $schema: SCHEMA,
@@ -10,6 +10,7 @@
10
10
  import { CHUNK_LENGTH } from "../json-chunks.ts";
11
11
  import { RenderError } from "./reporter.ts";
12
12
  import { utf8Bytes } from "./sha256.ts";
13
+ import { bytewise } from "../text/compare.ts";
13
14
 
14
15
  /** A template that does not parse, which refuses the invocation before any analysis. */
15
16
  export class TemplateError extends Error {
@@ -618,7 +619,7 @@ function formatted(value: Value, nested: boolean): string {
618
619
  }
619
620
  const object: ValueObject = value;
620
621
  const entries = Object.keys(object)
621
- .sort()
622
+ .sort(bytewise)
622
623
  .map((key) => `${key}:${formatted(object[key] ?? null, true)}`);
623
624
  return `map[${entries.join(" ")}]`;
624
625
  }
@@ -647,28 +648,11 @@ function compareBasic(name: string, a: Value, b: Value): number {
647
648
  return Number.NaN;
648
649
  }
649
650
  if (kind === "string") {
650
- return byCodePoints(a as string, b as string);
651
+ return bytewise(a as string, b as string);
651
652
  }
652
653
  return (a as number) < (b as number) ? -1 : 1;
653
654
  }
654
655
 
655
- /**
656
- * Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
657
- * code points: the order of their UTF-16 code units differs above U+FFFF.
658
- */
659
- function byCodePoints(a: string, b: string): number {
660
- let at = 0;
661
- while (at < a.length && at < b.length) {
662
- const x = a.codePointAt(at) ?? 0;
663
- const y = b.codePointAt(at) ?? 0;
664
- if (x !== y) {
665
- return x < y ? -1 : 1;
666
- }
667
- at += x > 0xffff ? 2 : 1;
668
- }
669
- return a.length - b.length;
670
- }
671
-
672
656
  /** The escapes `%q` writes for the control characters that have one of their own. */
673
657
  const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
674
658
  '"': '\\"',
@@ -1022,7 +1006,7 @@ class Execution {
1022
1006
  entries = value.map((one, i) => [i, one]);
1023
1007
  } else if (isObject(value)) {
1024
1008
  entries = Object.keys(value)
1025
- .sort()
1009
+ .sort(bytewise)
1026
1010
  .map((key) => [key, value[key] ?? null]);
1027
1011
  } else {
1028
1012
  throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
package/src/resolve.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  type TemplateDelimiters,
31
31
  } from "./config.ts";
32
32
  import { CONVENTION_ROWS } from "./convention-rows.ts";
33
+ import { EXEMPTION_CLASS_NAMES, type ExemptionClass } from "./exempt-classes.ts";
33
34
  import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.ts";
34
35
  import {
35
36
  checkDocument,
@@ -39,12 +40,14 @@ import {
39
40
  SEVERITY_SECTION,
40
41
  type KeyNode,
41
42
  } from "./schema.ts";
43
+ import { bytewise } from "./text/compare.ts";
44
+ import { isRecord } from "./json-document.ts";
45
+ import { message } from "./product.ts";
42
46
 
43
47
  /** Names the command-line flags in a refusal, where a document is named by its path. */
44
48
  const FLAGS_LABEL = "the command-line flags";
45
49
 
46
50
  const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
47
- const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
48
51
  const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
49
52
  const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
50
53
  const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
@@ -98,7 +101,7 @@ interface Doc {
98
101
  readonly consumersComplete: boolean | undefined;
99
102
  readonly rootPatterns: readonly string[] | undefined;
100
103
  readonly severity: ReadonlyMap<string, Severity> | undefined;
101
- readonly exemptionsDisabled: readonly string[] | undefined;
104
+ readonly exemptionsDisabled: readonly ExemptionClass[] | undefined;
102
105
  readonly formats: readonly Format[] | undefined;
103
106
  readonly sort: Sort | undefined;
104
107
  readonly cascade: Cascade | undefined;
@@ -134,10 +137,6 @@ function originOf(source: DocSource, path: string): Origin {
134
137
  return { source: source.kind, label: source.label };
135
138
  }
136
139
 
137
- function isRecord(value: unknown): value is Record<string, unknown> {
138
- return typeof value === "object" && value !== null && !Array.isArray(value);
139
- }
140
-
141
140
  function member(parent: Record<string, unknown> | undefined, name: string): unknown {
142
141
  if (parent === undefined || !Object.hasOwn(parent, name)) {
143
142
  return undefined;
@@ -505,7 +504,7 @@ function fixedByContract(code: string): string[] {
505
504
  return FIXED_SEVERITY_CODES.filter(
506
505
  (candidate) =>
507
506
  code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)),
508
- ).sort();
507
+ ).sort(bytewise);
509
508
  }
510
509
 
511
510
  /** The codes a refusal names: one on its own, and several with the last joined by and. */
@@ -543,7 +542,7 @@ function readSeverity(
543
542
  return undefined;
544
543
  }
545
544
  const severity = new Map<string, Severity>();
546
- for (const code of Object.keys(object).sort()) {
545
+ for (const code of Object.keys(object).sort(bytewise)) {
547
546
  const path = `${SEVERITY_SECTION}.${code}`;
548
547
  if (!SEVERITY_KEY.test(code)) {
549
548
  throw unimplementedSeverityKey(
@@ -577,26 +576,20 @@ function readSeverity(
577
576
  return severity;
578
577
  }
579
578
 
580
- /** The exemption classes named for switching off. */
579
+ /** The exemption classes named for switching off, each one the vocabulary holds. */
581
580
  function readExemptions(
582
581
  document: Record<string, unknown> | undefined,
583
582
  label: string,
584
- ): readonly string[] | undefined {
583
+ ): readonly ExemptionClass[] | undefined {
585
584
  const exemptions = section(document, "exemptions", "exemptions", label);
586
- const disabled = readStrings(exemptions, "disabled", "exemptions.disabled", label, 0);
587
- if (disabled === undefined) {
588
- return undefined;
589
- }
590
- for (const entry of disabled) {
591
- if (!EXEMPTION_CLASS.test(entry)) {
592
- throw malformed(
593
- label,
594
- "exemptions.disabled",
595
- `${JSON.stringify(entry)} is not an exemption class name`,
596
- );
597
- }
598
- }
599
- return disabled;
585
+ return readStrings(
586
+ exemptions,
587
+ "disabled",
588
+ "exemptions.disabled",
589
+ label,
590
+ 0,
591
+ EXEMPTION_CLASS_NAMES,
592
+ );
600
593
  }
601
594
 
602
595
  /** The members that name a declaration, each belonging to exactly one shape. */
@@ -905,7 +898,7 @@ function parseDocument(text: string, node: KeyNode, label: string): unknown {
905
898
  try {
906
899
  value = JSON.parse(text);
907
900
  } catch (error: unknown) {
908
- throw malformed(label, "", error instanceof Error ? error.message : String(error));
901
+ throw malformed(label, "", message(error));
909
902
  }
910
903
  checkDocument(text, node, label);
911
904
  return value;
@@ -1002,7 +995,7 @@ function settleSeverity(
1002
995
  }
1003
996
  }
1004
997
  const severity = new Map<string, Severity>();
1005
- for (const code of [...codes].sort()) {
998
+ for (const code of [...codes].sort(bytewise)) {
1006
999
  const path = `${SEVERITY_SECTION}.${code}`;
1007
1000
  for (const source of sources) {
1008
1001
  const value = source.doc.severity?.get(code);
@@ -1044,7 +1037,7 @@ function describeSource(
1044
1037
  * print a provenance value the closed key list refuses to read back.
1045
1038
  */
1046
1039
  function checkLabels(provenance: ReadonlyMap<string, Origin>): void {
1047
- for (const path of [...provenance.keys()].sort()) {
1040
+ for (const path of [...provenance.keys()].sort(bytewise)) {
1048
1041
  const origin = provenance.get(path);
1049
1042
  if (origin !== undefined && origin.source !== "default" && origin.label === "") {
1050
1043
  throw malformed(
package/src/roots.ts CHANGED
@@ -43,17 +43,19 @@ import {
43
43
  } from "./configuration-files.ts";
44
44
  import { conventionEntries, type Conventions } from "./conventions.ts";
45
45
  import { entryPoints, type EntryRule } from "./entry-points.ts";
46
- import { globExpression } from "./glob.ts";
47
46
  import type { Host } from "./host.ts";
48
47
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
49
48
  import type { Manifest, ManifestEntry } from "./manifest.ts";
50
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
49
+ import { ancestors, dirnamePath, relativePath, resolvePath } from "./paths.ts";
51
50
  import { byPosition } from "./position.ts";
52
51
  import { UNANSWERED } from "./query.ts";
53
52
  import type { ProjectView } from "./session.ts";
54
53
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
55
54
  import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
56
55
  import { typeQueryAliases } from "./type-query-alias.ts";
56
+ import { bytewise } from "./text/compare.ts";
57
+ import { holdsManifest } from "./fs/manifest.ts";
58
+ import { globExpression } from "./text/glob.ts";
57
59
 
58
60
  /** Why one declaration is a root. */
59
61
  export type RootKind =
@@ -197,15 +199,8 @@ const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
197
199
 
198
200
  /** Whether no `package.json` sits between one file and the manifest's directory, which holds it. */
199
201
  function ofPackage(host: Host, dir: string, file: string): boolean {
200
- for (let at = dirnamePath(file); at !== dir; at = dirnamePath(at)) {
201
- if (
202
- relativePath(dir, at) === undefined ||
203
- host.kindOf(joinPath(at, "package.json")) === "file"
204
- ) {
205
- return false;
206
- }
207
- }
208
- return true;
202
+ const between = ancestors(dirnamePath(file), dir);
203
+ return between.length > 0 && !between.slice(0, -1).some((at) => holdsManifest(host, at));
209
204
  }
210
205
 
211
206
  /**
@@ -331,14 +326,6 @@ export function matchRef(pattern: string, ref: string): boolean {
331
326
  return matchGlob(Array.from(pattern), Array.from(ref));
332
327
  }
333
328
 
334
- /** Two strings ordered bytewise, which is the order every set of a run is read in. */
335
- function compare(a: string, b: string): number {
336
- if (a === b) {
337
- return 0;
338
- }
339
- return a < b ? -1 : 1;
340
- }
341
-
342
329
  /** The members of one declaration, indexed by the container that holds them. */
343
330
  function childrenOf(held: Inventory): ReadonlyMap<string, readonly InventorySymbol[]> {
344
331
  const byParent = new Map<string, InventorySymbol[]>();
@@ -561,7 +548,7 @@ export function roots<Brand>(
561
548
  const left = sites.get(a.id);
562
549
  const right = sites.get(b.id);
563
550
  const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
564
- return site || compare(a.kind, b.kind) || compare(a.source, b.source);
551
+ return site || bytewise(a.kind, b.kind) || bytewise(a.source, b.source);
565
552
  });
566
553
 
567
554
  return {
package/src/run.ts CHANGED
@@ -15,6 +15,7 @@ import { describeVerb } from "./verbs/describe.ts";
15
15
  import { EXPLAIN_OPTIONS, explainVerb } from "./verbs/explain.ts";
16
16
  import { type Verb, type VerbOptions } from "./verbs/verb.ts";
17
17
  import { versionVerb } from "./verbs/version.ts";
18
+ import { diagnose, message, PRODUCT } from "./product.ts";
18
19
 
19
20
  /** One output stream of the command line. `process.stdout` and `process.stderr` satisfy it. */
20
21
  export interface Writer {
@@ -37,7 +38,7 @@ const VERBS: readonly {
37
38
  { name: "version", run: versionVerb },
38
39
  ];
39
40
 
40
- const USAGE = `usage: deadset-ts <verb> [options]\nverbs: ${VERBS.map((verb) => verb.name).join(", ")}\n`;
41
+ const USAGE = `usage: ${PRODUCT} <verb> [options]\nverbs: ${VERBS.map((verb) => verb.name).join(", ")}\n`;
41
42
 
42
43
  /**
43
44
  * The tokens whose presence in an option's name makes that option a request to
@@ -141,11 +142,7 @@ function readFileOr(host: Host, path: string, orUndefined: boolean): string | un
141
142
  if (orUndefined) {
142
143
  return undefined;
143
144
  }
144
- throw new ConfigError(
145
- "malformed",
146
- "",
147
- `${path}: ${error instanceof Error ? error.message : String(error)}`,
148
- );
145
+ throw new ConfigError("malformed", "", `${path}: ${message(error)}`);
149
146
  }
150
147
  }
151
148
 
@@ -209,9 +206,7 @@ export function run(
209
206
  ): number {
210
207
  const editing = sourceEditOption(args);
211
208
  if (editing !== undefined) {
212
- err.write(
213
- `deadset-ts: ${editing} is not supported: deadset-ts reports and never edits a source file\n`,
214
- );
209
+ diagnose(err, `${editing} is not supported: ${PRODUCT} reports and never edits a source file`);
215
210
  err.write(USAGE);
216
211
  return EXIT_USAGE;
217
212
  }
@@ -228,7 +223,7 @@ export function run(
228
223
  );
229
224
  }
230
225
  if (name !== undefined) {
231
- err.write(`deadset-ts: unknown verb ${JSON.stringify(name)}\n`);
226
+ diagnose(err, `unknown verb ${JSON.stringify(name)}`);
232
227
  }
233
228
  err.write(USAGE);
234
229
  return EXIT_USAGE;
@@ -274,7 +269,7 @@ function invoke(
274
269
  /** Turns one refusal into the exit code the Contract's table gives it. */
275
270
  function refuse(error: unknown, err: Writer): number {
276
271
  if (error instanceof ConfigError) {
277
- err.write(`deadset-ts: ${error.message}\n`);
272
+ diagnose(err, error.message);
278
273
  err.write(USAGE);
279
274
  return EXIT_USAGE;
280
275
  }
@@ -282,7 +277,7 @@ function refuse(error: unknown, err: Writer): number {
282
277
  for (const diagnostic of error.diagnostics) {
283
278
  err.write(`${renderDiagnostic(diagnostic)}\n`);
284
279
  }
285
- err.write(`deadset-ts: ${error.message}\n`);
280
+ diagnose(err, error.message);
286
281
  return EXIT_FAILURE;
287
282
  }
288
283
  if (error instanceof SetupError) {
@@ -292,7 +287,7 @@ function refuse(error: unknown, err: Writer): number {
292
287
  return EXIT_FAILURE;
293
288
  }
294
289
  if (error instanceof ScopeError) {
295
- err.write(`deadset-ts: ${error.message}\n`);
290
+ diagnose(err, error.message);
296
291
  return EXIT_FAILURE;
297
292
  }
298
293
  throw error;
package/src/schema.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ConfigError } from "./config.ts";
2
+ import { bytewise } from "./text/compare.ts";
2
3
 
3
4
  /**
4
5
  * How one node of the closed key list is written: `leaf`, a scalar or an array of
@@ -157,7 +158,7 @@ function collect(node: KeyNode, at: string, keys: string[], settingsOnly: boolea
157
158
  export function declaredKeys(): string[] {
158
159
  const keys: string[] = [];
159
160
  collect(SCHEMA_ROOT, "", keys, false);
160
- return keys.sort();
161
+ return keys.sort(bytewise);
161
162
  }
162
163
 
163
164
  /**
@@ -169,7 +170,7 @@ export function declaredKeys(): string[] {
169
170
  export function settingPaths(): string[] {
170
171
  const paths: string[] = [];
171
172
  collect(SCHEMA_ROOT, "", paths, true);
172
- return paths.sort();
173
+ return paths.sort(bytewise);
173
174
  }
174
175
 
175
176
  /**
@@ -445,7 +446,7 @@ export function checkDocument(text: string, node: KeyNode, label: string): void
445
446
  */
446
447
  export function nestFlags(flat: Readonly<Record<string, unknown>>, label: string): unknown {
447
448
  const nested: Record<string, unknown> = {};
448
- for (const path of Object.keys(flat).sort()) {
449
+ for (const path of Object.keys(flat).sort(bytewise)) {
449
450
  if (!declaresSetting(path)) {
450
451
  throw unimplementedKey(label, path);
451
452
  }
package/src/scope.ts CHANGED
@@ -2,6 +2,8 @@ import { ConfigError } from "./config.ts";
2
2
  import type { Host } from "./host.ts";
3
3
  import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
4
4
  import { checkDocument, type KeyNode } from "./schema.ts";
5
+ import { isRecord } from "./json-document.ts";
6
+ import { message } from "./product.ts";
5
7
 
6
8
  /** The roles a scope document declares for a module. */
7
9
  const ROLE_TARGET = "target";
@@ -60,10 +62,6 @@ const SCOPE_ROOT: KeyNode = {
60
62
  },
61
63
  };
62
64
 
63
- function isRecord(value: unknown): value is Record<string, unknown> {
64
- return typeof value === "object" && value !== null && !Array.isArray(value);
65
- }
66
-
67
65
  /**
68
66
  * One string member, or undefined where the document omits it. A member the
69
67
  * document writes is a string holding at least one character, so `null` and `""`
@@ -145,9 +143,7 @@ export function readScope(host: Host, file: string): Scope {
145
143
  try {
146
144
  text = host.readFile(absolute);
147
145
  } catch (error: unknown) {
148
- throw new ScopeError(
149
- `scope: ${file}: ${error instanceof Error ? error.message : String(error)}`,
150
- );
146
+ throw new ScopeError(`scope: ${file}: ${message(error)}`);
151
147
  }
152
148
  if (text.length > MAX_DOCUMENT_LENGTH) {
153
149
  throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
@@ -156,9 +152,7 @@ export function readScope(host: Host, file: string): Scope {
156
152
  try {
157
153
  value = JSON.parse(text);
158
154
  } catch (error: unknown) {
159
- throw new ScopeError(
160
- `scope: ${file}: ${error instanceof Error ? error.message : String(error)}`,
161
- );
155
+ throw new ScopeError(`scope: ${file}: ${message(error)}`);
162
156
  }
163
157
  if (!isRecord(value)) {
164
158
  throw new ScopeError(`scope: ${file} is not one JSON object`);