@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
@@ -1,309 +1,11 @@
1
1
  /**
2
2
  * Holds a report to `contract/report.schema.json` and `contract/finding.schema.json`
3
3
  * before it is written, reading the schemas the generator copies from the Contract, so
4
- * this analyzer never writes a document a reader refuses. It evaluates the keywords the
5
- * Contract's schemas use; the schemas' patterns are written to match alike in RE2 and in
6
- * ECMAScript.
4
+ * this analyzer never writes a document a reader refuses.
7
5
  */
8
6
 
9
7
  import { REPORT_SCHEMAS, type Report } from "./contract/report.generated.ts";
10
-
11
- /** One value a schema refuses: its JSON Pointer, the keyword, and why. */
12
- interface SchemaRefusal {
13
- readonly pointer: string;
14
- readonly keyword: string;
15
- readonly reason: string;
16
- }
17
-
18
- type Schema = Readonly<Record<string, unknown>>;
19
- type Documents = Readonly<Record<string, Schema>>;
20
-
21
- /**
22
- * Every keyword the validator evaluates or knows to carry no assertion. A schema naming
23
- * another fails every check rather than passing values the keyword would refuse.
24
- */
25
- const KEYWORDS: ReadonlySet<string> = new Set([
26
- ...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
27
- ...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
28
- ...["properties", "additionalProperties", "required", "minProperties"],
29
- ...["items", "minItems", "maxItems", "uniqueItems"],
30
- ...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
31
- ]);
32
-
33
- const patterns = new Map<string, RegExp>();
34
-
35
- function pattern(source: string): RegExp {
36
- let held = patterns.get(source);
37
- if (held === undefined) {
38
- held = new RegExp(source, "u");
39
- patterns.set(source, held);
40
- }
41
- return held;
42
- }
43
-
44
- function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
45
- return value !== null && typeof value === "object" && !Array.isArray(value);
46
- }
47
-
48
- function typeOf(value: unknown): string {
49
- if (value === null) {
50
- return "null";
51
- }
52
- if (Array.isArray(value)) {
53
- return "array";
54
- }
55
- if (typeof value === "number") {
56
- return Number.isInteger(value) ? "integer" : "number";
57
- }
58
- return typeof value;
59
- }
60
-
61
- function hasType(value: unknown, name: string): boolean {
62
- const held = typeOf(value);
63
- return held === name || (name === "number" && held === "integer");
64
- }
65
-
66
- /** One value's encoding with object members sorted, so two equal values encode alike. */
67
- function canonical(value: unknown): string {
68
- if (Array.isArray(value)) {
69
- return `[${value.map(canonical).join(",")}]`;
70
- }
71
- if (isObject(value)) {
72
- return `{${Object.keys(value)
73
- .sort()
74
- .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
75
- .join(",")}}`;
76
- }
77
- return JSON.stringify(value);
78
- }
79
-
80
- /** A string's length in code points, which JSON Schema's minLength counts. */
81
- function codePoints(value: string): number {
82
- let counted = 0;
83
- for (const _point of value) {
84
- counted += 1;
85
- }
86
- return counted;
87
- }
88
-
89
- const escape = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
90
-
91
- /** A schema evaluated in the document it is read from, so a relative `$ref` resolves there. */
92
- class Checker {
93
- private readonly documents: Documents;
94
-
95
- constructor(documents: Documents) {
96
- this.documents = documents;
97
- }
98
-
99
- resolve(doc: string, ref: string): readonly [string, Schema] {
100
- const [file = "", pointer = ""] = ref.split("#");
101
- const target = file === "" ? doc : file;
102
- let schema: unknown = this.documents[target];
103
- for (const token of pointer.split("/").filter((one) => one !== "")) {
104
- schema = isObject(schema)
105
- ? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
106
- : undefined;
107
- }
108
- if (!isObject(schema)) {
109
- throw new Error(`report schema: ${ref} names no schema`);
110
- }
111
- return [target, schema];
112
- }
113
-
114
- matches(doc: string, schema: Schema, value: unknown, at: string): boolean {
115
- const found: SchemaRefusal[] = [];
116
- this.check(doc, schema, value, at, found);
117
- return found.length === 0;
118
- }
119
-
120
- check(doc: string, schema: Schema, value: unknown, at: string, found: SchemaRefusal[]): void {
121
- for (const keyword of Object.keys(schema)) {
122
- if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
123
- throw new Error(
124
- `report schema: the keyword ${keyword} is not one this validator evaluates`,
125
- );
126
- }
127
- }
128
- const refuse = (keyword: string, reason: string, pointer = at): void => {
129
- found.push({ pointer, keyword, reason });
130
- };
131
- if (typeof schema["$ref"] === "string") {
132
- const [target, referenced] = this.resolve(doc, schema["$ref"]);
133
- this.check(target, referenced, value, at, found);
134
- }
135
- const types = schema["type"];
136
- const named = types === undefined ? [] : Array.isArray(types) ? types : [types];
137
- if (named.length > 0 && !named.some((one) => hasType(value, String(one)))) {
138
- refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
139
- return;
140
- }
141
- this.checkValue(schema, value, refuse);
142
- if (Array.isArray(value)) {
143
- this.checkArray(doc, schema, value, at, found, refuse);
144
- } else if (isObject(value)) {
145
- this.checkObject(doc, schema, value, at, found, refuse);
146
- }
147
- this.checkApplicators(doc, schema, value, at, found, refuse);
148
- }
149
-
150
- private checkValue(
151
- schema: Schema,
152
- value: unknown,
153
- refuse: (keyword: string, reason: string) => void,
154
- ): void {
155
- const values = schema["enum"];
156
- if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
157
- refuse("enum", `${canonical(value)} is not one of the values the schema lists`);
158
- }
159
- if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
160
- refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
161
- }
162
- if (typeof value === "string") {
163
- if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
164
- refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
165
- }
166
- if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
167
- refuse(
168
- "minLength",
169
- `${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`,
170
- );
171
- }
172
- }
173
- if (typeof value === "number") {
174
- if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
175
- refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
176
- }
177
- if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
178
- refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
179
- }
180
- }
181
- }
182
-
183
- private checkObject(
184
- doc: string,
185
- schema: Schema,
186
- value: Readonly<Record<string, unknown>>,
187
- at: string,
188
- found: SchemaRefusal[],
189
- refuse: (keyword: string, reason: string) => void,
190
- ): void {
191
- const properties = isObject(schema["properties"]) ? schema["properties"] : {};
192
- const additional = schema["additionalProperties"];
193
- for (const name of Object.keys(value).sort()) {
194
- const member = `${at}/${escape(name)}`;
195
- const declared = properties[name];
196
- if (isObject(declared)) {
197
- this.check(doc, declared, value[name], member, found);
198
- } else if (additional === false) {
199
- refuse(
200
- "additionalProperties",
201
- `the member ${JSON.stringify(name)} is not one the schema declares`,
202
- );
203
- } else if (isObject(additional)) {
204
- this.check(doc, additional, value[name], member, found);
205
- }
206
- }
207
- for (const name of Array.isArray(schema["required"]) ? (schema["required"] as string[]) : []) {
208
- if (!Object.hasOwn(value, name)) {
209
- refuse("required", `the member ${JSON.stringify(name)} is absent`);
210
- }
211
- }
212
- const fewest = schema["minProperties"];
213
- if (typeof fewest === "number" && Object.keys(value).length < fewest) {
214
- refuse(
215
- "minProperties",
216
- `the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`,
217
- );
218
- }
219
- }
220
-
221
- private checkArray(
222
- doc: string,
223
- schema: Schema,
224
- value: readonly unknown[],
225
- at: string,
226
- found: SchemaRefusal[],
227
- refuse: (keyword: string, reason: string) => void,
228
- ): void {
229
- const items = schema["items"];
230
- if (isObject(items)) {
231
- value.forEach((item, i) => {
232
- this.check(doc, items, item, `${at}/${String(i)}`, found);
233
- });
234
- }
235
- if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
236
- refuse(
237
- "minItems",
238
- `the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`,
239
- );
240
- }
241
- if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
242
- refuse(
243
- "maxItems",
244
- `the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`,
245
- );
246
- }
247
- if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
248
- refuse("uniqueItems", "the array lists one value twice");
249
- }
250
- }
251
-
252
- private checkApplicators(
253
- doc: string,
254
- schema: Schema,
255
- value: unknown,
256
- at: string,
257
- found: SchemaRefusal[],
258
- refuse: (keyword: string, reason: string) => void,
259
- ): void {
260
- const list = (keyword: string): readonly Schema[] =>
261
- Array.isArray(schema[keyword]) ? (schema[keyword] as Schema[]) : [];
262
- for (const each of list("allOf")) {
263
- this.check(doc, each, value, at, found);
264
- }
265
- const matching = (keyword: string): number =>
266
- list(keyword).filter((each) => this.matches(doc, each, value, at)).length;
267
- if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
268
- refuse("anyOf", "the value matches none of the alternatives");
269
- }
270
- if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
271
- refuse(
272
- "oneOf",
273
- `the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
274
- );
275
- }
276
- if (isObject(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
277
- refuse("not", "the value matches a schema it must not match");
278
- }
279
- if (isObject(schema["if"])) {
280
- const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
281
- if (isObject(branch)) {
282
- this.check(doc, branch, value, at, found);
283
- }
284
- }
285
- }
286
- }
287
-
288
- /**
289
- * A validator over one set of schema documents, each named as a `$ref` names it, rooted
290
- * at one of them: what it refuses in a value, none for an instance.
291
- */
292
- export function schemaRefusals(
293
- documents: Documents,
294
- root: string,
295
- ): (value: unknown) => readonly SchemaRefusal[] {
296
- const checker = new Checker(documents);
297
- const top = documents[root];
298
- if (top === undefined) {
299
- throw new Error(`the schema set holds no document ${root}`);
300
- }
301
- return (value) => {
302
- const found: SchemaRefusal[] = [];
303
- checker.check(root, top, value, "", found);
304
- return found;
305
- };
306
- }
8
+ import { schemaRefusals, type SchemaRefusal } from "./schema.ts";
307
9
 
308
10
  const reportSchema = schemaRefusals(REPORT_SCHEMAS, "report.schema.json");
309
11
 
@@ -323,7 +25,7 @@ function listingRefusals(report: Report): readonly SchemaRefusal[] {
323
25
  return [];
324
26
  }
325
27
  const refuse = (pointer: string, reason: string): SchemaRefusal[] => [
326
- { pointer, keyword: "components", reason },
28
+ { pointer, keyword: "components", reason, member: "" },
327
29
  ];
328
30
  const listed = new Set<string>();
329
31
  for (const [i, entry] of report.components.entries()) {
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)}`);