ata-validator 1.40.1 → 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
@@ -2093,7 +2349,14 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2093
2349
  if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
2094
2350
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
2095
2351
 
2096
- const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes }
2352
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
2353
+ keywords: (opts && opts.keywords) || null, kwEmitted: null }
2354
+ if (ctx.keywords) {
2355
+ // Macro keywords expand into a schema of their own, which this generator
2356
+ // does not apply; such a schema stays on the interpreted engine.
2357
+ if (usesMacroKeyword(schema, schemaMap, ctx.keywords)) return null
2358
+ ctx.kwEmitted = new Set()
2359
+ }
2097
2360
  const lines = []
2098
2361
  try {
2099
2362
  genCode(schema, 'd', lines, ctx)
@@ -2101,6 +2364,16 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2101
2364
  if (e === DECLINE) return null
2102
2365
  throw e
2103
2366
  }
2367
+ // Every schema node that carries a custom keyword must have had its check
2368
+ // written. A path that inlined a node without going through genCodeNode
2369
+ // would drop the keyword and accept what it should reject, so a node that
2370
+ // was not reached declines the whole schema to the interpreted engine
2371
+ // instead. Unreferenced definitions decline too: slower, never wrong.
2372
+ if (ctx.keywords) {
2373
+ for (const node of keywordNodes(schema, schemaMap, ctx.keywords)) {
2374
+ if (!ctx.kwEmitted.has(node)) return null
2375
+ }
2376
+ }
2104
2377
 
2105
2378
  // Append deferred checks (additionalProperties, unevaluatedProperties) at the end
2106
2379
  if (ctx.deferredChecks) {
@@ -2137,7 +2410,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2137
2410
  // are built when first asked for, not here: most validators never call
2138
2411
  // validateJSON or get extended, and rewriting and compiling the source twice
2139
2412
  // more on every schema was about a third of what a first call cost.
2140
- let body, hybridThunk, tailThunk
2413
+ let body, hybridThunk, tailThunk, resultThunk
2141
2414
  if (ctx.usesRecursion) {
2142
2415
  // Self-recursive: a named body reached through the cycle guard, so data
2143
2416
  // that points back at itself settles instead of exhausting the stack.
@@ -2147,6 +2420,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2147
2420
  body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
2148
2421
  // Hybrid: keep the boolean answer, wrap only the outer call
2149
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}`
2150
2424
  tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
2151
2425
  } else if (defSets.length > 0) {
2152
2426
  // No self-reference at the root, but a def on a cycle can still be
@@ -2154,10 +2428,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2154
2428
  const run = emitGuardedRun(`return _body(d)`, '', defSets)
2155
2429
  body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
2156
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}`
2157
2432
  tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
2158
2433
  } else {
2159
2434
  body = checkStr + '\n return true'
2160
2435
  hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
2436
+ resultThunk = () => replaceTopLevel(checkStr + '\n return true', `return ${RESULT_TRUE}`, `return ${RESULT_FALSE}`)
2161
2437
  // An early accept inside the checks has to reach the tail too, so every
2162
2438
  // top-level `return true` becomes the tail call, by the same rewrite the
2163
2439
  // hybrid above relies on and test_hybrid_agreement holds to the suite.
@@ -2197,6 +2473,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2197
2473
  return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
2198
2474
  }
2199
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
+
2200
2495
  // The same function with one more check where it would return true, for a
2201
2496
  // wrapper that enforces something the schema does not carry (see
2202
2497
  // Validator#_extendVerdict). Calling the check from here rather than from a
@@ -2274,40 +2569,75 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2274
2569
 
2275
2570
  // Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
2276
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
+
2277
2588
  function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
2278
2589
  let fnDepth = 0, result = '', i = 0
2279
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
+ }
2280
2604
  // A nested closure keeps its own returns. Both spellings count: a
2281
2605
  // `function` keyword and an arrow with a block body. The inline pattern
2282
2606
  // compiler emits `(()=>{...return false...return true})()` and rewriting
2283
2607
  // those returns turned the arrow's boolean into an object, which made
2284
2608
  // `!(cond && obj)` false and let the hybrid accept what the boolean
2285
2609
  // program rejected.
2286
- 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] || '')
2287
2611
  const isArrowBlock = code.startsWith('=>{', i)
