@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.
- package/dist/bin/node-host.js +21 -3
- package/dist/src/analysis.js +13 -14
- package/dist/src/config.js +6 -83
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +2 -2
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +36 -112
- package/dist/src/exempt-classes.js +0 -2
- package/dist/src/exempt.js +1 -1
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +1 -1
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +59 -0
- package/dist/src/injection-container.js +1 -1
- package/dist/src/json-document.js +194 -67
- package/dist/src/kinds.js +0 -7
- package/dist/src/print.js +10 -79
- package/dist/src/report-schema.js +3 -224
- package/dist/src/resolve.js +245 -715
- package/dist/src/run.js +48 -55
- package/dist/src/schema.js +320 -344
- package/dist/src/scope.js +33 -101
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/suppress-file.js +15 -17
- package/dist/src/suppress.js +3 -3
- package/dist/src/template-field.js +4 -4
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +12 -15
- package/dist/src/verbs/explain.js +3 -3
- package/dist/src/verbs/print-projects.js +2 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +30 -26
- package/src/config.ts +31 -298
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/discover.ts +1 -1
- package/src/edges.ts +39 -124
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +4 -4
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +3 -3
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +74 -0
- package/src/host.ts +12 -5
- package/src/injection-container.ts +1 -1
- package/src/json-document.ts +236 -76
- package/src/kinds.ts +0 -8
- package/src/print.ts +11 -92
- package/src/report-schema.ts +3 -299
- package/src/resolve.ts +281 -1105
- package/src/run.ts +50 -63
- package/src/schema.ts +391 -389
- package/src/scope.ts +39 -119
- package/src/serialization-contract.ts +1 -1
- package/src/suppress-file.ts +15 -20
- package/src/suppress.ts +3 -3
- package/src/template-field.ts +10 -7
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +12 -23
- package/src/verbs/explain.ts +3 -7
- package/src/verbs/print-projects.ts +2 -2
- package/src/verbs/verb.ts +2 -2
package/dist/src/schema.js
CHANGED
|
@@ -1,395 +1,371 @@
|
|
|
1
|
-
import { ConfigError } from "./config.js";
|
|
2
|
-
import { bytewise } from "./text/compare.js";
|
|
3
|
-
const leaf = { kind: "leaf" };
|
|
4
|
-
const integer = { kind: "leaf", integer: true };
|
|
5
|
-
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
6
|
-
const declarations = {
|
|
7
|
-
kind: "list",
|
|
8
|
-
members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
|
|
9
|
-
};
|
|
10
1
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
2
|
+
* Holds a value to a JSON Schema the generator copies from the Contract. It evaluates the
|
|
3
|
+
* keywords the Contract's schemas use; the schemas' patterns are written to match alike in
|
|
4
|
+
* RE2 and in ECMAScript.
|
|
14
5
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
members: {
|
|
18
|
-
contract_version: leaf,
|
|
19
|
-
target: {
|
|
20
|
-
kind: "section",
|
|
21
|
-
members: { kind: { kind: "object", members: { go: leaf, ts: leaf } } },
|
|
22
|
-
},
|
|
23
|
-
analysis: {
|
|
24
|
-
kind: "section",
|
|
25
|
-
members: {
|
|
26
|
-
languages: leaf,
|
|
27
|
-
min_confidence: leaf,
|
|
28
|
-
generated_files: leaf,
|
|
29
|
-
consumer_tests: leaf,
|
|
30
|
-
configurations: {
|
|
31
|
-
kind: "list",
|
|
32
|
-
members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
|
|
33
|
-
},
|
|
34
|
-
matrix: { kind: "section", members: { complete: leaf } },
|
|
35
|
-
template_dirs: leaf,
|
|
36
|
-
template_delimiters: {
|
|
37
|
-
kind: "object",
|
|
38
|
-
members: { left: leaf, right: leaf },
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
consumers: { kind: "section", members: { complete: leaf } },
|
|
43
|
-
roots: { kind: "section", members: { patterns: leaf } },
|
|
44
|
-
severity: { kind: "map" },
|
|
45
|
-
exemptions: { kind: "section", members: { disabled: leaf } },
|
|
46
|
-
reporters: {
|
|
47
|
-
kind: "section",
|
|
48
|
-
members: {
|
|
49
|
-
formats: leaf,
|
|
50
|
-
sort: leaf,
|
|
51
|
-
cascade: leaf,
|
|
52
|
-
max_findings: integer,
|
|
53
|
-
fail_on: leaf,
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
providers: {
|
|
57
|
-
kind: "section",
|
|
58
|
-
members: {
|
|
59
|
-
analyzers: {
|
|
60
|
-
kind: "list",
|
|
61
|
-
members: {
|
|
62
|
-
name: leaf,
|
|
63
|
-
languages: leaf,
|
|
64
|
-
command: leaf,
|
|
65
|
-
source: leaf,
|
|
66
|
-
version: leaf,
|
|
67
|
-
digest: leaf,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
go: { kind: "section", members: {} },
|
|
73
|
-
ts: {
|
|
74
|
-
kind: "section",
|
|
75
|
-
members: {
|
|
76
|
-
test_files: leaf,
|
|
77
|
-
entry_files: leaf,
|
|
78
|
-
component_extensions: leaf,
|
|
79
|
-
disabled_conventions: leaf,
|
|
80
|
-
injection_registrations: declarations,
|
|
81
|
-
lifecycle_contracts: {
|
|
82
|
-
kind: "list",
|
|
83
|
-
members: { components: declarations, bases: declarations, members: leaf },
|
|
84
|
-
},
|
|
85
|
-
serializers: declarations,
|
|
86
|
-
roots_complete: leaf,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
provenance: { kind: "map" },
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
/** The name of the severity object, whose member names the key list leaves open. */
|
|
93
|
-
export const SEVERITY_SECTION = "severity";
|
|
6
|
+
import { isRecord, pointerOf, pointerSegments } from "./json-document.js";
|
|
7
|
+
import { bytewise } from "./text/compare.js";
|
|
94
8
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* so the walk descends far enough to find a repeated member at any depth.
|
|
9
|
+
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
10
|
+
* another fails every check rather than passing values the keyword would refuse.
|
|
98
11
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
12
|
+
const KEYWORDS = new Set([
|
|
13
|
+
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
14
|
+
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
15
|
+
...["properties", "additionalProperties", "propertyNames", "required", "minProperties"],
|
|
16
|
+
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
17
|
+
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
18
|
+
]);
|
|
19
|
+
const patterns = new Map();
|
|
20
|
+
function pattern(source) {
|
|
21
|
+
let held = patterns.get(source);
|
|
22
|
+
if (held === undefined) {
|
|
23
|
+
held = new RegExp(source, "u");
|
|
24
|
+
patterns.set(source, held);
|
|
103
25
|
}
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
function joinKey(at, name) {
|
|
107
|
-
return at === "" ? name : `${at}.${name}`;
|
|
26
|
+
return held;
|
|
108
27
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!settingsOnly) {
|
|
113
|
-
keys.push(path);
|
|
114
|
-
collect(child, child.kind === "list" ? `${path}[]` : path, keys, settingsOnly);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (child.kind === "leaf" || child.kind === "list" || child.kind === "object") {
|
|
118
|
-
keys.push(path);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
collect(child, path, keys, settingsOnly);
|
|
28
|
+
function typeOf(value) {
|
|
29
|
+
if (value === null) {
|
|
30
|
+
return "null";
|
|
122
31
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* Every key the closed key list declares, as a dotted path, in ascending order:
|
|
126
|
-
* the sections, the settings, and the two open objects. A list's entry members
|
|
127
|
-
* are spelled under the list's own path followed by empty brackets.
|
|
128
|
-
*/
|
|
129
|
-
export function declaredKeys() {
|
|
130
|
-
const keys = [];
|
|
131
|
-
collect(SCHEMA_ROOT, "", keys, false);
|
|
132
|
-
return keys.sort(bytewise);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* The dotted path of every setting that holds one value, in ascending order. A
|
|
136
|
-
* list and a setting written as an object are each one setting, their members not
|
|
137
|
-
* addressable on their own, and the severity object is not among them: it
|
|
138
|
-
* resolves per code.
|
|
139
|
-
*/
|
|
140
|
-
export function settingPaths() {
|
|
141
|
-
const paths = [];
|
|
142
|
-
collect(SCHEMA_ROOT, "", paths, true);
|
|
143
|
-
return paths.sort(bytewise);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Whether one dotted path names a setting a source may supply: a setting of the
|
|
147
|
-
* closed key list, or one code of the severity object, whose member names the key
|
|
148
|
-
* list leaves open. It is the predicate the command maps its own flag names
|
|
149
|
-
* through, so a flag that would be refused as an unimplemented key is a failing
|
|
150
|
-
* test rather than a flag nothing reads.
|
|
151
|
-
*
|
|
152
|
-
* A section holds no value of its own, so `analysis` is false. A setting written
|
|
153
|
-
* as an object is supplied whole, so `analysis.template_delimiters` is true and
|
|
154
|
-
* `analysis.template_delimiters.left` is false. A severity code is a declared
|
|
155
|
-
* setting whatever it names, so `severity.DS9999` is true here and refused by
|
|
156
|
-
* resolution: this answers the shape of a path, and resolution answers the
|
|
157
|
-
* vocabulary.
|
|
158
|
-
*/
|
|
159
|
-
export function declaresSetting(path) {
|
|
160
|
-
if (settingPaths().includes(path)) {
|
|
161
|
-
return true;
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return "array";
|
|
162
34
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return false;
|
|
35
|
+
if (typeof value === "number") {
|
|
36
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
166
37
|
}
|
|
167
|
-
|
|
168
|
-
|
|
38
|
+
return typeof value;
|
|
39
|
+
}
|
|
40
|
+
function hasType(value, name) {
|
|
41
|
+
const held = typeOf(value);
|
|
42
|
+
return held === name || (name === "number" && held === "integer");
|
|
169
43
|
}
|
|
170
|
-
/** The
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let current = new Array(to.length + 1).fill(0);
|
|
176
|
-
for (let i = 1; i <= from.length; i += 1) {
|
|
177
|
-
current[0] = i;
|
|
178
|
-
for (let j = 1; j <= to.length; j += 1) {
|
|
179
|
-
const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
|
|
180
|
-
current[j] = Math.min(substitution, (previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1);
|
|
181
|
-
}
|
|
182
|
-
[previous, current] = [current, previous];
|
|
44
|
+
/** The types a schema names, none where it names no type. */
|
|
45
|
+
function typesOf(schema) {
|
|
46
|
+
const types = schema["type"];
|
|
47
|
+
if (types === undefined) {
|
|
48
|
+
return [];
|
|
183
49
|
}
|
|
184
|
-
return
|
|
50
|
+
return (Array.isArray(types) ? types : [types]).filter((one) => typeof one === "string");
|
|
185
51
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
52
|
+
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
53
|
+
function canonical(value) {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
190
56
|
}
|
|
191
|
-
|
|
57
|
+
if (isRecord(value)) {
|
|
58
|
+
return `{${Object.keys(value)
|
|
59
|
+
.sort(bytewise)
|
|
60
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
61
|
+
.join(",")}}`;
|
|
62
|
+
}
|
|
63
|
+
return JSON.stringify(value);
|
|
192
64
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
export function nearestKey(key) {
|
|
199
|
-
const segments = key.split(".");
|
|
200
|
-
let nearest = "";
|
|
201
|
-
let bestShared = -1;
|
|
202
|
-
let bestDistance = 0;
|
|
203
|
-
for (const candidate of declaredKeys()) {
|
|
204
|
-
const shared = sharedSegments(segments, candidate.split("."));
|
|
205
|
-
const distance = editDistance(key, candidate);
|
|
206
|
-
if (shared < bestShared || (shared === bestShared && distance >= bestDistance)) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
nearest = candidate;
|
|
210
|
-
bestShared = shared;
|
|
211
|
-
bestDistance = distance;
|
|
65
|
+
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
66
|
+
function codePoints(value) {
|
|
67
|
+
let counted = 0;
|
|
68
|
+
for (const _point of value) {
|
|
69
|
+
counted += 1;
|
|
212
70
|
}
|
|
213
|
-
return
|
|
71
|
+
return counted;
|
|
214
72
|
}
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
return
|
|
73
|
+
/** The schemas of a list keyword, none where the schema does not write it. */
|
|
74
|
+
function listOf(schema, keyword) {
|
|
75
|
+
const held = schema[keyword];
|
|
76
|
+
return Array.isArray(held) ? held : [];
|
|
219
77
|
}
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
function unimplementedKey(label, path) {
|
|
225
|
-
const nearest = nearestKey(path);
|
|
226
|
-
let hint = "";
|
|
227
|
-
if (nearest === path) {
|
|
228
|
-
hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
|
|
78
|
+
/** Whether a reader fixes refusal `a` before `b`: an object's before one inside it, then by kind. */
|
|
79
|
+
function before(a, b) {
|
|
80
|
+
if (a.pointer === b.pointer) {
|
|
81
|
+
return rank(a) < rank(b);
|
|
229
82
|
}
|
|
230
|
-
|
|
231
|
-
|
|
83
|
+
return b.pointer.startsWith(`${a.pointer}/`);
|
|
84
|
+
}
|
|
85
|
+
/** The order of the refusals of one place: an undeclared member, a missing one, any other. */
|
|
86
|
+
function rank(refusal) {
|
|
87
|
+
if (refusal.keyword === "additionalProperties" || refusal.keyword === "propertyNames") {
|
|
88
|
+
return 0;
|
|
232
89
|
}
|
|
233
|
-
return
|
|
90
|
+
return refusal.keyword === "required" ? 1 : 2;
|
|
234
91
|
}
|
|
235
|
-
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
92
|
+
/** The value one JSON Pointer segment names inside `value`, or undefined. */
|
|
93
|
+
function valueAt(value, segment) {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return /^[0-9]+$/u.test(segment) ? value[Number(segment)] : undefined;
|
|
239
96
|
}
|
|
97
|
+
return isRecord(value) && Object.hasOwn(value, segment) ? value[segment] : undefined;
|
|
240
98
|
}
|
|
241
99
|
/**
|
|
242
|
-
* The
|
|
243
|
-
*
|
|
244
|
-
* compares equal to the name it spells.
|
|
100
|
+
* The schema `properties` declares for the member `name`, or undefined: a member named as an
|
|
101
|
+
* `Object.prototype` property, such as `__proto__`, is declared only where it is written.
|
|
245
102
|
*/
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
103
|
+
function declaredMember(properties, name) {
|
|
104
|
+
return Object.hasOwn(properties, name) ? properties[name] : undefined;
|
|
105
|
+
}
|
|
106
|
+
/** The keyword checks over one set of schema documents, each named as a `$ref` names it. */
|
|
107
|
+
class Checker {
|
|
108
|
+
documents;
|
|
109
|
+
constructor(documents) {
|
|
110
|
+
this.documents = documents;
|
|
111
|
+
}
|
|
112
|
+
resolve(doc, ref) {
|
|
113
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
114
|
+
const target = file === "" ? doc : file;
|
|
115
|
+
let schema = this.documents[target];
|
|
116
|
+
for (const token of pointerSegments(pointer)) {
|
|
117
|
+
schema = isRecord(schema) ? schema[token] : undefined;
|
|
118
|
+
}
|
|
119
|
+
if (!isRecord(schema)) {
|
|
120
|
+
throw new Error(`schema: ${ref} names no schema`);
|
|
258
121
|
}
|
|
122
|
+
return [target, schema];
|
|
259
123
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
124
|
+
refusals(doc, schema, value, at) {
|
|
125
|
+
const found = [];
|
|
126
|
+
this.check(doc, schema, value, at, found);
|
|
127
|
+
return found;
|
|
128
|
+
}
|
|
129
|
+
matches(doc, schema, value, at) {
|
|
130
|
+
return this.refusals(doc, schema, value, at).length === 0;
|
|
131
|
+
}
|
|
132
|
+
check(doc, schema, value, at, found) {
|
|
133
|
+
for (const keyword of Object.keys(schema)) {
|
|
134
|
+
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
135
|
+
throw new Error(`schema: the keyword ${keyword} is not one this validator evaluates`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const refuse = (keyword, reason, member = "") => {
|
|
139
|
+
found.push({ pointer: at, keyword, reason, member });
|
|
140
|
+
};
|
|
141
|
+
if (typeof schema["$ref"] === "string") {
|
|
142
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
143
|
+
this.check(target, referenced, value, at, found);
|
|
144
|
+
}
|
|
145
|
+
const named = typesOf(schema);
|
|
146
|
+
if (named.length > 0 && !named.some((one) => hasType(value, one))) {
|
|
147
|
+
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
266
148
|
return;
|
|
267
149
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* Refuses a number written with a fraction or an exponent where the key list
|
|
277
|
-
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
278
|
-
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
279
|
-
* refuse by its type.
|
|
280
|
-
*/
|
|
281
|
-
function checkInteger(text, at, label) {
|
|
282
|
-
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
283
|
-
throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
|
|
150
|
+
this.checkValue(schema, value, refuse);
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
this.checkArray(doc, schema, value, at, found, refuse);
|
|
153
|
+
}
|
|
154
|
+
else if (isRecord(value)) {
|
|
155
|
+
this.checkObject(doc, schema, value, at, found, refuse);
|
|
156
|
+
}
|
|
157
|
+
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
284
158
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
159
|
+
checkValue(schema, value, refuse) {
|
|
160
|
+
const values = schema["enum"];
|
|
161
|
+
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
162
|
+
refuse("enum", `${canonical(value)} is not one of ${values.map(canonical).join(", ")}`);
|
|
163
|
+
}
|
|
164
|
+
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
165
|
+
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === "string") {
|
|
168
|
+
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
169
|
+
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
170
|
+
}
|
|
171
|
+
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
172
|
+
refuse("minLength", `${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (typeof value === "number") {
|
|
176
|
+
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
177
|
+
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
178
|
+
}
|
|
179
|
+
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
180
|
+
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
293
183
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
184
|
+
checkObject(doc, schema, value, at, found, refuse) {
|
|
185
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
186
|
+
const additional = schema["additionalProperties"];
|
|
187
|
+
const names = schema["propertyNames"];
|
|
188
|
+
for (const name of Object.keys(value).sort(bytewise)) {
|
|
189
|
+
const [refusedName] = isRecord(names) ? this.refusals(doc, names, name, at) : [];
|
|
190
|
+
if (refusedName !== undefined) {
|
|
191
|
+
refuse("propertyNames", `the member name ${JSON.stringify(name)}: ${refusedName.reason}`, name);
|
|
192
|
+
}
|
|
193
|
+
const member = `${at}${pointerOf([name])}`;
|
|
194
|
+
const declared = declaredMember(properties, name);
|
|
195
|
+
if (isRecord(declared)) {
|
|
196
|
+
this.check(doc, declared, value[name], member, found);
|
|
197
|
+
}
|
|
198
|
+
else if (additional === false) {
|
|
199
|
+
refuse("additionalProperties", `the member ${JSON.stringify(name)} is not one the schema declares`, name);
|
|
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"] : []) {
|
|
206
|
+
if (!Object.hasOwn(value, name)) {
|
|
207
|
+
refuse("required", `the member ${JSON.stringify(name)} is absent`, name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const fewest = schema["minProperties"];
|
|
211
|
+
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
212
|
+
refuse("minProperties", `the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`);
|
|
213
|
+
}
|
|
298
214
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
215
|
+
checkArray(doc, schema, value, at, found, refuse) {
|
|
216
|
+
const items = schema["items"];
|
|
217
|
+
if (isRecord(items)) {
|
|
218
|
+
value.forEach((item, i) => {
|
|
219
|
+
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
223
|
+
refuse("minItems", `the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`);
|
|
224
|
+
}
|
|
225
|
+
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
226
|
+
refuse("maxItems", `the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`);
|
|
227
|
+
}
|
|
228
|
+
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
229
|
+
refuse("uniqueItems", "the array lists one value twice");
|
|
230
|
+
}
|
|
302
231
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
232
|
+
checkApplicators(doc, schema, value, at, found, refuse) {
|
|
233
|
+
for (const each of listOf(schema, "allOf")) {
|
|
234
|
+
this.check(doc, each, value, at, found);
|
|
235
|
+
}
|
|
236
|
+
const matching = (keyword) => listOf(schema, keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
237
|
+
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
238
|
+
refuse("anyOf", "the value matches none of the alternatives");
|
|
239
|
+
}
|
|
240
|
+
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
241
|
+
refuse("oneOf", `the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`);
|
|
242
|
+
}
|
|
243
|
+
if (isRecord(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
244
|
+
refuse("not", "the value matches a schema it must not match");
|
|
245
|
+
}
|
|
246
|
+
if (isRecord(schema["if"])) {
|
|
247
|
+
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
248
|
+
if (isRecord(branch)) {
|
|
249
|
+
this.check(doc, branch, value, at, found);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
307
252
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
253
|
+
/**
|
|
254
|
+
* The refusal of `value` a reader fixes first, none for an instance: an object's
|
|
255
|
+
* refusal before any inside it, at one place an undeclared member, then a missing one,
|
|
256
|
+
* then any other, else the order the check finds them in. A oneOf no alternative
|
|
257
|
+
* matches is explained by the first refusal of the alternative the value was written
|
|
258
|
+
* for: the one whose type admits it, or of several, the one declaring the most of an
|
|
259
|
+
* object's members, then the fewest. Where two remain, the refusal is the oneOf's own.
|
|
260
|
+
*/
|
|
261
|
+
first(doc, schema, value, at) {
|
|
262
|
+
const found = [];
|
|
263
|
+
this.check(doc, schema, value, at, found);
|
|
264
|
+
let first = found[0];
|
|
265
|
+
for (const other of found.slice(1)) {
|
|
266
|
+
if (first !== undefined && before(other, first)) {
|
|
267
|
+
first = other;
|
|
268
|
+
}
|
|
317
269
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
continue;
|
|
270
|
+
if (first?.keyword !== "oneOf") {
|
|
271
|
+
return first;
|
|
321
272
|
}
|
|
322
|
-
|
|
323
|
-
|
|
273
|
+
let held = value;
|
|
274
|
+
let holders = [[doc, schema]];
|
|
275
|
+
for (const segment of pointerSegments(first.pointer.slice(at.length))) {
|
|
276
|
+
holders = holders.flatMap(([d, s]) => this.applied(d, s, held).flatMap(([d2, s2]) => this.member(d2, s2, held, segment)));
|
|
277
|
+
held = valueAt(held, segment);
|
|
324
278
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
279
|
+
for (const [d, holder] of holders) {
|
|
280
|
+
const alternatives = listOf(holder, "oneOf");
|
|
281
|
+
if (alternatives.some((each) => this.matches(d, each, held, first.pointer))) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const alternative = closest(alternatives, held);
|
|
285
|
+
const inner = alternative && this.first(d, alternative, held, first.pointer);
|
|
286
|
+
if (inner !== undefined) {
|
|
287
|
+
return inner;
|
|
288
|
+
}
|
|
329
289
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
290
|
+
return first;
|
|
291
|
+
}
|
|
292
|
+
/** The schemas checked at one place when `schema` is: it, its `$ref`, its allOf and its if branch. */
|
|
293
|
+
applied(doc, schema, value) {
|
|
294
|
+
const held = [[doc, schema]];
|
|
295
|
+
if (typeof schema["$ref"] === "string") {
|
|
296
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
297
|
+
held.push(...this.applied(target, referenced, value));
|
|
334
298
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
299
|
+
for (const each of listOf(schema, "allOf")) {
|
|
300
|
+
held.push(...this.applied(doc, each, value));
|
|
301
|
+
}
|
|
302
|
+
if (isRecord(schema["if"])) {
|
|
303
|
+
const branch = this.matches(doc, schema["if"], value, "") ? schema["then"] : schema["else"];
|
|
304
|
+
if (isRecord(branch)) {
|
|
305
|
+
held.push(...this.applied(doc, branch, value));
|
|
306
|
+
}
|
|
338
307
|
}
|
|
339
|
-
|
|
340
|
-
|
|
308
|
+
return held;
|
|
309
|
+
}
|
|
310
|
+
/** The schemas `schema` checks the member or the entry `segment` names in `value` with. */
|
|
311
|
+
member(doc, schema, value, segment) {
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
return isRecord(schema["items"]) ? [[doc, schema["items"]]] : [];
|
|
314
|
+
}
|
|
315
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
316
|
+
const declared = declaredMember(properties, segment);
|
|
317
|
+
if (isRecord(declared)) {
|
|
318
|
+
return [[doc, declared]];
|
|
319
|
+
}
|
|
320
|
+
const additional = schema["additionalProperties"];
|
|
321
|
+
return isRecord(additional) ? [[doc, additional]] : [];
|
|
341
322
|
}
|
|
342
323
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
324
|
+
/** The alternative a value matching none of them was written for, or none where none or two remain. */
|
|
325
|
+
function closest(alternatives, value) {
|
|
326
|
+
const typed = alternatives.filter((each) => {
|
|
327
|
+
const types = typesOf(each);
|
|
328
|
+
return types.length === 0 || types.some((one) => hasType(value, one));
|
|
329
|
+
});
|
|
330
|
+
if (typed.length === 1 || !isRecord(value)) {
|
|
331
|
+
return typed.length === 1 ? typed[0] : undefined;
|
|
332
|
+
}
|
|
333
|
+
let best = [];
|
|
334
|
+
let most = -1;
|
|
335
|
+
let fewest = 0;
|
|
336
|
+
for (const alternative of typed) {
|
|
337
|
+
const properties = isRecord(alternative["properties"]) ? alternative["properties"] : {};
|
|
338
|
+
const named = Object.keys(value).filter((name) => Object.hasOwn(properties, name)).length;
|
|
339
|
+
const declared = Object.keys(properties).length;
|
|
340
|
+
if (named > most || (named === most && declared < fewest)) {
|
|
341
|
+
[best, most, fewest] = [[alternative], named, declared];
|
|
352
342
|
}
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
continue;
|
|
343
|
+
else if (named === most && declared === fewest) {
|
|
344
|
+
best.push(alternative);
|
|
356
345
|
}
|
|
357
|
-
walkValue(cursor, `${at}[${index}]`, entry, label);
|
|
358
|
-
index += 1;
|
|
359
346
|
}
|
|
347
|
+
return best.length === 1 ? best[0] : undefined;
|
|
360
348
|
}
|
|
361
349
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* dotted path. The document is already known to parse.
|
|
350
|
+
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
351
|
+
* at one of them: what it refuses in a value, none for an instance.
|
|
365
352
|
*/
|
|
366
|
-
export function
|
|
367
|
-
const
|
|
368
|
-
|
|
353
|
+
export function schemaRefusals(documents, root) {
|
|
354
|
+
const checker = new Checker(documents);
|
|
355
|
+
const top = documents[root];
|
|
356
|
+
if (top === undefined) {
|
|
357
|
+
throw new Error(`the schema set holds no document ${root}`);
|
|
358
|
+
}
|
|
359
|
+
return (value) => {
|
|
360
|
+
const found = [];
|
|
361
|
+
checker.check(root, top, value, "", found);
|
|
362
|
+
return found;
|
|
363
|
+
};
|
|
369
364
|
}
|
|
370
365
|
/**
|
|
371
|
-
* The
|
|
372
|
-
*
|
|
373
|
-
* naming no setting is refused.
|
|
366
|
+
* The refusal of `value` a reader fixes first under one schema, none for an instance. A
|
|
367
|
+
* `$ref` the schema holds names a place in the schema itself.
|
|
374
368
|
*/
|
|
375
|
-
export function
|
|
376
|
-
|
|
377
|
-
for (const path of Object.keys(flat).sort(bytewise)) {
|
|
378
|
-
if (!declaresSetting(path)) {
|
|
379
|
-
throw unimplementedKey(label, path);
|
|
380
|
-
}
|
|
381
|
-
const segments = path.split(".");
|
|
382
|
-
let into = nested;
|
|
383
|
-
for (const segment of segments.slice(0, -1)) {
|
|
384
|
-
const child = into[segment] ?? {};
|
|
385
|
-
if (typeof child !== "object" || Array.isArray(child)) {
|
|
386
|
-
throw malformed(label, path, `the flag document sets ${JSON.stringify(segment)} twice`);
|
|
387
|
-
}
|
|
388
|
-
into[segment] = child;
|
|
389
|
-
into = child;
|
|
390
|
-
}
|
|
391
|
-
const last = segments[segments.length - 1] ?? "";
|
|
392
|
-
into[last] = flat[path];
|
|
393
|
-
}
|
|
394
|
-
return nested;
|
|
369
|
+
export function firstRefusal(schema, value) {
|
|
370
|
+
return new Checker({ "": schema }).first("", schema, value, "");
|
|
395
371
|
}
|