ata-validator 1.62.1 → 1.63.1
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 +63 -8
- package/build.mjs +1 -0
- package/index.browser.mjs +23 -4
- package/index.bundle.mjs +14 -4
- package/index.edge.mjs +14 -4
- package/index.js +7 -2
- package/lib/aot-build.js +18 -0
- package/lib/codegen-engine.js +72 -3
- package/lib/compiled-common.js +2 -1
- package/lib/email-literal.js +4 -1
- package/lib/formats-source.js +39 -40
- package/lib/interpreter-core.js +148 -70
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +707 -0
- package/lib/ir/front.mjs +865 -0
- package/lib/ir/index.mjs +10 -0
- package/lib/ir/refs.mjs +108 -0
- package/lib/ir/run.mjs +520 -0
- package/lib/ir/table.mjs +233 -0
- package/lib/js-compiler-errors.js +9 -4
- package/lib/kernels/collapse.mjs +114 -0
- package/lib/kernels/custom.mjs +70 -0
- package/lib/kernels/fill.mjs +6 -0
- package/lib/kernels/formats-small.mjs +40 -0
- package/lib/kernels/formats.mjs +29 -0
- package/lib/kernels/index.mjs +11 -0
- package/lib/kernels/kernels.mjs +285 -0
- package/lib/kernels/leaves.mjs +28 -0
- package/lib/kernels/order.mjs +25 -0
- package/lib/kernels/plan-rich.mjs +17 -0
- package/lib/kernels/plan.d.ts +9 -0
- package/lib/kernels/plan.mjs +128 -0
- package/lib/kernels/preprocess.mjs +223 -0
- package/lib/kernels/rank.mjs +97 -0
- package/lib/kernels/rt.mjs +249 -0
- package/lib/kernels/table.mjs +182 -0
- package/lib/kernels/unique.mjs +157 -0
- package/lib/native-positions.js +1 -1
- package/lib/openapi.d.ts +2 -0
- package/lib/openapi.js +128 -0
- package/lib/plan-compiler.js +113 -14
- package/lib/public-api.js +7 -2
- package/lib/rejections.js +3 -1
- package/lib/schema-helpers.js +12 -0
- package/lib/schema-helpers.mjs +20 -0
- package/lib/schema-order.js +4 -2
- package/lib/shape-classifier.js +13 -1
- package/lib/standard-schema.js +1 -1
- package/lib/tier0.js +38 -8
- package/lib/validator-core.js +59 -36
- package/lib/version.js +1 -1
- package/lite.js +7 -2
- package/package.json +26 -8
package/lib/ir/front.mjs
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
// The IR. Statements: seq, if, let, bind (locals for the rest of a sequence),
|
|
2
|
+
// keys (each key: Object.keys, for-in with an own filter, or own __proto__
|
|
3
|
+
// last), range, count/incr, do (an expression for its effect), ret, require
|
|
4
|
+
// (an in-place call whose failure is this node's), fail (verdict: return
|
|
5
|
+
// false; errors: push), branches (anyOf/oneOf through the collapse),
|
|
6
|
+
// setDefault and fcall (the default fill). Expressions: var, lit, get,
|
|
7
|
+
// field, own, not, and, or, cmp, isObj, isObjAny, isArr, len, plainProto,
|
|
8
|
+
// type, guard, kernel, kfail, known, re, seg, cat, str, obj, pcall, and the
|
|
9
|
+
// annotation operations (annKey, annHasKey, annPrefix, annAll, annAllSet,
|
|
10
|
+
// annAt, annAtHas, annN, firstUneval). A procedure is a schema compiled once
|
|
11
|
+
// per mode (verdict, dual, ann, fill) with the parameters d, e, s, P, V, SP,
|
|
12
|
+
// A; both backends memoize procedures by schema object, which closes
|
|
13
|
+
// reference cycles.
|
|
14
|
+
//
|
|
15
|
+
// IR front end: the only place keyword semantics live. It turns a
|
|
16
|
+
// schema into procedures of IR (README.md) that the two backends read:
|
|
17
|
+
// emit.mjs writes JavaScript, run.mjs builds closures.
|
|
18
|
+
//
|
|
19
|
+
// Everything the schema fixes is decided here: messages, expected text,
|
|
20
|
+
// params, schema paths where they are static, which guards a known type makes
|
|
21
|
+
// redundant, whether a node collects annotations, which resources a
|
|
22
|
+
// $dynamicRef can land on.
|
|
23
|
+
|
|
24
|
+
import { LEAVES } from '../kernels/leaves.mjs'
|
|
25
|
+
import { type, required, additionalProperties, ANNOTATIONS, TYPES, falseSchema, contains, not as notK, branchRank, unevaluatedProperties as uPK, unevaluatedItems as uIK, collapse, makeHintSuggest, GUARD_NAME } from '../kernels/kernels.mjs'
|
|
26
|
+
import { isObj } from '../kernels/rt.mjs'
|
|
27
|
+
import { buildIndex } from './refs.mjs'
|
|
28
|
+
import draft7 from '../draft7.js'
|
|
29
|
+
|
|
30
|
+
// Under ctx, where each keyword runs in the current engine's walk of one node
|
|
31
|
+
// (lib/interpreter-core.js eval): references, the value's own checks, the
|
|
32
|
+
// custom keywords, arrays, objects, the in-place applicators, then the
|
|
33
|
+
// unevaluated keywords and the custom keywords declared post.
|
|
34
|
+
const CTX_RANK = { $ref: 0, $dynamicRef: 1, type: 2, enum: 3, const: 4, multipleOf: 5, maximum: 5, exclusiveMaximum: 5, minimum: 5, exclusiveMinimum: 5, maxLength: 6, minLength: 6, pattern: 6, format: 6, minItems: 8, maxItems: 8.1, uniqueItems: 8.2, prefixItems: 8.3, items: 8.4, additionalItems: 8.45, contains: 8.5, required: 9, minProperties: 10, maxProperties: 10, dependentRequired: 11, propertyNames: 12, properties: 13, dependentSchemas: 14, allOf: 15, anyOf: 17, oneOf: 18, not: 19, if: 20, then: 20, else: 20 }
|
|
35
|
+
const CTX_CUSTOM = 7
|
|
36
|
+
const CTX_MACRO = 16
|
|
37
|
+
const CTX_LAST = 21
|
|
38
|
+
|
|
39
|
+
// Every keyword name some dialect or the current engine gives a meaning to.
|
|
40
|
+
// One of these the front end does not handle above is refused; a name
|
|
41
|
+
// outside the set is ignored, as the current engine ignores it.
|
|
42
|
+
const MEANINGFUL = new Set(['$schema', '$id', 'id', '$ref', '$defs', 'definitions', '$anchor', '$dynamicRef', '$dynamicAnchor', '$recursiveRef', '$recursiveAnchor', '$vocabulary', '$comment', 'type', 'enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', 'uniqueItems', 'maxContains', 'minContains', 'maxProperties', 'minProperties', 'required', 'dependentRequired', 'dependencies', 'items', 'prefixItems', 'additionalItems', 'contains', 'properties', 'patternProperties', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema', 'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly', 'examples',
|
|
43
|
+
// The current engine's own: OpenAPI's nullable, custom error messages and
|
|
44
|
+
// $data references. (The discriminator means something only to the compat
|
|
45
|
+
// layer; the Validator ignores it, and so does the IR.)
|
|
46
|
+
'nullable', 'errorMessage', '$data'])
|
|
47
|
+
import dialect from '../dialect.js'
|
|
48
|
+
import keywordsLib from '../keywords.js'
|
|
49
|
+
import vocabularies from '../vocabularies.js'
|
|
50
|
+
import { orderCustom } from '../kernels/custom.mjs'
|
|
51
|
+
|
|
52
|
+
// Older dialects are read the way the current engine reads them: normalized
|
|
53
|
+
// into 2020-12 by its own lib/draft7.js (tuple items to prefixItems,
|
|
54
|
+
// dependencies split, definitions, exclusive bounds, nullable), so errors
|
|
55
|
+
// carry the same normalized schema paths. The decision is
|
|
56
|
+
// lib/validator-core.js _normalizeCallerSchemaUncached, without its
|
|
57
|
+
// nothing-to-do scan.
|
|
58
|
+
const DIALECTS = new Set(['https://json-schema.org/draft/2020-12/schema', 'http://json-schema.org/draft-07/schema#', 'http://json-schema.org/draft-07/schema', 'http://json-schema.org/draft-06/schema#', 'http://json-schema.org/draft-06/schema', 'http://json-schema.org/draft-04/schema#', 'http://json-schema.org/draft-04/schema', 'https://json-schema.org/draft/2019-09/schema'])
|
|
59
|
+
export function normalize (s, inherit = 0) {
|
|
60
|
+
if (s === null || typeof s !== 'object') return s
|
|
61
|
+
const code = s.$schema !== undefined ? dialect.draftCode(s.$schema) : inherit
|
|
62
|
+
const needsDraft7 = code === 1 || code === 2 || code === 4
|
|
63
|
+
const copy = structuredClone(s)
|
|
64
|
+
if (needsDraft7) draft7.normalizeDraft7(copy, true, code === 2 || code === 4, code === 4)
|
|
65
|
+
if (code === 3) draft7.normalizeDraft2019(copy, true)
|
|
66
|
+
draft7.normalizeNullable(copy)
|
|
67
|
+
draft7.normalizeExclusiveBounds(copy)
|
|
68
|
+
return copy
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- IR constructors -------------------------------------------------------
|
|
72
|
+
export const lit = (v) => ({ t: 'lit', v })
|
|
73
|
+
export const vr = (n) => ({ t: 'var', n })
|
|
74
|
+
const seq = (...body) => ({ t: 'seq', body: body.flat().filter(Boolean) })
|
|
75
|
+
const iff = (c, then, els = null) => ({ t: 'if', c, then, else: els })
|
|
76
|
+
const not = (e) => ({ t: 'not', e })
|
|
77
|
+
const and = (...a) => a.reduce((x, y) => ({ t: 'and', a: x, b: y }))
|
|
78
|
+
const or = (...a) => a.reduce((x, y) => ({ t: 'or', a: x, b: y }))
|
|
79
|
+
const get = (o, k) => ({ t: 'get', o, k })
|
|
80
|
+
const own = (o, k) => ({ t: 'own', o, k })
|
|
81
|
+
const cmp = (op, a, b) => ({ t: 'cmp', op, a, b })
|
|
82
|
+
const obj = (fields) => ({ t: 'obj', fields })
|
|
83
|
+
const isLit = (e) => e.t === 'lit'
|
|
84
|
+
// A statement that does nothing a caller can see: no failure, no return, no
|
|
85
|
+
// call, no write. Its expressions come from a list of reads without effects,
|
|
86
|
+
// so a loop or a read whose body is one can be left out.
|
|
87
|
+
const PURE = new Set(['and', 'or', 'not', 'cmp', 'get', 'own', 'lit', 'var', 'known', 're', 'isArr', 'isObj', 'isObjAny', 'len', 'type', 'plainProto', 'seg', 'cat', 'str'])
|
|
88
|
+
const pure = (e) => !e || e.t === 'lit' || (PURE.has(e.t) && Object.values(e).every((v) => v === null || typeof v !== 'object' || Array.isArray(v) || pure(v)))
|
|
89
|
+
const nop = (n) => !n || (n.t === 'seq' && n.body.every(nop)) || (n.t === 'let' && pure(n.value) && nop(n.body)) || (n.t === 'if' && pure(n.c) && nop(n.then) && nop(n.else))
|
|
90
|
+
// A path or schema path: literals fold now, the rest joins at run time.
|
|
91
|
+
export const cat = (a, suffix) => catE(a, lit(suffix))
|
|
92
|
+
const catE = (a, b) => {
|
|
93
|
+
if (isLit(a) && isLit(b)) return lit(a.v + b.v)
|
|
94
|
+
// x + "a" + "b" is x + "ab"
|
|
95
|
+
if (a.t === 'cat' && isLit(a.b) && isLit(b)) return { t: 'cat', a: a.a, b: lit(a.b.v + b.v) }
|
|
96
|
+
return { t: 'cat', a, b }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const seg = (k) => k.replace(/~/g, '~0').replace(/\//g, '~1')
|
|
100
|
+
const INHERITED = (k) => k in Object.prototype
|
|
101
|
+
const IMPLIED = { string: ['isStr'], integer: ['isNum'], number: ['isNum'], array: ['isArr'], object: ['isObjV'] }
|
|
102
|
+
|
|
103
|
+
// `schemas`: the documents a reference may leave this one for, as the
|
|
104
|
+
// current engine's `schemas` option takes them (an object keyed by URI, or an
|
|
105
|
+
// array of schemas with $id); each is normalized in its own dialect, a
|
|
106
|
+
// document without $schema in the root's. `metaschemas`: a Map of the
|
|
107
|
+
// standard metaschemas, loaded when a reference names one.
|
|
108
|
+
export function front (schema, opts = {}) {
|
|
109
|
+
// Whether the dynamic scope is needed is only known once every document a
|
|
110
|
+
// reference reaches has been read: a $dynamicRef can sit in a registry
|
|
111
|
+
// document or a metaschema. A build that met one without tracking the
|
|
112
|
+
// scope is thrown away and done again with it (it never answers on its own).
|
|
113
|
+
const first = build(schema, opts, false)
|
|
114
|
+
return first.sawDynamic && !first.dynamic ? build(schema, opts, true) : first
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function build (schema, { assertFormat = true, schemas = null, metaschemas = null, errorMessages = false, formats = null, keywords = null, baseURI = null } = {}, forceDynamic) {
|
|
118
|
+
const rootCode = schema && typeof schema === 'object' && schema.$schema !== undefined ? dialect.draftCode(schema.$schema) : 0
|
|
119
|
+
schema = normalize(schema)
|
|
120
|
+
// The address the schema came from is its base when it declares no $id of
|
|
121
|
+
// its own (lib/validator-core.js _materializeSchema).
|
|
122
|
+
if (typeof baseURI === 'string' && baseURI !== '' && schema && typeof schema === 'object' && !Array.isArray(schema) && typeof schema.$id !== 'string') schema = { ...schema, $id: baseURI }
|
|
123
|
+
let registry = null
|
|
124
|
+
if (schemas) {
|
|
125
|
+
registry = new Map()
|
|
126
|
+
const add = (key, doc) => {
|
|
127
|
+
const n = normalize(doc, rootCode)
|
|
128
|
+
if (key) registry.set(key, n)
|
|
129
|
+
const id = n && typeof n === 'object' && typeof n.$id === 'string' ? n.$id : null
|
|
130
|
+
if (id && id !== key) registry.set(id, n)
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(schemas)) for (const d of schemas) add(null, d)
|
|
133
|
+
else for (const [k, d] of Object.entries(schemas)) add(k, d)
|
|
134
|
+
}
|
|
135
|
+
// A root that names a registered meta-schema with a $vocabulary loses the
|
|
136
|
+
// keywords of the vocabularies it leaves out, which then mean nothing, as
|
|
137
|
+
// in the current engine (lib/validator-core.js _applyVocabularies).
|
|
138
|
+
if (registry !== null && schema && typeof schema === 'object' && typeof schema.$schema === 'string') {
|
|
139
|
+
const meta = registry.get(schema.$schema)
|
|
140
|
+
if (meta && typeof meta === 'object' && meta.$vocabulary && typeof meta.$vocabulary === 'object') {
|
|
141
|
+
const enabled = vocabularies.enabledKeywords(meta)
|
|
142
|
+
if (enabled) schema = vocabularies.stripDisabledKeywords(schema, enabled)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const external = metaschemas ? (uri) => { const d = metaschemas.get(uri) || metaschemas.get(uri + '#'); return d === undefined ? undefined : normalize(d) } : null
|
|
146
|
+
const index = buildIndex(schema, registry, external)
|
|
147
|
+
// The v1 dialect, declared at the root (lib/dialect.js isV1Dialect).
|
|
148
|
+
const v1 = dialect.isV1Dialect(schema)
|
|
149
|
+
// Custom keywords (the `keywords` option), normalized as the current engine
|
|
150
|
+
// normalizes them, so a definition it refuses is refused here with the
|
|
151
|
+
// same message. One that reads its data's context is not taken yet.
|
|
152
|
+
const kw = keywords ? keywordsLib.normalizeKeywords(keywords) : null
|
|
153
|
+
// A keyword that reads its data's context (dataContext) may change the
|
|
154
|
+
// value where it sits. The current engine then answers from its collecting
|
|
155
|
+
// walk alone, in its own order, every subschema walked with paths; the IR
|
|
156
|
+
// follows that walk for such a schema (ctx).
|
|
157
|
+
const ctx = kw !== null && keywordsLib.schemaUsesDataContext(schema, kw)
|
|
158
|
+
// What a node's custom keyword compiled to, or its macro expanded to: once
|
|
159
|
+
// per node, as the interpreted engine builds them once per plan, though the
|
|
160
|
+
// IR builds a node for each mode.
|
|
161
|
+
const built = new WeakMap()
|
|
162
|
+
const customOf = (s, k) => {
|
|
163
|
+
let m = built.get(s); if (m === undefined) built.set(s, m = new Map())
|
|
164
|
+
if (m.has(k)) return m.get(k)
|
|
165
|
+
const def = kw[k]; const n = s[k]; let out
|
|
166
|
+
if (def.macro !== null) out = { macro: def.macro(n, s) }
|
|
167
|
+
else if (def.compile !== null) {
|
|
168
|
+
const fn = def.compile(n, s)
|
|
169
|
+
if (typeof fn !== 'function') throw new Error(`keyword "${k}": compile must return a function`)
|
|
170
|
+
out = { keyword: k, fn, types: def.types, before: def.before || null, post: def.post === true, ctx: def.dataContext === true }
|
|
171
|
+
} else {
|
|
172
|
+
const validate = def.validate
|
|
173
|
+
const fn = def.dataContext ? (data, c) => validate(n, data, s, c) : (data) => validate(n, data, s)
|
|
174
|
+
// Errors the function writes land on itself, read from here.
|
|
175
|
+
fn.source = validate
|
|
176
|
+
out = { keyword: k, fn, types: def.types, before: def.before || null, post: def.post === true, ctx: def.dataContext === true }
|
|
177
|
+
}
|
|
178
|
+
m.set(k, out)
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
const usesDyn = (v) => v !== null && typeof v === 'object' && (Array.isArray(v) ? v.some(usesDyn) : ('$dynamicRef' in v || Object.values(v).some(usesDyn)))
|
|
182
|
+
const dynamic = forceDynamic || usesDyn(schema)
|
|
183
|
+
let sawDynamic = false
|
|
184
|
+
const procs = new Map() // id -> { id, mode, base, body }
|
|
185
|
+
const memo = new Map() // mode -> Map(schema -> id)
|
|
186
|
+
let tmp = 0
|
|
187
|
+
const fresh = (p) => p + (++tmp)
|
|
188
|
+
let needsRelate = false
|
|
189
|
+
|
|
190
|
+
// A procedure: one schema object compiled once per mode. `verdict` returns
|
|
191
|
+
// at the first failure and builds no errors; `dual` takes the error list e
|
|
192
|
+
// (null for a verdict); `ann` is dual plus the annotation collector A.
|
|
193
|
+
// Named before its body so a reference cycle calls itself.
|
|
194
|
+
// `entry`: the root as the entry point, whose path is "" and schema path
|
|
195
|
+
// "#", so they fold into literals; a $ref back to the root uses the
|
|
196
|
+
// general procedure.
|
|
197
|
+
function proc (s, mode, entry = false) {
|
|
198
|
+
const key = entry ? mode + ':entry' : mode
|
|
199
|
+
let m = memo.get(key)
|
|
200
|
+
if (!m) memo.set(key, m = new Map())
|
|
201
|
+
if (m.has(s)) return m.get(s)
|
|
202
|
+
const id = fresh(mode[0])
|
|
203
|
+
m.set(s, id)
|
|
204
|
+
const p = { id, mode, base: index.baseOf(s), body: null }
|
|
205
|
+
procs.set(id, p)
|
|
206
|
+
const saved = cur
|
|
207
|
+
cur = { mode, lexBase: index.baseOf(s), ann: mode === 'ann' }
|
|
208
|
+
p.body = entry ? node(s, vr('d'), lit(''), lit('#'), vr('d')) : node(s, vr('d'), vr('P'), vr('SP'), vr('V'))
|
|
209
|
+
cur = saved
|
|
210
|
+
return id
|
|
211
|
+
}
|
|
212
|
+
let cur = null
|
|
213
|
+
|
|
214
|
+
// A failure: the backends turn it into `return false` in a verdict and a
|
|
215
|
+
// pushed error otherwise.
|
|
216
|
+
const fail = (err) => ({ t: 'fail', err })
|
|
217
|
+
// detail and suggest are kernel functions; `src` is how generated code
|
|
218
|
+
// names them (an import, or a construction such as a format's hint).
|
|
219
|
+
const ref = (fn, src) => fn ? { fn, src: src || fn.name } : null
|
|
220
|
+
const err = (code, kw, path, sp, params, msg, exp, rank, ev, detail, suggest) =>
|
|
221
|
+
({ code, kw, path, sp, params, msg, exp, rank, ev, detail: detail && detail.fn ? detail : ref(detail), suggest: suggest && suggest.fn ? suggest : ref(suggest) })
|
|
222
|
+
const paramsOf = (o) => obj(Object.entries(o).map(([k, v]) => [k, lit(v)]))
|
|
223
|
+
|
|
224
|
+
// Run a subschema in place (same value): call its procedure. Under
|
|
225
|
+
// annotations the call gets a collector of its own, merged when valid.
|
|
226
|
+
// `ann`: whether the calling node collects annotations (nodeBody's own
|
|
227
|
+
// flag; cur.ann is already cleared for the node's children by then).
|
|
228
|
+
const inPlace = (s, x, path, sp, ev, ann = cur.ann) => {
|
|
229
|
+
if (cur.mode === 'verdict' && !ann) return { t: 'require', e: { t: 'pcall', proc: proc(s, 'verdict'), x, e: 'null' } }
|
|
230
|
+
return { t: 'require', e: { t: 'pcall', proc: proc(s, ann ? 'ann' : 'dual'), x, path, sp, ev, e: 'inherit', ann: ann ? 'merge' : null } }
|
|
231
|
+
}
|
|
232
|
+
// A subschema's verdict. Under ctx the full walk with its paths, its errors
|
|
233
|
+
// set aside, as the current engine walks it (every keyword runs).
|
|
234
|
+
const verdictOf = (s, x, path, sp, ev) => ctx ? { t: 'pcall', proc: proc(s, 'dual'), x, path, sp, ev: ev || x, e: 'fresh' } : { t: 'pcall', proc: proc(s, 'verdict'), x, e: 'null' }
|
|
235
|
+
|
|
236
|
+
function node (s, x, path, sp, ev) {
|
|
237
|
+
if (s === true) return seq()
|
|
238
|
+
if (s === false) return fail(err(falseSchema.code, falseSchema.keyword, path, sp, obj([]), lit(falseSchema.msg), undefined, falseSchema.rank, ev))
|
|
239
|
+
if (!isObj(s)) throw new Error('ir: a schema must be an object or a boolean')
|
|
240
|
+
// A subschema of another resource puts that resource on the dynamic
|
|
241
|
+
// scope: it runs as a procedure, which does the push.
|
|
242
|
+
if (dynamic && !cur.ann && index.baseOf(s) !== cur.lexBase) return inPlace(s, x, path, sp, ev)
|
|
243
|
+
// A node that reads annotations runs as an `ann` procedure with a fresh
|
|
244
|
+
// collector; inside one, the collector is A.
|
|
245
|
+
if (!cur.ann && ('unevaluatedProperties' in s || 'unevaluatedItems' in s)) {
|
|
246
|
+
// In a verdict the names a node evaluates can often be read off the
|
|
247
|
+
// schema: properties and patternProperties here and behind $ref and
|
|
248
|
+
// allOf, which must all pass anyway, so a failing subschema dropping
|
|
249
|
+
// its annotations changes no verdict. Then unevaluatedProperties is a
|
|
250
|
+
// check of names, with no collector (yamllint, 25 such nodes).
|
|
251
|
+
if (cur.mode === 'verdict' && !dynamic && !('unevaluatedItems' in s)) {
|
|
252
|
+
const st = staticNames(s, 0, true)
|
|
253
|
+
if (st !== null) {
|
|
254
|
+
const saved = cur.ann
|
|
255
|
+
cur.ann = false
|
|
256
|
+
try { return nodeBody(s, x, path, sp, ev, false, st) } finally { cur.ann = saved }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (cur.mode === 'verdict') return { t: 'require', e: { t: 'pcall', proc: proc(s, 'ann'), x, path, sp, ev, e: 'null', ann: 'none' } }
|
|
260
|
+
return { t: 'require', e: { t: 'pcall', proc: proc(s, 'ann'), x, path, sp, ev, e: 'inherit', ann: 'none' } }
|
|
261
|
+
}
|
|
262
|
+
const ann = cur.ann
|
|
263
|
+
// Children at other instance locations start without a collector.
|
|
264
|
+
const saved = cur.ann
|
|
265
|
+
cur.ann = false
|
|
266
|
+
try { return nodeBody(s, x, path, sp, ev, ann) } finally { cur.ann = saved }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Children at another instance location: no collector, same procedure mode.
|
|
270
|
+
const child = (s, x, path, sp) => {
|
|
271
|
+
const saved = cur.ann
|
|
272
|
+
cur.ann = false
|
|
273
|
+
try { return node(s, x, path, sp, x) } finally { cur.ann = saved }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The property names and patterns a schema evaluates whenever it passes,
|
|
277
|
+
// when the schema alone says so: null if a keyword makes them depend on the
|
|
278
|
+
// value (anyOf, oneOf, if, dependentSchemas, a dynamic reference) or
|
|
279
|
+
// evaluates every name (additionalProperties, unevaluatedProperties).
|
|
280
|
+
const STATIC_STOP = new Set(['additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', 'dependencies', '$dynamicRef', '$recursiveRef'])
|
|
281
|
+
// A schema met again on the way down (a cycle through $ref) gives null;
|
|
282
|
+
// answers are kept per schema, so shared subschemas are read once (a null
|
|
283
|
+
// kept from a cycle only sends a node to the collector, which is exact).
|
|
284
|
+
const staticActive = new Set()
|
|
285
|
+
const staticMemo = new Map()
|
|
286
|
+
function staticNames (s, depth, top = false) {
|
|
287
|
+
if (s === true) return { names: [], pats: [] }
|
|
288
|
+
if (!isObj(s) || depth > 100 || staticActive.has(s)) return null
|
|
289
|
+
if (!top && staticMemo.has(s)) return staticMemo.get(s)
|
|
290
|
+
staticActive.add(s)
|
|
291
|
+
let r
|
|
292
|
+
try { r = staticWalk(s, depth, top) } finally { staticActive.delete(s) }
|
|
293
|
+
if (!top) staticMemo.set(s, r)
|
|
294
|
+
return r
|
|
295
|
+
}
|
|
296
|
+
function staticWalk (s, depth, top) {
|
|
297
|
+
const names = []; const pats = []
|
|
298
|
+
const add = (r) => { if (r === null) return false; names.push(...r.names); pats.push(...r.pats); return true }
|
|
299
|
+
for (const k in s) {
|
|
300
|
+
if (top && k === 'unevaluatedProperties') continue
|
|
301
|
+
if (STATIC_STOP.has(k)) return null
|
|
302
|
+
if (k === 'properties' && isObj(s.properties)) names.push(...Object.keys(s.properties))
|
|
303
|
+
else if (k === 'patternProperties' && isObj(s.patternProperties)) pats.push(...Object.keys(s.patternProperties))
|
|
304
|
+
else if (k === '$ref') {
|
|
305
|
+
let t; try { t = index.resolve(s.$ref, index.baseOf(s)) } catch { return null }
|
|
306
|
+
if (!add(staticNames(t, depth + 1))) return null
|
|
307
|
+
} else if (k === 'allOf') {
|
|
308
|
+
if (!Array.isArray(s.allOf)) return null
|
|
309
|
+
for (const b of s.allOf) if (!add(staticNames(b, depth + 1))) return null
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { names: [...new Set(names)], pats: [...new Set(pats)] }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function nodeBody (s, x, path, sp, ev, ann, staticUP = null) {
|
|
316
|
+
const verdict = cur.mode === 'verdict'
|
|
317
|
+
// In a verdict the first failure returns, so a single type checked first
|
|
318
|
+
// makes the type known to every later keyword: the guards it implies go.
|
|
319
|
+
const known = verdict && typeof s.type === 'string' && TYPES[s.type] ? s.type : null
|
|
320
|
+
const implied = (g) => known !== null && (IMPLIED[known] || []).includes(g)
|
|
321
|
+
const objKw = ['properties', 'required', 'additionalProperties', 'patternProperties', 'propertyNames', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties'].some((k) => k in s)
|
|
322
|
+
const knownObj = known === 'object' && objKw
|
|
323
|
+
const o = vr(fresh('o')); const plain = vr(fresh('p'))
|
|
324
|
+
// Is x an object, and is its prototype Object.prototype (then a defined
|
|
325
|
+
// value under a name Object.prototype lacks is an own property).
|
|
326
|
+
const has = (name, kExpr) => name !== null && INHERITED(name) ? own(x, kExpr) : or(plain, own(x, kExpr))
|
|
327
|
+
const props = isObj(s.properties) ? s.properties : {}
|
|
328
|
+
const req = knownObj && Array.isArray(s.required) ? s.required : []
|
|
329
|
+
const typedReq = new Set(req.filter((r) => !INHERITED(r) && isObj(props[r]) && 'type' in props[r]))
|
|
330
|
+
let out = []
|
|
331
|
+
const lets = []
|
|
332
|
+
if (objKw && !knownObj) lets.push([o.n, { t: 'isObj', x }], [plain.n, and(o, { t: 'plainProto', x })])
|
|
333
|
+
const keys = known !== null ? ['type', ...Object.keys(s).filter((k) => k !== 'type')] : Object.keys(s)
|
|
334
|
+
const customOps = []
|
|
335
|
+
// A verdict with patternProperties beside additionalProperties: false
|
|
336
|
+
// walks the object's names once: each pattern tested once per name, a
|
|
337
|
+
// name no pattern matches and properties does not list fails. Two loops
|
|
338
|
+
// tested every pattern twice per name (minecraft-custom-main-menu-mod,
|
|
339
|
+
// 2.7x the compiled module). Errors keep the two loops: their order is
|
|
340
|
+
// the current engine's.
|
|
341
|
+
let patsFused = false
|
|
342
|
+
const fusedPats = () => {
|
|
343
|
+
const pats = s.patternProperties; const kv = vr(fresh('k')); const m = fresh('m')
|
|
344
|
+
const inner = []
|
|
345
|
+
for (const p in pats) {
|
|
346
|
+
const y = vr(fresh('x'))
|
|
347
|
+
const body = child(pats[p], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(p)))
|
|
348
|
+
inner.push(iff({ t: 're', src: p, k: kv }, seq({ t: 'incr', name: m }, nop(body) ? null : { t: 'let', name: y.n, value: get(x, kv), body })))
|
|
349
|
+
}
|
|
350
|
+
const names = Object.keys(props)
|
|
351
|
+
inner.push(iff(and(cmp('===', vr(m), lit(0)), ...(names.length ? [not({ t: 'known', k: kv, names, pats: [] })] : []), has(null, kv)), { t: 'ret' }))
|
|
352
|
+
patsFused = true
|
|
353
|
+
return iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: { t: 'count', name: m, body: seq(inner) } })
|
|
354
|
+
}
|
|
355
|
+
const canFuse = verdict && !ann && kw === null && s.additionalProperties === false && isObj(s.patternProperties)
|
|
356
|
+
// Under ctx each keyword's statements go to its place in the current
|
|
357
|
+
// engine's walk (lib/interpreter-core.js eval), not schema order.
|
|
358
|
+
const buckets = ctx ? new Map() : null
|
|
359
|
+
const bucket = (r) => { let b = buckets.get(r); if (b === undefined) buckets.set(r, b = []); return b }
|
|
360
|
+
for (const k of keys) {
|
|
361
|
+
const n = s[k]
|
|
362
|
+
if (ctx) {
|
|
363
|
+
out = bucket(kw[k] !== undefined && kw[k].macro !== null ? CTX_MACRO : CTX_RANK[k] !== undefined ? CTX_RANK[k] : CTX_LAST)
|
|
364
|
+
// Folded into the one walk over the object's own keys, below.
|
|
365
|
+
if (k === 'properties' || k === 'patternProperties' || k === 'additionalProperties') continue
|
|
366
|
+
// Missing, read the current engine's way: an own key, and for a name
|
|
367
|
+
// properties does not declare, a defined value.
|
|
368
|
+
if (k === 'required' && Array.isArray(n)) {
|
|
369
|
+
needsRelate = needsRelate || s.additionalProperties !== undefined
|
|
370
|
+
out.push(iff(o, seq(n.map((r) => iff(r in props ? not(own(x, lit(r))) : or(not(own(x, lit(r))), cmp('===', get(x, lit(r)), lit(undefined))), fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if (k === 'dependentRequired' && isObj(n)) {
|
|
374
|
+
const inner = []
|
|
375
|
+
for (const a in n) inner.push(iff(own(x, lit(a)), seq(n[a].map((r) => iff(not(own(x, lit(r))), fail(err('ATA7005', 'required', path, cat(sp, '/dependentRequired'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
|
|
376
|
+
out.push(iff(o, seq(inner)))
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (kw !== null && kw[k] !== undefined) {
|
|
381
|
+
const c = customOf(s, k)
|
|
382
|
+
if (c.macro === undefined) { if (c.fn) customOps.push(c); continue }
|
|
383
|
+
// A macro: the schema it returns runs in place, under the keyword's
|
|
384
|
+
// path, and the keyword reports an error of its own when it fails.
|
|
385
|
+
const m = c.macro
|
|
386
|
+
if (m !== null && typeof m === 'object' && JSON.stringify(m).includes('"$ref"')) throw new Error(`ir: macro keyword "${k}" returns a $ref`)
|
|
387
|
+
const msp = cat(sp, '/' + k)
|
|
388
|
+
const own = fail(err('ATA9001', k, path, msp, paramsOf({ keyword: k }), lit(`must pass "${k}" keyword validation`), undefined, 2, ev))
|
|
389
|
+
out.push(iff(not(verdict ? verdictOf(m, x, path, msp, ev) : { t: 'pcall', proc: proc(m, ann ? 'ann' : 'dual'), x, path, sp: msp, ev, e: 'inherit', ann: ann ? 'merge' : null }), own))
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
if (k === 'type') {
|
|
393
|
+
out.push(iff(not({ t: 'type', x, types: typeof n === 'string' ? [n] : n }), fail(err(type.code, 'type', path, cat(sp, '/type'), obj([['type', lit(n)]]), lit(type.msg(n)), type.exp(n), type.rank, ev, type.detail, type.suggest))))
|
|
394
|
+
if (knownObj) {
|
|
395
|
+
out.push({ t: 'bind', names: [[o.n, lit(true)], [plain.n, { t: 'plainProto', x }]] })
|
|
396
|
+
if (req.length) {
|
|
397
|
+
out.push(iff(and(not(plain), not(and(...req.map((r) => own(x, lit(r)))))), { t: 'ret' }))
|
|
398
|
+
for (const r of req) if (!typedReq.has(r)) out.push(iff(INHERITED(r) ? or(cmp('===', get(x, lit(r)), lit(undefined)), not(own(x, lit(r)))) : cmp('===', get(x, lit(r)), lit(undefined)), { t: 'ret' }))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else if (k === 'properties') {
|
|
402
|
+
const inner = []
|
|
403
|
+
// The entries that check their own presence, by name, for a backend
|
|
404
|
+
// that would rather visit the object's keys than look up every name
|
|
405
|
+
// (a verdict on an object that names hundreds of properties). Not
|
|
406
|
+
// with annotations or custom keywords, whose order is observable.
|
|
407
|
+
const keyed = []
|
|
408
|
+
for (const p in n) {
|
|
409
|
+
const y = vr(fresh('x'))
|
|
410
|
+
const body = child(n[p], y, isLit(path) && path.v === '' ? lit('/' + seg(p)) : cat(path, '/' + seg(p)), cat(sp, '/properties/' + seg(p)))
|
|
411
|
+
// An own key is evaluated even when its value is undefined.
|
|
412
|
+
if (ann) inner.push(iff(own(x, lit(p)), { t: 'do', e: { t: 'annKey', k: lit(p) } }))
|
|
413
|
+
if (!ann && nop(body)) continue
|
|
414
|
+
if (typedReq.has(p)) inner.push({ t: 'let', name: y.n, value: get(x, lit(p)), body })
|
|
415
|
+
else { keyed.push([p, inner.length]); inner.push({ t: 'let', name: y.n, value: get(x, lit(p)), body: iff(and(cmp('!==', y, lit(undefined)), has(p, lit(p))), body) }) }
|
|
416
|
+
}
|
|
417
|
+
// closed: every name properties lists, where additionalProperties is
|
|
418
|
+
// false with no patterns, for a verdict that tells the unknown names
|
|
419
|
+
// apart in the same pass over the object's names (lib/ir/emit.mjs).
|
|
420
|
+
const closed = s.additionalProperties === false && !isObj(s.patternProperties) && !ann && kw === null ? Object.keys(props) : null
|
|
421
|
+
if (inner.length) out.push(iff(o, { t: 'props', x, body: inner, keyed: ann || kw !== null ? null : keyed, closed }))
|
|
422
|
+
} else if (k === 'patternProperties') {
|
|
423
|
+
if (patsFused) continue
|
|
424
|
+
if (canFuse) { out.push(fusedPats()); continue }
|
|
425
|
+
const kv = vr(fresh('k'))
|
|
426
|
+
const inner = []
|
|
427
|
+
for (const p in n) {
|
|
428
|
+
const y = vr(fresh('x'))
|
|
429
|
+
const body = child(n[p], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(p)))
|
|
430
|
+
if (!ann && nop(body)) continue
|
|
431
|
+
inner.push(iff({ t: 're', src: p, k: kv }, seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body })))
|
|
432
|
+
}
|
|
433
|
+
if (inner.length) out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: seq(inner) }))
|
|
434
|
+
} else if (k === 'required') {
|
|
435
|
+
needsRelate = needsRelate || s.additionalProperties !== undefined
|
|
436
|
+
if (knownObj) continue
|
|
437
|
+
out.push(iff(o, seq(n.map((r) => iff(or(cmp('===', get(x, lit(r)), lit(undefined)), not(has(r, lit(r)))), fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
|
|
438
|
+
} else if (k === 'dependentRequired') {
|
|
439
|
+
const inner = []
|
|
440
|
+
for (const a in n) inner.push(iff(own(x, lit(a)), seq(n[a].map((r) => iff(or(cmp('===', get(x, lit(r)), lit(undefined)), not(has(r, lit(r)))), fail(err('ATA7005', 'required', path, cat(sp, '/dependentRequired'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
|
|
441
|
+
out.push(iff(o, seq(inner)))
|
|
442
|
+
} else if (k === 'additionalProperties') {
|
|
443
|
+
if (patsFused) continue
|
|
444
|
+
if (canFuse) { out.push(fusedPats()); continue }
|
|
445
|
+
const known2 = Object.keys(s.properties || {})
|
|
446
|
+
const pats = Object.keys(s.patternProperties || {})
|
|
447
|
+
const kv = vr(fresh('k'))
|
|
448
|
+
// Is the key named by properties or matched by patternProperties.
|
|
449
|
+
const isKnown = { t: 'known', k: kv, names: known2, pats }
|
|
450
|
+
if (n === false) {
|
|
451
|
+
// for-in visits own enumerable keys in Object.keys order; the own
|
|
452
|
+
// check drops inherited ones.
|
|
453
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'forin', apClosed: !pats.length && !ann && kw === null, body: iff(and(not(isKnown), has(null, kv)), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, fail(err(additionalProperties.code, 'additionalProperties', path, cat(sp, '/additionalProperties'), obj([['additionalProperty', kv]]), lit(additionalProperties.msg()), undefined, additionalProperties.rank, ev, additionalProperties.detail)))) }))
|
|
454
|
+
} else {
|
|
455
|
+
const y = vr(fresh('x'))
|
|
456
|
+
const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/additionalProperties'))
|
|
457
|
+
if (!ann && nop(body)) continue
|
|
458
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'protoLast', body: iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body })) }))
|
|
459
|
+
}
|
|
460
|
+
} else if (k === 'propertyNames') {
|
|
461
|
+
// The name is checked; the errors report the object.
|
|
462
|
+
const kv = vr(fresh('k'))
|
|
463
|
+
const saved = cur.ann; cur.ann = false
|
|
464
|
+
const body = node(n, kv, path, cat(sp, '/propertyNames'), x)
|
|
465
|
+
cur.ann = saved
|
|
466
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body }))
|
|
467
|
+
} else if (k === 'prefixItems') {
|
|
468
|
+
const inner = []
|
|
469
|
+
if (ann) inner.push({ t: 'do', e: { t: 'annPrefix', n: n.length, arr: x } })
|
|
470
|
+
n.forEach((ps, i) => {
|
|
471
|
+
const y = vr(fresh('x'))
|
|
472
|
+
inner.push(iff(cmp('>', { t: 'len', x }, lit(i)), { t: 'let', name: y.n, value: get(x, lit(i)), body: child(ps, y, isLit(path) && path.v === '' ? lit('/' + i) : cat(path, '/' + i), cat(sp, '/prefixItems/' + i)) }))
|
|
473
|
+
})
|
|
474
|
+
out.push(iff({ t: 'isArr', x }, seq(inner)))
|
|
475
|
+
} else if (k === 'items') {
|
|
476
|
+
const start = Array.isArray(s.prefixItems) ? s.prefixItems.length : 0
|
|
477
|
+
const j = vr(fresh('j')); const y = vr(fresh('x'))
|
|
478
|
+
const body = child(n, y, catE(path, catE(lit('/'), j)), cat(sp, '/items'))
|
|
479
|
+
out.push(iff({ t: 'isArr', x }, seq(ann ? iff(cmp('>', { t: 'len', x }, lit(start)), { t: 'do', e: { t: 'annAll' } }) : null, { t: 'range', arr: x, from: start, i: j.n, body: { t: 'let', name: y.n, value: get(x, j), body } })))
|
|
480
|
+
} else if (k === 'contains') {
|
|
481
|
+
const min = s.minContains === undefined ? 1 : s.minContains
|
|
482
|
+
const max = s.maxContains
|
|
483
|
+
const c = vr(fresh('c')); const i = vr(fresh('i'))
|
|
484
|
+
const checks = [iff(cmp('<', c, lit(min)), fail(err(contains.code, 'contains', path, cat(sp, '/contains'), obj([['minContains', lit(min)]]), lit(`must contain at least ${min} valid item(s)`), undefined, contains.rank, ev)))]
|
|
485
|
+
if (max !== undefined) checks.push(iff(cmp('>', c, lit(max)), fail(err(contains.code, 'contains', path, cat(sp, '/contains'), obj([['minContains', lit(min)], ['maxContains', lit(max)]]), lit(`must NOT contain more than ${max} valid item(s)`), undefined, contains.rank, ev))))
|
|
486
|
+
out.push(iff({ t: 'isArr', x }, { t: 'count', name: c.n, body: seq({ t: 'range', arr: x, from: 0, i: i.n, body: iff(ctx ? verdictOf(n, get(x, i), catE(path, catE(lit('/'), i)), cat(sp, '/contains')) : { t: 'pcall', proc: proc(n, 'verdict'), x: get(x, i), e: 'null' }, seq({ t: 'incr', name: c.n }, ann ? { t: 'do', e: { t: 'annAt', i } } : null)) }, checks) }))
|
|
487
|
+
} else if (k === 'allOf') {
|
|
488
|
+
n.forEach((sub, i) => { out.push(ann ? inPlace(sub, x, path, cat(sp, '/allOf/' + i), ev, true) : node(sub, x, path, cat(sp, '/allOf/' + i), ev)) })
|
|
489
|
+
} else if (k === 'anyOf' || k === 'oneOf') {
|
|
490
|
+
const mode = verdict ? 'verdict' : ann ? 'ann' : 'dual'
|
|
491
|
+
out.push({ t: 'branches', kw: k, procs: n.map((sub) => proc(sub, mode)), sps: n.map((_, i) => cat(sp, '/' + k + '/' + i)), titles: n.map((sub) => isObj(sub) ? sub.title : undefined), x, path, sp: cat(sp, '/' + k), ev, ann: ann ? (k === 'oneOf' ? 'onlyIfOne' : 'each') : null, rank: branchRank, collapse })
|
|
492
|
+
} else if (k === 'not') {
|
|
493
|
+
out.push(iff(verdictOf(n, x, path, cat(sp, '/not'), ev), fail(err(notK.code, notK.keyword, path, cat(sp, '/not'), obj([]), lit(notK.msg), undefined, notK.rank, ev))))
|
|
494
|
+
} else if (k === 'if') {
|
|
495
|
+
const thenB = 'then' in s ? (ann ? inPlace(s.then, x, path, cat(sp, '/then'), ev, true) : node(s.then, x, path, cat(sp, '/then'), ev)) : seq()
|
|
496
|
+
const elseB = 'else' in s ? (ann ? inPlace(s.else, x, path, cat(sp, '/else'), ev, true) : node(s.else, x, path, cat(sp, '/else'), ev)) : seq()
|
|
497
|
+
const cond = ann ? { t: 'pcall', proc: proc(n, 'ann'), x, path, sp: cat(sp, '/if'), ev, e: ctx ? 'fresh' : 'null', ann: 'merge' } : verdictOf(n, x, path, cat(sp, '/if'), ev)
|
|
498
|
+
out.push(iff(cond, thenB, elseB))
|
|
499
|
+
} else if (k === 'then' || k === 'else') {
|
|
500
|
+
// Read by if; without an if they do nothing.
|
|
501
|
+
} else if (k === 'dependentSchemas') {
|
|
502
|
+
for (const a in n) out.push(iff(and(o, own(x, lit(a))), ann ? inPlace(n[a], x, path, cat(sp, '/dependentSchemas/' + seg(a)), ev, true) : node(n[a], x, path, cat(sp, '/dependentSchemas/' + seg(a)), ev)))
|
|
503
|
+
} else if (k === '$ref') {
|
|
504
|
+
// The target runs as if it stood here: same value, same paths. One
|
|
505
|
+
// that names nothing fails every value that reaches it, with the
|
|
506
|
+
// current engine's error, rather than refusing the schema.
|
|
507
|
+
let target
|
|
508
|
+
try { target = index.resolve(n, index.baseOf(s)) } catch (e) {
|
|
509
|
+
if (e.unresolved !== true) throw e
|
|
510
|
+
out.push(fail(err('ATA5001', '$ref', path, cat(sp, '/$ref'), paramsOf({ ref: n }), lit(`cannot resolve $ref ${n}`), undefined, 2, ev)))
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
out.push(inPlace(target, x, path, sp, ev, ann))
|
|
514
|
+
} else if (k === '$dynamicRef') {
|
|
515
|
+
sawDynamic = true
|
|
516
|
+
// Resolved like $ref first; when that target declares the anchor the
|
|
517
|
+
// fragment names, the outermost resource on the scope that declares
|
|
518
|
+
// it wins. The candidates are known now, the choice at run time.
|
|
519
|
+
const hash = n.indexOf('#')
|
|
520
|
+
const name = hash !== -1 && !n.slice(hash + 1).startsWith('/') ? n.slice(hash + 1) : null
|
|
521
|
+
// Under v1 the scope is searched even when the reference resolves to
|
|
522
|
+
// nothing on its own; with no anchor of that name on the scope it
|
|
523
|
+
// fails where it runs (lib/interpreter-core.js).
|
|
524
|
+
let target
|
|
525
|
+
try { target = index.resolve(n, index.baseOf(s)) } catch (e) { if (!(v1 && e.unresolved && name)) throw e }
|
|
526
|
+
if (target === undefined) {
|
|
527
|
+
const missing = err('ATA9001', '$dynamicRef', path, cat(sp, '/$dynamicRef'), paramsOf({ ref: n }), lit(`cannot resolve $dynamicRef ${n}`), undefined, 2, ev)
|
|
528
|
+
const candidates = index.dynCandidates(name)
|
|
529
|
+
if (candidates.length === 0) { out.push(fail(missing)); continue }
|
|
530
|
+
const mode = verdict && !ann ? 'verdict' : ann ? 'ann' : 'dual'
|
|
531
|
+
const call = mode === 'verdict' ? { t: 'pcall', proc: null, x, e: 'null' } : { t: 'pcall', proc: null, x, path, sp, ev, e: 'inherit', ann: ann ? 'merge' : null }
|
|
532
|
+
call.dynamic = candidates.map(([b, t]) => [b, proc(t, mode)])
|
|
533
|
+
call.missing = missing
|
|
534
|
+
out.push({ t: 'require', e: call })
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
// Under 2020-12 the target must carry the anchor itself (bookending)
|
|
538
|
+
// before the scope is consulted; v1 drops that.
|
|
539
|
+
const dyn = name !== null && name !== '' && (v1 || (isObj(target) && target.$dynamicAnchor === name))
|
|
540
|
+
const r = inPlace(target, x, path, sp, ev, ann)
|
|
541
|
+
if (dyn) {
|
|
542
|
+
const mode = r.e.proc[0] === 'v' ? 'verdict' : r.e.proc[0] === 'a' ? 'ann' : 'dual'
|
|
543
|
+
r.e.dynamic = index.dynCandidates(name).map(([b, t]) => [b, proc(t, mode)])
|
|
544
|
+
}
|
|
545
|
+
out.push(r)
|
|
546
|
+
} else if (k === 'unevaluatedProperties' || k === 'unevaluatedItems') {
|
|
547
|
+
// Run after every other keyword of the node, below.
|
|
548
|
+
} else if (k === '$id' || k === '$anchor' || k === '$dynamicAnchor' || k === '$vocabulary' || k === '$defs' || k === 'definitions' || k === 'minContains' || k === 'maxContains' || k === 'additionalItems' || k === 'contentMediaType' || k === 'contentEncoding' || k === 'contentSchema') {
|
|
549
|
+
// additionalItems that normalization left in place has no effect (items is not a tuple);
|
|
550
|
+
// the content keywords are annotations, as in the current engine.
|
|
551
|
+
// Read by the index or by another keyword.
|
|
552
|
+
} else if (k === 'errorMessage' && errorMessages) {
|
|
553
|
+
// Applied to the finished errors by the backend that asked for it
|
|
554
|
+
// (lib/error-messages.js, as the current engine applies it).
|
|
555
|
+
} else if (k === '$schema') {
|
|
556
|
+
// Any other URI is read as the default dialect, as the current engine
|
|
557
|
+
// reads it (lib/dialect.js); v1 is told apart at the root.
|
|
558
|
+
} else if (k === 'format' && !assertFormat) {
|
|
559
|
+
// An annotation unless format assertion is asked for.
|
|
560
|
+
} else if (LEAVES[k]) {
|
|
561
|
+
const K = LEAVES[k]
|
|
562
|
+
// A format the caller registered as a function (the `formats` option)
|
|
563
|
+
// replaces the built-in check of that name, keeps its error code and
|
|
564
|
+
// loses its hint, as in the current engine; any other value there is
|
|
565
|
+
// ignored.
|
|
566
|
+
const user = k === 'format' && formats !== null && typeof n === 'string' && Object.prototype.hasOwnProperty.call(formats, n) && typeof formats[n] === 'function' ? formats[n] : null
|
|
567
|
+
const pv = user !== null ? user : K.prepare ? K.prepare(n) : n
|
|
568
|
+
// A format the current engine does not know is not checked.
|
|
569
|
+
if (k === 'format' && pv === null) continue
|
|
570
|
+
const gname = GUARD_NAME.get(K.on)
|
|
571
|
+
if (gname === undefined) throw new Error(`ir: keyword "${k}" has no guard`)
|
|
572
|
+
const guard = implied(gname) ? null : { t: 'guard', g: gname, x }
|
|
573
|
+
const code = typeof K.code === 'function' ? K.code(n) : K.code
|
|
574
|
+
let suggest = K.suggest || null
|
|
575
|
+
if (K.suggestFor) { const sf = user === null ? K.suggestFor(n) : null; suggest = sf ? ref(sf, `makeHintSuggest(${n}Hint)`) : null }
|
|
576
|
+
if (K.fail) {
|
|
577
|
+
const f = vr(fresh('f'))
|
|
578
|
+
out.push(iff(guard || lit(true), { t: 'let', name: f.n, value: { t: 'kfail', K, x, n, pv }, body: iff(cmp('!==', f, lit(null)), fail(err(code, k, path, cat(sp, '/' + k), { t: 'field', o: f, k: 'params' }, { t: 'field', o: f, k: 'msg' }, K.exp(n), K.rank, ev, K.detail, suggest))) }))
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
const check = not({ t: 'kernel', K, kw: k, x, n, pv })
|
|
582
|
+
out.push(iff(guard ? and(guard, check) : check, fail(err(code, k, path, cat(sp, '/' + k), paramsOf(K.params(n)), lit(K.msg(n)), K.exp(n), K.rank, ev, K.detail, suggest))))
|
|
583
|
+
} else if (MEANINGFUL.has(k) && !ANNOTATIONS.has(k)) {
|
|
584
|
+
throw new Error(`ir: keyword "${k}" is not supported`)
|
|
585
|
+
}
|
|
586
|
+
// Any other name means nothing to the current engine, which ignores it
|
|
587
|
+
// (tsType, markdownDescription, x-...), and so does the IR.
|
|
588
|
+
}
|
|
589
|
+
// The node's custom keywords, in the order they declare (orderCustom):
|
|
590
|
+
// each runs on the value when its `type` admits it.
|
|
591
|
+
const ordered = customOps.length ? orderCustom(customOps) : []
|
|
592
|
+
let post = null
|
|
593
|
+
if (!ctx) { if (ordered.length) out.push({ t: 'custom', ops: ordered, x, path, sp, ev }) } else {
|
|
594
|
+
const now = ordered.filter((op) => !op.post)
|
|
595
|
+
if (now.length) bucket(CTX_CUSTOM).push({ t: 'custom', ops: now, x, path, sp, ev })
|
|
596
|
+
const later = ordered.filter((op) => op.post)
|
|
597
|
+
// Run after everything else at the node, on the value read again from
|
|
598
|
+
// where it sits.
|
|
599
|
+
if (later.length) post = { t: 'custom', ops: later, x, path, sp, ev, here: true }
|
|
600
|
+
if ('properties' in s || 'patternProperties' in s || 'additionalProperties' in s) bucket(CTX_RANK.properties).push(ctxObjectWalk(s, x, path, sp, ev, ann, o, props))
|
|
601
|
+
out = []
|
|
602
|
+
for (const r of [...buckets.keys()].sort((a, b) => a - b)) out.push(...buckets.get(r))
|
|
603
|
+
}
|
|
604
|
+
if (staticUP !== null) {
|
|
605
|
+
// A verdict whose evaluated names are known: the names outside them
|
|
606
|
+
// meet unevaluatedProperties, as additionalProperties checks its own.
|
|
607
|
+
const n = s.unevaluatedProperties
|
|
608
|
+
const kv = vr(fresh('k'))
|
|
609
|
+
const isKnown = { t: 'known', k: kv, names: staticUP.names, pats: staticUP.pats }
|
|
610
|
+
if (n === false) out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not(isKnown), { t: 'ret' }) }))
|
|
611
|
+
else if (n !== true) {
|
|
612
|
+
const y = vr(fresh('x'))
|
|
613
|
+
const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/unevaluatedProperties'))
|
|
614
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not(isKnown), { t: 'let', name: y.n, value: get(x, kv), body }) }))
|
|
615
|
+
}
|
|
616
|
+
} else if ('unevaluatedProperties' in s) {
|
|
617
|
+
const n = s.unevaluatedProperties
|
|
618
|
+
const kv = vr(fresh('k'))
|
|
619
|
+
if (n === false) {
|
|
620
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: iff(not({ t: 'annHasKey', k: kv }), fail(err(uPK.code, 'unevaluatedProperties', path, cat(sp, '/unevaluatedProperties'), obj([['unevaluatedProperty', kv]]), lit(uPK.msg), undefined, uPK.rank, ev, uPK.detail))) }))
|
|
621
|
+
} else {
|
|
622
|
+
const y = vr(fresh('x'))
|
|
623
|
+
const body = child(n, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/unevaluatedProperties'))
|
|
624
|
+
out.push(iff(o, { t: 'keys', obj: x, k: kv.n, order: 'protoLast', body: iff(not({ t: 'annHasKey', k: kv }), seq({ t: 'let', name: y.n, value: get(x, kv), body }, { t: 'do', e: { t: 'annKey', k: kv } })) }))
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if ('unevaluatedItems' in s) {
|
|
628
|
+
const n = s.unevaluatedItems
|
|
629
|
+
const j = vr(fresh('j'))
|
|
630
|
+
if (n === false) {
|
|
631
|
+
out.push(iff(and({ t: 'isArr', x }, not({ t: 'annAllSet' })), seq({ t: 'let', name: j.n, value: { t: 'firstUneval', x }, body: iff(cmp('<', j, { t: 'len', x }), fail(err(uIK.code, 'unevaluatedItems', path, cat(sp, '/unevaluatedItems'), obj([['limit', j]]), { t: 'str', parts: [lit('must NOT have more than '), j, lit(' items')] }, undefined, uIK.rank, ev))) }, { t: 'do', e: { t: 'annAll' } })))
|
|
632
|
+
} else {
|
|
633
|
+
const y = vr(fresh('x'))
|
|
634
|
+
const body = child(n, y, catE(path, catE(lit('/'), j)), cat(sp, '/unevaluatedItems'))
|
|
635
|
+
out.push(iff(and({ t: 'isArr', x }, not({ t: 'annAllSet' })), seq({ t: 'range', arr: x, from: { t: 'annN' }, i: j.n, body: iff(not({ t: 'annAtHas', i: j }), { t: 'let', name: y.n, value: get(x, j), body }) }, { t: 'do', e: { t: 'annAll' } })))
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (post !== null) out.push(post)
|
|
639
|
+
// A value nothing below reads is not computed: an item schema of a type
|
|
640
|
+
// alone read each array element's prototype for a flag no check used
|
|
641
|
+
// (bitrise-step, half its verdict).
|
|
642
|
+
const text = JSON.stringify(out)
|
|
643
|
+
for (const n of out) if (n.t === 'bind') n.names = n.names.filter(([name]) => text.includes('"n":' + JSON.stringify(name)))
|
|
644
|
+
out = out.filter((n) => n.t !== 'bind' || n.names.length)
|
|
645
|
+
let body = seq(out)
|
|
646
|
+
for (let i = lets.length - 1; i >= 0; i--) if (text.includes('"n":' + JSON.stringify(lets[i][0]))) body = { t: 'let', name: lets[i][0], value: lets[i][1], body }
|
|
647
|
+
return body
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// The object's own keys, read once, each sent where the current engine
|
|
651
|
+
// sends it, in the document's order: to its declared property (a value of
|
|
652
|
+
// undefined is skipped, and is missing when required), to every pattern it
|
|
653
|
+
// matches, and to additionalProperties when neither took it.
|
|
654
|
+
function ctxObjectWalk (s, x, path, sp, ev, ann, o, props) {
|
|
655
|
+
const kv = vr(fresh('k'))
|
|
656
|
+
const reqIn = new Set(Array.isArray(s.required) ? s.required.filter((r) => r in props) : [])
|
|
657
|
+
const body = []
|
|
658
|
+
for (const p in props) {
|
|
659
|
+
const y = vr(fresh('x'))
|
|
660
|
+
const missing = reqIn.has(p) ? fail(err(required.code, 'required', path, cat(sp, '/required'), paramsOf(required.params(p)), lit(required.msg(p)), required.exp(p), required.rank, ev, required.detail, required.suggest)) : seq()
|
|
661
|
+
body.push(iff(cmp('===', kv, lit(p)), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: iff(cmp('!==', y, lit(undefined)), child(props[p], y, isLit(path) && path.v === '' ? lit('/' + seg(p)) : cat(path, '/' + seg(p)), cat(sp, '/properties/' + seg(p))), missing) })))
|
|
662
|
+
}
|
|
663
|
+
const pats = isObj(s.patternProperties) ? s.patternProperties : {}
|
|
664
|
+
for (const src in pats) {
|
|
665
|
+
const y = vr(fresh('x'))
|
|
666
|
+
body.push(iff({ t: 're', src, k: kv }, seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: child(pats[src], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(src))) })))
|
|
667
|
+
}
|
|
668
|
+
const ap = s.additionalProperties
|
|
669
|
+
if (ap !== undefined) {
|
|
670
|
+
const isKnown = { t: 'known', k: kv, names: Object.keys(props), pats: Object.keys(pats) }
|
|
671
|
+
if (ap === false) body.push(iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, fail(err(additionalProperties.code, 'additionalProperties', path, cat(sp, '/additionalProperties'), obj([['additionalProperty', kv]]), lit(additionalProperties.msg()), undefined, additionalProperties.rank, ev, additionalProperties.detail)))))
|
|
672
|
+
else {
|
|
673
|
+
const y = vr(fresh('x'))
|
|
674
|
+
body.push(iff(not(isKnown), seq(ann ? { t: 'do', e: { t: 'annKey', k: kv } } : null, { t: 'let', name: y.n, value: get(x, kv), body: child(ap, y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/additionalProperties')) })))
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: seq(body) })
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ---- Defaults ------------------------------------------------------------
|
|
681
|
+
// A specialized fill, in the order and by the rules of lib/defaults.js:
|
|
682
|
+
// a node's own property defaults (absent own keys get a fresh copy), then
|
|
683
|
+
// the property schemas for own keys present, then array elements (prefixItems
|
|
684
|
+
// by position, items for the rest), then dependentSchemas for keys present,
|
|
685
|
+
// then $ref targets and allOf members on the same value. Declined (null)
|
|
686
|
+
// when the fill graph has a cycle or patternProperties/additionalProperties
|
|
687
|
+
// lead to a default: there lib/defaults.js, with its guards for values that
|
|
688
|
+
// contain themselves, is used as it is.
|
|
689
|
+
const fills = new Map()
|
|
690
|
+
let fillDeclined = false
|
|
691
|
+
const fillStack = new Set()
|
|
692
|
+
const reaches = new Map()
|
|
693
|
+
const reachesDefault = (n) => {
|
|
694
|
+
if (!isObj(n)) return false
|
|
695
|
+
if (reaches.has(n)) return reaches.get(n)
|
|
696
|
+
reaches.set(n, false)
|
|
697
|
+
const props = isObj(n.properties) ? Object.values(n.properties) : []
|
|
698
|
+
let yes = props.some((p) => isObj(p) && p.default !== undefined)
|
|
699
|
+
const below = [...props, typeof n.$ref === 'string' ? safeResolve(n.$ref, n) : null, ...(Array.isArray(n.allOf) ? n.allOf : []), ...(Array.isArray(n.prefixItems) ? n.prefixItems : []), n.items, n.additionalProperties, ...(isObj(n.patternProperties) ? Object.values(n.patternProperties) : []), ...(isObj(n.dependentSchemas) ? Object.values(n.dependentSchemas) : [])]
|
|
700
|
+
for (const b of below) if (!yes && reachesDefault(b)) yes = true
|
|
701
|
+
reaches.set(n, yes)
|
|
702
|
+
return yes
|
|
703
|
+
}
|
|
704
|
+
const safeResolve = (ref, n) => { try { return index.resolve(ref, index.baseOf(n)) } catch { return null } }
|
|
705
|
+
// From this many properties with defaults inside, a fill walks the object's
|
|
706
|
+
// own names (see fillWalk below).
|
|
707
|
+
const FILL_WALK_MIN = 16
|
|
708
|
+
const MASK_MIN = 24
|
|
709
|
+
const fillProc = (n) => {
|
|
710
|
+
if (!isObj(n) || !reachesDefault(n)) return null
|
|
711
|
+
if (fills.has(n)) return fills.get(n)
|
|
712
|
+
if (fillStack.has(n)) { fillDeclined = true; return null }
|
|
713
|
+
if (isObj(n.patternProperties) && Object.values(n.patternProperties).some(reachesDefault)) fillDeclined = true
|
|
714
|
+
if (reachesDefault(n.additionalProperties)) fillDeclined = true
|
|
715
|
+
fillStack.add(n)
|
|
716
|
+
const id = fresh('f')
|
|
717
|
+
const d = vr('d')
|
|
718
|
+
const out = []
|
|
719
|
+
const props = isObj(n.properties) ? n.properties : {}
|
|
720
|
+
// Whether d is an ordinary object, read once: then a defined value under a
|
|
721
|
+
// name Object.prototype lacks is an own property, and a read decides
|
|
722
|
+
// whether a property's own fill runs.
|
|
723
|
+
const pl = vr(fresh('p'))
|
|
724
|
+
const fast = (k) => !(k in Object.prototype) && k !== '__proto__'
|
|
725
|
+
const inner = Object.keys(props).map((k) => [k, fillProc(props[k])]).filter(([, c]) => c)
|
|
726
|
+
const walk = inner.length >= FILL_WALK_MIN && inner.every(([k]) => fast(k))
|
|
727
|
+
const defaults = Object.keys(props).filter((k) => isObj(props[k]) && props[k].default !== undefined)
|
|
728
|
+
if (!walk && inner.some(([k]) => fast(k))) out.push({ t: 'bind', names: [[pl.n, { t: 'plainProto', x: d }]] })
|
|
729
|
+
// Many defaults: one pass over the object's own names sets a bit for each
|
|
730
|
+
// default name present, then each default reads its bit. A question per
|
|
731
|
+
// name cost about 10 ns each, and an object carrying most of eighty
|
|
732
|
+
// defaults paid it eighty times (jshintrc 934 to 543 ns, rustfmt 1371 to
|
|
733
|
+
// 751). Below MASK_MIN the pass over every name an object has can cost
|
|
734
|
+
// more than the questions (expo with 12 defaults, 6-9% slower at 8).
|
|
735
|
+
const masked = defaults.filter(fast)
|
|
736
|
+
if (masked.length >= MASK_MIN) {
|
|
737
|
+
out.push({ t: 'defaultsMask', o: d, entries: masked.map((k) => [k, props[k].default]) })
|
|
738
|
+
for (const k of defaults) if (!fast(k)) out.push({ t: 'setDefault', o: d, k, v: props[k].default })
|
|
739
|
+
} else for (const k of defaults) out.push({ t: 'setDefault', o: d, k, v: props[k].default })
|
|
740
|
+
// The properties whose values have defaults inside: many of them (a
|
|
741
|
+
// linter's rules, each a schema with its own defaults) are looked up in a
|
|
742
|
+
// table from the object's own names, so the cost follows the names the
|
|
743
|
+
// object has, not the names the schema lists (tslint: 150 listed, about
|
|
744
|
+
// 20 present). A name Object.prototype has keeps the lookup per name.
|
|
745
|
+
if (walk) out.push({ t: 'fillWalk', o: d, entries: inner })
|
|
746
|
+
else for (const [k, c] of inner) {
|
|
747
|
+
// An own name holding undefined has nothing to fill either.
|
|
748
|
+
out.push(iff(fast(k) ? or(and(pl, cmp('!==', get(d, lit(k)), lit(undefined))), and(not(pl), own(d, lit(k)))) : own(d, lit(k)), { t: 'fcall', proc: c, x: get(d, lit(k)) }))
|
|
749
|
+
}
|
|
750
|
+
const tuple = Array.isArray(n.prefixItems) ? n.prefixItems.map(fillProc) : []
|
|
751
|
+
const each = fillProc(n.items)
|
|
752
|
+
if (tuple.some(Boolean) || each) {
|
|
753
|
+
const i = vr(fresh('i'))
|
|
754
|
+
const per = tuple.map((t, ix) => t ? iff(cmp('===', i, lit(ix)), { t: 'fcall', proc: t, x: get(d, i) }) : null).filter(Boolean)
|
|
755
|
+
out.push(iff({ t: 'isArr', x: d }, { t: 'range', arr: d, from: 0, i: i.n, body: seq(...per, each ? iff(not(cmp('<', i, lit(tuple.length))), { t: 'fcall', proc: each, x: get(d, i) }) : null) }))
|
|
756
|
+
}
|
|
757
|
+
if (isObj(n.dependentSchemas)) for (const k of Object.keys(n.dependentSchemas)) { const c = fillProc(n.dependentSchemas[k]); if (c) out.push(iff(and(not({ t: 'isArr', x: d }), own(d, lit(k))), { t: 'fcall', proc: c, x: d })) }
|
|
758
|
+
if (typeof n.$ref === 'string') { const c = fillProc(safeResolve(n.$ref, n)); if (c) out.push({ t: 'fcall', proc: c, x: d }) }
|
|
759
|
+
if (Array.isArray(n.allOf)) for (const sub of n.allOf) { const c = fillProc(sub); if (c) out.push({ t: 'fcall', proc: c, x: d }) }
|
|
760
|
+
fillStack.delete(n)
|
|
761
|
+
// A value that is not an object has nothing to fill.
|
|
762
|
+
procs.set(id, { id, mode: 'fill', base: index.baseOf(n), body: iff({ t: 'isObjAny', x: d }, seq(out)) })
|
|
763
|
+
fills.set(n, id)
|
|
764
|
+
return id
|
|
765
|
+
}
|
|
766
|
+
const fillRoot = fillProc(schema)
|
|
767
|
+
|
|
768
|
+
// The root as dual and as verdict procedures.
|
|
769
|
+
cur = { mode: 'dual', lexBase: index.baseOf(schema), ann: false }
|
|
770
|
+
const dual = proc(schema, 'dual', true)
|
|
771
|
+
const verdictRoot = proc(schema, 'verdict', true)
|
|
772
|
+
// `schema`: the normalized schema, which the generic default pass must read
|
|
773
|
+
// too (it fills what validation applies: a draft-07 $ref drops its siblings).
|
|
774
|
+
// A call on the same value that lies on a cycle (a schema whose allOf or
|
|
775
|
+
// anyOf refers back to it, as SchemaStore's cloudify does) would go round
|
|
776
|
+
// without end. The current engine cuts a reference that it meets again with
|
|
777
|
+
// the same value, the inner entry answering with no error; such calls are
|
|
778
|
+
// marked `guard` for the backends to do the same, each with its own record
|
|
779
|
+
// of the values it is on. A call is on the same value when its value is the
|
|
780
|
+
// calling procedure's own `d`.
|
|
781
|
+
const same = (x) => x && x.t === 'var' && x.n === 'd'
|
|
782
|
+
const eachNode = (n, f) => {
|
|
783
|
+
if (n === null || typeof n !== 'object') return
|
|
784
|
+
if (Array.isArray(n)) { for (const x of n) eachNode(x, f); return }
|
|
785
|
+
f(n)
|
|
786
|
+
for (const k in n) if (k !== 'proc' && k !== 'procs' && k !== 'K' && k !== 'dynamic') eachNode(n[k], f)
|
|
787
|
+
}
|
|
788
|
+
const targets = (n) => n.t === 'pcall' && n.proc !== null ? [n.proc, ...(n.dynamic ? n.dynamic.map(([, t]) => t) : [])] : n.t === 'branches' ? n.procs : []
|
|
789
|
+
const inPlaceCalls = new Map()
|
|
790
|
+
for (const [id, pr] of procs) {
|
|
791
|
+
if (pr.mode === 'fill') continue
|
|
792
|
+
const out = new Set()
|
|
793
|
+
eachNode(pr.body, (n) => { if ((n.t === 'pcall' || n.t === 'branches') && same(n.x)) for (const t of targets(n)) out.add(t) })
|
|
794
|
+
inPlaceCalls.set(id, out)
|
|
795
|
+
}
|
|
796
|
+
const reachesInPlace = (from, to) => {
|
|
797
|
+
const seen = new Set(); const stack = [from]
|
|
798
|
+
while (stack.length) { const t = stack.pop(); if (t === to) return true; if (seen.has(t)) continue; seen.add(t); const next = inPlaceCalls.get(t); if (next) stack.push(...next) }
|
|
799
|
+
return false
|
|
800
|
+
}
|
|
801
|
+
for (const [id, pr] of procs) {
|
|
802
|
+
if (pr.mode === 'fill' || !inPlaceCalls.get(id).size) continue
|
|
803
|
+
eachNode(pr.body, (n) => { if (n.t === 'pcall' && n.proc !== null && same(n.x) && targets(n).some((t) => reachesInPlace(t, id))) n.guard = true })
|
|
804
|
+
}
|
|
805
|
+
return { procs, dual, verdict: verdictRoot, dynamic, sawDynamic, relate: needsRelate, fill: fillDeclined ? 'generic' : fillRoot, schema, custom: kw !== null && keywordsLib.schemaUsesKeywords(schema, kw), ctx }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// What the error order reads of a schema: every key in its place, every
|
|
809
|
+
// subschema, and the values that identify a resource or point at one; any
|
|
810
|
+
// other value (a description, an enum list) becomes 0. Ranks over it are the
|
|
811
|
+
// ranks over the schema.
|
|
812
|
+
const KEEP = new Set(['$ref', '$id', 'id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveRef', '$recursiveAnchor', '$schema'])
|
|
813
|
+
const SUBS = new Set(['additionalProperties', 'additionalItems', 'items', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'unevaluatedItems', 'unevaluatedProperties', 'allOf', 'anyOf', 'oneOf', 'prefixItems', 'properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'])
|
|
814
|
+
export function orderSkeleton (node, inMap = false) {
|
|
815
|
+
if (node === null || typeof node !== 'object') return 0
|
|
816
|
+
if (Array.isArray(node)) return node.map((x) => orderSkeleton(x))
|
|
817
|
+
const out = {}
|
|
818
|
+
for (const k of Object.keys(node)) {
|
|
819
|
+
const v = node[k]
|
|
820
|
+
const s = inMap ? orderSkeleton(v) : KEEP.has(k) && (typeof v === 'string' || typeof v === 'boolean') ? v : SUBS.has(k) ? orderSkeleton(v, ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'].includes(k)) : 0
|
|
821
|
+
Object.defineProperty(out, k, { value: s, enumerable: true, writable: true, configurable: true })
|
|
822
|
+
}
|
|
823
|
+
return out
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Whether the errors of a schema can come out of the IR in an order other
|
|
827
|
+
// than schema declaration order, so that the module must sort them. The IR
|
|
828
|
+
// writes each node's keywords in the order they are declared, and a
|
|
829
|
+
// reference's target where the reference stands, so they can not only where
|
|
830
|
+
// a loop over the instance repeats a body that fails in more than one place
|
|
831
|
+
// (every item's type before any item's pattern, in schema order; a body
|
|
832
|
+
// behind a reference counts as such), and wherever this check does not look
|
|
833
|
+
// further: dynamic references, branches, conditionals, annotations collected
|
|
834
|
+
// across keywords. Erring towards true costs size, never order.
|
|
835
|
+
const ALWAYS = new Set(['$dynamicRef', '$recursiveRef', 'anyOf', 'oneOf', 'if', 'unevaluatedProperties', 'unevaluatedItems', 'dependentSchemas', 'dependencies', 'contains', 'propertyNames', 'patternProperties'])
|
|
836
|
+
const LOOPS = new Set(['items', 'prefixItems', 'additionalItems', 'additionalProperties'])
|
|
837
|
+
function failPlaces (s) {
|
|
838
|
+
if (s === false) return 1
|
|
839
|
+
if (s === null || typeof s !== 'object') return 0
|
|
840
|
+
if (Array.isArray(s)) { let n = 0; for (const x of s) n += failPlaces(x); return n }
|
|
841
|
+
let n = 0
|
|
842
|
+
for (const k of Object.keys(s)) {
|
|
843
|
+
if (ANNOTATIONS.has(k) || k === '$defs' || k === 'definitions' || k === '$id' || k === '$anchor' || k === '$dynamicAnchor' || k === '$comment' || k === '$schema') continue
|
|
844
|
+
// A reference can fail anywhere its target can: counted as several.
|
|
845
|
+
if (k === '$ref' || k === '$dynamicRef' || k === '$recursiveRef') { n += 2; continue }
|
|
846
|
+
if (k === 'properties' || k === 'patternProperties' || k === 'dependentSchemas') { for (const x of Object.values(s[k] || {})) n += failPlaces(x); continue }
|
|
847
|
+
if (k === 'allOf' || k === 'anyOf' || k === 'oneOf' || k === 'prefixItems' || k === 'items' || k === 'additionalItems' || k === 'additionalProperties' || k === 'not' || k === 'contains' || k === 'propertyNames' || k === 'if' || k === 'then' || k === 'else') { n += k === 'not' ? 1 : Math.max(1, failPlaces(s[k])); continue }
|
|
848
|
+
n++
|
|
849
|
+
}
|
|
850
|
+
return n
|
|
851
|
+
}
|
|
852
|
+
export function needsOrder (s) {
|
|
853
|
+
if (s === null || typeof s !== 'object') return false
|
|
854
|
+
if (Array.isArray(s)) return s.some(needsOrder)
|
|
855
|
+
for (const k of Object.keys(s)) {
|
|
856
|
+
if (ALWAYS.has(k)) return true
|
|
857
|
+
if (LOOPS.has(k) && failPlaces(s[k]) > 1) return true
|
|
858
|
+
}
|
|
859
|
+
for (const k of Object.keys(s)) {
|
|
860
|
+
if (ANNOTATIONS.has(k) && k !== 'default') continue
|
|
861
|
+
if (k === 'enum' || k === 'const' || k === 'default' || k === 'required' || k === 'examples') continue
|
|
862
|
+
if (needsOrder(s[k])) return true
|
|
863
|
+
}
|
|
864
|
+
return false
|
|
865
|
+
}
|