2288
2612
  if (isFunctionKw || isArrowBlock) {
2289
- // 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.
2290
2615
  let j = isArrowBlock ? i + 2 : i + 8
2291
2616
  while (j < code.length && code[j] !== '{') j++
2292
2617
  result += code.slice(i, j + 1)
2293
2618
  i = j + 1
2294
- // Track braces inside this function body
2295
2619
  let braceDepth = 1
2296
2620
  while (i < code.length && braceDepth > 0) {
2297
- if (code[i] === '{') braceDepth++
2298
- else if (code[i] === '}') braceDepth--
2299
- if (braceDepth > 0) result += code[i]
2300
- 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
2301
2631
  i++
2302
2632
  }
2303
- } 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]))) {
2304
2634
  result += onFalse
2305
2635
  i += 12
2306
- } 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]))) {
2307
2637
  result += onTrue
2308
2638
  i += 11
2309
2639
  } else {
2310
- result += code[i]
2640
+ result += ch
2311
2641
  i++
2312
2642
  }
2313
2643
  }
@@ -2449,6 +2779,115 @@ function _deferOrInline(ctx, lines, v, check) {
2449
2779
  // 'object' = we know v is a non-null non-array object
2450
2780
  // 'array' = we know v is an array
2451
2781
  // 'string' / 'number' / 'integer' = we know the primitive type
2782
+ // Custom keywords (the `keywords` option, normalized by lib/keywords.js) as
2783
+ // calls into the caller's functions, threaded in as closure variables the way
2784
+ // user formats are. Each check is built here with the interpreter's own
2785
+ // semantics, so the two engines cannot disagree on one: the keyword is skipped
2786
+ // for data outside its declared types (the interpreter's type bits), the
2787
+ // compile form is built once per schema node, and any errors the function
2788
+ // leaves on itself are cleared after the call, as runCustomV does. Written
2789
+ // first in the node, before anything that could return early.
2790
+ // The interpreter's type bits (lib/interpreter.js typeBit and dataBits),
2791
+ // copied rather than required: loading the interpreter added about a
2792
+ // millisecond to the first call of a schema that never needs it.
2793
+ // tests/test_custom_keyword_codegen.js holds the copies equal to the originals.
2794
+ const KW_T_ANY = 127
2795
+ function kwTypeBit (name) {
2796
+ switch (name) {
2797
+ case 'string': return 1
2798
+ case 'number': return 2
2799
+ case 'integer': return 4
2800
+ case 'boolean': return 8
2801
+ case 'null': return 16
2802
+ case 'object': return 32
2803
+ case 'array': return 64
2804
+ default: return KW_T_ANY
2805
+ }
2806
+ }
2807
+ function kwDataBits (d) {
2808
+ switch (typeof d) {
2809
+ case 'string': return 1
2810
+ case 'number':
2811
+ if (!isFinite(d)) return 0
2812
+ return Number.isInteger(d) ? 2 | 4 : 2
2813
+ case 'boolean': return 8
2814
+ case 'object':
2815
+ if (d === null) return 16
2816
+ return Array.isArray(d) ? 64 : 32
2817
+ default: return 0
2818
+ }
2819
+ }
2820
+ function genCustomKeywords (schema, v, lines, ctx) {
2821
+ for (const key of Object.keys(schema)) {
2822
+ const def = ctx.keywords[key]
2823
+ if (def === undefined || def.macro !== null) continue
2824
+ const value = schema[key]
2825
+ const dataBits = kwDataBits
2826
+ const T_ANY = KW_T_ANY
2827
+ let mask = T_ANY
2828
+ if (def.types !== null) {
2829
+ mask = 0
2830
+ for (const t of def.types) mask |= kwTypeBit(t)
2831
+ }
2832
+ let fn
2833
+ if (def.compile !== null) {
2834
+ fn = def.compile(value, schema)
2835
+ if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`)
2836
+ } else {
2837
+ const validate = def.validate
2838
+ fn = (data) => validate(value, data, schema)
2839
+ fn.source = validate
2840
+ }
2841
+ const holder = () => fn.source || fn
2842
+ const check = mask === T_ANY
2843
+ ? (data) => { const ok = fn(data); const h = holder(); if (h.errors) h.errors = null; return !!ok }
2844
+ : (data) => {
2845
+ if ((dataBits(data) & mask) === 0) return true
2846
+ const ok = fn(data); const h = holder(); if (h.errors) h.errors = null; return !!ok
2847
+ }
2848
+ const name = '_kwc' + ctx.closureVars.length
2849
+ ctx.closureVars.push(name)
2850
+ ctx.closureVals.push(check)
2851
+ lines.push(`if(!${name}(${v}))return false`)
2852
+ }
2853
+ ctx.kwEmitted.add(schema)
2854
+ }
2855
+
2856
+ // Map keys under these are names, not keywords (lib/keywords.js NAME_MAPS).
2857
+ const KW_NAME_MAPS = new Set([
2858
+ 'properties', 'patternProperties', '$defs', 'definitions',
2859
+ 'dependentSchemas', 'dependentRequired', 'dependencies', 'propertyDependencies',
2860
+ ])
2861
+ function forEachKeywordNode (schema, schemaMap, keywords, visit) {
2862
+ const seen = new Set()
2863
+ const walk = (node, keysAreNames) => {
2864
+ if (node === null || typeof node !== 'object' || seen.has(node)) return
2865
+ seen.add(node)
2866
+ if (Array.isArray(node)) { for (const item of node) walk(item, false); return }
2867
+ let has = false
2868
+ for (const key of Object.keys(node)) {
2869
+ if (!keysAreNames && keywords[key] !== undefined) has = true
2870
+ const val = node[key]
2871
+ if (val !== null && typeof val === 'object') walk(val, !keysAreNames && KW_NAME_MAPS.has(key))
2872
+ }
2873
+ if (has) visit(node)
2874
+ }
2875
+ walk(schema, false)
2876
+ if (schemaMap) for (const s of schemaMap.values()) walk(s, false)
2877
+ }
2878
+ function keywordNodes (schema, schemaMap, keywords) {
2879
+ const out = []
2880
+ forEachKeywordNode(schema, schemaMap, keywords, (n) => out.push(n))
2881
+ return out
2882
+ }
2883
+ function usesMacroKeyword (schema, schemaMap, keywords) {
2884
+ let macro = false
2885
+ forEachKeywordNode(schema, schemaMap, keywords, (n) => {
2886
+ for (const key of Object.keys(n)) if (keywords[key] !== undefined && keywords[key].macro !== null) macro = true
2887
+ })
2888
+ return macro
2889
+ }
2890
+
2452
2891
  function genCode(schema, v, lines, ctx, knownType) {
2453
2892
  return withPlain(schema, v, lines, ctx, () => genCodeNode(schema, v, lines, ctx, knownType))
2454
2893
  }
@@ -2459,6 +2898,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2459
2898
  if (schema === false) { lines.push('return false'); return }
2460
2899
  if (schema === true) return
2461
2900
  if (typeof schema !== 'object' || schema === null) return
2901
+ if (ctx.keywords) genCustomKeywords(schema, v, lines, ctx)
2462
2902
  if (!ctx.regExpMap) {
2463
2903
  ctx.regExpMap = new Map();
2464
2904
  }
@@ -3381,14 +3821,32 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3381
3821
  return handledPropertyNames
3382
3822
  }
3383
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.
3384
3828
  function unevalChecksAnyOf(schema, ctx) {
3385
3829
  if (schema.unevaluatedProperties !== false) return false
3386
3830
  const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3387
- 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))
3388
3842
  }
3389
3843
 
3390
3844
  function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
3391
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
+ }
3392
3850
 
3393
3851
  if (evalResult.allProps || schema.unevaluatedProperties === true) {
3394
3852
  // All props evaluated or unevaluatedProperties:true — no-op
@@ -4204,7 +4662,7 @@ function unevalLocalOk (node, key) {
4204
4662
  // generic and may visit data positions (an enum value, say); judging those is
4205
4663
  // harmless, since a data node that fails the test only makes the answer more
4206
4664
  // conservative and the generators never read data positions as schemas.
4207
- function unevalAllProvablyLocal (root) {
4665
+ function unevalAllProvablyLocal (root, only) {
4208
4666
  const seen = new Set()
4209
4667
  const stack = [root]
4210
4668
  while (stack.length) {
@@ -4212,7 +4670,7 @@ function unevalAllProvablyLocal (root) {
4212
4670
  if (n === null || typeof n !== 'object' || seen.has(n)) continue
4213
4671
  seen.add(n)
4214
4672
  if (!Array.isArray(n)) {
4215
- if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
4673
+ if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
4216
4674
  if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
4217
4675
  }
4218
4676
  for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
@@ -4269,7 +4727,7 @@ function genCharCodeSwitch(keys, v) {
4269
4727
  // Valid path is still fast — only error path does extra work.
4270
4728
  function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
4271
4729
  const inputSchema = schema
4272
- schema = normalizePropertyNames(schema)
4730
+ schema = prepareForCodegen(schema, schemaMap)
4273
4731
  // unevaluated* is generated only where it is provably its plain counterpart
4274
4732
  // (see unevalLocalOk); any occurrence that is not keeps the whole schema on
4275
4733
  // the interpreted engine's error path, exactly as before.
@@ -4502,7 +4960,14 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4502
4960
  const subLines = []
4503
4961
  const subSp = branchSp + '/' + i
4504
4962
  if (typeof sub === 'object' && sub !== null) {
4505
- 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
+ }
4506
4971
  } else if (sub === false) {
4507
4972
  // A false branch never matches; it reports the same error the
4508
4973
  // interpreter attaches to a false schema.
@@ -4526,8 +4991,19 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4526
4991
  const pp = pathExpr || '""'
4527
4992
  ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
4528
4993
  const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
4529
- const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
4530
- 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}}`
4531
5007
  if (keyword === 'anyOf') {
4532
5008
  // anyOf reports nothing once a branch passes, so the branches are first
4533
5009
  // only asked whether they pass. They are called with `_all` set to 0, a
@@ -4541,12 +5017,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4541
5017
  // no one read. Only when no branch passes do they run again with the
4542
5018
  // caller's `_all`, and the collapse sees them all, as before.
4543
5019
  const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
4544
- 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}}}`)
4545
5021
  } else {
4546
5022
  // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
4547
5023
  // the passing branches and answers; the branch errors of a probe are not
4548
5024
  // errors anyone reads and are not collapsed.
4549
- 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}}}`)
4550
5026
  }
