@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.10
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/README.md +1 -1
- package/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +245 -309
- package/dist/src/augmentations.js +124 -18
- package/dist/src/automatic-types.js +170 -0
- package/dist/src/build-scripts.js +425 -0
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +9 -9
- package/dist/src/config.js +6 -81
- package/dist/src/configuration-files.js +78 -188
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/consumers.js +3 -30
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/contract/config.generated.js +558 -0
- package/dist/src/contract/contract.generated.js +1244 -0
- package/dist/src/contract/report.generated.js +862 -0
- package/dist/src/convention-rows.js +146 -1
- package/dist/src/conventions.js +136 -70
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +280 -62
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +9 -18
- package/dist/src/entry-points.js +106 -21
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +1 -63
- package/dist/src/exempt.js +21 -40
- package/dist/src/failure.js +36 -0
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +225 -63
- package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
- package/dist/src/findings/emitters.js +6 -6
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +53 -19
- package/dist/src/findings/non-code-artifacts.js +71 -39
- package/dist/src/findings/reads-and-writes.js +16 -13
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +57 -28
- package/dist/src/findings/visibility-narrowing.js +40 -18
- package/dist/src/findings-pass.js +8 -15
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +72 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/generated-file.js +18 -4
- package/dist/src/git-ignore.js +99 -0
- package/dist/src/graph.js +9 -4
- package/dist/src/html-entries.js +6 -33
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/in-operands.js +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/injection-container.js +1 -1
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +341 -69
- package/dist/src/inventory.js +54 -29
- package/dist/src/javascript-file.js +33 -0
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +3 -366
- package/dist/src/load/classify.js +270 -0
- package/dist/src/load/diagnostics.js +80 -0
- package/dist/src/load/discover.js +277 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +27 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +313 -0
- package/dist/src/load/specifiers.js +115 -0
- package/dist/src/load/synthesize.js +140 -0
- package/dist/src/load/test-globals.js +207 -0
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
- package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
- package/dist/src/{workspace.js → load/workspace.js} +62 -124
- package/dist/src/manifest.js +49 -54
- package/dist/src/matrix.js +94 -13
- package/dist/src/mdx-entries.js +174 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/overrides.js +41 -10
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -78
- package/dist/src/product.js +13 -0
- package/dist/src/query.js +1 -0
- package/dist/src/ref.js +11 -0
- package/dist/src/references.js +52 -31
- package/dist/src/report-schema.js +51 -0
- package/dist/src/report.js +59 -41
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +16 -10
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -727
- package/dist/src/roots.js +374 -116
- package/dist/src/run.js +66 -77
- package/dist/src/schema.js +320 -342
- package/dist/src/scope.js +38 -115
- package/dist/src/serialization-contract.js +649 -29
- package/dist/src/session.js +41 -29
- package/dist/src/source-files.js +40 -15
- package/dist/src/suppress-file.js +34 -29
- package/dist/src/suppress-inline.js +21 -4
- package/dist/src/suppress.js +40 -56
- package/dist/src/sweep.js +32 -17
- package/dist/src/symbol-keys.js +105 -6
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-support.js +119 -18
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/named-files.js +54 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/text/yaml.js +393 -0
- package/dist/src/type-errors.js +27 -221
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/unseen-loads.js +100 -0
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +181 -125
- package/dist/src/verbs/describe.js +9 -16
- package/dist/src/verbs/explain.js +14 -9
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +6 -4
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +14 -3
- package/dist/src/workflow-steps.js +128 -33
- package/jsr.json +1 -1
- package/package.json +5 -4
- package/src/analysis.ts +333 -457
- package/src/augmentations.ts +152 -23
- package/src/automatic-types.ts +228 -0
- package/src/build-scripts.ts +543 -0
- package/src/component-layer.ts +4 -2
- package/src/components.ts +9 -10
- package/src/config.ts +31 -299
- package/src/configuration-files.ts +100 -191
- package/src/configured-declarations.ts +18 -23
- package/src/conformance-record.ts +2 -2
- package/src/consumers.ts +6 -57
- package/src/contextual-uses.ts +64 -12
- package/src/contract/config.generated.ts +818 -0
- package/src/contract/contract.generated.ts +1308 -0
- package/src/contract/report.generated.ts +1237 -0
- package/src/convention-rows.ts +186 -1
- package/src/conventions.ts +166 -90
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +356 -78
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +11 -20
- package/src/entry-points.ts +124 -28
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +1 -75
- package/src/exempt.ts +36 -50
- package/src/failure.ts +63 -0
- package/src/file-facts.ts +9 -24
- package/src/finding.ts +24 -9
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +297 -77
- package/src/findings/dependencies-and-module-machinery.ts +90 -31
- package/src/findings/emitter.ts +8 -9
- package/src/findings/emitters.ts +8 -8
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +64 -20
- package/src/findings/non-code-artifacts.ts +88 -50
- package/src/findings/reads-and-writes.ts +20 -16
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +65 -28
- package/src/findings/visibility-narrowing.ts +51 -19
- package/src/findings-pass.ts +10 -19
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +92 -0
- package/src/fs/tree.ts +110 -0
- package/src/generated-file.ts +23 -4
- package/src/git-ignore.ts +122 -0
- package/src/graph.ts +11 -3
- package/src/host.ts +17 -14
- package/src/html-entries.ts +6 -33
- package/src/import-bindings.ts +128 -0
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/indexed-access.ts +152 -30
- package/src/injection-container.ts +1 -1
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +449 -85
- package/src/inventory.ts +73 -27
- package/src/javascript-file.ts +37 -0
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +5 -413
- package/src/load/classify.ts +397 -0
- package/src/load/diagnostics.ts +144 -0
- package/src/load/discover.ts +455 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +73 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +504 -0
- package/src/load/specifiers.ts +168 -0
- package/src/load/synthesize.ts +211 -0
- package/src/load/test-globals.ts +251 -0
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
- package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
- package/src/{workspace.ts → load/workspace.ts} +90 -134
- package/src/manifest.ts +81 -75
- package/src/matrix.ts +154 -18
- package/src/mdx-entries.ts +218 -0
- package/src/module-declarations.ts +11 -13
- package/src/outside-tests.ts +1 -1
- package/src/overrides.ts +51 -12
- package/src/partial.ts +3 -2
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -91
- package/src/product.ts +18 -0
- package/src/query.ts +11 -0
- package/src/ref.ts +13 -0
- package/src/references.ts +62 -32
- package/src/report-schema.ts +68 -0
- package/src/report.ts +105 -215
- package/src/reporters/annotations.ts +22 -11
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -4
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +29 -16
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +283 -1111
- package/src/roots.ts +490 -137
- package/src/run.ts +75 -91
- package/src/schema.ts +391 -387
- package/src/scope.ts +46 -138
- package/src/serialization-contract.ts +818 -26
- package/src/session.ts +77 -54
- package/src/source-files.ts +67 -17
- package/src/suppress-file.ts +36 -34
- package/src/suppress-inline.ts +20 -3
- package/src/suppress.ts +52 -63
- package/src/sweep.ts +53 -18
- package/src/symbol-keys.ts +131 -7
- package/src/template-field.ts +15 -25
- package/src/test-support.ts +155 -18
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/named-files.inputs.json +1522 -0
- package/src/text/named-files.ts +68 -0
- package/src/text/shell.ts +161 -0
- package/src/text/yaml.ts +423 -0
- package/src/type-errors.ts +35 -273
- package/src/type-parameter-reads.ts +227 -0
- package/src/unseen-loads.ts +134 -0
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +245 -161
- package/src/verbs/describe.ts +14 -15
- package/src/verbs/explain.ts +15 -13
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +6 -4
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +2 -8
- package/src/verbs/version.ts +15 -3
- package/src/workflow-steps.ts +149 -34
- package/dist/src/discover.js +0 -347
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/setup-failure.js +0 -23
- package/dist/src/test-joins.js +0 -101
- package/dist/src/version.js +0 -7
- package/src/discover.ts +0 -510
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/setup-failure.ts +0 -39
- package/src/test-joins.ts +0 -120
- package/src/version.ts +0 -7
package/src/schema.ts
CHANGED
|
@@ -1,465 +1,469 @@
|
|
|
1
|
-
import { ConfigError } from "./config.ts";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* array of objects holding every admitted shape's members; `object`, one setting
|
|
8
|
-
* written as an object of declared members and supplied whole. Which shape an entry or
|
|
9
|
-
* a value takes, where the key list admits several, is the decoder's refusal.
|
|
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.
|
|
10
5
|
*/
|
|
11
|
-
|
|
6
|
+
|
|
7
|
+
import { isRecord, pointerOf, pointerSegments } from "./json-document.ts";
|
|
8
|
+
import { bytewise } from "./text/compare.ts";
|
|
12
9
|
|
|
13
10
|
/**
|
|
14
|
-
* One
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* One value a schema refuses: its JSON Pointer, the keyword, and why. `member` names the
|
|
12
|
+
* member a `required`, `additionalProperties` or `propertyNames` refusal is about, the
|
|
13
|
+
* object being at `pointer`, and is empty for every other keyword.
|
|
17
14
|
*/
|
|
18
|
-
export interface
|
|
19
|
-
readonly
|
|
20
|
-
readonly
|
|
21
|
-
readonly
|
|
15
|
+
export interface SchemaRefusal {
|
|
16
|
+
readonly pointer: string;
|
|
17
|
+
readonly keyword: string;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
readonly member: string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
28
|
-
const declarations: KeyNode = {
|
|
29
|
-
kind: "list",
|
|
30
|
-
members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
|
|
31
|
-
};
|
|
22
|
+
export type Schema = Readonly<Record<string, unknown>>;
|
|
23
|
+
type Documents = Readonly<Record<string, Schema>>;
|
|
32
24
|
|
|
33
25
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* adds a key fails there rather than being silently unimplemented.
|
|
26
|
+
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
27
|
+
* another fails every check rather than passing values the keyword would refuse.
|
|
37
28
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
analysis: {
|
|
47
|
-
kind: "section",
|
|
48
|
-
members: {
|
|
49
|
-
languages: leaf,
|
|
50
|
-
min_confidence: leaf,
|
|
51
|
-
generated_files: leaf,
|
|
52
|
-
consumer_tests: leaf,
|
|
53
|
-
configurations: {
|
|
54
|
-
kind: "list",
|
|
55
|
-
members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
|
|
56
|
-
},
|
|
57
|
-
matrix: { kind: "section", members: { complete: leaf } },
|
|
58
|
-
template_dirs: leaf,
|
|
59
|
-
template_delimiters: {
|
|
60
|
-
kind: "object",
|
|
61
|
-
members: { left: leaf, right: leaf },
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
consumers: { kind: "section", members: { complete: leaf } },
|
|
66
|
-
roots: { kind: "section", members: { patterns: leaf } },
|
|
67
|
-
severity: { kind: "map" },
|
|
68
|
-
exemptions: { kind: "section", members: { disabled: leaf } },
|
|
69
|
-
reporters: {
|
|
70
|
-
kind: "section",
|
|
71
|
-
members: {
|
|
72
|
-
formats: leaf,
|
|
73
|
-
sort: leaf,
|
|
74
|
-
cascade: leaf,
|
|
75
|
-
max_findings: integer,
|
|
76
|
-
fail_on: leaf,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
providers: {
|
|
80
|
-
kind: "section",
|
|
81
|
-
members: {
|
|
82
|
-
analyzers: {
|
|
83
|
-
kind: "list",
|
|
84
|
-
members: {
|
|
85
|
-
name: leaf,
|
|
86
|
-
languages: leaf,
|
|
87
|
-
command: leaf,
|
|
88
|
-
source: leaf,
|
|
89
|
-
version: leaf,
|
|
90
|
-
digest: leaf,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
go: { kind: "section", members: {} },
|
|
96
|
-
ts: {
|
|
97
|
-
kind: "section",
|
|
98
|
-
members: {
|
|
99
|
-
test_files: leaf,
|
|
100
|
-
entry_files: leaf,
|
|
101
|
-
component_extensions: leaf,
|
|
102
|
-
disabled_conventions: leaf,
|
|
103
|
-
injection_registrations: declarations,
|
|
104
|
-
lifecycle_contracts: {
|
|
105
|
-
kind: "list",
|
|
106
|
-
members: { components: declarations, bases: declarations, members: leaf },
|
|
107
|
-
},
|
|
108
|
-
serializers: declarations,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
provenance: { kind: "map" },
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** The name of the severity object, whose member names the key list leaves open. */
|
|
116
|
-
export const SEVERITY_SECTION = "severity";
|
|
29
|
+
const KEYWORDS: ReadonlySet<string> = new Set([
|
|
30
|
+
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
31
|
+
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
32
|
+
...["properties", "additionalProperties", "propertyNames", "required", "minProperties"],
|
|
33
|
+
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
34
|
+
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
35
|
+
]);
|
|
117
36
|
|
|
118
|
-
|
|
119
|
-
* The node one member name resolves to, and whether the key list declares it. A
|
|
120
|
-
* member of an open object, and any member below a setting, resolves to a leaf,
|
|
121
|
-
* so the walk descends far enough to find a repeated member at any depth.
|
|
122
|
-
*/
|
|
123
|
-
function memberOf(node: KeyNode, name: string): KeyNode | undefined {
|
|
124
|
-
if (node.kind === "section" || node.kind === "list" || node.kind === "object") {
|
|
125
|
-
const members = node.members ?? {};
|
|
126
|
-
return Object.hasOwn(members, name) ? members[name] : undefined;
|
|
127
|
-
}
|
|
128
|
-
return leaf;
|
|
129
|
-
}
|
|
37
|
+
const patterns = new Map<string, RegExp>();
|
|
130
38
|
|
|
131
|
-
function
|
|
132
|
-
|
|
39
|
+
function pattern(source: string): RegExp {
|
|
40
|
+
let held = patterns.get(source);
|
|
41
|
+
if (held === undefined) {
|
|
42
|
+
held = new RegExp(source, "u");
|
|
43
|
+
patterns.set(source, held);
|
|
44
|
+
}
|
|
45
|
+
return held;
|
|
133
46
|
}
|
|
134
47
|
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!settingsOnly) {
|
|
139
|
-
keys.push(path);
|
|
140
|
-
collect(child, child.kind === "list" ? `${path}[]` : path, keys, settingsOnly);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (child.kind === "leaf" || child.kind === "list" || child.kind === "object") {
|
|
144
|
-
keys.push(path);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
collect(child, path, keys, settingsOnly);
|
|
48
|
+
function typeOf(value: unknown): string {
|
|
49
|
+
if (value === null) {
|
|
50
|
+
return "null";
|
|
148
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;
|
|
149
59
|
}
|
|
150
60
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* are spelled under the list's own path followed by empty brackets.
|
|
155
|
-
*/
|
|
156
|
-
export function declaredKeys(): string[] {
|
|
157
|
-
const keys: string[] = [];
|
|
158
|
-
collect(SCHEMA_ROOT, "", keys, false);
|
|
159
|
-
return keys.sort();
|
|
61
|
+
function hasType(value: unknown, name: string): boolean {
|
|
62
|
+
const held = typeOf(value);
|
|
63
|
+
return held === name || (name === "number" && held === "integer");
|
|
160
64
|
}
|
|
161
65
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const paths: string[] = [];
|
|
170
|
-
collect(SCHEMA_ROOT, "", paths, true);
|
|
171
|
-
return paths.sort();
|
|
66
|
+
/** The types a schema names, none where it names no type. */
|
|
67
|
+
function typesOf(schema: Schema): readonly string[] {
|
|
68
|
+
const types = schema["type"];
|
|
69
|
+
if (types === undefined) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return (Array.isArray(types) ? types : [types]).filter((one) => typeof one === "string");
|
|
172
73
|
}
|
|
173
74
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
* through, so a flag that would be refused as an unimplemented key is a failing
|
|
179
|
-
* test rather than a flag nothing reads.
|
|
180
|
-
*
|
|
181
|
-
* A section holds no value of its own, so `analysis` is false. A setting written
|
|
182
|
-
* as an object is supplied whole, so `analysis.template_delimiters` is true and
|
|
183
|
-
* `analysis.template_delimiters.left` is false. A severity code is a declared
|
|
184
|
-
* setting whatever it names, so `severity.DS9999` is true here and refused by
|
|
185
|
-
* resolution: this answers the shape of a path, and resolution answers the
|
|
186
|
-
* vocabulary.
|
|
187
|
-
*/
|
|
188
|
-
export function declaresSetting(path: string): boolean {
|
|
189
|
-
if (settingPaths().includes(path)) {
|
|
190
|
-
return true;
|
|
75
|
+
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
76
|
+
function canonical(value: unknown): string {
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
191
79
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
80
|
+
if (isRecord(value)) {
|
|
81
|
+
return `{${Object.keys(value)
|
|
82
|
+
.sort(bytewise)
|
|
83
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
84
|
+
.join(",")}}`;
|
|
195
85
|
}
|
|
196
|
-
|
|
197
|
-
return path.slice(0, dot) === SEVERITY_SECTION && code !== "" && !code.includes(".");
|
|
86
|
+
return JSON.stringify(value);
|
|
198
87
|
}
|
|
199
88
|
|
|
200
|
-
/**
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
let current = new Array<number>(to.length + 1).fill(0);
|
|
206
|
-
for (let i = 1; i <= from.length; i += 1) {
|
|
207
|
-
current[0] = i;
|
|
208
|
-
for (let j = 1; j <= to.length; j += 1) {
|
|
209
|
-
const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
|
|
210
|
-
current[j] = Math.min(substitution, (previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1);
|
|
211
|
-
}
|
|
212
|
-
[previous, current] = [current, previous];
|
|
89
|
+
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
90
|
+
function codePoints(value: string): number {
|
|
91
|
+
let counted = 0;
|
|
92
|
+
for (const _point of value) {
|
|
93
|
+
counted += 1;
|
|
213
94
|
}
|
|
214
|
-
return
|
|
95
|
+
return counted;
|
|
215
96
|
}
|
|
216
97
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
return shared;
|
|
98
|
+
/** The schemas of a list keyword, none where the schema does not write it. */
|
|
99
|
+
function listOf(schema: Schema, keyword: string): readonly Schema[] {
|
|
100
|
+
const held = schema[keyword];
|
|
101
|
+
return Array.isArray(held) ? (held as Schema[]) : [];
|
|
223
102
|
}
|
|
224
103
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
*/
|
|
230
|
-
export function nearestKey(key: string): string {
|
|
231
|
-
const segments = key.split(".");
|
|
232
|
-
let nearest = "";
|
|
233
|
-
let bestShared = -1;
|
|
234
|
-
let bestDistance = 0;
|
|
235
|
-
for (const candidate of declaredKeys()) {
|
|
236
|
-
const shared = sharedSegments(segments, candidate.split("."));
|
|
237
|
-
const distance = editDistance(key, candidate);
|
|
238
|
-
if (shared < bestShared || (shared === bestShared && distance >= bestDistance)) {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
nearest = candidate;
|
|
242
|
-
bestShared = shared;
|
|
243
|
-
bestDistance = distance;
|
|
104
|
+
/** Whether a reader fixes refusal `a` before `b`: an object's before one inside it, then by kind. */
|
|
105
|
+
function before(a: SchemaRefusal, b: SchemaRefusal): boolean {
|
|
106
|
+
if (a.pointer === b.pointer) {
|
|
107
|
+
return rank(a) < rank(b);
|
|
244
108
|
}
|
|
245
|
-
return
|
|
109
|
+
return b.pointer.startsWith(`${a.pointer}/`);
|
|
246
110
|
}
|
|
247
111
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
112
|
+
/** The order of the refusals of one place: an undeclared member, a missing one, any other. */
|
|
113
|
+
function rank(refusal: SchemaRefusal): number {
|
|
114
|
+
if (refusal.keyword === "additionalProperties" || refusal.keyword === "propertyNames") {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
return refusal.keyword === "required" ? 1 : 2;
|
|
252
118
|
}
|
|
253
119
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
function unimplementedKey(label: string, path: string): ConfigError {
|
|
259
|
-
const nearest = nearestKey(path);
|
|
260
|
-
let hint = "";
|
|
261
|
-
if (nearest === path) {
|
|
262
|
-
hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
|
|
263
|
-
} else if (nearest !== "") {
|
|
264
|
-
hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
|
|
120
|
+
/** The value one JSON Pointer segment names inside `value`, or undefined. */
|
|
121
|
+
function valueAt(value: unknown, segment: string): unknown {
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
return /^[0-9]+$/u.test(segment) ? value[Number(segment)] : undefined;
|
|
265
124
|
}
|
|
266
|
-
return
|
|
267
|
-
"unimplemented-key",
|
|
268
|
-
path,
|
|
269
|
-
`${label}: key ${JSON.stringify(path)} is not implemented${hint}`,
|
|
270
|
-
);
|
|
125
|
+
return isRecord(value) && Object.hasOwn(value, segment) ? value[segment] : undefined;
|
|
271
126
|
}
|
|
272
127
|
|
|
273
128
|
/**
|
|
274
|
-
* The
|
|
275
|
-
*
|
|
276
|
-
* asks: a member written twice at one level is a refusal, and a parse keeps the
|
|
277
|
-
* last value, so the document reads as though it named the member once. Nothing
|
|
278
|
-
* here produces a value; the value is the parse's.
|
|
129
|
+
* The schema `properties` declares for the member `name`, or undefined: a member named as an
|
|
130
|
+
* `Object.prototype` property, such as `__proto__`, is declared only where it is written.
|
|
279
131
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
at: number;
|
|
132
|
+
function declaredMember(properties: Readonly<Record<string, unknown>>, name: string): unknown {
|
|
133
|
+
return Object.hasOwn(properties, name) ? properties[name] : undefined;
|
|
283
134
|
}
|
|
284
135
|
|
|
285
|
-
|
|
136
|
+
/** A schema with the document it is read from, so a relative `$ref` resolves there. */
|
|
137
|
+
type Located = readonly [string, Schema];
|
|
286
138
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
}
|
|
139
|
+
/** The keyword checks over one set of schema documents, each named as a `$ref` names it. */
|
|
140
|
+
class Checker {
|
|
141
|
+
private readonly documents: Documents;
|
|
292
142
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
* finds the literal's extent and the parse reads it, so an escaped member name
|
|
296
|
-
* compares equal to the name it spells.
|
|
297
|
-
*/
|
|
298
|
-
function readName(cursor: Cursor, at: string, label: string): string {
|
|
299
|
-
const start = cursor.at;
|
|
300
|
-
cursor.at += 1;
|
|
301
|
-
while (cursor.at < cursor.text.length) {
|
|
302
|
-
const ch = cursor.text.charCodeAt(cursor.at);
|
|
303
|
-
if (ch === 0x5c) {
|
|
304
|
-
cursor.at += 2;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
cursor.at += 1;
|
|
308
|
-
if (ch === 0x22) {
|
|
309
|
-
return JSON.parse(cursor.text.slice(start, cursor.at)) as string;
|
|
310
|
-
}
|
|
143
|
+
constructor(documents: Documents) {
|
|
144
|
+
this.documents = documents;
|
|
311
145
|
}
|
|
312
|
-
throw malformed(label, at, "a member name is not terminated");
|
|
313
|
-
}
|
|
314
146
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
147
|
+
resolve(doc: string, ref: string): Located {
|
|
148
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
149
|
+
const target = file === "" ? doc : file;
|
|
150
|
+
let schema: unknown = this.documents[target];
|
|
151
|
+
for (const token of pointerSegments(pointer)) {
|
|
152
|
+
schema = isRecord(schema) ? schema[token] : undefined;
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(schema)) {
|
|
155
|
+
throw new Error(`schema: ${ref} names no schema`);
|
|
320
156
|
}
|
|
321
|
-
|
|
157
|
+
return [target, schema];
|
|
322
158
|
}
|
|
323
|
-
}
|
|
324
159
|
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
160
|
+
refusals(doc: string, schema: Schema, value: unknown, at: string): readonly SchemaRefusal[] {
|
|
161
|
+
const found: SchemaRefusal[] = [];
|
|
162
|
+
this.check(doc, schema, value, at, found);
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
330
165
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
334
|
-
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
335
|
-
* refuse by its type.
|
|
336
|
-
*/
|
|
337
|
-
function checkInteger(text: string, at: string, label: string): void {
|
|
338
|
-
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
339
|
-
throw malformed(
|
|
340
|
-
label,
|
|
341
|
-
at,
|
|
342
|
-
`${text} is written with a fraction or an exponent, and an integer is digits alone`,
|
|
343
|
-
);
|
|
166
|
+
matches(doc: string, schema: Schema, value: unknown, at: string): boolean {
|
|
167
|
+
return this.refusals(doc, schema, value, at).length === 0;
|
|
344
168
|
}
|
|
345
|
-
}
|
|
346
169
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
170
|
+
check(doc: string, schema: Schema, value: unknown, at: string, found: SchemaRefusal[]): void {
|
|
171
|
+
for (const keyword of Object.keys(schema)) {
|
|
172
|
+
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
173
|
+
throw new Error(`schema: the keyword ${keyword} is not one this validator evaluates`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const refuse = (keyword: string, reason: string, member = ""): void => {
|
|
177
|
+
found.push({ pointer: at, keyword, reason, member });
|
|
178
|
+
};
|
|
179
|
+
if (typeof schema["$ref"] === "string") {
|
|
180
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
181
|
+
this.check(target, referenced, value, at, found);
|
|
182
|
+
}
|
|
183
|
+
const named = typesOf(schema);
|
|
184
|
+
if (named.length > 0 && !named.some((one) => hasType(value, one))) {
|
|
185
|
+
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.checkValue(schema, value, refuse);
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
this.checkArray(doc, schema, value, at, found, refuse);
|
|
191
|
+
} else if (isRecord(value)) {
|
|
192
|
+
this.checkObject(doc, schema, value, at, found, refuse);
|
|
193
|
+
}
|
|
194
|
+
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
354
195
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
196
|
+
|
|
197
|
+
private checkValue(
|
|
198
|
+
schema: Schema,
|
|
199
|
+
value: unknown,
|
|
200
|
+
refuse: (keyword: string, reason: string) => void,
|
|
201
|
+
): void {
|
|
202
|
+
const values = schema["enum"];
|
|
203
|
+
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
204
|
+
refuse("enum", `${canonical(value)} is not one of ${values.map(canonical).join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
207
|
+
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === "string") {
|
|
210
|
+
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
211
|
+
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
212
|
+
}
|
|
213
|
+
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
214
|
+
refuse(
|
|
215
|
+
"minLength",
|
|
216
|
+
`${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (typeof value === "number") {
|
|
221
|
+
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
222
|
+
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
223
|
+
}
|
|
224
|
+
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
225
|
+
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
359
228
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
229
|
+
|
|
230
|
+
private checkObject(
|
|
231
|
+
doc: string,
|
|
232
|
+
schema: Schema,
|
|
233
|
+
value: Readonly<Record<string, unknown>>,
|
|
234
|
+
at: string,
|
|
235
|
+
found: SchemaRefusal[],
|
|
236
|
+
refuse: (keyword: string, reason: string, member?: string) => void,
|
|
237
|
+
): void {
|
|
238
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
239
|
+
const additional = schema["additionalProperties"];
|
|
240
|
+
const names = schema["propertyNames"];
|
|
241
|
+
for (const name of Object.keys(value).sort(bytewise)) {
|
|
242
|
+
const [refusedName] = isRecord(names) ? this.refusals(doc, names, name, at) : [];
|
|
243
|
+
if (refusedName !== undefined) {
|
|
244
|
+
refuse(
|
|
245
|
+
"propertyNames",
|
|
246
|
+
`the member name ${JSON.stringify(name)}: ${refusedName.reason}`,
|
|
247
|
+
name,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const member = `${at}${pointerOf([name])}`;
|
|
251
|
+
const declared = declaredMember(properties, name);
|
|
252
|
+
if (isRecord(declared)) {
|
|
253
|
+
this.check(doc, declared, value[name], member, found);
|
|
254
|
+
} else if (additional === false) {
|
|
255
|
+
refuse(
|
|
256
|
+
"additionalProperties",
|
|
257
|
+
`the member ${JSON.stringify(name)} is not one the schema declares`,
|
|
258
|
+
name,
|
|
259
|
+
);
|
|
260
|
+
} else if (isRecord(additional)) {
|
|
261
|
+
this.check(doc, additional, value[name], member, found);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const name of Array.isArray(schema["required"]) ? (schema["required"] as string[]) : []) {
|
|
265
|
+
if (!Object.hasOwn(value, name)) {
|
|
266
|
+
refuse("required", `the member ${JSON.stringify(name)} is absent`, name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const fewest = schema["minProperties"];
|
|
270
|
+
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
271
|
+
refuse(
|
|
272
|
+
"minProperties",
|
|
273
|
+
`the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
363
276
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
277
|
+
|
|
278
|
+
private checkArray(
|
|
279
|
+
doc: string,
|
|
280
|
+
schema: Schema,
|
|
281
|
+
value: readonly unknown[],
|
|
282
|
+
at: string,
|
|
283
|
+
found: SchemaRefusal[],
|
|
284
|
+
refuse: (keyword: string, reason: string) => void,
|
|
285
|
+
): void {
|
|
286
|
+
const items = schema["items"];
|
|
287
|
+
if (isRecord(items)) {
|
|
288
|
+
value.forEach((item, i) => {
|
|
289
|
+
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
293
|
+
refuse(
|
|
294
|
+
"minItems",
|
|
295
|
+
`the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
299
|
+
refuse(
|
|
300
|
+
"maxItems",
|
|
301
|
+
`the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
305
|
+
refuse("uniqueItems", "the array lists one value twice");
|
|
306
|
+
}
|
|
368
307
|
}
|
|
369
|
-
}
|
|
370
308
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
309
|
+
private checkApplicators(
|
|
310
|
+
doc: string,
|
|
311
|
+
schema: Schema,
|
|
312
|
+
value: unknown,
|
|
313
|
+
at: string,
|
|
314
|
+
found: SchemaRefusal[],
|
|
315
|
+
refuse: (keyword: string, reason: string) => void,
|
|
316
|
+
): void {
|
|
317
|
+
for (const each of listOf(schema, "allOf")) {
|
|
318
|
+
this.check(doc, each, value, at, found);
|
|
379
319
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
320
|
+
const matching = (keyword: string): number =>
|
|
321
|
+
listOf(schema, keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
322
|
+
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
323
|
+
refuse("anyOf", "the value matches none of the alternatives");
|
|
383
324
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
325
|
+
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
326
|
+
refuse(
|
|
327
|
+
"oneOf",
|
|
328
|
+
`the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (isRecord(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
332
|
+
refuse("not", "the value matches a schema it must not match");
|
|
386
333
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
334
|
+
if (isRecord(schema["if"])) {
|
|
335
|
+
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
336
|
+
if (isRecord(branch)) {
|
|
337
|
+
this.check(doc, branch, value, at, found);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The refusal of `value` a reader fixes first, none for an instance: an object's
|
|
344
|
+
* refusal before any inside it, at one place an undeclared member, then a missing one,
|
|
345
|
+
* then any other, else the order the check finds them in. A oneOf no alternative
|
|
346
|
+
* matches is explained by the first refusal of the alternative the value was written
|
|
347
|
+
* for: the one whose type admits it, or of several, the one declaring the most of an
|
|
348
|
+
* object's members, then the fewest. Where two remain, the refusal is the oneOf's own.
|
|
349
|
+
*/
|
|
350
|
+
first(doc: string, schema: Schema, value: unknown, at: string): SchemaRefusal | undefined {
|
|
351
|
+
const found: SchemaRefusal[] = [];
|
|
352
|
+
this.check(doc, schema, value, at, found);
|
|
353
|
+
let first = found[0];
|
|
354
|
+
for (const other of found.slice(1)) {
|
|
355
|
+
if (first !== undefined && before(other, first)) {
|
|
356
|
+
first = other;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (first?.keyword !== "oneOf") {
|
|
360
|
+
return first;
|
|
361
|
+
}
|
|
362
|
+
let held = value;
|
|
363
|
+
let holders: readonly Located[] = [[doc, schema]];
|
|
364
|
+
for (const segment of pointerSegments(first.pointer.slice(at.length))) {
|
|
365
|
+
holders = holders.flatMap(([d, s]) =>
|
|
366
|
+
this.applied(d, s, held).flatMap(([d2, s2]) => this.member(d2, s2, held, segment)),
|
|
394
367
|
);
|
|
368
|
+
held = valueAt(held, segment);
|
|
369
|
+
}
|
|
370
|
+
for (const [d, holder] of holders) {
|
|
371
|
+
const alternatives = listOf(holder, "oneOf");
|
|
372
|
+
if (alternatives.some((each) => this.matches(d, each, held, first.pointer))) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const alternative = closest(alternatives, held);
|
|
376
|
+
const inner = alternative && this.first(d, alternative, held, first.pointer);
|
|
377
|
+
if (inner !== undefined) {
|
|
378
|
+
return inner;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return first;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** The schemas checked at one place when `schema` is: it, its `$ref`, its allOf and its if branch. */
|
|
385
|
+
private applied(doc: string, schema: Schema, value: unknown): readonly Located[] {
|
|
386
|
+
const held: Located[] = [[doc, schema]];
|
|
387
|
+
if (typeof schema["$ref"] === "string") {
|
|
388
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
389
|
+
held.push(...this.applied(target, referenced, value));
|
|
395
390
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (child === undefined) {
|
|
399
|
-
throw unimplementedKey(label, path);
|
|
391
|
+
for (const each of listOf(schema, "allOf")) {
|
|
392
|
+
held.push(...this.applied(doc, each, value));
|
|
400
393
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
394
|
+
if (isRecord(schema["if"])) {
|
|
395
|
+
const branch = this.matches(doc, schema["if"], value, "") ? schema["then"] : schema["else"];
|
|
396
|
+
if (isRecord(branch)) {
|
|
397
|
+
held.push(...this.applied(doc, branch, value));
|
|
398
|
+
}
|
|
404
399
|
}
|
|
405
|
-
|
|
406
|
-
walkValue(cursor, path, child, label);
|
|
400
|
+
return held;
|
|
407
401
|
}
|
|
408
|
-
}
|
|
409
402
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
for (;;) {
|
|
415
|
-
skipSpace(cursor);
|
|
416
|
-
const ch = cursor.text.charCodeAt(cursor.at);
|
|
417
|
-
if (ch === 0x5d) {
|
|
418
|
-
cursor.at += 1;
|
|
419
|
-
return;
|
|
403
|
+
/** The schemas `schema` checks the member or the entry `segment` names in `value` with. */
|
|
404
|
+
private member(doc: string, schema: Schema, value: unknown, segment: string): readonly Located[] {
|
|
405
|
+
if (Array.isArray(value)) {
|
|
406
|
+
return isRecord(schema["items"]) ? [[doc, schema["items"]]] : [];
|
|
420
407
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
408
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
409
|
+
const declared = declaredMember(properties, segment);
|
|
410
|
+
if (isRecord(declared)) {
|
|
411
|
+
return [[doc, declared]];
|
|
412
|
+
}
|
|
413
|
+
const additional = schema["additionalProperties"];
|
|
414
|
+
return isRecord(additional) ? [[doc, additional]] : [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The alternative a value matching none of them was written for, or none where none or two remain. */
|
|
419
|
+
function closest(alternatives: readonly Schema[], value: unknown): Schema | undefined {
|
|
420
|
+
const typed = alternatives.filter((each) => {
|
|
421
|
+
const types = typesOf(each);
|
|
422
|
+
return types.length === 0 || types.some((one) => hasType(value, one));
|
|
423
|
+
});
|
|
424
|
+
if (typed.length === 1 || !isRecord(value)) {
|
|
425
|
+
return typed.length === 1 ? typed[0] : undefined;
|
|
426
|
+
}
|
|
427
|
+
let best: Schema[] = [];
|
|
428
|
+
let most = -1;
|
|
429
|
+
let fewest = 0;
|
|
430
|
+
for (const alternative of typed) {
|
|
431
|
+
const properties = isRecord(alternative["properties"]) ? alternative["properties"] : {};
|
|
432
|
+
const named = Object.keys(value).filter((name) => Object.hasOwn(properties, name)).length;
|
|
433
|
+
const declared = Object.keys(properties).length;
|
|
434
|
+
if (named > most || (named === most && declared < fewest)) {
|
|
435
|
+
[best, most, fewest] = [[alternative], named, declared];
|
|
436
|
+
} else if (named === most && declared === fewest) {
|
|
437
|
+
best.push(alternative);
|
|
424
438
|
}
|
|
425
|
-
walkValue(cursor, `${at}[${index}]`, entry, label);
|
|
426
|
-
index += 1;
|
|
427
439
|
}
|
|
440
|
+
return best.length === 1 ? best[0] : undefined;
|
|
428
441
|
}
|
|
429
442
|
|
|
430
443
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* dotted path. The document is already known to parse.
|
|
444
|
+
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
445
|
+
* at one of them: what it refuses in a value, none for an instance.
|
|
434
446
|
*/
|
|
435
|
-
export function
|
|
436
|
-
|
|
437
|
-
|
|
447
|
+
export function schemaRefusals(
|
|
448
|
+
documents: Documents,
|
|
449
|
+
root: string,
|
|
450
|
+
): (value: unknown) => readonly SchemaRefusal[] {
|
|
451
|
+
const checker = new Checker(documents);
|
|
452
|
+
const top = documents[root];
|
|
453
|
+
if (top === undefined) {
|
|
454
|
+
throw new Error(`the schema set holds no document ${root}`);
|
|
455
|
+
}
|
|
456
|
+
return (value) => {
|
|
457
|
+
const found: SchemaRefusal[] = [];
|
|
458
|
+
checker.check(root, top, value, "", found);
|
|
459
|
+
return found;
|
|
460
|
+
};
|
|
438
461
|
}
|
|
439
462
|
|
|
440
463
|
/**
|
|
441
|
-
* The
|
|
442
|
-
*
|
|
443
|
-
* naming no setting is refused.
|
|
464
|
+
* The refusal of `value` a reader fixes first under one schema, none for an instance. A
|
|
465
|
+
* `$ref` the schema holds names a place in the schema itself.
|
|
444
466
|
*/
|
|
445
|
-
export function
|
|
446
|
-
|
|
447
|
-
for (const path of Object.keys(flat).sort()) {
|
|
448
|
-
if (!declaresSetting(path)) {
|
|
449
|
-
throw unimplementedKey(label, path);
|
|
450
|
-
}
|
|
451
|
-
const segments = path.split(".");
|
|
452
|
-
let into = nested;
|
|
453
|
-
for (const segment of segments.slice(0, -1)) {
|
|
454
|
-
const child = into[segment] ?? {};
|
|
455
|
-
if (typeof child !== "object" || Array.isArray(child)) {
|
|
456
|
-
throw malformed(label, path, `the flag document sets ${JSON.stringify(segment)} twice`);
|
|
457
|
-
}
|
|
458
|
-
into[segment] = child;
|
|
459
|
-
into = child as Record<string, unknown>;
|
|
460
|
-
}
|
|
461
|
-
const last = segments[segments.length - 1] ?? "";
|
|
462
|
-
into[last] = flat[path];
|
|
463
|
-
}
|
|
464
|
-
return nested;
|
|
467
|
+
export function firstRefusal(schema: Schema, value: unknown): SchemaRefusal | undefined {
|
|
468
|
+
return new Checker({ "": schema }).first("", schema, value, "");
|
|
465
469
|
}
|