@cplieger/deadset-ts 8.0.0-dev.6 → 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 (193) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +24 -5
  3. package/dist/src/analysis.js +37 -56
  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 +6 -82
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/configured-declarations.js +15 -20
  11. package/dist/src/consumers.js +2 -2
  12. package/dist/src/contract/config.generated.js +544 -0
  13. package/dist/src/contract/contract.generated.js +13 -0
  14. package/dist/src/contract/report.generated.js +3 -2
  15. package/dist/src/conventions.js +20 -60
  16. package/dist/src/dependencies.js +25 -43
  17. package/dist/src/discover.js +14 -40
  18. package/dist/src/edges.js +38 -122
  19. package/dist/src/element-access.js +4 -6
  20. package/dist/src/entry-point-gaps.js +2 -8
  21. package/dist/src/entry-points.js +3 -10
  22. package/dist/src/enum-group.js +3 -10
  23. package/dist/src/erased-stores.js +4 -6
  24. package/dist/src/exempt-classes.js +0 -4
  25. package/dist/src/exempt.js +6 -22
  26. package/dist/src/file-facts.js +2 -25
  27. package/dist/src/findings/completion.js +5 -4
  28. package/dist/src/findings/emitters.js +1 -1
  29. package/dist/src/findings/intra-function.js +1 -1
  30. package/dist/src/findings/non-code-artifacts.js +2 -2
  31. package/dist/src/findings/reads-and-writes.js +2 -1
  32. package/dist/src/findings/self-check.js +3 -2
  33. package/dist/src/findings/unused-declarations.js +1 -1
  34. package/dist/src/findings/visibility-narrowing.js +2 -2
  35. package/dist/src/findings-pass.js +7 -12
  36. package/dist/src/framework-lifecycle.js +7 -7
  37. package/dist/src/fs/document.js +59 -0
  38. package/dist/src/fs/manifest.js +43 -0
  39. package/dist/src/fs/tree.js +88 -0
  40. package/dist/src/git-ignore.js +34 -25
  41. package/dist/src/html-entries.js +6 -33
  42. package/dist/src/in-operands.js +2 -1
  43. package/dist/src/injection-container.js +1 -1
  44. package/dist/src/intra-function-parts.js +10 -13
  45. package/dist/src/inventory.js +2 -21
  46. package/dist/src/json-document.js +198 -67
  47. package/dist/src/keyof-comparisons.js +4 -6
  48. package/dist/src/kinds.js +0 -7
  49. package/dist/src/manifest.js +5 -43
  50. package/dist/src/mdx-entries.js +3 -26
  51. package/dist/src/partial.js +2 -1
  52. package/dist/src/paths.js +26 -15
  53. package/dist/src/position.js +2 -1
  54. package/dist/src/print.js +11 -79
  55. package/dist/src/product.js +13 -0
  56. package/dist/src/references.js +14 -27
  57. package/dist/src/report-schema.js +3 -225
  58. package/dist/src/report.js +22 -31
  59. package/dist/src/reporters/sarif.js +4 -2
  60. package/dist/src/reporters/template.js +4 -19
  61. package/dist/src/resolve.js +245 -724
  62. package/dist/src/roots.js +7 -17
  63. package/dist/src/run.js +55 -61
  64. package/dist/src/schema.js +320 -343
  65. package/dist/src/scope.js +34 -103
  66. package/dist/src/serialization-contract.js +5 -9
  67. package/dist/src/session.js +2 -1
  68. package/dist/src/suppress-file.js +21 -25
  69. package/dist/src/suppress.js +3 -3
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +9 -19
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/usage.js +10 -0
  82. package/dist/src/verbs/analyze.js +25 -26
  83. package/dist/src/verbs/describe.js +4 -4
  84. package/dist/src/verbs/explain.js +8 -6
  85. package/dist/src/verbs/print-projects.js +4 -3
  86. package/dist/src/verbs/print-retained.js +2 -1
  87. package/dist/src/verbs/print-roots.js +2 -1
  88. package/dist/src/verbs/version.js +2 -1
  89. package/dist/src/workflow-steps.js +9 -46
  90. package/dist/src/workspace-manifests.js +2 -2
  91. package/dist/src/workspace-programs.js +5 -3
  92. package/dist/src/workspace-resolution.js +13 -17
  93. package/dist/src/workspace.js +28 -116
  94. package/jsr.json +1 -1
  95. package/package.json +1 -1
  96. package/src/analysis.ts +55 -76
  97. package/src/automatic-types.ts +11 -29
  98. package/src/build-scripts.ts +17 -38
  99. package/src/component-layer.ts +4 -2
  100. package/src/components.ts +2 -4
  101. package/src/config.ts +31 -296
  102. package/src/configuration-files.ts +27 -170
  103. package/src/configured-declarations.ts +18 -23
  104. package/src/consumers.ts +4 -4
  105. package/src/contract/config.generated.ts +798 -0
  106. package/src/contract/contract.generated.ts +25 -0
  107. package/src/contract/report.generated.ts +3 -2
  108. package/src/conventions.ts +26 -74
  109. package/src/dependencies.ts +32 -50
  110. package/src/discover.ts +15 -40
  111. package/src/edges.ts +41 -140
  112. package/src/element-access.ts +4 -7
  113. package/src/entry-point-gaps.ts +3 -9
  114. package/src/entry-points.ts +2 -13
  115. package/src/enum-group.ts +2 -11
  116. package/src/erased-stores.ts +3 -7
  117. package/src/exempt-classes.ts +0 -5
  118. package/src/exempt.ts +8 -30
  119. package/src/file-facts.ts +6 -24
  120. package/src/findings/completion.ts +5 -6
  121. package/src/findings/emitter.ts +2 -2
  122. package/src/findings/emitters.ts +1 -1
  123. package/src/findings/intra-function.ts +1 -1
  124. package/src/findings/non-code-artifacts.ts +4 -4
  125. package/src/findings/reads-and-writes.ts +2 -1
  126. package/src/findings/self-check.ts +3 -2
  127. package/src/findings/unused-declarations.ts +1 -1
  128. package/src/findings/visibility-narrowing.ts +2 -2
  129. package/src/findings-pass.ts +9 -15
  130. package/src/framework-lifecycle.ts +7 -7
  131. package/src/fs/document.ts +74 -0
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/host.ts +12 -5
  136. package/src/html-entries.ts +6 -33
  137. package/src/in-operands.ts +2 -1
  138. package/src/injection-container.ts +1 -1
  139. package/src/intra-function-parts.ts +10 -14
  140. package/src/inventory.ts +2 -20
  141. package/src/json-document.ts +241 -76
  142. package/src/keyof-comparisons.ts +3 -7
  143. package/src/kinds.ts +0 -8
  144. package/src/manifest.ts +5 -46
  145. package/src/mdx-entries.ts +3 -26
  146. package/src/partial.ts +2 -1
  147. package/src/paths.ts +27 -15
  148. package/src/position.ts +2 -1
  149. package/src/print.ts +12 -92
  150. package/src/product.ts +18 -0
  151. package/src/references.ts +14 -29
  152. package/src/report-schema.ts +3 -301
  153. package/src/report.ts +24 -34
  154. package/src/reporters/sarif.ts +4 -4
  155. package/src/reporters/template.ts +4 -20
  156. package/src/resolve.ts +283 -1114
  157. package/src/roots.ts +7 -20
  158. package/src/run.ts +57 -75
  159. package/src/schema.ts +391 -388
  160. package/src/scope.ts +40 -126
  161. package/src/serialization-contract.ts +5 -10
  162. package/src/session.ts +2 -1
  163. package/src/suppress-file.ts +21 -29
  164. package/src/suppress.ts +3 -3
  165. package/src/symbol-keys.ts +2 -5
  166. package/src/template-field.ts +15 -25
  167. package/src/test-globals.ts +9 -27
  168. package/src/test-joins.ts +6 -8
  169. package/src/text/compare.ts +27 -0
  170. package/src/text/glob.ts +210 -0
  171. package/src/text/shell.ts +161 -0
  172. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  173. package/src/type-errors.ts +25 -36
  174. package/src/unseen-loads.ts +1 -12
  175. package/src/unwrap.ts +34 -0
  176. package/src/usage.ts +10 -0
  177. package/src/verbs/analyze.ts +32 -42
  178. package/src/verbs/describe.ts +4 -4
  179. package/src/verbs/explain.ts +8 -10
  180. package/src/verbs/print-projects.ts +4 -3
  181. package/src/verbs/print-retained.ts +2 -1
  182. package/src/verbs/print-roots.ts +2 -1
  183. package/src/verbs/verb.ts +2 -2
  184. package/src/verbs/version.ts +2 -1
  185. package/src/workflow-steps.ts +9 -44
  186. package/src/workspace-manifests.ts +2 -2
  187. package/src/workspace-programs.ts +5 -3
  188. package/src/workspace-resolution.ts +21 -17
  189. package/src/workspace.ts +35 -121
  190. package/dist/src/glob.js +0 -65
  191. package/dist/src/pnpm-workspace.js +0 -120
  192. package/src/glob.ts +0 -67
  193. package/src/pnpm-workspace.ts +0 -135