4551
5027
  }
4552
5028
 
@@ -4574,6 +5050,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4574
5050
  }
4575
5051
  // $ref — resolve local and cross-schema refs
4576
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
4577
5060
  // A self-reference cannot be expressed here yet: the root helper takes no
4578
5061
  // path argument, so errors raised through it would carry the path of the
4579
5062
  // root rather than of the value that failed. This used to fall through as
@@ -4674,7 +5157,9 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4674
5157
  }
4675
5158
  })
4676
5159
  const expected = types.join(', ')
4677
- 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)
4678
5163
  {
4679
5164
  const typeSp = `${schemaPrefix}/type`
4680
5165
  const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
@@ -4705,7 +5190,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4705
5190
  if (cv === null || typeof cv !== 'object') {
4706
5191
  lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
4707
5192
  } else {
4708
- 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'")}}`)
4709
5194
  }
4710
5195
  }
4711
5196
 
@@ -4875,6 +5360,11 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4875
5360
  // to emit. The error keeps the keyword's own identity: the interpreted
4876
5361
  // engine reports these as unevaluatedProperties with the key named, and the
4877
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
4878
5368
  if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
4879
5369
 
4880
5370
  // unevaluatedItems: false under the same rule. The interpreted engine
@@ -4985,18 +5475,10 @@ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4985
5475
 
