ata-validator 1.41.0 → 1.42.0
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/index.js +122 -0
- package/lib/branch-collapse.js +17 -8
- package/lib/enrich-error.js +46 -30
- package/lib/enrich-site.js +58 -0
- package/lib/interpreter.js +6 -0
- package/lib/js-compiler.js +815 -111
- package/lib/plan-compiler.js +7 -2
- package/lib/rejections.js +20 -3
- package/lib/validator-core.js +99 -10
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -101,6 +101,38 @@ function emitUq(ctx) {
|
|
|
101
101
|
return '_uq'
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// The error and combined generators need the duplicate pair, not only the
|
|
105
|
+
// verdict. `_uqp` returns the index of the duplicate (-1 when every item is
|
|
106
|
+
// distinct) and leaves the item it repeats in `_uqpI`: the first item that has
|
|
107
|
+
// a later equal, paired with its first later equal. That is the pair the
|
|
108
|
+
// interpreted engine reports, and the generators used to report a different
|
|
109
|
+
// one (the earliest repeat, paired with what it repeats), so the same document
|
|
110
|
+
// named a different pair depending on the engine that answered. Short arrays
|
|
111
|
+
// compare pairwise and allocate nothing; longer ones key a Map by value, or by
|
|
112
|
+
// canonical string once an object appears. Building a canonical string for
|
|
113
|
+
// every item on every call, as the generators did, made reading a uniqueItems
|
|
114
|
+
// error up to ten times the cost of the verdict.
|
|
115
|
+
const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
|
|
116
|
+
function emitUqp(ctx) {
|
|
117
|
+
emitUq(ctx)
|
|
118
|
+
hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
|
|
119
|
+
return '_uqp'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// An object or array a const error reports as `allowedValue`, parsed once per
|
|
123
|
+
// compiled function rather than on every error: parsing it per error made
|
|
124
|
+
// reading a const error two to five times what the check costs. A copy of its
|
|
125
|
+
// own, not the compared constant, so an error a caller edits cannot change
|
|
126
|
+
// what the validator compares against. Declared in the function's helper
|
|
127
|
+
// code, so standalone output carries it the same way.
|
|
128
|
+
function emitParamConstant(ctx, value) {
|
|
129
|
+
const name = `_pc${ctx.varCounter++}`
|
|
130
|
+
const decl = `const ${name}=JSON.parse(${JSON.stringify(JSON.stringify(value))})`
|
|
131
|
+
if (ctx.preamble) ctx.preamble.push(decl)
|
|
132
|
+
else ctx.helperCode.push(decl)
|
|
133
|
+
return name
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
// The compared value, parsed once at compile time. `JSON.parse` rather than a
|
|
105
137
|
// JS object literal so a `__proto__` member stays an ordinary property.
|
|
106
138
|
function emitConstant(ctx, value) {
|
|
@@ -1390,6 +1422,25 @@ function unevalPropsModeled(schema, schemaMap) {
|
|
|
1390
1422
|
return true
|
|
1391
1423
|
}
|
|
1392
1424
|
|
|
1425
|
+
// Every `$ref` string under a node, once per node.
|
|
1426
|
+
const _refStrings = new WeakMap()
|
|
1427
|
+
function refStringsOf(node) {
|
|
1428
|
+
let set = _refStrings.get(node)
|
|
1429
|
+
if (set !== undefined) return set
|
|
1430
|
+
set = new Set()
|
|
1431
|
+
const seen = new Set()
|
|
1432
|
+
const walk = (n) => {
|
|
1433
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
1434
|
+
seen.add(n)
|
|
1435
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return }
|
|
1436
|
+
if (typeof n.$ref === 'string') set.add(n.$ref)
|
|
1437
|
+
for (const k of Object.keys(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(n[k])
|
|
1438
|
+
}
|
|
1439
|
+
walk(node)
|
|
1440
|
+
_refStrings.set(node, set)
|
|
1441
|
+
return set
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1393
1444
|
function codegenSafe(schema, schemaMap) {
|
|
1394
1445
|
if (typeof schema === 'boolean') return true
|
|
1395
1446
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -1505,8 +1556,9 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1505
1556
|
// that only the interpreted engine provides.
|
|
1506
1557
|
if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
|
|
1507
1558
|
const evalResult = collectEvaluated(schema, schemaMap)
|
|
1508
|
-
|
|
1509
|
-
|
|
1559
|
+
// What the static model cannot represent is built at run time
|
|
1560
|
+
// (genUnevalDynamicVerdict); shapes that cannot be (dynamic references,
|
|
1561
|
+
// reference cycles) decline there.
|
|
1510
1562
|
}
|
|
1511
1563
|
}
|
|
1512
1564
|
// unevaluatedItems: allow boolean and schema values
|
|
@@ -1527,7 +1579,12 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1527
1579
|
// Defs on a $ref cycle compile as named functions (see genCode); nothing
|
|
1528
1580
|
// to decline here. cyclicDefNames() is the walk that used to bail.
|
|
1529
1581
|
for (const [name, def] of Object.entries(defs)) {
|
|
1530
|
-
|
|
1582
|
+
// A name the generators would have to spell inside a path or an
|
|
1583
|
+
// identifier. Only a problem when a reference names it: tsconfig.json
|
|
1584
|
+
// keeps a comment object under the definition `//`, which nothing
|
|
1585
|
+
// references, and declined whole for it. A definition reached through
|
|
1586
|
+
// a deeper pointer was renamed by normalizeDeepRefs.
|
|
1587
|
+
if (/[~/"']/.test(name) && refStringsOf(schema).has('#/' + (schema.$defs === defs ? '$defs' : 'definitions') + '/' + name.replace(/~/g, '~0').replace(/\//g, '~1'))) return false
|
|
1531
1588
|
if (typeof def === 'boolean') return false
|
|
1532
1589
|
if (typeof def === 'object' && def !== null) {
|
|
1533
1590
|
// A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
|
|
@@ -1587,37 +1644,6 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1587
1644
|
return true
|
|
1588
1645
|
}
|
|
1589
1646
|
|
|
1590
|
-
// Returns true if the schema (or any nested sub-schema reachable through the
|
|
1591
|
-
// usual keywords) has additionalProperties as a schema value. Used by the
|
|
1592
|
-
// combined-codegen bail since that path does not emit the per-key loop yet.
|
|
1593
|
-
function hasAdditionalPropertiesSchema(schema) {
|
|
1594
|
-
if (typeof schema !== 'object' || schema === null) return false
|
|
1595
|
-
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) return true
|
|
1596
|
-
const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
1597
|
-
for (const key of objSubs) {
|
|
1598
|
-
if (schema[key] && typeof schema[key] === 'object') {
|
|
1599
|
-
for (const v of Object.values(schema[key])) {
|
|
1600
|
-
if (hasAdditionalPropertiesSchema(v)) return true
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
1605
|
-
for (const key of arrSubs) {
|
|
1606
|
-
if (Array.isArray(schema[key])) {
|
|
1607
|
-
for (const s of schema[key]) {
|
|
1608
|
-
if (hasAdditionalPropertiesSchema(s)) return true
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else', 'propertyNames']
|
|
1613
|
-
for (const key of singleSubs) {
|
|
1614
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
1615
|
-
if (hasAdditionalPropertiesSchema(schema[key])) return true
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return false
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
1647
|
// Schema-bearing keywords, so the walk below never mistakes a property named
|
|
1622
1648
|
// "$ref" for an actual reference.
|
|
1623
1649
|
const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
@@ -2020,9 +2046,239 @@ function normalizePropertyNamesWalk(root) {
|
|
|
2020
2046
|
return walk(root)
|
|
2021
2047
|
}
|
|
2022
2048
|
|
|
2049
|
+
// A local `$ref` that is a JSON pointer to anything other than a top-level
|
|
2050
|
+
// definition (`#/definitions/item/properties/id`, `#/properties/foo`,
|
|
2051
|
+
// `#/$defs/a/items`) became the generators' own definition reference: the
|
|
2052
|
+
// pointer is resolved here, at compile time, and its target added as a
|
|
2053
|
+
// definition under a name of its own, so every path that already handles
|
|
2054
|
+
// `#/$defs/name` handles it, cycles included. The generators knew only the
|
|
2055
|
+
// top-level form and declined the rest, which kept 40 of 980 SchemaStore
|
|
2056
|
+
// schemas, tsconfig.json among them, on the interpreted engine for every
|
|
2057
|
+
// document. Left alone, as before: a pointer that does not resolve to a
|
|
2058
|
+
// schema, one that passes through a subschema with its own `$id` (another
|
|
2059
|
+
// base, whose pointers mean something else), and every `$ref` inside such a
|
|
2060
|
+
// subschema. Copy on write; errors keep their paths, which name the place of
|
|
2061
|
+
// the reference, not of its target, and are ordered against the schema as
|
|
2062
|
+
// the caller wrote it.
|
|
2063
|
+
const DEF_REF = /^#\/(?:\$defs|definitions)\/[^/]+$/
|
|
2064
|
+
const _deepRefsNormalized = new WeakMap()
|
|
2065
|
+
function normalizeDeepRefs(root) {
|
|
2066
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2067
|
+
const hit = _deepRefsNormalized.get(root)
|
|
2068
|
+
if (hit !== undefined) return hit
|
|
2069
|
+
const out = normalizeDeepRefsWalk(root)
|
|
2070
|
+
_deepRefsNormalized.set(root, out)
|
|
2071
|
+
return out
|
|
2072
|
+
}
|
|
2073
|
+
function resolveLocalPointer(root, ref) {
|
|
2074
|
+
let node = root
|
|
2075
|
+
const segs = ref.slice(2).split('/')
|
|
2076
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2077
|
+
let seg = segs[i].replace(/~1/g, '/').replace(/~0/g, '~')
|
|
2078
|
+
// Draft 7 schemas reach the generators with `definitions` renamed to
|
|
2079
|
+
// `$defs` (lib/draft7.js) and their references as written, which the
|
|
2080
|
+
// top-level form already reads either way; so does this.
|
|
2081
|
+
if (i === 0 && (seg === 'definitions' || seg === '$defs') && !Object.prototype.hasOwnProperty.call(node, seg)) seg = seg === 'definitions' ? '$defs' : 'definitions'
|
|
2082
|
+
if (node === null || typeof node !== 'object') return undefined
|
|
2083
|
+
if (Array.isArray(node)) {
|
|
2084
|
+
if (!/^(?:0|[1-9]\d*)$/.test(seg) || +seg >= node.length) return undefined
|
|
2085
|
+
node = node[+seg]
|
|
2086
|
+
} else {
|
|
2087
|
+
if (!Object.prototype.hasOwnProperty.call(node, seg)) return undefined
|
|
2088
|
+
node = node[seg]
|
|
2089
|
+
}
|
|
2090
|
+
if (node !== null && typeof node === 'object' && !Array.isArray(node) && typeof node.$id === 'string') return undefined
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof node === 'boolean') return node
|
|
2093
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return undefined
|
|
2094
|
+
return node
|
|
2095
|
+
}
|
|
2096
|
+
function normalizeDeepRefsWalk(root) {
|
|
2097
|
+
// First pass: the deep pointers used outside any nested `$id`, and their
|
|
2098
|
+
// targets.
|
|
2099
|
+
const targets = new Map()
|
|
2100
|
+
const scan = (node, nested, seen) => {
|
|
2101
|
+
if (node === null || typeof node !== 'object') return
|
|
2102
|
+
if (seen.has(node)) return
|
|
2103
|
+
seen.add(node)
|
|
2104
|
+
if (Array.isArray(node)) { for (const n of node) scan(n, nested, seen); return }
|
|
2105
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2106
|
+
if (!inner && typeof node.$ref === 'string' && node.$ref.startsWith('#/') && !DEF_REF.test(node.$ref) && !targets.has(node.$ref)) {
|
|
2107
|
+
const t = resolveLocalPointer(root, node.$ref)
|
|
2108
|
+
if (t !== undefined) targets.set(node.$ref, t)
|
|
2109
|
+
}
|
|
2110
|
+
for (const k of Object.keys(node)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') scan(node[k], inner, seen)
|
|
2111
|
+
}
|
|
2112
|
+
scan(root, false, new Set())
|
|
2113
|
+
if (targets.size === 0) return root
|
|
2114
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2115
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2116
|
+
const names = new Map()
|
|
2117
|
+
let n = 0
|
|
2118
|
+
for (const ref of targets.keys()) {
|
|
2119
|
+
let name
|
|
2120
|
+
do { name = '__ata_ptr_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2121
|
+
names.set(ref, name)
|
|
2122
|
+
}
|
|
2123
|
+
// Second pass: the references rewritten, copy on write, sharing one copy
|
|
2124
|
+
// per node so a target reached twice stays one object.
|
|
2125
|
+
const memo = new Map()
|
|
2126
|
+
const walk = (node, nested) => {
|
|
2127
|
+
if (node === null || typeof node !== 'object') return node
|
|
2128
|
+
const key = nested ? null : node
|
|
2129
|
+
if (key !== null && memo.has(key)) return memo.get(key)
|
|
2130
|
+
if (Array.isArray(node)) {
|
|
2131
|
+
let out = node
|
|
2132
|
+
if (key !== null) memo.set(key, out)
|
|
2133
|
+
for (let i = 0; i < node.length; i++) {
|
|
2134
|
+
const w = walk(node[i], nested)
|
|
2135
|
+
if (w !== node[i]) { if (out === node) { out = node.slice(); if (key !== null) memo.set(key, out) } out[i] = w }
|
|
2136
|
+
}
|
|
2137
|
+
return out
|
|
2138
|
+
}
|
|
2139
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2140
|
+
if (inner) return node
|
|
2141
|
+
let out = node
|
|
2142
|
+
memo.set(node, out)
|
|
2143
|
+
const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
|
|
2144
|
+
if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
|
|
2145
|
+
for (const k of Object.keys(node)) {
|
|
2146
|
+
if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
2147
|
+
const v = node[k]
|
|
2148
|
+
if (v === null || typeof v !== 'object') continue
|
|
2149
|
+
const w = walk(v, false)
|
|
2150
|
+
if (w !== v) own(k, w)
|
|
2151
|
+
}
|
|
2152
|
+
return out
|
|
2153
|
+
}
|
|
2154
|
+
const rewritten = walk(root, false)
|
|
2155
|
+
const out = rewritten === root ? { ...root } : rewritten
|
|
2156
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2157
|
+
for (const [ref, name] of names) {
|
|
2158
|
+
const t = targets.get(ref)
|
|
2159
|
+
defs[name] = typeof t === 'boolean' ? t : walk(t, false)
|
|
2160
|
+
}
|
|
2161
|
+
out[defsKey] = defs
|
|
2162
|
+
return out
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Every reference the interpreted engine can resolve becomes a definition
|
|
2166
|
+
// reference the generators handle: URNs, `$id`-relative and absolute URIs,
|
|
2167
|
+
// anchors, pointers into a resource with its own `$id`, names with characters
|
|
2168
|
+
// the generators cannot spell, documents registered through `schemas`. The
|
|
2169
|
+
// target of each is found by the interpreter's own resolver (indexSchemas and
|
|
2170
|
+
// resolveRef in lib/interpreter.js), so the two engines cannot resolve a
|
|
2171
|
+
// reference differently, and is added under a name of its own as with
|
|
2172
|
+
// normalizeDeepRefs. Once every reference names a definition, nested `$id`s
|
|
2173
|
+
// no longer change what anything means and are dropped from the copy, which
|
|
2174
|
+
// is what kept those schemas from compiling. Left to normalizeDeepRefs, as
|
|
2175
|
+
// before: a schema with dynamic references (their meaning depends on the
|
|
2176
|
+
// resources at run time), one with an embedded resource in another dialect,
|
|
2177
|
+
// and one with any reference the resolver cannot resolve.
|
|
2178
|
+
const _refsNormalized = new WeakMap()
|
|
2179
|
+
function normalizeRefs(root, schemaMap) {
|
|
2180
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2181
|
+
let byMap = _refsNormalized.get(root)
|
|
2182
|
+
const mapKey = schemaMap || _refsNormalized
|
|
2183
|
+
if (byMap !== undefined && byMap.has(mapKey)) return byMap.get(mapKey)
|
|
2184
|
+
let out
|
|
2185
|
+
try { out = normalizeRefsWalk(root, schemaMap) } catch { out = null }
|
|
2186
|
+
if (out === null) out = normalizeDeepRefs(root)
|
|
2187
|
+
if (byMap === undefined) { byMap = new Map(); _refsNormalized.set(root, byMap) }
|
|
2188
|
+
byMap.set(mapKey, out)
|
|
2189
|
+
return out
|
|
2190
|
+
}
|
|
2191
|
+
const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
|
|
2192
|
+
function normalizeRefsWalk(root, schemaMap) {
|
|
2193
|
+
const text = JSON.stringify(root)
|
|
2194
|
+
if (!text.includes('"$ref"')) return root
|
|
2195
|
+
if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
|
|
2196
|
+
const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
|
|
2197
|
+
const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
|
|
2198
|
+
const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
|
|
2199
|
+
// Resolve every reference reachable from the root, following into targets
|
|
2200
|
+
// in other documents.
|
|
2201
|
+
const names = new Map() // target node -> synthetic name
|
|
2202
|
+
const refTarget = new Map() // reference site node -> target node
|
|
2203
|
+
const visited = new Set()
|
|
2204
|
+
let fail = false
|
|
2205
|
+
const visit = (node) => {
|
|
2206
|
+
if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
|
|
2207
|
+
visited.add(node)
|
|
2208
|
+
if (Array.isArray(node)) { for (const n of node) visit(n); return }
|
|
2209
|
+
if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
|
|
2210
|
+
// A dynamic reference reached through another document means the same as
|
|
2211
|
+
// one in the root: its target depends on the resources at run time.
|
|
2212
|
+
if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined || node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
|
|
2213
|
+
if (typeof node.$ref === 'string' && state.nodeBase.has(node)) {
|
|
2214
|
+
const base = state.nodeBase.get(node)
|
|
2215
|
+
const ref = node.$ref
|
|
2216
|
+
const local = base === state.rootBase && (ref === '#' || (DEF_REF.test(ref) && !/[~%]/.test(ref)))
|
|
2217
|
+
if (!local) {
|
|
2218
|
+
const { node: t } = resolveRef(ref, base, state)
|
|
2219
|
+
if (t === undefined || (typeof t !== 'boolean' && (t === null || typeof t !== 'object' || Array.isArray(t)))) { fail = true; return }
|
|
2220
|
+
refTarget.set(node, t)
|
|
2221
|
+
// The root, however it is spelled, is `#`: a copy of it under its own
|
|
2222
|
+
// definitions would contain itself.
|
|
2223
|
+
if (t === root) { /* named '#' below */ }
|
|
2224
|
+
else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
|
|
2225
|
+
if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
|
|
2229
|
+
}
|
|
2230
|
+
visit(root)
|
|
2231
|
+
if (fail) return null
|
|
2232
|
+
if (refTarget.size === 0) return root
|
|
2233
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2234
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2235
|
+
let n = 0
|
|
2236
|
+
for (const t of names.keys()) {
|
|
2237
|
+
let name
|
|
2238
|
+
do { name = '__ata_ref_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2239
|
+
names.set(t, name)
|
|
2240
|
+
}
|
|
2241
|
+
// The copy: references renamed, nested `$id`s dropped.
|
|
2242
|
+
const memo = new Map()
|
|
2243
|
+
const walk = (node) => {
|
|
2244
|
+
if (node === null || typeof node !== 'object') return node
|
|
2245
|
+
if (memo.has(node)) return memo.get(node)
|
|
2246
|
+
if (Array.isArray(node)) {
|
|
2247
|
+
const out = []
|
|
2248
|
+
memo.set(node, out)
|
|
2249
|
+
for (const x of node) out.push(walk(x))
|
|
2250
|
+
return out
|
|
2251
|
+
}
|
|
2252
|
+
const out = {}
|
|
2253
|
+
memo.set(node, out)
|
|
2254
|
+
for (const k of Object.keys(node)) {
|
|
2255
|
+
if (k === '$id' && node !== root) continue
|
|
2256
|
+
if (k === '$ref' && refTarget.has(node)) {
|
|
2257
|
+
const t = refTarget.get(node)
|
|
2258
|
+
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2259
|
+
continue
|
|
2260
|
+
}
|
|
2261
|
+
out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
|
|
2262
|
+
}
|
|
2263
|
+
return out
|
|
2264
|
+
}
|
|
2265
|
+
const out = walk(root)
|
|
2266
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2267
|
+
for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
|
|
2268
|
+
out[defsKey] = defs
|
|
2269
|
+
return out
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// What the three generators compile from: the schema with deep local pointers
|
|
2273
|
+
// turned into definition references, then propertyNames normalized. The same
|
|
2274
|
+
// for all three, so they decline and agree on the same schemas.
|
|
2275
|
+
function prepareForCodegen(schema, schemaMap) {
|
|
2276
|
+
return normalizePropertyNames(normalizeRefs(schema, schemaMap))
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2023
2279
|
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
2024
2280
|
const inputSchema = schema
|
|
2025
|
-
schema =
|
|
2281
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
2026
2282
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
2027
2283
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2028
2284
|
// removeAdditional: a verdict function that deletes the keys the remover
|
|
@@ -2154,7 +2410,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2154
2410
|
// are built when first asked for, not here: most validators never call
|
|
2155
2411
|
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
2156
2412
|
// more on every schema was about a third of what a first call cost.
|
|
2157
|
-
let body, hybridThunk, tailThunk
|
|
2413
|
+
let body, hybridThunk, tailThunk, resultThunk
|
|
2158
2414
|
if (ctx.usesRecursion) {
|
|
2159
2415
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
2160
2416
|
// that points back at itself settles instead of exhausting the stack.
|
|
@@ -2164,6 +2420,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2164
2420
|
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2165
2421
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
2166
2422
|
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2423
|
+
resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2167
2424
|
tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2168
2425
|
} else if (defSets.length > 0) {
|
|
2169
2426
|
// No self-reference at the root, but a def on a cycle can still be
|
|
@@ -2171,10 +2428,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2171
2428
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
2172
2429
|
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2173
2430
|
hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2431
|
+
resultThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2174
2432
|
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2175
2433
|
} else {
|
|
2176
2434
|
body = checkStr + '\n return true'
|
|
2177
2435
|
hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
|
|
2436
|
+
resultThunk = () => replaceTopLevel(checkStr + '\n return true', `return ${RESULT_TRUE}`, `return ${RESULT_FALSE}`)
|
|
2178
2437
|
// An early accept inside the checks has to reach the tail too, so every
|
|
2179
2438
|
// top-level `return true` becomes the tail call, by the same rewrite the
|
|
2180
2439
|
// hybrid above relies on and test_hybrid_agreement holds to the suite.
|
|
@@ -2214,6 +2473,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2214
2473
|
return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
|
|
2215
2474
|
}
|
|
2216
2475
|
|
|
2476
|
+
// validate() itself: the same checks returning the result objects, for a
|
|
2477
|
+
// validator past its first calls (see validator-core). One function per
|
|
2478
|
+
// schema means the result is built at an allocation site of its own and
|
|
2479
|
+
// nothing between the caller and the checks is shared with other schemas.
|
|
2480
|
+
// Shared wrappers are what made reading results slow across many schemas:
|
|
2481
|
+
// every validator's calls went through the same few call sites. The
|
|
2482
|
+
// rejection is the lazy one, built from the arguments.
|
|
2483
|
+
let resultFactory
|
|
2484
|
+
boolFn._resultFactory = (LR, B, W, EE) => {
|
|
2485
|
+
if (resultFactory === undefined) {
|
|
2486
|
+
try {
|
|
2487
|
+
resultFactory = new Function(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
|
|
2488
|
+
} catch {
|
|
2489
|
+
resultFactory = null
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2217
2495
|
// The same function with one more check where it would return true, for a
|
|
2218
2496
|
// wrapper that enforces something the schema does not carry (see
|
|
2219
2497
|
// Validator#_extendVerdict). Calling the check from here rather than from a
|
|
@@ -2291,40 +2569,75 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2291
2569
|
|
|
2292
2570
|
// Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
|
|
2293
2571
|
// Tracks function nesting depth to preserve nested function internals.
|
|
2572
|
+
const RESULT_TRUE = '{valid:true,data:d,errors:__ataEE}'
|
|
2573
|
+
const RESULT_FALSE = 'new __ataLR(__ataB,d,__ataW)'
|
|
2574
|
+
// The end of the string literal that starts at `i` (a quote character), or
|
|
2575
|
+
// the end of the code when it does not close.
|
|
2576
|
+
function skipString(code, i) {
|
|
2577
|
+
const q = code[i]
|
|
2578
|
+
let j = i + 1
|
|
2579
|
+
while (j < code.length) {
|
|
2580
|
+
const c = code[j]
|
|
2581
|
+
if (c === '\\') { j += 2; continue }
|
|
2582
|
+
if (c === q) return j + 1
|
|
2583
|
+
j++
|
|
2584
|
+
}
|
|
2585
|
+
return code.length
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2294
2588
|
function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
|
|
2295
2589
|
let fnDepth = 0, result = '', i = 0
|
|
2296
2590
|
while (i < code.length) {
|
|
2591
|
+
// String literals are copied as they are. A property called `function`
|
|
2592
|
+
// reads `d["function"]`, and the scan took the word for a nested function
|
|
2593
|
+
// and left every return after it alone, so the hybrid returned a bare
|
|
2594
|
+
// `false` where a result was due: validateJSON answered `false` instead
|
|
2595
|
+
// of a rejection for such schemas. Text such as `return false` or a brace
|
|
2596
|
+
// inside a string is not code either.
|
|
2597
|
+
const ch = code[i]
|
|
2598
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2599
|
+
const j = skipString(code, i)
|
|
2600
|
+
result += code.slice(i, j)
|
|
2601
|
+
i = j
|
|
2602
|
+
continue
|
|
2603
|
+
}
|
|
2297
2604
|
// A nested closure keeps its own returns. Both spellings count: a
|
|
2298
2605
|
// `function` keyword and an arrow with a block body. The inline pattern
|
|
2299
2606
|
// compiler emits `(()=>{...return false...return true})()` and rewriting
|
|
2300
2607
|
// those returns turned the arrow's boolean into an object, which made
|
|
2301
2608
|
// `!(cond && obj)` false and let the hybrid accept what the boolean
|
|
2302
2609
|
// program rejected.
|
|
2303
|
-
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
|
|
2610
|
+
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1])) && !/[a-zA-Z0-9_$]/.test(code[i + 8] || '')
|
|
2304
2611
|
const isArrowBlock = code.startsWith('=>{', i)
|
|
2305
2612
|
if (isFunctionKw || isArrowBlock) {
|
|
2306
|
-
// Skip to the opening brace, then track all braces inside the body
|
|
2613
|
+
// Skip to the opening brace, then track all braces inside the body,
|
|
2614
|
+
// strings skipped as above.
|
|
2307
2615
|
let j = isArrowBlock ? i + 2 : i + 8
|
|
2308
2616
|
while (j < code.length && code[j] !== '{') j++
|
|
2309
2617
|
result += code.slice(i, j + 1)
|
|
2310
2618
|
i = j + 1
|
|
2311
|
-
// Track braces inside this function body
|
|
2312
2619
|
let braceDepth = 1
|
|
2313
2620
|
while (i < code.length && braceDepth > 0) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2621
|
+
const c = code[i]
|
|
2622
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
2623
|
+
const k = skipString(code, i)
|
|
2624
|
+
result += code.slice(i, k)
|
|
2625
|
+
i = k
|
|
2626
|
+
continue
|
|
2627
|
+
}
|
|
2628
|
+
if (c === '{') braceDepth++
|
|
2629
|
+
else if (c === '}') braceDepth--
|
|
2630
|
+
result += c
|
|
2318
2631
|
i++
|
|
2319
2632
|
}
|
|
2320
|
-
} else if (code.startsWith('return false', i)) {
|
|
2633
|
+
} else if (code.startsWith('return false', i) && (i + 12 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 12]))) {
|
|
2321
2634
|
result += onFalse
|
|
2322
2635
|
i += 12
|
|
2323
|
-
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-
|
|
2636
|
+
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 11]))) {
|
|
2324
2637
|
result += onTrue
|
|
2325
2638
|
i += 11
|
|
2326
2639
|
} else {
|
|
2327
|
-
result +=
|
|
2640
|
+
result += ch
|
|
2328
2641
|
i++
|
|
2329
2642
|
}
|
|
2330
2643
|
}
|
|
@@ -3508,14 +3821,32 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3508
3821
|
return handledPropertyNames
|
|
3509
3822
|
}
|
|
3510
3823
|
|
|
3824
|
+
// Whether the static unevaluatedProperties model checks the anyOf itself, so
|
|
3825
|
+
// genCodeNode leaves it out. Not when the set is built at run time
|
|
3826
|
+
// (unevalBuiltAtRuntime): that form only collects, and dropping the anyOf
|
|
3827
|
+
// there accepted a document no branch matched.
|
|
3511
3828
|
function unevalChecksAnyOf(schema, ctx) {
|
|
3512
3829
|
if (schema.unevaluatedProperties !== false) return false
|
|
3513
3830
|
const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3514
|
-
return r.dynamic && !r.allProps
|
|
3831
|
+
return r.dynamic && !r.allProps && !unevalBuiltAtRuntime(schema, ctx, r)
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// The unevaluatedProperties forms the static model cannot represent; those
|
|
3835
|
+
// build the evaluated set at run time (genUnevalDynamicVerdict). Decided in
|
|
3836
|
+
// one place so genCodeNode and genUnevaluatedPropertiesNode cannot disagree
|
|
3837
|
+
// about which of them checks the node's anyOf.
|
|
3838
|
+
function unevalBuiltAtRuntime(schema, ctx, r) {
|
|
3839
|
+
if (schema.unevaluatedProperties === true || schema.unevaluatedProperties === undefined) return false
|
|
3840
|
+
if (r === undefined) r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3841
|
+
return r.dynamic && (r.allProps || !unevalPropsModeled(schema, ctx.schemaMap))
|
|
3515
3842
|
}
|
|
3516
3843
|
|
|
3517
3844
|
function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
|
|
3518
3845
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3846
|
+
if (unevalBuiltAtRuntime(schema, ctx, evalResult)) {
|
|
3847
|
+
genUnevalDynamicVerdict(schema, v, lines, ctx, isObj)
|
|
3848
|
+
return
|
|
3849
|
+
}
|
|
3519
3850
|
|
|
3520
3851
|
if (evalResult.allProps || schema.unevaluatedProperties === true) {
|
|
3521
3852
|
// All props evaluated or unevaluatedProperties:true — no-op
|
|
@@ -4331,7 +4662,7 @@ function unevalLocalOk (node, key) {
|
|
|
4331
4662
|
// generic and may visit data positions (an enum value, say); judging those is
|
|
4332
4663
|
// harmless, since a data node that fails the test only makes the answer more
|
|
4333
4664
|
// conservative and the generators never read data positions as schemas.
|
|
4334
|
-
function unevalAllProvablyLocal (root) {
|
|
4665
|
+
function unevalAllProvablyLocal (root, only) {
|
|
4335
4666
|
const seen = new Set()
|
|
4336
4667
|
const stack = [root]
|
|
4337
4668
|
while (stack.length) {
|
|
@@ -4339,7 +4670,7 @@ function unevalAllProvablyLocal (root) {
|
|
|
4339
4670
|
if (n === null || typeof n !== 'object' || seen.has(n)) continue
|
|
4340
4671
|
seen.add(n)
|
|
4341
4672
|
if (!Array.isArray(n)) {
|
|
4342
|
-
if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4673
|
+
if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4343
4674
|
if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
|
|
4344
4675
|
}
|
|
4345
4676
|
for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
|
|
@@ -4396,7 +4727,7 @@ function genCharCodeSwitch(keys, v) {
|
|
|
4396
4727
|
// Valid path is still fast — only error path does extra work.
|
|
4397
4728
|
function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
|
|
4398
4729
|
const inputSchema = schema
|
|
4399
|
-
schema =
|
|
4730
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
4400
4731
|
// unevaluated* is generated only where it is provably its plain counterpart
|
|
4401
4732
|
// (see unevalLocalOk); any occurrence that is not keeps the whole schema on
|
|
4402
4733
|
// the interpreted engine's error path, exactly as before.
|
|
@@ -4629,7 +4960,14 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4629
4960
|
const subLines = []
|
|
4630
4961
|
const subSp = branchSp + '/' + i
|
|
4631
4962
|
if (typeof sub === 'object' && sub !== null) {
|
|
4632
|
-
|
|
4963
|
+
// A branch is a function of its own with a local `_e`; a collapse
|
|
4964
|
+
// inside it reports there, not into the combined function's lists.
|
|
4965
|
+
ctx.inBranchFn = (ctx.inBranchFn || 0) + 1
|
|
4966
|
+
try {
|
|
4967
|
+
genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
|
|
4968
|
+
} finally {
|
|
4969
|
+
ctx.inBranchFn--
|
|
4970
|
+
}
|
|
4633
4971
|
} else if (sub === false) {
|
|
4634
4972
|
// A false branch never matches; it reports the same error the
|
|
4635
4973
|
// interpreter attaches to a false schema.
|
|
@@ -4653,8 +4991,19 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4653
4991
|
const pp = pathExpr || '""'
|
|
4654
4992
|
ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
|
|
4655
4993
|
const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
|
|
4656
|
-
|
|
4657
|
-
|
|
4994
|
+
// branchSp is source text for a quoted literal, with backslashes and quotes
|
|
4995
|
+
// escaped; the ordinal is looked up by the path itself. Looked up by the
|
|
4996
|
+
// text, a path through a pattern such as `^\\d+$` found no node, or the
|
|
4997
|
+
// wrong one, and the collapsed error sorted out of place against the other
|
|
4998
|
+
// engines.
|
|
4999
|
+
// In the runtime shape there is no ordering key (the reader looks the path
|
|
5000
|
+
// up), so the collapsed error is in its final shape as it is built.
|
|
5001
|
+
const ord = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
|
|
5002
|
+
// In the combined function `_e` is created on the first error and every
|
|
5003
|
+
// error is collected (`_all` is true there), so the report only pushes.
|
|
5004
|
+
const report = ctx.inCombined && !ctx.inBranchFn
|
|
5005
|
+
? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
|
|
5006
|
+
: `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
4658
5007
|
if (keyword === 'anyOf') {
|
|
4659
5008
|
// anyOf reports nothing once a branch passes, so the branches are first
|
|
4660
5009
|
// only asked whether they pass. They are called with `_all` set to 0, a
|
|
@@ -4668,12 +5017,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4668
5017
|
// no one read. Only when no branch passes do they run again with the
|
|
4669
5018
|
// caller's `_all`, and the collapse sees them all, as before.
|
|
4670
5019
|
const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
|
|
4671
|
-
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors
|
|
5020
|
+
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
|
|
4672
5021
|
} else {
|
|
4673
5022
|
// Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
|
|
4674
5023
|
// the passing branches and answers; the branch errors of a probe are not
|
|
4675
5024
|
// errors anyone reads and are not collapsed.
|
|
4676
|
-
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){
|
|
5025
|
+
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
|
|
4677
5026
|
}
|
|
4678
5027
|
}
|
|
4679
5028
|
|
|
@@ -4701,6 +5050,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4701
5050
|
}
|
|
4702
5051
|
// $ref — resolve local and cross-schema refs
|
|
4703
5052
|
if (schema.$ref) {
|
|
5053
|
+
// The reference is followed and the rest of the node is not compiled; a
|
|
5054
|
+
// keyword beside it that validates would be dropped, so the node
|
|
5055
|
+
// declines (as in genCodeCNode). Reached since unevaluatedProperties
|
|
5056
|
+
// next to a $ref stopped declining the combined function, whose oneOf
|
|
5057
|
+
// and anyOf branches come from this generator: a branch was taken as
|
|
5058
|
+
// passing with its siblings unchecked.
|
|
5059
|
+
for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
|
|
4704
5060
|
// A self-reference cannot be expressed here yet: the root helper takes no
|
|
4705
5061
|
// path argument, so errors raised through it would carry the path of the
|
|
4706
5062
|
// root rather than of the value that failed. This used to fall through as
|
|
@@ -4801,7 +5157,9 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4801
5157
|
}
|
|
4802
5158
|
})
|
|
4803
5159
|
const expected = types.join(', ')
|
|
4804
|
-
|
|
5160
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
5161
|
+
// reports it.
|
|
5162
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
4805
5163
|
{
|
|
4806
5164
|
const typeSp = `${schemaPrefix}/type`
|
|
4807
5165
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
@@ -4832,7 +5190,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4832
5190
|
if (cv === null || typeof cv !== 'object') {
|
|
4833
5191
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4834
5192
|
} else {
|
|
4835
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
5193
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4836
5194
|
}
|
|
4837
5195
|
}
|
|
4838
5196
|
|
|
@@ -5002,6 +5360,11 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5002
5360
|
// to emit. The error keeps the keyword's own identity: the interpreted
|
|
5003
5361
|
// engine reports these as unevaluatedProperties with the key named, and the
|
|
5004
5362
|
// two engines are pinned to identical output.
|
|
5363
|
+
// Inside the combined function (a oneOf or anyOf branch, see
|
|
5364
|
+
// emitBranchCollapse) this generator only has the static form. A branch
|
|
5365
|
+
// whose evaluated set depends on its own branches would report the wrong
|
|
5366
|
+
// errors, and the collapse would pick the wrong closest branch; declined.
|
|
5367
|
+
if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
|
|
5005
5368
|
if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
5006
5369
|
|
|
5007
5370
|
// unevaluatedItems: false under the same rule. The interpreted engine
|
|
@@ -5112,18 +5475,10 @@ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5112
5475
|
|
|
5113
5476
|
function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
5114
5477
|
const si = ctx.varCounter++
|
|
5115
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
5116
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
5117
|
-
const maxItems = schema.maxItems
|
|
5118
5478
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
} else if (isPrim) {
|
|
5123
|
-
inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
|
|
5124
|
-
} else {
|
|
5125
|
-
inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
|
|
5126
|
-
}
|
|
5479
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
5480
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
5481
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
5127
5482
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
5128
5483
|
}
|
|
5129
5484
|
|
|
@@ -5338,29 +5693,80 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5338
5693
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
5339
5694
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
5340
5695
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
5696
|
+
// An error literal the combined function shares between calls, frozen. Two
|
|
5697
|
+
// non-enumerable fields ride along, invisible to Object.keys, spread and
|
|
5698
|
+
// JSON: `_s` says the object is shared, so the reader copies it before handing
|
|
5699
|
+
// it out (Object.isFrozen, the check it replaces, cost more than the copy),
|
|
5700
|
+
// and `_o` is its schema ordinal, known here and otherwise looked up by path
|
|
5701
|
+
// on every read. An `_o` the literal already carries, for standalone output,
|
|
5702
|
+
// is kept. `_t` is an empty slot lib/enrich-error.js fills once.
|
|
5703
|
+
function sharedErr (lit, rootSchema) {
|
|
5704
|
+
Object.defineProperty(lit, '_s', { value: true })
|
|
5705
|
+
// A slot the enrichment fills on the first read with the fields that depend
|
|
5706
|
+
// only on this literal (code, expected, rank), so later reads skip them.
|
|
5707
|
+
Object.defineProperty(lit, '_t', { value: {} })
|
|
5708
|
+
if (lit._o === undefined) {
|
|
5709
|
+
const o = ordinalFor(rootSchema, lit.schemaPath)
|
|
5710
|
+
if (typeof o === 'number') Object.defineProperty(lit, '_o', { value: o })
|
|
5711
|
+
}
|
|
5712
|
+
return Object.freeze(lit)
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
// The push for one error in the combined generator. With `ctx.rich` (the
|
|
5716
|
+
// validator's errors are being read and enriched, see validator-core) the
|
|
5717
|
+
// error is built in its final enriched shape where it happens, by makeRich in
|
|
5718
|
+
// lib/enrich-site.js with a site record made here at compile time; otherwise
|
|
5719
|
+
// it is the plain literal `lean` the generator always emitted. `valueExpr` is
|
|
5720
|
+
// the value at `pathExpr`, which enrichment otherwise finds again by walking
|
|
5721
|
+
// the document. `paramsShared` names a frozen params object hoisted for a
|
|
5722
|
+
// static site, so a static error's params stay one shared object as before.
|
|
5723
|
+
function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
|
|
5724
|
+
if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
|
|
5725
|
+
if (!ctx.richMk) {
|
|
5726
|
+
ctx.richMk = true
|
|
5727
|
+
const { makeRich } = require('./enrich-site')
|
|
5728
|
+
ctx.closureVars.push('_mk'); ctx.closureVals.push(makeRich)
|
|
5729
|
+
}
|
|
5730
|
+
let paramsVal
|
|
5731
|
+
try { paramsVal = Function('return ' + paramsCode)() } catch { paramsVal = undefined }
|
|
5732
|
+
const fmt = keyword === 'format' && paramsVal ? paramsVal.format : undefined
|
|
5733
|
+
const { errorSite } = require('./enrich-site')
|
|
5734
|
+
const siteVar = `_S${ctx.varCounter++}`
|
|
5735
|
+
ctx.closureVars.push(siteVar)
|
|
5736
|
+
ctx.closureVals.push(errorSite(keyword, unescapeSp(sp), paramsVal, fmt))
|
|
5737
|
+
return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d))`
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5341
5740
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
5342
5741
|
const inputSchema = schema
|
|
5343
|
-
schema =
|
|
5742
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
5344
5743
|
// Same provably-local rule as the error generator above.
|
|
5345
5744
|
if (typeof schema === 'object' && schema !== null) {
|
|
5346
5745
|
const s = JSON.stringify(schema)
|
|
5347
|
-
|
|
5746
|
+
// unevaluatedProperties that is not provably local is built at run time
|
|
5747
|
+
// (genUnevaluatedDynamicC); unevaluatedItems still has to be local.
|
|
5748
|
+
if (s.includes('unevaluatedItems') && !unevalAllProvablyLocal(schema, 'items')) return null
|
|
5348
5749
|
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
5349
5750
|
if (s.includes('"$ref":"#"')) return null
|
|
5350
5751
|
// Defs on a $ref cycle compile as named functions in the boolean and
|
|
5351
5752
|
// errors generators; this generator has no preamble for them, so it
|
|
5352
5753
|
// declines and the router uses the errors generator.
|
|
5353
5754
|
if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
|
|
5354
|
-
// Bail on additionalProperties as a schema — combined path doesn't emit the
|
|
5355
|
-
// per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
|
|
5356
|
-
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
5357
5755
|
// Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
|
|
5358
5756
|
// Letting genCodeC emit the legacy "must match exactly one" placeholder
|
|
5359
5757
|
// would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
|
|
5360
5758
|
// fast path plus errFn fallback handles these schemas with collapse.
|
|
5361
|
-
|
|
5759
|
+
// Branches are collapsed into one error by the error generator's emitter
|
|
5760
|
+
// (emitBranchCollapse). Its errors are plain, so the rich form, which
|
|
5761
|
+
// builds every error enriched where it fails, declines them.
|
|
5762
|
+
// In the rich form the collapsed error and its branch errors come out
|
|
5763
|
+
// plain (the branches are the error generator's) and are enriched when
|
|
5764
|
+
// presented; every other error is built enriched. See richBuilder.
|
|
5362
5765
|
}
|
|
5363
5766
|
if (typeof schema === 'boolean') {
|
|
5767
|
+
// A `false` root has one fixed error; the rich form leaves it to enrich.
|
|
5768
|
+
if (!schema && opts && (opts.rich || opts.resultShape)) return null
|
|
5769
|
+
if (opts && opts.resultShape) return null
|
|
5364
5770
|
return schema
|
|
5365
5771
|
? () => VALID_RESULT
|
|
5366
5772
|
: () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
@@ -5419,7 +5825,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5419
5825
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
5420
5826
|
|
|
5421
5827
|
// runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
|
|
5422
|
-
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5828
|
+
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5423
5829
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
|
|
5424
5830
|
const lines = []
|
|
5425
5831
|
try {
|
|
@@ -5436,14 +5842,49 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5436
5842
|
// helper compiles a pattern through it, which the boolean generator does for
|
|
5437
5843
|
// subschemas under not/if/contains; without the binding those threw a
|
|
5438
5844
|
// ReferenceError on the first document that reached them.
|
|
5845
|
+
if (opts && opts.resultShape) {
|
|
5846
|
+
ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
|
|
5847
|
+
ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
|
|
5848
|
+
}
|
|
5439
5849
|
const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
|
|
5440
5850
|
const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
|
|
5441
5851
|
const closureParams = closureNames.join(',')
|
|
5852
|
+
// A definition the error generator wrote as a named function inside a
|
|
5853
|
+
// branch (one on a cycle, or referenced from several places) reads the
|
|
5854
|
+
// cycle-guard state its own function declares, which this one does not:
|
|
5855
|
+
// the first document to reach it threw a ReferenceError. Declined; the
|
|
5856
|
+
// error function, which has that state, answers instead.
|
|
5857
|
+
if (ctx.usesBranchCollapse && ctx.defFns && ctx.defFns.size) return null
|
|
5858
|
+
// Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
|
|
5859
|
+
// the branch functions (this function collects everything), and the
|
|
5860
|
+
// runtime shape the error generator gives its literals, without code and
|
|
5861
|
+
// docUrl. The collapsed error itself keeps its code, as everywhere.
|
|
5862
|
+
if (ctx.usesBranchCollapse) ctx.helperCode.push(require('./branch-collapse').embedSource())
|
|
5863
|
+
if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
|
|
5864
|
+
const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
|
|
5865
|
+
for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
|
|
5866
|
+
for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
|
|
5867
|
+
}
|
|
5442
5868
|
const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
5443
5869
|
// Lazy error array: no allocation for valid data (the common case)
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5870
|
+
// resultShape: validate() itself for a validator whose errors are being read
|
|
5871
|
+
// (see validator-core): the result objects built here, so one pass decides,
|
|
5872
|
+
// collects and answers, with nothing between the caller and the checks.
|
|
5873
|
+
const rs = opts && opts.resultShape
|
|
5874
|
+
// In the result shape this function is validate() itself, so a document
|
|
5875
|
+
// that makes it throw is answered by the caller's previous path instead
|
|
5876
|
+
// (`_FB`). A thrown error reaching the caller is the one thing this form
|
|
5877
|
+
// must not add; a try that does not throw costs nothing measurable.
|
|
5878
|
+
const inner = rs
|
|
5879
|
+
// With collapsed branches, collecting runs every branch of every oneOf
|
|
5880
|
+
// and anyOf in full, which an accepted document does not need: the
|
|
5881
|
+
// verdict answers it first.
|
|
5882
|
+
? `try{${ctx.usesBranchCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};const _all=true;' : ''}let _e;\n ` + lines.join('\n ') +
|
|
5883
|
+
// A collapsed branch error is presented even alone (its ordinal and
|
|
5884
|
+
// raw branch errors stay inside); otherwise only a list of two or more
|
|
5885
|
+
// needs ordering.
|
|
5886
|
+
`\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
|
|
5887
|
+
: (ctx.usesBranchCollapse ? 'const _all=true;' : '') + `let _e;\n ` + lines.join('\n ') + `\n return _e?{valid:false,errors:_e}:R`
|
|
5447
5888
|
|
|
5448
5889
|
try {
|
|
5449
5890
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
@@ -5459,6 +5900,34 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5459
5900
|
// Combined code generator: type-aware like genCode, error-collecting like genCodeE.
|
|
5460
5901
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
5461
5902
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
5903
|
+
// What of a node still applies to a value that failed its `type`, for the
|
|
5904
|
+
// combined generator: the node without `type`, without what it emits outside
|
|
5905
|
+
// the type block anyway (enum, const, anyOf, oneOf), and without the keywords
|
|
5906
|
+
// of the declared types, which no value of another type meets. Null when
|
|
5907
|
+
// nothing that validates is left.
|
|
5908
|
+
const TYPE_KEYWORDS = {
|
|
5909
|
+
object: ['properties', 'patternProperties', 'additionalProperties', 'required', 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired', 'dependentSchemas', 'dependencies', 'unevaluatedProperties'],
|
|
5910
|
+
array: ['items', 'prefixItems', 'additionalItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems'],
|
|
5911
|
+
string: ['minLength', 'maxLength', 'pattern'],
|
|
5912
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
5913
|
+
}
|
|
5914
|
+
TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
|
|
5915
|
+
const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
|
|
5916
|
+
function typeMismatchRest(schema, types) {
|
|
5917
|
+
const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
|
|
5918
|
+
for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
|
|
5919
|
+
// A non-integer number reaches the type block already (numberToo).
|
|
5920
|
+
let rest = null
|
|
5921
|
+
for (const k of Object.keys(schema)) {
|
|
5922
|
+
if (drop.has(k)) continue
|
|
5923
|
+
if (rest === null) rest = {}
|
|
5924
|
+
rest[k] = schema[k]
|
|
5925
|
+
}
|
|
5926
|
+
if (rest === null) return null
|
|
5927
|
+
for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
|
|
5928
|
+
return null
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5462
5931
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
5463
5932
|
return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
5464
5933
|
}
|
|
@@ -5469,7 +5938,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5469
5938
|
if (schema === false) {
|
|
5470
5939
|
// Leading semicolon: the previous generated line may end in an
|
|
5471
5940
|
// expression, and a line starting with `(` would be parsed as a call.
|
|
5472
|
-
lines.push(
|
|
5941
|
+
lines.push(';' + errPushC(ctx, 'not', schemaPrefix, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'}`))
|
|
5473
5942
|
return
|
|
5474
5943
|
}
|
|
5475
5944
|
if (schema === true) return
|
|
@@ -5477,6 +5946,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5477
5946
|
|
|
5478
5947
|
// $ref — resolve local, anchor, and cross-schema refs
|
|
5479
5948
|
if (schema.$ref) {
|
|
5949
|
+
// The reference is followed and the rest of the node is not compiled, so
|
|
5950
|
+
// a node with a keyword beside its $ref that validates (2020-12 applies
|
|
5951
|
+
// both) declines: compiled, the sibling was dropped and a document it
|
|
5952
|
+
// rejects was accepted. Only reachable since unevaluatedProperties next
|
|
5953
|
+
// to a $ref stopped declining the whole function.
|
|
5954
|
+
for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
|
|
5480
5955
|
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
5481
5956
|
if (schema.$ref === '#') return
|
|
5482
5957
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
@@ -5530,7 +6005,26 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5530
6005
|
const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5531
6006
|
const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
|
|
5532
6007
|
const sp = schemaPrefix + '/' + schemaSuffix
|
|
5533
|
-
if (
|
|
6008
|
+
if (ctx.rich) {
|
|
6009
|
+
// A static site shares one frozen params object between calls, as its
|
|
6010
|
+
// hoisted literal does below; a dynamic one builds its params per call.
|
|
6011
|
+
let shared = null
|
|
6012
|
+
if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
6013
|
+
let pv
|
|
6014
|
+
try { pv = Function('return ' + paramsCode)() } catch { pv = undefined }
|
|
6015
|
+
if (pv !== undefined) {
|
|
6016
|
+
shared = `_P${ctx.varCounter++}`
|
|
6017
|
+
ctx.closureVars.push(shared); ctx.closureVals.push(Object.freeze(pv))
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
return errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, v, null, shared)
|
|
6021
|
+
}
|
|
6022
|
+
// Hoisting a static error as one shared literal keeps a function that
|
|
6023
|
+
// also answers accepted documents small. In the runtime shape this
|
|
6024
|
+
// function only runs for documents being rejected (the verdict is its own
|
|
6025
|
+
// function), and a shared literal has to be copied for the caller on
|
|
6026
|
+
// every read, so there it is written fresh.
|
|
6027
|
+
if (isStaticPath && !ctx.noOrdinal && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
5534
6028
|
// Try to evaluate paramsCode as a static constant
|
|
5535
6029
|
let paramsVal
|
|
5536
6030
|
try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
|
|
@@ -5557,7 +6051,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5557
6051
|
const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
|
|
5558
6052
|
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
5559
6053
|
if (o !== null) lit._o = o
|
|
5560
|
-
ctx.closureVals.push(
|
|
6054
|
+
ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
|
|
5561
6055
|
return `(_e||(_e=[])).push(${errVar})`
|
|
5562
6056
|
}
|
|
5563
6057
|
}
|
|
@@ -5582,7 +6076,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5582
6076
|
if (cv === null || typeof cv !== 'object') {
|
|
5583
6077
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5584
6078
|
} else {
|
|
5585
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
6079
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5586
6080
|
}
|
|
5587
6081
|
}
|
|
5588
6082
|
}
|
|
@@ -5601,7 +6095,9 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5601
6095
|
}
|
|
5602
6096
|
})
|
|
5603
6097
|
const expected = types.join(', ')
|
|
5604
|
-
|
|
6098
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
6099
|
+
// reports it.
|
|
6100
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
5605
6101
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
5606
6102
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
5607
6103
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
@@ -5614,7 +6110,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5614
6110
|
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
5615
6111
|
}
|
|
5616
6112
|
emitEnumConst()
|
|
5617
|
-
|
|
6113
|
+
// A number that is not an integer fails `integer` and is still a number:
|
|
6114
|
+
// minimum, multipleOf and the rest apply to it, and the other engines
|
|
6115
|
+
// report them next to the type error. The block opens for any number.
|
|
6116
|
+
const numberToo = types.includes('integer') && !types.includes('number')
|
|
6117
|
+
lines.push(numberToo ? `if(${typeOk}||typeof ${v}==='number'){` : `if(${typeOk}){`)
|
|
5618
6118
|
} else {
|
|
5619
6119
|
emitEnumConst()
|
|
5620
6120
|
}
|
|
@@ -5641,29 +6141,29 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5641
6141
|
}
|
|
5642
6142
|
for (const key of schema.required) {
|
|
5643
6143
|
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
|
|
5644
|
-
if (isStaticPath) {
|
|
6144
|
+
if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
|
|
5645
6145
|
const ei = ctx.varCounter++
|
|
5646
6146
|
const errVar = `_E${ei}`
|
|
5647
6147
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5648
6148
|
ctx.closureVars.push(errVar)
|
|
5649
|
-
ctx.closureVals.push(
|
|
6149
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
5650
6150
|
lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
|
|
5651
6151
|
} else {
|
|
5652
|
-
lines.push(`if(${check}){(
|
|
6152
|
+
lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
5653
6153
|
}
|
|
5654
6154
|
}
|
|
5655
6155
|
} else if (schema.required) {
|
|
5656
6156
|
for (const key of schema.required) {
|
|
5657
6157
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5658
|
-
if (isStatic) {
|
|
6158
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5659
6159
|
const ei = ctx.varCounter++
|
|
5660
6160
|
const errVar = `_E${ei}`
|
|
5661
6161
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5662
6162
|
ctx.closureVars.push(errVar)
|
|
5663
|
-
ctx.closureVals.push(
|
|
6163
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
5664
6164
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5665
6165
|
} else {
|
|
5666
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(
|
|
6166
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
5667
6167
|
}
|
|
5668
6168
|
}
|
|
5669
6169
|
}
|
|
@@ -5786,7 +6286,13 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5786
6286
|
// unevaluatedProperties: false where it is provably additionalProperties
|
|
5787
6287
|
// (the entry bail guaranteed that), keeping the keyword's own identity so
|
|
5788
6288
|
// the two engines report the same error.
|
|
5789
|
-
if (schema.unevaluatedProperties
|
|
6289
|
+
if (schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true) {
|
|
6290
|
+
if (unevalLocalOk(schema, 'unevaluatedProperties')) {
|
|
6291
|
+
if (schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6292
|
+
} else {
|
|
6293
|
+
genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
5790
6296
|
|
|
5791
6297
|
// unevaluatedItems: false under the same rule: one error for the array,
|
|
5792
6298
|
// the first uncovered position as the limit. Adjacent schema-form `items`
|
|
@@ -5798,15 +6304,15 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5798
6304
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
5799
6305
|
for (const dep of deps) {
|
|
5800
6306
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5801
|
-
if (isStatic) {
|
|
6307
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5802
6308
|
const ei = ctx.varCounter++
|
|
5803
6309
|
const errVar = `_E${ei}`
|
|
5804
6310
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5805
6311
|
ctx.closureVars.push(errVar)
|
|
5806
|
-
ctx.closureVals.push(
|
|
6312
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
|
|
5807
6313
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5808
6314
|
} else {
|
|
5809
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(
|
|
6315
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
|
|
5810
6316
|
}
|
|
5811
6317
|
}
|
|
5812
6318
|
}
|
|
@@ -5839,6 +6345,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5839
6345
|
if (schema.patternProperties) {
|
|
5840
6346
|
if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
|
|
5841
6347
|
}
|
|
6348
|
+
// additionalProperties as a schema: each key that is neither declared nor
|
|
6349
|
+
// matched by a pattern, validated where it is (see genAdditionalSchemaC).
|
|
6350
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
6351
|
+
|
|
5842
6352
|
|
|
5843
6353
|
// dependentSchemas
|
|
5844
6354
|
if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
|
|
@@ -5868,11 +6378,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5868
6378
|
// allOf
|
|
5869
6379
|
if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
|
|
5870
6380
|
|
|
5871
|
-
// anyOf
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
if (schema.oneOf) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6381
|
+
// anyOf, oneOf. A collapsed branch applies to a value of any type, as enum
|
|
6382
|
+
// and const do: the other engines report it next to a failed `type`, so
|
|
6383
|
+
// in the combined function it goes after the type-success block below.
|
|
6384
|
+
if (schema.anyOf && !ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6385
|
+
if (schema.oneOf && !ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
5876
6386
|
|
|
5877
6387
|
// not
|
|
5878
6388
|
if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
@@ -5883,7 +6393,22 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5883
6393
|
// Close type-success block if opened
|
|
5884
6394
|
if (types) {
|
|
5885
6395
|
lines.push(`}`)
|
|
6396
|
+
// A value of another type still meets every keyword that applies to its
|
|
6397
|
+
// own type, and the applicators (allOf, not, if/then/else, $ref): the
|
|
6398
|
+
// other engines report those next to the type error, and the block above
|
|
6399
|
+
// skipped them all. The keywords of the declared types cannot apply to
|
|
6400
|
+
// it and are left out, so the else is small and does not repeat the
|
|
6401
|
+
// declared type's subtree.
|
|
6402
|
+
const rest = typeMismatchRest(schema, types)
|
|
6403
|
+
if (rest !== null) {
|
|
6404
|
+
lines.push(`else{`)
|
|
6405
|
+
genCodeC(rest, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6406
|
+
lines.push(`}`)
|
|
6407
|
+
}
|
|
5886
6408
|
}
|
|
6409
|
+
if (schema.anyOf && ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6410
|
+
if (schema.oneOf && ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6411
|
+
|
|
5887
6412
|
}
|
|
5888
6413
|
|
|
5889
6414
|
// Rarely used keyword families of genCodeCNode, kept out of it so their compile is
|
|
@@ -5909,20 +6434,158 @@ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5909
6434
|
|
|
5910
6435
|
function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
5911
6436
|
const si = ctx.varCounter++
|
|
5912
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
5913
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
5914
|
-
const maxItems = schema.maxItems
|
|
5915
6437
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
6438
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
6439
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
6440
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
6441
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
6442
|
+
}
|
|
6443
|
+
|
|
6444
|
+
// The verdict form of genUnevaluatedDynamicC: the same set, built the same
|
|
6445
|
+
// way, and the answer false at the first key outside it.
|
|
6446
|
+
function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
|
|
6447
|
+
if (schema.additionalProperties !== undefined) return
|
|
6448
|
+
const ui = ctx.varCounter++
|
|
6449
|
+
const S = `_ev${ui}`
|
|
6450
|
+
const body = [`const ${S}=new Set()`]
|
|
6451
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6452
|
+
addApplicatorAnnotations(schema, v, S, body, ctx, true)
|
|
6453
|
+
const kVar = `_uk${ui}`
|
|
6454
|
+
const up = schema.unevaluatedProperties
|
|
6455
|
+
if (up === false) {
|
|
6456
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar}))return false`)
|
|
6457
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6458
|
+
const sub = []
|
|
6459
|
+
genCode(up, `${v}[${kVar}]`, sub, ctx)
|
|
6460
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
|
|
5922
6461
|
} else {
|
|
5923
|
-
|
|
6462
|
+
throw DECLINE
|
|
6463
|
+
}
|
|
6464
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
// unevaluatedProperties where what is evaluated depends on which branches
|
|
6468
|
+
// pass: the set is built at run time with the interpreted engine's rules.
|
|
6469
|
+
// The node's own properties, patternProperties and additionalProperties
|
|
6470
|
+
// count whether or not their values pass; an allOf, anyOf or dependentSchemas
|
|
6471
|
+
// branch, an if, then or else, or a $ref target counts only when it passes,
|
|
6472
|
+
// a oneOf only when exactly one branch passes, and `not` never. Each branch is
|
|
6473
|
+
// asked through an annotation function (emitAnnot), which validates first and
|
|
6474
|
+
// only then adds what it evaluated, so nothing has to be rolled back.
|
|
6475
|
+
function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
6476
|
+
// additionalProperties evaluates every key the others do not, pass or fail.
|
|
6477
|
+
if (schema.additionalProperties !== undefined) return
|
|
6478
|
+
const ui = ctx.varCounter++
|
|
6479
|
+
const S = `_ev${ui}`
|
|
6480
|
+
const body = [`const ${S}=new Set()`]
|
|
6481
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6482
|
+
addApplicatorAnnotations(schema, v, S, body, ctx, true)
|
|
6483
|
+
const kVar = `_uk${ui}`
|
|
6484
|
+
const up = schema.unevaluatedProperties
|
|
6485
|
+
if (up === false) {
|
|
6486
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
|
|
6487
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6488
|
+
const pe = emitPtrEsc(ctx)
|
|
6489
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
6490
|
+
const sub = []
|
|
6491
|
+
genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
|
|
6492
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
|
|
6493
|
+
} else {
|
|
6494
|
+
throw DECLINE
|
|
6495
|
+
}
|
|
6496
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6497
|
+
}
|
|
6498
|
+
|
|
6499
|
+
// The names a node's own keywords evaluate, added to `S`: present declared
|
|
6500
|
+
// properties, keys matching a pattern.
|
|
6501
|
+
function addLocalAnnotations(node, v, S, out, ctx) {
|
|
6502
|
+
for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.add(${JSON.stringify(k)})`)
|
|
6503
|
+
const pats = Object.keys(node.patternProperties || {})
|
|
6504
|
+
if (pats.length) {
|
|
6505
|
+
const tests = []
|
|
6506
|
+
const kv = `_pk${ctx.varCounter++}`
|
|
6507
|
+
for (const pat of pats) {
|
|
6508
|
+
const fast = fastPrefixCheck(pat, kv)
|
|
6509
|
+
if (fast) { tests.push(fast); continue }
|
|
6510
|
+
const ri = ctx.varCounter++
|
|
6511
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
6512
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
6513
|
+
tests.push(`_re${ri}.test(${kv})`)
|
|
6514
|
+
}
|
|
6515
|
+
out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.add(${kv})`)
|
|
5924
6516
|
}
|
|
5925
|
-
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
// What a node's applicators add to `S`. At the node that owns the
|
|
6520
|
+
// unevaluatedProperties (`top`), a oneOf counts only with exactly one passing
|
|
6521
|
+
// branch; inside a branch that passed, its oneOf has exactly one.
|
|
6522
|
+
function addApplicatorAnnotations(node, v, S, out, ctx, top) {
|
|
6523
|
+
for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6524
|
+
for (const b of node.anyOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6525
|
+
if (Array.isArray(node.oneOf)) {
|
|
6526
|
+
if (top) {
|
|
6527
|
+
const t = `_one${ctx.varCounter++}`
|
|
6528
|
+
const calls = node.oneOf.map((b, i) => `{const _s=new Set();if(${emitAnnot(b, ctx)}(${v},_s)){${t}c++;${t}s=_s}}`).join('')
|
|
6529
|
+
out.push(`{let ${t}c=0,${t}s=null;${calls}if(${t}c===1)for(const _x of ${t}s)${S}.add(_x)}`)
|
|
6530
|
+
} else {
|
|
6531
|
+
for (const b of node.oneOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
if (node.if !== undefined) {
|
|
6535
|
+
const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
|
|
6536
|
+
const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
|
|
6537
|
+
out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
|
|
6538
|
+
}
|
|
6539
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
|
|
6540
|
+
for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
|
|
6541
|
+
}
|
|
6542
|
+
if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
|
|
6543
|
+
if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6546
|
+
function refTargetFor(ref, ctx) {
|
|
6547
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
6548
|
+
const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
|
|
6549
|
+
if (t === undefined) throw DECLINE
|
|
6550
|
+
return t
|
|
6551
|
+
}
|
|
6552
|
+
|
|
6553
|
+
// A function `(value, S) => valid` for one subschema: the subschema's verdict,
|
|
6554
|
+
// and when it passes, what it evaluated added to S. One per subschema object.
|
|
6555
|
+
function emitAnnot(sub, ctx) {
|
|
6556
|
+
if (sub === true) return '_anT'
|
|
6557
|
+
if (sub === false) return '_anF'
|
|
6558
|
+
if (sub === null || typeof sub !== 'object') throw DECLINE
|
|
6559
|
+
if (!ctx.annFns) {
|
|
6560
|
+
ctx.annFns = new Map()
|
|
6561
|
+
;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}')
|
|
6562
|
+
}
|
|
6563
|
+
const hit = ctx.annFns.get(sub)
|
|
6564
|
+
if (hit !== undefined) {
|
|
6565
|
+
if (hit === null) throw DECLINE // a reference cycle: not modelled here
|
|
6566
|
+
return hit
|
|
6567
|
+
}
|
|
6568
|
+
ctx.annFns.set(sub, null)
|
|
6569
|
+
const name = `_an${ctx.varCounter++}`
|
|
6570
|
+
const vl = []
|
|
6571
|
+
nestedGenCode(sub, '_av', vl, ctx)
|
|
6572
|
+
const body = []
|
|
6573
|
+
// additionalProperties, or an unevaluatedProperties that passed, evaluate
|
|
6574
|
+
// every remaining key.
|
|
6575
|
+
if (sub.additionalProperties !== undefined || (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== false)) {
|
|
6576
|
+
body.push(`for(const _ka in d)S.add(_ka)`)
|
|
6577
|
+
} else if (sub.unevaluatedProperties === false) {
|
|
6578
|
+
body.push(`for(const _ka in d)S.add(_ka)`)
|
|
6579
|
+
} else {
|
|
6580
|
+
addLocalAnnotations(sub, 'd', 'S', body, ctx)
|
|
6581
|
+
addApplicatorAnnotations(sub, 'd', 'S', body, ctx, false)
|
|
6582
|
+
}
|
|
6583
|
+
// The verdict generator keeps its declarations in the preamble (its
|
|
6584
|
+
// helperCode is only scanned for patterns); the error generators in
|
|
6585
|
+
// helperCode.
|
|
6586
|
+
;(ctx.preamble || ctx.helperCode).push(`function ${name}_v(_av){${vl.join('\n ')}\n return true}function ${name}(d,S){if(!${name}_v(d))return false;if(typeof d!=='object'||d===null||Array.isArray(d))return true;${body.join(';')};return true}`)
|
|
6587
|
+
ctx.annFns.set(sub, name)
|
|
6588
|
+
return name
|
|
5926
6589
|
}
|
|
5927
6590
|
|
|
5928
6591
|
function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
@@ -5952,6 +6615,41 @@ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix
|
|
|
5952
6615
|
}
|
|
5953
6616
|
}
|
|
5954
6617
|
|
|
6618
|
+
// The error generator's per-key loop (genAdditionalSchemaE), in the combined
|
|
6619
|
+
// generator: the subschema compiled by genCodeC at each additional key, with
|
|
6620
|
+
// that key's path. Patterns go through the same matchers patternProperties
|
|
6621
|
+
// uses here.
|
|
6622
|
+
function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
|
|
6623
|
+
const ki = ctx.varCounter++
|
|
6624
|
+
const kVar = `_ak${ki}`
|
|
6625
|
+
const known = Object.keys(schema.properties || {})
|
|
6626
|
+
const conds = []
|
|
6627
|
+
if (known.length) {
|
|
6628
|
+
const set = `_akn${ki}`
|
|
6629
|
+
ctx.closureVars.push(set)
|
|
6630
|
+
ctx.closureVals.push(new Set(known))
|
|
6631
|
+
conds.push(`!${set}.has(${kVar})`)
|
|
6632
|
+
}
|
|
6633
|
+
const pats = []
|
|
6634
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
6635
|
+
const fast = fastPrefixCheck(pat, kVar)
|
|
6636
|
+
if (fast) { pats.push(fast); continue }
|
|
6637
|
+
const ri = ctx.varCounter++
|
|
6638
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
6639
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
6640
|
+
pats.push(`_re${ri}.test(${kVar})`)
|
|
6641
|
+
}
|
|
6642
|
+
if (pats.length) conds.push(`!(${pats.join('||')})`)
|
|
6643
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
6644
|
+
const keep = conds.length ? `if(${conds.join('&&')})` : ''
|
|
6645
|
+
const pe = emitPtrEsc(ctx)
|
|
6646
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
6647
|
+
const sub = []
|
|
6648
|
+
genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
|
|
6649
|
+
if (sub.length === 0) return
|
|
6650
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
|
|
6651
|
+
}
|
|
6652
|
+
|
|
5955
6653
|
function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
5956
6654
|
let handledPropertyNames = false
|
|
5957
6655
|
const ppEntries = Object.entries(schema.patternProperties)
|
|
@@ -6070,7 +6768,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6070
6768
|
function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
6071
6769
|
const ki = ctx.varCounter++
|
|
6072
6770
|
const p = pathExpr || '""'
|
|
6073
|
-
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(
|
|
6771
|
+
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
|
|
6074
6772
|
}
|
|
6075
6773
|
|
|
6076
6774
|
function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
@@ -6124,12 +6822,18 @@ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
|
6124
6822
|
}
|
|
6125
6823
|
|
|
6126
6824
|
function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
6825
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
6826
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
6827
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6127
6828
|
const fi = ctx.varCounter++
|
|
6128
6829
|
const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
|
|
6129
6830
|
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
|
|
6130
6831
|
}
|
|
6131
6832
|
|
|
6132
6833
|
function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
6834
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
6835
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
6836
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6133
6837
|
const fi = ctx.varCounter++
|
|
6134
6838
|
const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
|
|
6135
6839
|
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
|