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.
@@ -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
- if (evalResult.dynamic && evalResult.allProps) return false
1509
- if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
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
- if (/[~/"']/.test(name)) return false // special chars in def name
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 = normalizePropertyNames(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
- if (code[i] === '{') braceDepth++
2315
- else if (code[i] === '}') braceDepth--
2316
- if (braceDepth > 0) result += code[i]
2317
- else result += '}' // closing brace of function
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-Z_$]/.test(code[i + 11]))) {
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 += code[i]
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 = normalizePropertyNames(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
- genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
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
- const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
4657
- const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
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,title:_bt${fi}[_bi]})}${report}}}`)
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++){const _br=${call};${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}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}}}`)
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
- const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
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:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
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
- let inner
5120
- if (isPrim && maxItems && maxItems <= 16) {
5121
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
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 = normalizePropertyNames(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
- if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
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
- if (s.includes('"oneOf"') || s.includes('"anyOf"')) return null
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
- const inner = `let _e;\n ` +
5445
- lines.join('\n ') +
5446
- `\n return _e?{valid:false,errors:_e}:R`
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(`;(_e||(_e=[])).push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'})`)
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 (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
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(Object.freeze(lit))
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:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
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
- const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
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
- lines.push(`if(${typeOk}){`)
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(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
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}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
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(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
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)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(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 === false && schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
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(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
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)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(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
- if (schema.anyOf) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5873
-
5874
- // oneOf
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
- let inner
5917
- if (isPrim && maxItems && maxItems <= 16) {
5918
- // Small primitive arrays: O(n²) nested loop, zero allocation
5919
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
5920
- } else if (isPrim) {
5921
- 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)}`
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
- 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)}`
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
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
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}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
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'")}}}`)