4986
5476
  function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
4987
5477
  const si = ctx.varCounter++
4988
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4989
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4990
- const maxItems = schema.maxItems
4991
5478
  const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
4992
- let inner
4993
- if (isPrim && maxItems && maxItems <= 16) {
4994
- 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}}}`
4995
- } else if (isPrim) {
4996
- 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)}`
4997
- } else {
4998
- 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)}`
4999
- }
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)}}`
5000
5482
  lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
5001
5483
  }
5002
5484
 
@@ -5211,29 +5693,80 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5211
5693
  // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
5212
5694
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
5213
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
+
5214
5740
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
5215
5741
  const inputSchema = schema
5216
- schema = normalizePropertyNames(schema)
5742
+ schema = prepareForCodegen(schema, schemaMap)
5217
5743
  // Same provably-local rule as the error generator above.
5218
5744
  if (typeof schema === 'object' && schema !== null) {
5219
5745
  const s = JSON.stringify(schema)
5220
- 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
5221
5749
  // Bail on self-referencing schemas — combined codegen doesn't support recursion
5222
5750
  if (s.includes('"$ref":"#"')) return null
5223
5751
  // Defs on a $ref cycle compile as named functions in the boolean and
5224
5752
  // errors generators; this generator has no preamble for them, so it
5225
5753
  // declines and the router uses the errors generator.
5226
5754
  if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
5227
- // Bail on additionalProperties as a schema — combined path doesn't emit the
5228
- // per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
5229
- if (hasAdditionalPropertiesSchema(schema)) return null
5230
5755
  // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
5231
5756
  // Letting genCodeC emit the legacy "must match exactly one" placeholder
5232
5757
  // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
5233
5758
  // fast path plus errFn fallback handles these schemas with collapse.
5234
- 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.
5235
5765
  }
