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

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