package/src/scope.ts CHANGED
@@ -1,11 +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
-
6
- /** The roles a scope document declares for a module. */
7
- const ROLE_TARGET = "target";
8
- const ROLE_CONSUMER = "consumer";
5
+ import { message } from "./product.ts";
6
+ import { firstRefusal } from "./schema.ts";
7
+ import { UsageError } from "./usage.ts";
9
8
 
10
9
  /**
11
10
  * A scope document above this length is refused rather than decoded. It names one
@@ -14,8 +13,14 @@ const ROLE_CONSUMER = "consumer";
14
13
  const MAX_DOCUMENT_LENGTH = 1 << 20;
15
14
 
16
15
  /**
17
- * A scope document that cannot be read as one. It ends the run with the failure
18
- * 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.
19
24
  */
20
25
  export class ScopeError extends Error {
21
26
  constructor(message: string) {
@@ -25,12 +30,8 @@ export class ScopeError extends Error {
25
30
  }
26
31
 
27
32
  /**
28
- * One module the run reads: the target or a declared consumer.
29
- *
30
- * The role a document declares is not carried here, because the resolved scope
31
- * holds exactly one target and a list of consumers, so a module's role is where it
32
- * sits. What a document declares is read and refused where it contradicts that
33
- * 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.
34
35
  */
35
36
  export interface Module {
36
37
  /** The package name. A document may state it; otherwise it is empty. */
@@ -45,139 +46,52 @@ export interface Scope {
45
46
  readonly consumers: readonly Module[];
46
47
  }
47
48
 
48
- const MODULE: KeyNode = {
49
- kind: "section",
50
- members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
51
- };
52
-
53
- /** The closed key list of the scope document, compared as bytes. */
54
- const SCOPE_ROOT: KeyNode = {
55
- kind: "section",
56
- members: {
57
- target: MODULE,
58
- workspace: { kind: "leaf" },
59
- consumers: { kind: "list", members: MODULE.members ?? {} },
60
- },
61
- };
62
-
63
- function isRecord(value: unknown): value is Record<string, unknown> {
64
- return typeof value === "object" && value !== null && !Array.isArray(value);
65
- }
66
-
67
- /**
68
- * One string member, or undefined where the document omits it. A member the
69
- * document writes is a string holding at least one character, so `null` and `""`
70
- * are refused rather than read as absent.
71
- */
72
- function stringMember(
73
- parent: Record<string, unknown>,
74
- name: string,
75
- at: string,
76
- ): string | undefined {
77
- if (!Object.hasOwn(parent, name)) {
78
- return undefined;
79
- }
80
- const value = parent[name];
81
- if (typeof value !== "string") {
82
- throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
83
- }
84
- if (value === "") {
85
- throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
86
- }
87
- return value;
88
- }
89
-
90
- function readModule(value: unknown, at: string, role: string, documentDir: string): Module {
91
- if (!isRecord(value)) {
92
- throw new ScopeError(`scope: ${at} is not an object`);
93
- }
94
- const declared = stringMember(value, "role", at);
95
- if (declared !== undefined && declared !== role) {
96
- throw new ScopeError(
97
- `scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`,
98
- );
99
- }
100
- const path = stringMember(value, "path", at);
101
- if (path === undefined) {
102
- throw new ScopeError(`scope: ${at}.path names nothing`);
103
- }
104
- return {
105
- id: stringMember(value, "id", at) ?? "",
106
- path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
107
- };
49
+ interface DeclaredModule {
50
+ readonly id?: string;
51
+ readonly path: string;
108
52
  }
109
53
 
110
- /**
111
- * Walks the document's text against the closed key list, refusing a key the list
112
- * does not declare, compared as bytes, and a key one object writes twice, which a
113
- * parse cannot report because it keeps the last value.
114
- */
115
- function checkKeys(text: string, file: string): void {
116
- try {
117
- checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
118
- } catch (error: unknown) {
119
- if (error instanceof ConfigError) {
120
- // The configuration's refusal names the nearest configuration key, which a
121
- // scope document does not have.
122
- throw new ScopeError(
123
- error.kind === "unimplemented-key"
124
- ? `scope: ${file}: ${error.key} is not a declared key`
125
- : error.message,
126
- );
127
- }
128
- throw error;
129
- }
54
+ function refusal(file: string, place: string, reason: string): ScopeError {
55
+ return new ScopeError(`scope: ${file}: ${place === "" ? "" : `${place}: `}${reason}`);
130
56
  }
131
57
 
132
58
  /**
133
- * Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
134
- * key written twice in one object, and a value of a type the document does not
135
- * admit, `null` included, are errors, and a relative path inside the document
136
- * resolves against the document's own directory.
137
- *
138
- * The `workspace` key the format declares is checked and carries nothing here: it
139
- * names the file a Go consumer resolves the target through, and a TypeScript
140
- * 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.
141
63
  */
142
64
  export function readScope(host: Host, file: string): Scope {
143
65
  const absolute = resolvePath(host.workingDirectory(), file);
144
66
  let text: string;
145
67
  try {
146
- text = host.readFile(absolute);
68
+ text = host.readFile(absolute, MAX_DOCUMENT_BYTES);
147
69
  } catch (error: unknown) {
148
- throw new ScopeError(
149
- `scope: ${file}: ${error instanceof Error ? error.message : String(error)}`,
150
- );
70
+ throw new UsageError(`scope: ${file}: ${message(error)}`);
151
71
  }
152
72
  if (text.length > MAX_DOCUMENT_LENGTH) {
153
73
  throw new ScopeError(`scope: ${file} is longer than ${String(MAX_DOCUMENT_LENGTH)} characters`);
154
74
  }
155
75
  let value: unknown;
156
76
  try {
157
- value = JSON.parse(text);
77
+ value = readStrict(text).value;
158
78
  } catch (error: unknown) {
159
- throw new ScopeError(
160
- `scope: ${file}: ${error instanceof Error ? error.message : String(error)}`,
161
- );
79
+ if (!(error instanceof StrictJSONError)) {
80
+ throw error;
81
+ }
82
+ throw refusal(file, dotted(error.place), locatedFault(text, error));
162
83
  }
163
- if (!isRecord(value)) {
164
- 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);
165
87
  }
166
- checkKeys(text, file);
167
- stringMember(value, "workspace", "");
88
+ const declared = value as { target: DeclaredModule; consumers?: readonly DeclaredModule[] };
168
89
  const documentDir = dirnamePath(absolute);
169
- if (!Object.hasOwn(value, "target")) {
170
- throw new ScopeError(`scope: ${file} names no target`);
171
- }
172
- const target = readModule(value["target"], "target", ROLE_TARGET, documentDir);
173
- const listed = Object.hasOwn(value, "consumers") ? value["consumers"] : [];
174
- if (!Array.isArray(listed)) {
175
- throw new ScopeError(`scope: ${file}: consumers is not an array`);
176
- }
177
- const consumers = (listed as unknown[]).map((entry, index) =>
178
- readModule(entry, `consumers[${String(index)}]`, ROLE_CONSUMER, documentDir),
179
- );
180
- 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) };
181
95
  }
182
96
 
183
97
  /** The single-package scope one directory is, when no scope document is given. */
@@ -53,6 +53,7 @@ import {
53
53
  type SourceFile,
54
54
  type SpreadAssignment,
55
55
  } from "@typescript/native/ast";
56
+ import { parenthesesRemoved } from "./unwrap.ts";
56
57
  import {
57
58
  SignatureKind,
58
59
  SymbolFlags,
@@ -81,7 +82,7 @@ import { erasedStores, type ErasedStores } from "./erased-stores.ts";
81
82
  import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
82
83
 
83
84
  /** The serializer every run reads, whatever the configuration names. */
84
- const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
85
+ const JSON_STRINGIFY: DeclarationEntry = { global: "JSON.stringify" };
85
86
 
86
87
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
87
88
  const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
@@ -227,12 +228,6 @@ interface MethodForm {
227
228
  readonly invocation: Invocation;
228
229
  }
229
230
 
230
- function unparenthesized(expression: Expression): Expression {
231
- return isParenthesizedExpression(expression)
232
- ? unparenthesized(expression.expression)
233
- : expression;
234
- }
235
-
236
231
  /**
237
232
  * The invocation a call writes through a method of `Function.prototype`, read from
238
233
  * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
@@ -241,12 +236,12 @@ function unparenthesized(expression: Expression): Expression {
241
236
  * carries. Whether the method is the prototype's is resolution's to say.
242
237
  */
243
238
  function throughMethod(call: CallExpression): MethodForm | undefined {
244
- const expression = unparenthesized(call.expression);
239
+ const expression = parenthesesRemoved(call.expression);
245
240
  if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
246
241
  const [, ...rest] = call.arguments;
247
242
  let passed: readonly Expression[] = rest;
248
243
  if (expression.name.text === "apply") {
249
- const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
244
+ const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
250
245
  passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
251
246
  }
252
247
  return {
@@ -255,7 +250,7 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
255
250
  };
256
251
  }
257
252
  if (isCallExpression(expression)) {
258
- const bound = unparenthesized(expression.expression);
253
+ const bound = parenthesesRemoved(expression.expression);
259
254
  if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
260
255
  return {
261
256
  method: bound.name,
package/src/session.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  type Queries,
27
27
  type UnansweredQuestion,
28
28
  } from "./query.ts";
29
+ import { MANIFEST } from "./fs/manifest.ts";
29
30
 
30
31
  /**
31
32
  * The compiler the analysis reads through: a client over a protocol to a separate
@@ -115,7 +116,7 @@ export function openEngine(options: EngineOptions): Engine {
115
116
  if (components !== undefined && components.extensions.length > 0) {
116
117
  const made = components.host.temporaryDirectory();
117
118
  try {
118
- components.host.writeDocument(joinPath(made.path, "package.json"), [
119
+ components.host.writeDocument(joinPath(made.path, MANIFEST), [
119
120
  mapperManifest(components.host.componentMapperCommand()),
120
121
  ]);
121
122
  } catch (error: unknown) {
@@ -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 { 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 {
@@ -26,6 +26,7 @@ import {
26
26
  type SuppressionRecord,
27
27
  type Suppressions,
28
28
  } from "./suppress.ts";
29
+ import { message } from "./product.ts";
29
30
 
30
31
  /** The name and the location the grammar fixes for the ignore file. */
31
32
  export const IGNORE_FILE = "deadset-ignore.json";
@@ -79,10 +80,6 @@ function documentSite(shape: Shape): Position {
79
80
  return { path: shape.file, line: 0, column: 0 };
80
81
  }
81
82
 
82
- function isObject(value: unknown): value is Record<string, unknown> {
83
- return typeof value === "object" && value !== null && !Array.isArray(value);
84
- }
85
-
86
83
  /** What one entry or row resolves to. */
87
84
  interface Read {
88
85
  readonly records: readonly SuppressionRecord[];
@@ -100,7 +97,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
100
97
  const refuse = (text: string, want: string): never => {
101
98
  throw new SuppressionError(shape.mechanism, siteText(site), text, want);
102
99
  };
103
- if (!isObject(value)) {
100
+ if (!isRecord(value)) {
104
101
  return refuse(JSON.stringify(value), shape.recordWant);
105
102
  }
106
103
  for (const [key, held] of Object.entries(value)) {
@@ -188,30 +185,26 @@ function readDocument(
188
185
  symbols: readonly InventorySymbol[],
189
186
  shape: Shape,
190
187
  ): Suppressions {
191
- const file = joinPath(targetRoot, shape.file);
192
- if (host.kindOf(file) === "absent") {
188
+ const text = readTargetDocument(host, targetRoot, shape.file);
189
+ if (text === undefined) {
193
190
  return { records: [], refusals: [] };
194
191
  }
195
- const text = host.readFile(file);
196
192
  const malformed = (detail: string, site = documentSite(shape)): never => {
197
193
  throw new SuppressionError(shape.mechanism, siteText(site), detail, shape.documentWant);
198
194
  };
199
- let parsed: unknown;
195
+ let document;
200
196
  try {
201
- parsed = JSON.parse(text);
197
+ document = readStrict(text, shape.array);
202
198
  } catch (error: unknown) {
203
- return malformed(`${shape.file}: ${error instanceof Error ? error.message : String(error)}`);
199
+ if (!(error instanceof StrictJSONError)) {
200
+ throw error;
201
+ }
202
+ return malformed(error.message, positionAt(text, shape.file, error.offset));
204
203
  }
205
- if (!isObject(parsed)) {
204
+ const parsed = document.value;
205
+ if (!isRecord(parsed)) {
206
206
  return malformed(shape.file);
207
207
  }
208
- const walk = walkDocument(text, shape.array);
209
- if (walk.twice !== undefined) {
210
- malformed(
211
- `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
212
- positionAt(text, shape.file, walk.twice.offset),
213
- );
214
- }
215
208
  for (const key of Object.keys(parsed)) {
216
209
  if (key !== "description" && key !== shape.array) {
217
210
  malformed(`the member ${JSON.stringify(key)} is not one the grammar declares`);
@@ -227,7 +220,7 @@ function readDocument(
227
220
  const records: SuppressionRecord[] = [];
228
221
  const refusals: Refusal[] = [];
229
222
  (held as readonly unknown[]).forEach((value, index) => {
230
- const offset = walk.starts[index];
223
+ const offset = document.starts.get(index);
231
224
  const site = offset === undefined ? documentSite(shape) : positionAt(text, shape.file, offset);
232
225
  const read = readRecord(value, site, byReference, shape);
233
226
  records.push(...read.records);
@@ -328,9 +321,9 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
328
321
  try {
329
322
  parsed = JSON.parse(text);
330
323
  } catch (error: unknown) {
331
- return refuse(error instanceof Error ? error.message : String(error));
324
+ return refuse(message(error));
332
325
  }
333
- const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
326
+ const held = isRecord(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
334
327
  if (!Array.isArray(held)) {
335
328
  return refuse(BASELINE_SHAPE.documentWant);
336
329
  }
@@ -338,7 +331,7 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
338
331
  try {
339
332
  readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
340
333
  } catch (error: unknown) {
341
- return refuse(error instanceof Error ? error.message : String(error));
334
+ return refuse(message(error));
342
335
  }
343
336
  // readRecord has checked every member is a string and the code and symbol are present.
344
337
  const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
@@ -385,10 +378,9 @@ function* separatorsEscaped(pieces: Iterable<string>): Generator<string, void, u
385
378
  * A baseline holding one row per finding, in the order given, which is the report's, and
386
379
  * the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
387
380
  * the order given, so two analyzers writing one document in either order write the same
388
- * bytes, in the form grammar/suppression.md fixes. Every recorded row carries the
389
- * provenance as its reason. A finding
390
- * missing a value a row needs, and an identity missing its name or version, are refused
391
- * 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.
392
384
  */
393
385
  export function writeBaseline(
394
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
  }
@@ -12,7 +12,6 @@ import {
12
12
  isIdentifier,
13
13
  isNoSubstitutionTemplateLiteral,
14
14
  isNumericLiteral,
15
- isParenthesizedExpression,
16
15
  isPropertyAccessExpression,
17
16
  isSpreadElement,
18
17
  isStringLiteral,
@@ -21,6 +20,7 @@ import {
21
20
  type Node,
22
21
  type SourceFile,
23
22
  } from "@typescript/native/ast";
23
+ import { parenthesesRemoved } from "./unwrap.ts";
24
24
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
25
25
  import type { AliasChains } from "./alias-chain.ts";
26
26
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
@@ -52,10 +52,7 @@ interface KeyedUse {
52
52
 
53
53
  /** The identifier an element access's argument names its key by, where it is a name. */
54
54
  export function keyNameOf(argument: Node): Node | undefined {
55
- let at = argument;
56
- while (isParenthesizedExpression(at)) {
57
- at = at.expression;
58
- }
55
+ const at = parenthesesRemoved(argument);
59
56
  if (isIdentifier(at)) {
60
57
  return at;
61
58
  }
@@ -13,13 +13,16 @@
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";
20
20
  import { memberNames } from "./member-names.ts";
21
21
  import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
+ import { bytewise } from "./text/compare.ts";
24
+ import { filesBelow } from "./fs/tree.ts";
25
+ import { UsageError } from "./usage.ts";
23
26
 
24
27
  /** One template, by its path below the target root. */
25
28
  interface TemplateFile {
@@ -50,14 +53,6 @@ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE,
50
53
  /** One identifier, as the grammar of the analyzed language spells one. */
51
54
  const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
52
55
 
53
- /** Two strings ordered bytewise. */
54
- function compare(a: string, b: string): number {
55
- if (a === b) {
56
- return 0;
57
- }
58
- return a < b ? -1 : 1;
59
- }
60
-
61
56
  /**
62
57
  * Every template under the configured directories, read once for the run, in path
63
58
  * order and each file once. A directory is named relative to the target root, and
@@ -66,25 +61,22 @@ function compare(a: string, b: string): number {
66
61
  * exemption it was configured for off. A symbolic link to a directory is not
67
62
  * followed.
68
63
  */
69
- 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 {
70
69
  const files = new Map<string, string>();
71
70
  const read = (dir: string): void => {
72
- const entries = [...host.readDirectory(joinPath(targetRoot, dir))].sort((a, b) =>
73
- compare(a.name, b.name),
74
- );
75
- for (const entry of entries) {
76
- const path = joinPath(dir, entry.name);
77
- if (entry.directory) {
78
- read(path);
79
- continue;
80
- }
71
+ for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
72
+ const path = joinPath(dir, below);
81
73
  const absolute = joinPath(targetRoot, path);
82
74
  if (!files.has(path) && host.kindOf(absolute) === "file") {
83
75
  files.set(path, host.readFile(absolute));
84
76
  }
85
77
  }
86
78
  };
87
- for (const configured of analysis.templateDirs) {
79
+ for (const configured of analysis.template_dirs) {
88
80
  const dir = normalizePath(configured);
89
81
  if (
90
82
  isAbsolutePath(dir) ||
@@ -92,19 +84,17 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
92
84
  dir.startsWith("../") ||
93
85
  host.kindOf(joinPath(targetRoot, dir)) !== "directory"
94
86
  ) {
95
- throw new ConfigError(
96
- "malformed",
97
- "analysis.template_dirs",
87
+ throw new UsageError(
98
88
  `analysis.template_dirs: ${JSON.stringify(configured)} is not a directory below the target root`,
99
89
  );
100
90
  }
101
91
  read(dir);
102
92
  }
103
93
  return {
104
- delimiters: analysis.templateDelimiters,
94
+ delimiters: analysis.template_delimiters,
105
95
  files: [...files]
106
96
  .map(([path, text]) => ({ path, text }))
107
- .sort((a, b) => compare(a.path, b.path)),
97
+ .sort((a, b) => bytewise(a.path, b.path)),
108
98
  };
109
99
  }
110
100
 
@@ -25,10 +25,12 @@ import {
25
25
  } from "@typescript/native/ast";
26
26
  import type { ParseFile } from "./configuration-files.ts";
27
27
  import type { Host } from "./host.ts";
28
- import { dirnamePath, joinPath } from "./paths.ts";
28
+ import { ancestors, dirnamePath, joinPath } from "./paths.ts";
29
29
  import { UNANSWERED } from "./query.ts";
30
30
  import type { Engine, ProjectView } from "./session.ts";
31
31
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
32
+ import { filesIn } from "./fs/tree.ts";
33
+ import { holdsManifest, INSTALLED_PACKAGES } from "./fs/manifest.ts";
32
34
 
33
35
  /** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
34
36
  export const GLOBALS_RUNNER = "vitest";
@@ -120,16 +122,7 @@ function setsGlobals(file: SourceFile): boolean {
120
122
 
121
123
  /** Whether the runner's configuration in one manifest's directory injects the globals. */
122
124
  function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
123
- let names: readonly string[];
124
- try {
125
- names = host
126
- .readDirectory(dir)
127
- .filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
128
- .map((entry) => entry.name);
129
- } catch {
130
- return false;
131
- }
132
- return names.some((name) => {
125
+ return filesIn(host, dir, (name) => RUNNER_CONFIG.test(name)).some((name) => {
133
126
  const path = joinPath(dir, name);
134
127
  try {
135
128
  return setsGlobals(parse(path, host.readFile(path)));
@@ -141,26 +134,15 @@ function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
141
134
 
142
135
  /** Whether a file declaring the globals is installed where the configuration resolves packages from. */
143
136
  function globalsInstalled(host: Host, configFile: string): boolean {
144
- for (let dir = dirnamePath(configFile); ; dir = dirnamePath(dir)) {
145
- if (host.kindOf(joinPath(dir, "node_modules", GLOBALS_RUNNER, "globals.d.ts")) === "file") {
146
- return true;
147
- }
148
- if (dirnamePath(dir) === dir) {
149
- return false;
150
- }
151
- }
137
+ return ancestors(dirnamePath(configFile)).some(
138
+ (dir) =>
139
+ host.kindOf(joinPath(dir, INSTALLED_PACKAGES, GLOBALS_RUNNER, "globals.d.ts")) === "file",
140
+ );
152
141
  }
153
142
 
154
143
  /** The directory of the manifest nearest one configuration file, at or above its own. */
155
144
  function manifestDir(host: Host, configFile: string): string | undefined {
156
- for (let dir = dirnamePath(configFile); ; dir = dirnamePath(dir)) {
157
- if (host.kindOf(joinPath(dir, "package.json")) === "file") {
158
- return dir;
159
- }
160
- if (dirnamePath(dir) === dir) {
161
- return undefined;
162
- }
163
- }
145
+ return ancestors(dirnamePath(configFile)).find((dir) => holdsManifest(host, dir));
164
146
  }
165
147
 
166
148
  function typesOf(value: unknown): readonly string[] | undefined {
package/src/test-joins.ts CHANGED
@@ -8,9 +8,11 @@
8
8
 
9
9
  import { sourcePaths } from "./file-facts.ts";
10
10
  import type { Host } from "./host.ts";
11
- import { dirnamePath, joinPath, relativePath } from "./paths.ts";
11
+ import { ancestors, dirnamePath, joinPath } from "./paths.ts";
12
12
  import type { Engine } from "./session.ts";
13
13
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
14
+ import { bytewise } from "./text/compare.ts";
15
+ import { holdsManifest } from "./fs/manifest.ts";
14
16
 
15
17
  const PREFERRED = "/tsconfig.json";
16
18
 
@@ -18,7 +20,7 @@ const PREFERRED = "/tsconfig.json";
18
20
  function byPreference(a: string, b: string): number {
19
21
  const first = a.endsWith(PREFERRED) ? 0 : 1;
20
22
  const second = b.endsWith(PREFERRED) ? 0 : 1;
21
- return first - second || (a < b ? -1 : a > b ? 1 : 0);
23
+ return first - second || bytewise(a, b);
22
24
  }
23
25
 
24
26
  /**
@@ -54,17 +56,13 @@ export function joinedTestFiles(
54
56
  if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
55
57
  continue;
56
58
  }
57
- for (let dir = dirnamePath(file); ; dir = dirnamePath(dir)) {
59
+ for (const dir of ancestors(dirnamePath(file), root)) {
58
60
  const [nearest] = byDir.get(dir) ?? [];
59
61
  if (nearest !== undefined) {
60
62
  joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
61
63
  break;
62
64
  }
63
- if (
64
- host.kindOf(joinPath(dir, "package.json")) === "file" ||
65
- dir === root ||
66
- relativePath(root, dir) === undefined
67
- ) {
65
+ if (holdsManifest(host, dir)) {
68
66
  break;
69
67
  }
70
68
  }