5236
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
5237
5770
  return schema
5238
5771
  ? () => VALID_RESULT
5239
5772
  : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
@@ -5292,7 +5825,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5292
5825
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5293
5826
 
5294
5827
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
5295
- 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],
5296
5829
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5297
5830
  const lines = []
5298
5831
  try {
@@ -5309,14 +5842,49 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5309
5842
  // helper compiles a pattern through it, which the boolean generator does for
5310
5843
  // subschemas under not/if/contains; without the binding those threw a
5311
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
+ }
5312
5849
  const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
5313
5850
  const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
5314
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
+ }
5315
5868
  const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5316
5869
  // Lazy error array: no allocation for valid data (the common case)
5317
- const inner = `let _e;\n ` +
5318
- lines.join('\n ') +
5319
- `\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`
5320
5888
 
5321
5889
  try {
5322
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 + ' ===')
@@ -5332,6 +5900,34 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5332
5900
  // Combined code generator: type-aware like genCode, error-collecting like genCodeE.
5333
5901
  // After type check passes → use optimizations (destructuring, no guards).
5334
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
+
5335
5931
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5336
5932
  return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
5337
5933
  }
@@ -5342,7 +5938,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5342
5938
  if (schema === false) {
5343
5939
  // Leading semicolon: the previous generated line may end in an
5344
5940
  // expression, and a line starting with `(` would be parsed as a call.
5345
- 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'}`))
5346
5942
  return
5347
5943
  }
5348
5944
  if (schema === true) return
@@ -5350,6 +5946,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5350
5946
 
5351
5947
  // $ref — resolve local, anchor, and cross-schema refs
5352
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
5353
5955
  // Self-reference "#" — no-op (permissive) to avoid infinite recursion
5354
5956
  if (schema.$ref === '#') return
5355
5957
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
@@ -5403,7 +6005,26 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5403
6005
  const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5404
6006
  const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
5405
6007
  const sp = schemaPrefix + '/' + schemaSuffix
5406
- 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('+')) {
5407
6028
  // Try to evaluate paramsCode as a static constant
5408
6029
  let paramsVal
5409
6030
  try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
@@ -5430,7 +6051,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5430
6051
  const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
5431
6052
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
5432
6053
  if (o !== null) lit._o = o
5433
- ctx.closureVals.push(Object.freeze(lit))
6054
+ ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
5434
6055
  return `(_e||(_e=[])).push(${errVar})`
5435
6056
  }
5436
6057
  }
@@ -5455,7 +6076,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5455
6076
  if (cv === null || typeof cv !== 'object') {
5456
6077
  lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5457
6078
  } else {
5458
- 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'")}}`)
5459
6080
  }
5460
6081
  }
5461
6082
  }
@@ -5474,7 +6095,9 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5474
6095
  }
5475
6096
  })
5476
6097
  const expected = types.join(', ')
5477
- 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)
5478
6101
  // Type check: push error but continue — wrap remaining in type-success block
5479
6102
  const typeOk = `_tok${ctx.varCounter++}`
5480
6103
  lines.push(`const ${typeOk}=${conds.join('||')}`)
@@ -5487,7 +6110,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5487
6110
  isNum = types[0] === 'number' || types[0] === 'integer'
5488
6111
  }
5489
6112
  emitEnumConst()
5490
- 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}){`)
5491
6118
  } else {
5492
6119
  emitEnumConst()
5493
6120
  }
@@ -5514,29 +6141,29 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5514
6141
  }
5515
6142
  for (const key of schema.required) {
5516
6143
  const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
5517
- if (isStaticPath) {
6144
+ if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
5518
6145
  const ei = ctx.varCounter++
5519
6146
  const errVar = `_E${ei}`
5520
6147
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5521
6148
  ctx.closureVars.push(errVar)
5522
- 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))
5523
6150
  lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
5524
6151
  } else {
5525
- 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)}'"}`)}}`)
5526
6153
  }
5527
6154
  }
5528
6155
  } else if (schema.required) {
5529
6156
  for (const key of schema.required) {
5530
6157
  const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5531
- if (isStatic) {
6158
+ if (isStatic && !ctx.rich && !ctx.noOrdinal) {
5532
6159
  const ei = ctx.varCounter++
5533
6160
  const errVar = `_E${ei}`
5534
6161
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5535
6162
  ctx.closureVars.push(errVar)
5536
- 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))
5537
6164
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
5538
6165
  } else {
5539
- 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)}'"}`)}}`)
5540
6167
  }
5541
6168
  }
5542
6169
  }
@@ -5659,7 +6286,13 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5659
6286
  // unevaluatedProperties: false where it is provably additionalProperties
5660
6287
  // (the entry bail guaranteed that), keeping the keyword's own identity so
5661
6288
  // the two engines report the same error.
5662
- 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
+ }
5663
6296
 
5664
6297
  // unevaluatedItems: false under the same rule: one error for the array,
5665
6298
  // the first uncovered position as the limit. Adjacent schema-form `items`
@@ -5671,15 +6304,15 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5671
6304
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
5672
6305
  for (const dep of deps) {
5673
6306
  const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5674
- if (isStatic) {
6307
+ if (isStatic && !ctx.rich && !ctx.noOrdinal) {
5675
6308
  const ei = ctx.varCounter++
5676
6309
  const errVar = `_E${ei}`
5677
6310
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5678
6311
  ctx.closureVars.push(errVar)
5679
- 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))
5680
6313
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
5681
6314
  } else {
5682
- 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)}'"}`)}}`)
5683
6316
  }
5684
6317
  }
5685
6318
  }
@@ -5712,6 +6345,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5712
6345
  if (schema.patternProperties) {
5713
6346
  if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
5714
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
+
5715
6352
 
5716
6353
  // dependentSchemas
5717
6354
  if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
@@ -5741,11 +6378,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5741
6378
  // allOf
5742
6379
  if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
5743
6380
 
5744
- // anyOf
5745
- if (schema.anyOf) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5746
-
5747
- // oneOf
5748
- 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)
5749
6386
 
5750
6387
  // not
5751
6388
  if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
@@ -5756,7 +6393,22 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5756
6393
  // Close type-success block if opened
5757
6394
  if (types) {
5758
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
+ }
5759
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
+
5760
6412
  }
5761
6413
 
5762
6414
  // Rarely used keyword families of genCodeCNode, kept out of it so their compile is
@@ -5782,20 +6434,158 @@ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5782
6434
 
5783
6435
  function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
5784
6436
  const si = ctx.varCounter++
5785
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
5786
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
5787
- const maxItems = schema.maxItems
5788
6437
  const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
5789
- let inner
5790
- if (isPrim && maxItems && maxItems <= 16) {
5791
- // Small primitive arrays: O(n²) nested loop, zero allocation
5792
- 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}}}`
5793
- } else if (isPrim) {
5794
- 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 }`)
5795
6461
  } else {
5796
- 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
5797
6463
  }
5798
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
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})`)
6516
+ }
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
5799
6589
  }
5800
6590
 
5801
6591
  function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
@@ -5825,6 +6615,41 @@ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix
5825
6615
  }
5826
6616
  }
5827
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
+
5828
6653
  function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
5829
6654
  let handledPropertyNames = false
5830
6655
  const ppEntries = Object.entries(schema.patternProperties)
@@ -5943,7 +6768,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5943
6768
  function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5944
6769
  const ki = ctx.varCounter++
5945
6770
  const p = pathExpr || '""'
5946
- 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'}`)}}}`)
5947
6772
  }
5948
6773
 
5949
6774
  function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
@@ -5997,12 +6822,18 @@ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
5997
6822
  }
5998
6823
 
5999
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)
6000
6828
  const fi = ctx.varCounter++
6001
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}` })
6002
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'")}}}`)
6003
6831
  }
6004
6832
 
6005
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)
6006
6837
  const fi = ctx.varCounter++
6007
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}` })
6008
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'")}}}`)
@@ -6162,3 +6993,5 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
6162
6993
  }
6163
6994
 
6164
6995
  module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES, _setCodeBudget }
6996
+ // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
6997
+ Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })