ata-validator 1.41.0 → 1.43.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) {
@@ -144,7 +176,24 @@ const { compileSafe, patternIsSafe } = require('./safe-regex')
144
176
  let _nativeIsLinear = null
145
177
  // The linearity check only parses; it runs first so a pattern bound for the
146
178
  // RegExp is never compiled for the engine just to be told it could have been.
179
+ // Unicode property escapes need a real RegExp in unicode mode, as the
180
+ // interpreter compiles them (Interpreter#pattern): the linear engine does not
181
+ // know them, and without the flag `\p{L}` reads as the letters p, {, L, }.
182
+ // The flag only where the pattern is valid with it, again as the interpreter
183
+ // does. These patterns used to send the whole schema to the interpreter.
184
+ const PROP_ESCAPE = /\\[pP]\{/
185
+ function reFlags (src) {
186
+ if (!PROP_ESCAPE.test(src)) return ''
187
+ try { new RegExp(src, 'u'); return 'u' } catch { return '' }
188
+ }
189
+ // The flag argument for generated `new RegExp(<json literal>)` source.
190
+ function reFlagArg (jsonLit) {
191
+ let raw
192
+ try { raw = JSON.parse(jsonLit) } catch { return '' }
193
+ return typeof raw === 'string' && reFlags(raw) ? ",'u'" : ''
194
+ }
147
195
  const useSafeEngine = (src) => {
196
+ if (PROP_ESCAPE.test(src)) return false
148
197
  if (_nativeIsLinear === null) _nativeIsLinear = require('./regex-linear').nativeIsLinear
149
198
  if (_nativeIsLinear(src)) return false
150
199
  return patternIsSafe(src)
@@ -155,7 +204,7 @@ const useSafeEngine = (src) => {
155
204
  // standalone embeds the engine), else a plain RegExp. Both expose `.test`.
156
205
  function safeReClosure (ctx, src) {
157
206
  if (useSafeEngine(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
158
- return new RegExp(src)
207
+ return new RegExp(src, reFlags(src))
159
208
  }
160
209
 
161
210
  // Compile a JSON Schema into a pure JS validator function.
@@ -196,7 +245,7 @@ function literalValue (lit) {
196
245
  }
197
246
 
198
247
  function unescapeSp (sp) {
199
- return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
248
+ return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\u([0-9a-fA-F]{4})|\\(.)/g, (m, h, c) => (h !== undefined ? String.fromCharCode(parseInt(h, 16)) : c))
200
249
  }
201
250
 
202
251
  const FRAME_INLINE_MAX = 120
@@ -306,6 +355,7 @@ function compileToJS(schema, defs, schemaMap) {
306
355
  // A $ref cycle on one value recurses here as it does in generated code; see
307
356
  // hasSameInstanceRefCycle.
308
357
  if (!defs && hasSameInstanceRefCycle(schema)) return null
358
+ if (!defs && bothDefsContainers(schema)) return null
309
359
 
310
360
  // unevaluatedProperties / unevaluatedItems need annotation tracking this
311
361
  // path does not do. Decline so the schema reaches an engine that does.
@@ -557,7 +607,7 @@ function compileToJS(schema, defs, schemaMap) {
557
607
  }
558
608
  if (schema.pattern) {
559
609
  try {
560
- const re = useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern)
610
+ const re = useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern))
561
611
  checks.push((d) => typeof d !== 'string' || re.test(d))
562
612
  } catch {
563
613
  return null
@@ -1390,6 +1440,25 @@ function unevalPropsModeled(schema, schemaMap) {
1390
1440
  return true
1391
1441
  }
1392
1442
 
1443
+ // Every `$ref` string under a node, once per node.
1444
+ const _refStrings = new WeakMap()
1445
+ function refStringsOf(node) {
1446
+ let set = _refStrings.get(node)
1447
+ if (set !== undefined) return set
1448
+ set = new Set()
1449
+ const seen = new Set()
1450
+ const walk = (n) => {
1451
+ if (n === null || typeof n !== 'object' || seen.has(n)) return
1452
+ seen.add(n)
1453
+ if (Array.isArray(n)) { for (const x of n) walk(x); return }
1454
+ if (typeof n.$ref === 'string') set.add(n.$ref)
1455
+ for (const k of Object.keys(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(n[k])
1456
+ }
1457
+ walk(node)
1458
+ _refStrings.set(node, set)
1459
+ return set
1460
+ }
1461
+
1393
1462
  function codegenSafe(schema, schemaMap) {
1394
1463
  if (typeof schema === 'boolean') return true
1395
1464
  if (typeof schema !== 'object' || schema === null) return true
@@ -1430,7 +1499,6 @@ function codegenSafe(schema, schemaMap) {
1430
1499
  }
1431
1500
 
1432
1501
  // Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
1433
- if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
1434
1502
 
1435
1503
  // $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
1436
1504
  if (schema.$ref) {
@@ -1465,17 +1533,11 @@ function codegenSafe(schema, schemaMap) {
1465
1533
  !resolvedTarget.propertyNames
1466
1534
  if (!targetSimple && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
1467
1535
  }
1468
- // In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
1469
- // (unevaluated schemas need $ref + siblings to work properly): without one, the
1470
- // generators emit the $ref and skip the rest of the node, so a sibling that
1471
- // validates would be dropped. Schema-organization keywords and annotations
1472
- // validate nothing and do not count. Annotations are listed rather than
1473
- // inferred from what is not a validation keyword, so a keyword this list
1474
- // does not know still declines instead of being skipped. Measured on the
1475
- // 977 SchemaStore schemas: a $ref with a description or title beside it sent
1476
- // 223 of them to the interpreted engine.
1477
- const siblings = Object.keys(schema).filter(k => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
1478
- if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
1536
+ // In Draft 2020-12, $ref with siblings applies both. The verdict
1537
+ // generator compiles the siblings after the reference (genCodeNode), the
1538
+ // combined one too (genCodeCNode), and the error generator declines such
1539
+ // a node, so a sibling is never dropped. This used to decline the whole
1540
+ // schema unless an unevaluated keyword was beside the reference.
1479
1541
  }
1480
1542
 
1481
1543
  // additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
@@ -1505,8 +1567,9 @@ function codegenSafe(schema, schemaMap) {
1505
1567
  // that only the interpreted engine provides.
1506
1568
  if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
1507
1569
  const evalResult = collectEvaluated(schema, schemaMap)
1508
- if (evalResult.dynamic && evalResult.allProps) return false
1509
- if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
1570
+ // What the static model cannot represent is built at run time
1571
+ // (genUnevalDynamicVerdict); shapes that cannot be (dynamic references,
1572
+ // reference cycles) decline there.
1510
1573
  }
1511
1574
  }
1512
1575
  // unevaluatedItems: allow boolean and schema values
@@ -1527,7 +1590,12 @@ function codegenSafe(schema, schemaMap) {
1527
1590
  // Defs on a $ref cycle compile as named functions (see genCode); nothing
1528
1591
  // to decline here. cyclicDefNames() is the walk that used to bail.
1529
1592
  for (const [name, def] of Object.entries(defs)) {
1530
- if (/[~/"']/.test(name)) return false // special chars in def name
1593
+ // A name the generators would have to spell inside a path or an
1594
+ // identifier. Only a problem when a reference names it: tsconfig.json
1595
+ // keeps a comment object under the definition `//`, which nothing
1596
+ // references, and declined whole for it. A definition reached through
1597
+ // a deeper pointer was renamed by normalizeDeepRefs.
1598
+ if (/[~/"']/.test(name) && refStringsOf(schema).has('#/' + (schema.$defs === defs ? '$defs' : 'definitions') + '/' + name.replace(/~/g, '~0').replace(/\//g, '~1'))) return false
1531
1599
  if (typeof def === 'boolean') return false
1532
1600
  if (typeof def === 'object' && def !== null) {
1533
1601
  // A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
@@ -1551,7 +1619,6 @@ function codegenSafe(schema, schemaMap) {
1551
1619
  // compiled to code that ignored it.
1552
1620
  if (schema.patternProperties) {
1553
1621
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
1554
- if (/\\[pP]\{/.test(pat)) return false
1555
1622
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
1556
1623
  }
1557
1624
  }
@@ -1587,37 +1654,6 @@ function codegenSafe(schema, schemaMap) {
1587
1654
  return true
1588
1655
  }
1589
1656
 
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
1657
  // Schema-bearing keywords, so the walk below never mistakes a property named
1622
1658
  // "$ref" for an actual reference.
1623
1659
  const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
@@ -1921,8 +1957,20 @@ function hasSameInstanceRefCycle(root) {
1921
1957
  return found
1922
1958
  }
1923
1959
 
1960
+ // The generators look a definition up by name in one table, `$defs` or else
1961
+ // `definitions`, whichever container the pointer named. With both at the root,
1962
+ // `#/definitions/x` read `$defs/x`: a draft 7 schema carrying both, with
1963
+ // different schemas under one name, rejected what it allowed and accepted
1964
+ // what it rejected. The interpreter resolves the pointer as written, so a
1965
+ // schema with both goes there. None of SchemaStore's 980 has both.
1966
+ function bothDefsContainers(schema) {
1967
+ return schema.$defs !== null && typeof schema.$defs === 'object' &&
1968
+ schema.definitions !== null && typeof schema.definitions === 'object'
1969
+ }
1970
+
1924
1971
  function sharedCodegenGate(schema, schemaMap) {
1925
1972
  if (typeof schema !== 'object' || schema === null) return true
1973
+ if (bothDefsContainers(schema)) return false
1926
1974
  if (hasSameInstanceRefCycle(schema)) return false
1927
1975
  if (!codegenSafe(schema, schemaMap)) return false
1928
1976
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
@@ -2020,9 +2068,248 @@ function normalizePropertyNamesWalk(root) {
2020
2068
  return walk(root)
2021
2069
  }
2022
2070
 
2071
+ // A local `$ref` that is a JSON pointer to anything other than a top-level
2072
+ // definition (`#/definitions/item/properties/id`, `#/properties/foo`,
2073
+ // `#/$defs/a/items`) became the generators' own definition reference: the
2074
+ // pointer is resolved here, at compile time, and its target added as a
2075
+ // definition under a name of its own, so every path that already handles
2076
+ // `#/$defs/name` handles it, cycles included. The generators knew only the
2077
+ // top-level form and declined the rest, which kept 40 of 980 SchemaStore
2078
+ // schemas, tsconfig.json among them, on the interpreted engine for every
2079
+ // document. Left alone, as before: a pointer that does not resolve to a
2080
+ // schema, one that passes through a subschema with its own `$id` (another
2081
+ // base, whose pointers mean something else), and every `$ref` inside such a
2082
+ // subschema. Copy on write; errors keep their paths, which name the place of
2083
+ // the reference, not of its target, and are ordered against the schema as
2084
+ // the caller wrote it.
2085
+ const DEF_REF = /^#\/(?:\$defs|definitions)\/[^/]+$/
2086
+ const _deepRefsNormalized = new WeakMap()
2087
+ function normalizeDeepRefs(root) {
2088
+ if (!root || typeof root !== 'object' || Array.isArray(root)) return root
2089
+ const hit = _deepRefsNormalized.get(root)
2090
+ if (hit !== undefined) return hit
2091
+ const out = normalizeDeepRefsWalk(root)
2092
+ _deepRefsNormalized.set(root, out)
2093
+ return out
2094
+ }
2095
+ function resolveLocalPointer(root, ref) {
2096
+ let node = root
2097
+ const segs = ref.slice(2).split('/')
2098
+ for (let i = 0; i < segs.length; i++) {
2099
+ let seg = segs[i].replace(/~1/g, '/').replace(/~0/g, '~')
2100
+ // Draft 7 schemas reach the generators with `definitions` renamed to
2101
+ // `$defs` (lib/draft7.js) and their references as written, which the
2102
+ // top-level form already reads either way; so does this.
2103
+ if (i === 0 && (seg === 'definitions' || seg === '$defs') && !Object.prototype.hasOwnProperty.call(node, seg)) seg = seg === 'definitions' ? '$defs' : 'definitions'
2104
+ if (node === null || typeof node !== 'object') return undefined
2105
+ if (Array.isArray(node)) {
2106
+ if (!/^(?:0|[1-9]\d*)$/.test(seg) || +seg >= node.length) return undefined
2107
+ node = node[+seg]
2108
+ } else {
2109
+ if (!Object.prototype.hasOwnProperty.call(node, seg)) return undefined
2110
+ node = node[seg]
2111
+ }
2112
+ if (node !== null && typeof node === 'object' && !Array.isArray(node) && typeof node.$id === 'string') return undefined
2113
+ }
2114
+ if (typeof node === 'boolean') return node
2115
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) return undefined
2116
+ return node
2117
+ }
2118
+ function normalizeDeepRefsWalk(root) {
2119
+ // First pass: the deep pointers used outside any nested `$id`, and their
2120
+ // targets.
2121
+ const targets = new Map()
2122
+ const scan = (node, nested, seen) => {
2123
+ if (node === null || typeof node !== 'object') return
2124
+ if (seen.has(node)) return
2125
+ seen.add(node)
2126
+ if (Array.isArray(node)) { for (const n of node) scan(n, nested, seen); return }
2127
+ const inner = nested || (node !== root && typeof node.$id === 'string')
2128
+ if (!inner && typeof node.$ref === 'string' && node.$ref.startsWith('#/') && !DEF_REF.test(node.$ref) && !targets.has(node.$ref)) {
2129
+ const t = resolveLocalPointer(root, node.$ref)
2130
+ if (t !== undefined) targets.set(node.$ref, t)
2131
+ }
2132
+ for (const k of Object.keys(node)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') scan(node[k], inner, seen)
2133
+ }
2134
+ scan(root, false, new Set())
2135
+ if (targets.size === 0) return root
2136
+ const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
2137
+ const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
2138
+ const names = new Map()
2139
+ let n = 0
2140
+ for (const ref of targets.keys()) {
2141
+ let name
2142
+ do { name = '__ata_ptr_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
2143
+ names.set(ref, name)
2144
+ }
2145
+ // Second pass: the references rewritten, copy on write, sharing one copy
2146
+ // per node so a target reached twice stays one object.
2147
+ const memo = new Map()
2148
+ const walk = (node, nested) => {
2149
+ if (node === null || typeof node !== 'object') return node
2150
+ const key = nested ? null : node
2151
+ if (key !== null && memo.has(key)) return memo.get(key)
2152
+ if (Array.isArray(node)) {
2153
+ let out = node
2154
+ if (key !== null) memo.set(key, out)
2155
+ for (let i = 0; i < node.length; i++) {
2156
+ const w = walk(node[i], nested)
2157
+ if (w !== node[i]) { if (out === node) { out = node.slice(); if (key !== null) memo.set(key, out) } out[i] = w }
2158
+ }
2159
+ return out
2160
+ }
2161
+ const inner = nested || (node !== root && typeof node.$id === 'string')
2162
+ if (inner) return node
2163
+ let out = node
2164
+ memo.set(node, out)
2165
+ const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
2166
+ if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
2167
+ for (const k of Object.keys(node)) {
2168
+ if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
2169
+ const v = node[k]
2170
+ if (v === null || typeof v !== 'object') continue
2171
+ const w = walk(v, false)
2172
+ if (w !== v) own(k, w)
2173
+ }
2174
+ return out
2175
+ }
2176
+ const rewritten = walk(root, false)
2177
+ const out = rewritten === root ? { ...root } : rewritten
2178
+ const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
2179
+ for (const [ref, name] of names) {
2180
+ const t = targets.get(ref)
2181
+ defs[name] = typeof t === 'boolean' ? t : walk(t, false)
2182
+ }
2183
+ out[defsKey] = defs
2184
+ return out
2185
+ }
2186
+
2187
+ // Every reference the interpreted engine can resolve becomes a definition
2188
+ // reference the generators handle: URNs, `$id`-relative and absolute URIs,
2189
+ // anchors, pointers into a resource with its own `$id`, names with characters
2190
+ // the generators cannot spell, documents registered through `schemas`. The
2191
+ // target of each is found by the interpreter's own resolver (indexSchemas and
2192
+ // resolveRef in lib/interpreter.js), so the two engines cannot resolve a
2193
+ // reference differently, and is added under a name of its own as with
2194
+ // normalizeDeepRefs. Once every reference names a definition, nested `$id`s
2195
+ // no longer change what anything means and are dropped from the copy, which
2196
+ // is what kept those schemas from compiling. Left to normalizeDeepRefs, as
2197
+ // before: a schema with dynamic references (their meaning depends on the
2198
+ // resources at run time), one with an embedded resource in another dialect,
2199
+ // and one with any reference the resolver cannot resolve.
2200
+ const _refsNormalized = new WeakMap()
2201
+ function normalizeRefs(root, schemaMap) {
2202
+ if (!root || typeof root !== 'object' || Array.isArray(root)) return root
2203
+ let byMap = _refsNormalized.get(root)
2204
+ const mapKey = schemaMap || _refsNormalized
2205
+ if (byMap !== undefined && byMap.has(mapKey)) return byMap.get(mapKey)
2206
+ let out
2207
+ try { out = normalizeRefsWalk(root, schemaMap) } catch { out = null }
2208
+ if (out === null) out = normalizeDeepRefs(root)
2209
+ if (byMap === undefined) { byMap = new Map(); _refsNormalized.set(root, byMap) }
2210
+ byMap.set(mapKey, out)
2211
+ return out
2212
+ }
2213
+ const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
2214
+ function normalizeRefsWalk(root, schemaMap) {
2215
+ const text = JSON.stringify(root)
2216
+ if (!text.includes('"$ref"')) return root
2217
+ if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
2218
+ const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
2219
+ const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
2220
+ const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
2221
+ // Resolve every reference reachable from the root, following into targets
2222
+ // in other documents.
2223
+ const names = new Map() // target node -> synthetic name
2224
+ const refTarget = new Map() // reference site node -> target node
2225
+ const visited = new Set()
2226
+ let fail = false
2227
+ const visit = (node) => {
2228
+ if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
2229
+ visited.add(node)
2230
+ if (Array.isArray(node)) { for (const n of node) visit(n); return }
2231
+ if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
2232
+ // A dynamic reference reached through another document means the same as
2233
+ // one in the root: its target depends on the resources at run time.
2234
+ if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined || node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
2235
+ if (typeof node.$ref === 'string' && state.nodeBase.has(node)) {
2236
+ const base = state.nodeBase.get(node)
2237
+ const ref = node.$ref
2238
+ const local = base === state.rootBase && (ref === '#' || (DEF_REF.test(ref) && !/[~%]/.test(ref)))
2239
+ if (!local) {
2240
+ const { node: t } = resolveRef(ref, base, state)
2241
+ if (t === undefined || (typeof t !== 'boolean' && (t === null || typeof t !== 'object' || Array.isArray(t)))) { fail = true; return }
2242
+ refTarget.set(node, t)
2243
+ // The root, however it is spelled, is `#`: a copy of it under its own
2244
+ // definitions would contain itself.
2245
+ if (t === root) { /* named '#' below */ }
2246
+ else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
2247
+ if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
2248
+ }
2249
+ }
2250
+ for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
2251
+ }
2252
+ visit(root)
2253
+ if (fail) return null
2254
+ if (refTarget.size === 0) return root
2255
+ const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
2256
+ const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
2257
+ let n = 0
2258
+ for (const t of names.keys()) {
2259
+ let name
2260
+ do { name = '__ata_ref_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
2261
+ names.set(t, name)
2262
+ }
2263
+ // The copy: references renamed, nested `$id`s dropped.
2264
+ const memo = new Map()
2265
+ const walk = (node) => {
2266
+ if (node === null || typeof node !== 'object') return node
2267
+ if (memo.has(node)) return memo.get(node)
2268
+ if (Array.isArray(node)) {
2269
+ const out = []
2270
+ memo.set(node, out)
2271
+ for (const x of node) out.push(walk(x))
2272
+ return out
2273
+ }
2274
+ const out = {}
2275
+ memo.set(node, out)
2276
+ for (const k of Object.keys(node)) {
2277
+ if (k === '$id' && node !== root) continue
2278
+ if (k === '$ref' && refTarget.has(node)) {
2279
+ const t = refTarget.get(node)
2280
+ out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
2281
+ continue
2282
+ }
2283
+ out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
2284
+ }
2285
+ return out
2286
+ }
2287
+ const out = walk(root)
2288
+ const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
2289
+ for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
2290
+ out[defsKey] = defs
2291
+ return out
2292
+ }
2293
+
2294
+ // What the three generators compile from: the schema with deep local pointers
2295
+ // turned into definition references, then propertyNames normalized. The same
2296
+ // for all three, so they decline and agree on the same schemas.
2297
+ function prepareForCodegen(schema, schemaMap) {
2298
+ return normalizePropertyNames(normalizeRefs(schema, schemaMap))
2299
+ }
2300
+
2301
+ const VERDICT_SPLIT = 64 * 1024
2302
+ function sourceSize (lines, ctx) {
2303
+ let n = 0
2304
+ for (const l of lines) n += l.length
2305
+ for (const l of ctx.preamble) n += l.length
2306
+ if (ctx.deferredChecks) for (const l of ctx.deferredChecks) n += l.length
2307
+ return n
2308
+ }
2309
+
2023
2310
  function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2024
2311
  const inputSchema = schema
2025
- schema = normalizePropertyNames(schema)
2312
+ schema = prepareForCodegen(schema, schemaMap)
2026
2313
  if (typeof schema === 'boolean') return schema ? () => true : () => false
2027
2314
  if (typeof schema !== 'object' || schema === null) return null
2028
2315
  // removeAdditional: a verdict function that deletes the keys the remover
@@ -2044,7 +2331,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2044
2331
  // patternProperties: bail only on boolean sub-schemas or unicode property escapes
2045
2332
  if (schema.patternProperties) {
2046
2333
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
2047
- if (/\\[pP]\{/.test(pat)) return null
2048
2334
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
2049
2335
  }
2050
2336
  }
@@ -2093,20 +2379,28 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2093
2379
  if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
2094
2380
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
2095
2381
 
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,
2097
- keywords: (opts && opts.keywords) || null, kwEmitted: null }
2098
- if (ctx.keywords) {
2099
- // Macro keywords expand into a schema of their own, which this generator
2100
- // does not apply; such a schema stays on the interpreted engine.
2101
- if (usesMacroKeyword(schema, schemaMap, ctx.keywords)) return null
2102
- ctx.kwEmitted = new Set()
2103
- }
2104
- const lines = []
2105
- try {
2106
- genCode(schema, 'd', lines, ctx)
2107
- } catch (e) {
2108
- if (e === DECLINE) return null
2109
- throw e
2382
+ // Macro keywords expand into a schema of their own, which this generator
2383
+ // does not apply; such a schema stays on the interpreted engine.
2384
+ if (opts && opts.keywords && usesMacroKeyword(schema, schemaMap, opts.keywords)) return null
2385
+ // Every reference is first expanded where it is used, which is the fastest
2386
+ // form for the checks. Past VERDICT_SPLIT of source the function is
2387
+ // generated again with each definition referenced from several places
2388
+ // written once, as a function in the preamble: expanded, SchemaStore's sarif
2389
+ // (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
2390
+ // half of the first validate() call.
2391
+ let ctx, lines
2392
+ for (const share of [false, true]) {
2393
+ 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,
2394
+ keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
2395
+ if (ctx.keywords) ctx.kwEmitted = new Set()
2396
+ lines = []
2397
+ try {
2398
+ genCode(schema, 'd', lines, ctx)
2399
+ } catch (e) {
2400
+ if (e === DECLINE) return null
2401
+ throw e
2402
+ }
2403
+ if (share || sourceSize(lines, ctx) <= VERDICT_SPLIT) break
2110
2404
  }
2111
2405
  // Every schema node that carries a custom keyword must have had its check
2112
2406
  // written. A path that inlined a node without going through genCodeNode
@@ -2140,10 +2434,10 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2140
2434
  closureValues.push(compileSafe(JSON.parse(safeMatch[2])))
2141
2435
  continue
2142
2436
  }
2143
- const match = code.match(/^const (_re\d+)=new RegExp\((.+)\)$/)
2437
+ const match = code.match(/^const (_re\d+)=new RegExp\((.+?)(?:,'(u)')?\)$/)
2144
2438
  if (match) {
2145
2439
  closureNames.push(match[1])
2146
- closureValues.push(new RegExp(JSON.parse(match[2])))
2440
+ closureValues.push(new RegExp(JSON.parse(match[2]), match[3] || ''))
2147
2441
  }
2148
2442
  }
2149
2443
 
@@ -2154,7 +2448,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2154
2448
  // are built when first asked for, not here: most validators never call
2155
2449
  // validateJSON or get extended, and rewriting and compiling the source twice
2156
2450
  // more on every schema was about a third of what a first call cost.
2157
- let body, hybridThunk, tailThunk
2451
+ let body, hybridThunk, tailThunk, resultThunk
2158
2452
  if (ctx.usesRecursion) {
2159
2453
  // Self-recursive: a named body reached through the cycle guard, so data
2160
2454
  // that points back at itself settles instead of exhausting the stack.
@@ -2164,6 +2458,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2164
2458
  body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
2165
2459
  // Hybrid: keep the boolean answer, wrap only the outer call
2166
2460
  hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
2461
+ resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
2167
2462
  tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
2168
2463
  } else if (defSets.length > 0) {
2169
2464
  // No self-reference at the root, but a def on a cycle can still be
@@ -2171,10 +2466,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2171
2466
  const run = emitGuardedRun(`return _body(d)`, '', defSets)
2172
2467
  body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
2173
2468
  hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
2469
+ resultThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
2174
2470
  tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
2175
2471
  } else {
2176
2472
  body = checkStr + '\n return true'
2177
2473
  hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
2474
+ resultThunk = () => replaceTopLevel(checkStr + '\n return true', `return ${RESULT_TRUE}`, `return ${RESULT_FALSE}`)
2178
2475
  // An early accept inside the checks has to reach the tail too, so every
2179
2476
  // top-level `return true` becomes the tail call, by the same rewrite the
2180
2477
  // hybrid above relies on and test_hybrid_agreement holds to the suite.
@@ -2214,6 +2511,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2214
2511
  return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
2215
2512
  }
2216
2513
 
2514
+ // validate() itself: the same checks returning the result objects, for a
2515
+ // validator past its first calls (see validator-core). One function per
2516
+ // schema means the result is built at an allocation site of its own and
2517
+ // nothing between the caller and the checks is shared with other schemas.
2518
+ // Shared wrappers are what made reading results slow across many schemas:
2519
+ // every validator's calls went through the same few call sites. The
2520
+ // rejection is the lazy one, built from the arguments.
2521
+ let resultFactory
2522
+ boolFn._resultFactory = (LR, B, W, EE) => {
2523
+ if (resultFactory === undefined) {
2524
+ try {
2525
+ resultFactory = new Function(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
2526
+ } catch {
2527
+ resultFactory = null
2528
+ }
2529
+ }
2530
+ return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
2531
+ }
2532
+
2217
2533
  // The same function with one more check where it would return true, for a
2218
2534
  // wrapper that enforces something the schema does not carry (see
2219
2535
  // Validator#_extendVerdict). Calling the check from here rather than from a
@@ -2291,40 +2607,75 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2291
2607
 
2292
2608
  // Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
2293
2609
  // Tracks function nesting depth to preserve nested function internals.
2610
+ const RESULT_TRUE = '{valid:true,data:d,errors:__ataEE}'
2611
+ const RESULT_FALSE = 'new __ataLR(__ataB,d,__ataW)'
2612
+ // The end of the string literal that starts at `i` (a quote character), or
2613
+ // the end of the code when it does not close.
2614
+ function skipString(code, i) {
2615
+ const q = code[i]
2616
+ let j = i + 1
2617
+ while (j < code.length) {
2618
+ const c = code[j]
2619
+ if (c === '\\') { j += 2; continue }
2620
+ if (c === q) return j + 1
2621
+ j++
2622
+ }
2623
+ return code.length
2624
+ }
2625
+
2294
2626
  function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
2295
2627
  let fnDepth = 0, result = '', i = 0
2296
2628
  while (i < code.length) {
2629
+ // String literals are copied as they are. A property called `function`
2630
+ // reads `d["function"]`, and the scan took the word for a nested function
2631
+ // and left every return after it alone, so the hybrid returned a bare
2632
+ // `false` where a result was due: validateJSON answered `false` instead
2633
+ // of a rejection for such schemas. Text such as `return false` or a brace
2634
+ // inside a string is not code either.
2635
+ const ch = code[i]
2636
+ if (ch === '"' || ch === "'" || ch === '`') {
2637
+ const j = skipString(code, i)
2638
+ result += code.slice(i, j)
2639
+ i = j
2640
+ continue
2641
+ }
2297
2642
  // A nested closure keeps its own returns. Both spellings count: a
2298
2643
  // `function` keyword and an arrow with a block body. The inline pattern
2299
2644
  // compiler emits `(()=>{...return false...return true})()` and rewriting
2300
2645
  // those returns turned the arrow's boolean into an object, which made
2301
2646
  // `!(cond && obj)` false and let the hybrid accept what the boolean
2302
2647
  // program rejected.
2303
- const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
2648
+ const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1])) && !/[a-zA-Z0-9_$]/.test(code[i + 8] || '')
2304
2649
  const isArrowBlock = code.startsWith('=>{', i)
2305
2650
  if (isFunctionKw || isArrowBlock) {
2306
- // Skip to the opening brace, then track all braces inside the body.
2651
+ // Skip to the opening brace, then track all braces inside the body,
2652
+ // strings skipped as above.
2307
2653
  let j = isArrowBlock ? i + 2 : i + 8
2308
2654
  while (j < code.length && code[j] !== '{') j++
2309
2655
  result += code.slice(i, j + 1)
2310
2656
  i = j + 1
2311
- // Track braces inside this function body
2312
2657
  let braceDepth = 1
2313
2658
  while (i < code.length && braceDepth > 0) {
2314
- if (code[i] === '{') braceDepth++
2315
- else if (code[i] === '}') braceDepth--
2316
- if (braceDepth > 0) result += code[i]
2317
- else result += '}' // closing brace of function
2659
+ const c = code[i]
2660
+ if (c === '"' || c === "'" || c === '`') {
2661
+ const k = skipString(code, i)
2662
+ result += code.slice(i, k)
2663
+ i = k
2664
+ continue
2665
+ }
2666
+ if (c === '{') braceDepth++
2667
+ else if (c === '}') braceDepth--
2668
+ result += c
2318
2669
  i++
2319
2670
  }
2320
- } else if (code.startsWith('return false', i)) {
2671
+ } else if (code.startsWith('return false', i) && (i + 12 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 12]))) {
2321
2672
  result += onFalse
2322
2673
  i += 12
2323
- } else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-Z_$]/.test(code[i + 11]))) {
2674
+ } else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 11]))) {
2324
2675
  result += onTrue
2325
2676
  i += 11
2326
2677
  } else {
2327
- result += code[i]
2678
+ result += ch
2328
2679
  i++
2329
2680
  }
2330
2681
  }
@@ -2600,7 +2951,9 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2600
2951
  // $ref — guard against circular references
2601
2952
  // In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
2602
2953
  // Only when THIS schema has unevaluated keywords directly (not via $ref target)
2603
- const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
2954
+ // Any keyword beside $ref that validates is compiled after the reference
2955
+ // (2020-12 applies both; draft 7 drops such siblings when it is read).
2956
+ const hasSiblings = !!schema.$ref && Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
2604
2957
  if (schema.$ref) {
2605
2958
  if (genRefNode(schema, v, lines, ctx, knownType, hasSiblings)) return
2606
2959
  }
@@ -2808,7 +3161,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2808
3161
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
2809
3162
  ctx.usesSafeRe = true
2810
3163
  } else {
2811
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
3164
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
2812
3165
  }
2813
3166
  }
2814
3167
  const ri = ctx.regExpMap.get(pattern);
@@ -3330,6 +3683,31 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3330
3683
  }
3331
3684
  lines.push(`if(!${fnName}(${v}))return false`)
3332
3685
  if (!hasSiblings) return true
3686
+ } else if (ctx.shareDefs && !ctx.refStack.has(schema.$ref) && (ctx.sharedDefs || (ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs))).has(defName)) {
3687
+ // A shared definition off any cycle, as a plain function in the
3688
+ // preamble (see compileToJSCodegen): no guard, since it cannot re-enter
3689
+ // itself. Generated over its own `d` like the cyclic ones above.
3690
+ if (!ctx.sharedDefFns) ctx.sharedDefFns = new Map()
3691
+ let fnName = ctx.sharedDefFns.get(defName)
3692
+ if (!fnName) {
3693
+ fnName = '_dv' + ctx.sharedDefFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
3694
+ ctx.sharedDefFns.set(defName, fnName)
3695
+ const bodyLines = []
3696
+ const outerDeferred = ctx.deferredChecks
3697
+ ctx.deferredChecks = null
3698
+ ctx.refStack.add(schema.$ref)
3699
+ try {
3700
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
3701
+ } finally {
3702
+ ctx.refStack.delete(schema.$ref)
3703
+ }
3704
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
3705
+ ctx.deferredChecks = outerDeferred
3706
+ if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
3707
+ hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
3708
+ }
3709
+ lines.push(`if(!${fnName}(${v}))return false`)
3710
+ if (!hasSiblings) return true
3333
3711
  } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
3334
3712
  else {
3335
3713
  ctx.refStack.add(schema.$ref)
@@ -3508,14 +3886,32 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3508
3886
  return handledPropertyNames
3509
3887
  }
3510
3888
 
3889
+ // Whether the static unevaluatedProperties model checks the anyOf itself, so
3890
+ // genCodeNode leaves it out. Not when the set is built at run time
3891
+ // (unevalBuiltAtRuntime): that form only collects, and dropping the anyOf
3892
+ // there accepted a document no branch matched.
3511
3893
  function unevalChecksAnyOf(schema, ctx) {
3512
3894
  if (schema.unevaluatedProperties !== false) return false
3513
3895
  const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3514
- return r.dynamic && !r.allProps
3896
+ return r.dynamic && !r.allProps && !unevalBuiltAtRuntime(schema, ctx, r)
3897
+ }
3898
+
3899
+ // The unevaluatedProperties forms the static model cannot represent; those
3900
+ // build the evaluated set at run time (genUnevalDynamicVerdict). Decided in
3901
+ // one place so genCodeNode and genUnevaluatedPropertiesNode cannot disagree
3902
+ // about which of them checks the node's anyOf.
3903
+ function unevalBuiltAtRuntime(schema, ctx, r) {
3904
+ if (schema.unevaluatedProperties === true || schema.unevaluatedProperties === undefined) return false
3905
+ if (r === undefined) r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3906
+ return r.dynamic && (r.allProps || !unevalPropsModeled(schema, ctx.schemaMap))
3515
3907
  }
3516
3908
 
3517
3909
  function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
3518
3910
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3911
+ if (unevalBuiltAtRuntime(schema, ctx, evalResult)) {
3912
+ genUnevalDynamicVerdict(schema, v, lines, ctx, isObj)
3913
+ return
3914
+ }
3519
3915
 
3520
3916
  if (evalResult.allProps || schema.unevaluatedProperties === true) {
3521
3917
  // All props evaluated or unevaluatedProperties:true — no-op
@@ -4086,7 +4482,11 @@ const FORMAT_CODEGEN = {
4086
4482
  function esc(s) { return JSON.stringify(s).slice(1, -1).replace(/'/g, "\\'") }
4087
4483
  // A schema pointer segment for a pattern: JSON Pointer escaping, then made
4088
4484
  // safe for the single-quoted literal the error emitters use.
4089
- function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'") }
4485
+ // A pointer segment as text for a single-quoted literal. Control characters
4486
+ // and the two line separators go in as \u escapes: written raw, a property
4487
+ // named "foo\nbar" broke the literal, the combined function failed to compile
4488
+ // and every schema with such a name lost it.
4489
+ function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/[\u0000-\u001f\u2028\u2029]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')) }
4090
4490
 
4091
4491
  // Resolve child path at codegen time when parent is a static string literal.
4092
4492
  // This enables frozen pre-allocation for ALL nested error objects.
@@ -4331,7 +4731,7 @@ function unevalLocalOk (node, key) {
4331
4731
  // generic and may visit data positions (an enum value, say); judging those is
4332
4732
  // harmless, since a data node that fails the test only makes the answer more
4333
4733
  // conservative and the generators never read data positions as schemas.
4334
- function unevalAllProvablyLocal (root) {
4734
+ function unevalAllProvablyLocal (root, only) {
4335
4735
  const seen = new Set()
4336
4736
  const stack = [root]
4337
4737
  while (stack.length) {
@@ -4339,7 +4739,7 @@ function unevalAllProvablyLocal (root) {
4339
4739
  if (n === null || typeof n !== 'object' || seen.has(n)) continue
4340
4740
  seen.add(n)
4341
4741
  if (!Array.isArray(n)) {
4342
- if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
4742
+ if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
4343
4743
  if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
4344
4744
  }
4345
4745
  for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
@@ -4396,7 +4796,7 @@ function genCharCodeSwitch(keys, v) {
4396
4796
  // Valid path is still fast — only error path does extra work.
4397
4797
  function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
4398
4798
  const inputSchema = schema
4399
- schema = normalizePropertyNames(schema)
4799
+ schema = prepareForCodegen(schema, schemaMap)
4400
4800
  // unevaluated* is generated only where it is provably its plain counterpart
4401
4801
  // (see unevalLocalOk); any occurrence that is not keeps the whole schema on
4402
4802
  // the interpreted engine's error path, exactly as before.
@@ -4413,7 +4813,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4413
4813
  if (!sharedCodegenGate(schema, schemaMap)) return null
4414
4814
  if (schema.patternProperties) {
4415
4815
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
4416
- if (/\\[pP]\{/.test(pat)) return null
4417
4816
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
4418
4817
  }
4419
4818
  }
@@ -4610,6 +5009,50 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4610
5009
  // Emit codegen for oneOf/anyOf that runs each branch with error collection,
4611
5010
  // aggregates results, and uses __ataCollapse to surface a single best-branch
4612
5011
  // error. Wraps the existing per-branch evaluation; does not replace it.
5012
+ const BRANCH_VERDICT_MAX = 4096
5013
+
5014
+ // What undoing a trial emission needs: during one, the context's arrays only
5015
+ // grow and its maps and sets only gain keys (a reference pushed onto refStack
5016
+ // by a declined branch is such a key), so their lengths and sizes are enough,
5017
+ // and what was added is cut off the end, in insertion order. Copying them
5018
+ // instead made the trial quadratic: 297 oneOf and anyOf on apollo-router,
5019
+ // each copying a helper list thousands long.
5020
+ function snapshotCtx (ctx) {
5021
+ const out = new Map()
5022
+ for (const k of Object.keys(ctx)) {
5023
+ const x = ctx[k]
5024
+ if (Array.isArray(x)) out.set(k, { kind: 'a', ref: x, n: x.length })
5025
+ else if (x instanceof Map || x instanceof Set) out.set(k, { kind: 'c', ref: x, n: x.size })
5026
+ else out.set(k, { kind: 'v', ref: x })
5027
+ }
5028
+ return out
5029
+ }
5030
+ function restoreCtx (ctx, saved) {
5031
+ for (const k of Object.keys(ctx)) if (!saved.has(k)) delete ctx[k]
5032
+ for (const [k, e] of saved) {
5033
+ if (e.kind === 'a') { e.ref.length = Math.min(e.ref.length, e.n); ctx[k] = e.ref }
5034
+ else if (e.kind === 'c') {
5035
+ const extra = e.ref.size - e.n
5036
+ if (extra > 0) {
5037
+ const added = []
5038
+ let i = 0
5039
+ for (const key of e.ref.keys()) if (i++ >= e.n) added.push(key)
5040
+ for (const key of added) e.ref.delete(key)
5041
+ }
5042
+ ctx[k] = e.ref
5043
+ } else ctx[k] = e.ref
5044
+ }
5045
+ }
5046
+
5047
+ // Whether a node holds a reference anywhere below it.
5048
+ function holdsRef (node, seen = new Set()) {
5049
+ if (node === null || typeof node !== 'object' || seen.has(node)) return false
5050
+ seen.add(node)
5051
+ if (typeof node.$ref === 'string' || typeof node.$dynamicRef === 'string') return true
5052
+ for (const k of Object.keys(node)) if (holdsRef(node[k], seen)) return true
5053
+ return false
5054
+ }
5055
+
4613
5056
  function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
4614
5057
  ctx.usesBranchCollapse = true
4615
5058
  const fi = ctx.varCounter++
@@ -4629,7 +5072,14 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4629
5072
  const subLines = []
4630
5073
  const subSp = branchSp + '/' + i
4631
5074
  if (typeof sub === 'object' && sub !== null) {
4632
- genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
5075
+ // A branch is a function of its own with a local `_e`; a collapse
5076
+ // inside it reports there, not into the combined function's lists.
5077
+ ctx.inBranchFn = (ctx.inBranchFn || 0) + 1
5078
+ try {
5079
+ genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
5080
+ } finally {
5081
+ ctx.inBranchFn--
5082
+ }
4633
5083
  } else if (sub === false) {
4634
5084
  // A false branch never matches; it reports the same error the
4635
5085
  // interpreter attaches to a false schema.
@@ -4653,8 +5103,101 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4653
5103
  const pp = pathExpr || '""'
4654
5104
  ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
4655
5105
  const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
4656
- const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
4657
- const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5106
+ // branchSp is source text for a quoted literal, with backslashes and quotes
5107
+ // escaped; the ordinal is looked up by the path itself. Looked up by the
5108
+ // text, a path through a pattern such as `^\\d+$` found no node, or the
5109
+ // wrong one, and the collapsed error sorted out of place against the other
5110
+ // engines.
5111
+ // In the runtime shape there is no ordering key (the reader looks the path
5112
+ // up), so the collapsed error is in its final shape as it is built.
5113
+ const ord = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5114
+ // In the combined function `_e` is created on the first error and every
5115
+ // error is collected (`_all` is true there), so the report only pushes.
5116
+ const report = ctx.inCombined && !ctx.inBranchFn
5117
+ ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5118
+ : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5119
+ // The node's runtime unevaluatedProperties already counted how many of
5120
+ // these branches pass, through the annotation functions, which answer the
5121
+ // same validity. A oneOf with exactly one, an anyOf with any, holds and
5122
+ // reports nothing, so its branches are not run a second time to say so.
5123
+ // Only in the combined function proper, where that count is in scope.
5124
+ const counted = ctx.inCombined && !ctx.inBranchFn && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5125
+ const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
5126
+ let guard = counted !== undefined
5127
+ ? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
5128
+ : ''
5129
+ // More than one oneOf branch passed: the error names them and carries no
5130
+ // branch errors, so it is built from the bits the count left.
5131
+ if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5132
+ const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5133
+ guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
5134
+ }
5135
+ // Without such a count, each branch also gets a verdict function, and the
5136
+ // passing branches are counted with those: a oneOf with exactly one and an
5137
+ // anyOf with any hold, and a oneOf with more than one is reported from the
5138
+ // bits, so the error-collecting branches run only when no branch passes
5139
+ // and the collapse needs their errors to pick the closest. Running them to
5140
+ // learn the count built a result object and an error list per branch,
5141
+ // which made a oneOf matched twice cost 69 ns where counting costs about
5142
+ // 12. A branch the verdict generator declines keeps the old path.
5143
+ let vfArr = null
5144
+ // Branches that hold a reference are left to the error branches: their
5145
+ // verdicts would expand every definition again, as a second copy, where the
5146
+ // error branches call the shared one.
5147
+ if (guard === '' && !branches.some((b) => holdsRef(b))) {
5148
+ // A trial: the generators are not exception-safe on the context (a
5149
+ // reference left on refStack by a declined branch made the next error
5150
+ // function write that branch as nothing, which read as valid), so the
5151
+ // context is put back as it was when the verdicts decline.
5152
+ const saved = snapshotCtx(ctx)
5153
+ try {
5154
+ const vfs = branches.map((sub) => {
5155
+ if (sub === true) return 'function(){return true}'
5156
+ if (sub === false) return 'function(){return false}'
5157
+ const bl = []
5158
+ nestedGenCode(sub, '_bv', bl, ctx)
5159
+ return bl.length === 0 ? 'function(){return true}' : `function(_bv){${bl.join(';')};return true}`
5160
+ })
5161
+ // Only while the verdicts are small: they are a second copy of the
5162
+ // branches, with every reference expanded, and on large branches that
5163
+ // copy cost more to compile than counting saves (apollo-router's error
5164
+ // function grew from 1.5 to 2.5 MB of source).
5165
+ if (!vfs.some((b) => b.includes(SP_PLACEHOLDER)) && vfs.reduce((n, b) => n + b.length, 0) <= BRANCH_VERDICT_MAX) {
5166
+ // One constant per branch, called directly: through an array the one
5167
+ // call site sees every branch and none of them is inlined.
5168
+ vfArr = `_bvf${fi}`
5169
+ ctx.helperCode.push(vfs.map((b, i) => `const ${vfArr}_${i}=${b}`).join(';'))
5170
+ } else {
5171
+ restoreCtx(ctx, saved)
5172
+ }
5173
+ } catch (e) {
5174
+ if (e !== DECLINE) throw e
5175
+ restoreCtx(ctx, saved)
5176
+ }
5177
+ }
5178
+ if (vfArr !== null) {
5179
+ const L = branches.length
5180
+ const top = ctx.inCombined && !ctx.inBranchFn
5181
+ // An accepted document is still answered by the verdict first (see the
5182
+ // result shape in compileToJSCombined): that is the fastest path to it,
5183
+ // and dropping it measured 20 ns slower per accepted oneOf.
5184
+ if (top) ctx.unguardedCollapse = true
5185
+ const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
5186
+ if (keyword === 'anyOf') {
5187
+ lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5188
+ } else {
5189
+ const mk = L <= 30
5190
+ const collect = mk
5191
+ ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5192
+ : ''
5193
+ const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5194
+ const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5195
+ const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5196
+ lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
5197
+ }
5198
+ return
5199
+ }
5200
+ if (guard === '' && ctx.inCombined && !ctx.inBranchFn) ctx.unguardedCollapse = true
4658
5201
  if (keyword === 'anyOf') {
4659
5202
  // anyOf reports nothing once a branch passes, so the branches are first
4660
5203
  // only asked whether they pass. They are called with `_all` set to 0, a
@@ -4668,12 +5211,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4668
5211
  // no one read. Only when no branch passes do they run again with the
4669
5212
  // caller's `_all`, and the collapse sees them all, as before.
4670
5213
  const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
4671
- lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors,title:_bt${fi}[_bi]})}${report}}}`)
5214
+ lines.push(`${guard}{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
4672
5215
  } else {
4673
5216
  // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
4674
5217
  // the passing branches and answers; the branch errors of a probe are not
4675
5218
  // errors anyone reads and are not collapsed.
4676
- lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
5219
+ lines.push(`${guard}{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
4677
5220
  }
4678
5221
  }
4679
5222
 
@@ -4701,6 +5244,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4701
5244
  }
4702
5245
  // $ref — resolve local and cross-schema refs
4703
5246
  if (schema.$ref) {
5247
+ // The reference is followed and the rest of the node is not compiled; a
5248
+ // keyword beside it that validates would be dropped, so the node
5249
+ // declines (as in genCodeCNode). Reached since unevaluatedProperties
5250
+ // next to a $ref stopped declining the combined function, whose oneOf
5251
+ // and anyOf branches come from this generator: a branch was taken as
5252
+ // passing with its siblings unchecked.
5253
+ for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
4704
5254
  // A self-reference cannot be expressed here yet: the root helper takes no
4705
5255
  // path argument, so errors raised through it would carry the path of the
4706
5256
  // root rather than of the value that failed. This used to fall through as
@@ -4801,7 +5351,9 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4801
5351
  }
4802
5352
  })
4803
5353
  const expected = types.join(', ')
4804
- const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
5354
+ // As written: a one-element array stays an array, as the interpreted engine
5355
+ // reports it.
5356
+ const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
4805
5357
  {
4806
5358
  const typeSp = `${schemaPrefix}/type`
4807
5359
  const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
@@ -4832,7 +5384,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4832
5384
  if (cv === null || typeof cv !== 'object') {
4833
5385
  lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
4834
5386
  } else {
4835
- lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
5387
+ lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
4836
5388
  }
4837
5389
  }
4838
5390
 
@@ -4901,7 +5453,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4901
5453
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4902
5454
  ctx.usesSafeRe = true
4903
5455
  } else {
4904
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5456
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
4905
5457
  }
4906
5458
  }
4907
5459
  const ri = ctx.regExpMap.get(pattern);
@@ -4976,7 +5528,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4976
5528
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4977
5529
  ctx.usesSafeRe = true
4978
5530
  } else {
4979
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5531
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
4980
5532
  }
4981
5533
  }
4982
5534
  patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
@@ -5002,6 +5554,12 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5002
5554
  // to emit. The error keeps the keyword's own identity: the interpreted
5003
5555
  // engine reports these as unevaluatedProperties with the key named, and the
5004
5556
  // two engines are pinned to identical output.
5557
+ // Inside the combined function (a oneOf or anyOf branch, see
5558
+ // emitBranchCollapse) this generator only has the static form. A branch
5559
+ // whose evaluated set depends on its own branches would report the wrong
5560
+ // errors, and the collapse would pick the wrong closest branch; declined.
5561
+ if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
5562
+ if (ctx.inCombined && schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true && !unevalLocalOk(schema, 'unevaluatedItems')) throw DECLINE
5005
5563
  if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
5006
5564
 
5007
5565
  // unevaluatedItems: false under the same rule. The interpreted engine
@@ -5112,18 +5670,10 @@ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5112
5670
 
5113
5671
  function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
5114
5672
  const si = ctx.varCounter++
5115
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
5116
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
5117
- const maxItems = schema.maxItems
5118
5673
  const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
5119
- let inner
5120
- if (isPrim && maxItems && maxItems <= 16) {
5121
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
5122
- } else if (isPrim) {
5123
- inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
5124
- } else {
5125
- inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
5126
- }
5674
+ // Only reached once the verdict has rejected, or when errors are wanted, so
5675
+ // one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
5676
+ const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
5127
5677
  lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
5128
5678
  }
5129
5679
 
@@ -5142,7 +5692,7 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
5142
5692
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5143
5693
  ctx.usesSafeRe = true
5144
5694
  } else {
5145
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5695
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5146
5696
  }
5147
5697
  }
5148
5698
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
@@ -5174,7 +5724,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5174
5724
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5175
5725
  ctx.usesSafeRe = true
5176
5726
  } else {
5177
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5727
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5178
5728
  }
5179
5729
  }
5180
5730
  const ri = ctx.regExpMap.get(pattern);
@@ -5204,7 +5754,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5204
5754
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5205
5755
  ctx.usesSafeRe = true
5206
5756
  } else {
5207
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5757
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5208
5758
  }
5209
5759
  }
5210
5760
  patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
@@ -5256,7 +5806,7 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5256
5806
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5257
5807
  ctx.usesSafeRe = true
5258
5808
  } else {
5259
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5809
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5260
5810
  }
5261
5811
  }
5262
5812
  const ri = ctx.regExpMap.get(pattern);
@@ -5338,29 +5888,80 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5338
5888
  // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
5339
5889
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
5340
5890
  // Uses type-aware optimizations: after type check passes, skip guards.
5891
+ // An error literal the combined function shares between calls, frozen. Two
5892
+ // non-enumerable fields ride along, invisible to Object.keys, spread and
5893
+ // JSON: `_s` says the object is shared, so the reader copies it before handing
5894
+ // it out (Object.isFrozen, the check it replaces, cost more than the copy),
5895
+ // and `_o` is its schema ordinal, known here and otherwise looked up by path
5896
+ // on every read. An `_o` the literal already carries, for standalone output,
5897
+ // is kept. `_t` is an empty slot lib/enrich-error.js fills once.
5898
+ function sharedErr (lit, rootSchema) {
5899
+ Object.defineProperty(lit, '_s', { value: true })
5900
+ // A slot the enrichment fills on the first read with the fields that depend
5901
+ // only on this literal (code, expected, rank), so later reads skip them.
5902
+ Object.defineProperty(lit, '_t', { value: {} })
5903
+ if (lit._o === undefined) {
5904
+ const o = ordinalFor(rootSchema, lit.schemaPath)
5905
+ if (typeof o === 'number') Object.defineProperty(lit, '_o', { value: o })
5906
+ }
5907
+ return Object.freeze(lit)
5908
+ }
5909
+
5910
+ // The push for one error in the combined generator. With `ctx.rich` (the
5911
+ // validator's errors are being read and enriched, see validator-core) the
5912
+ // error is built in its final enriched shape where it happens, by makeRich in
5913
+ // lib/enrich-site.js with a site record made here at compile time; otherwise
5914
+ // it is the plain literal `lean` the generator always emitted. `valueExpr` is
5915
+ // the value at `pathExpr`, which enrichment otherwise finds again by walking
5916
+ // the document. `paramsShared` names a frozen params object hoisted for a
5917
+ // static site, so a static error's params stay one shared object as before.
5918
+ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
5919
+ if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
5920
+ if (!ctx.richMk) {
5921
+ ctx.richMk = true
5922
+ const { makeRich } = require('./enrich-site')
5923
+ ctx.closureVars.push('_mk'); ctx.closureVals.push(makeRich)
5924
+ }
5925
+ let paramsVal
5926
+ try { paramsVal = Function('return ' + paramsCode)() } catch { paramsVal = undefined }
5927
+ const fmt = keyword === 'format' && paramsVal ? paramsVal.format : undefined
5928
+ const { errorSite } = require('./enrich-site')
5929
+ const siteVar = `_S${ctx.varCounter++}`
5930
+ ctx.closureVars.push(siteVar)
5931
+ // Inside a shared definition (emitSharedDefC) the site holds the path past
5932
+ // the definition and the reference's own comes in as `_sp`.
5933
+ const rel = sp.startsWith(SP_PLACEHOLDER)
5934
+ ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
5935
+ return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
5936
+ }
5937
+
5341
5938
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
5342
5939
  const inputSchema = schema
5343
- schema = normalizePropertyNames(schema)
5940
+ schema = prepareForCodegen(schema, schemaMap)
5344
5941
  // Same provably-local rule as the error generator above.
5345
5942
  if (typeof schema === 'object' && schema !== null) {
5346
5943
  const s = JSON.stringify(schema)
5347
- if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
5348
- // Bail on self-referencing schemas — combined codegen doesn't support recursion
5349
- if (s.includes('"$ref":"#"')) return null
5350
- // Defs on a $ref cycle compile as named functions in the boolean and
5351
- // errors generators; this generator has no preamble for them, so it
5352
- // declines and the router uses the errors generator.
5353
- if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
5354
- // Bail on additionalProperties as a schema — combined path doesn't emit the
5355
- // per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
5356
- if (hasAdditionalPropertiesSchema(schema)) return null
5944
+ // unevaluatedProperties and unevaluatedItems that are not provably local
5945
+ // are worked out at run time (genUnevaluatedDynamicC,
5946
+ // genUnevaluatedItemsDynamicC).
5947
+ // A schema that references itself, through `$ref: "#"` or a definition on
5948
+ // a cycle, has those references written as calls under the cycle guard
5949
+ // (emitDefFnC).
5357
5950
  // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
5358
5951
  // Letting genCodeC emit the legacy "must match exactly one" placeholder
5359
5952
  // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
5360
5953
  // fast path plus errFn fallback handles these schemas with collapse.
5361
- if (s.includes('"oneOf"') || s.includes('"anyOf"')) return null
5954
+ // Branches are collapsed into one error by the error generator's emitter
5955
+ // (emitBranchCollapse). Its errors are plain, so the rich form, which
5956
+ // builds every error enriched where it fails, declines them.
5957
+ // In the rich form the collapsed error and its branch errors come out
5958
+ // plain (the branches are the error generator's) and are enriched when
5959
+ // presented; every other error is built enriched. See richBuilder.
5362
5960
  }
5363
5961
  if (typeof schema === 'boolean') {
5962
+ // A `false` root has one fixed error; the rich form leaves it to enrich.
5963
+ if (!schema && opts && (opts.rich || opts.resultShape)) return null
5964
+ if (opts && opts.resultShape) return null
5364
5965
  return schema
5365
5966
  ? () => VALID_RESULT
5366
5967
  : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
@@ -5369,7 +5970,6 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5369
5970
  if (!sharedCodegenGate(schema, schemaMap)) return null
5370
5971
  if (schema.patternProperties) {
5371
5972
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
5372
- if (/\\[pP]\{/.test(pat)) return null
5373
5973
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5374
5974
  }
5375
5975
  }
@@ -5419,7 +6019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5419
6019
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5420
6020
 
5421
6021
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
5422
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6022
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5423
6023
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5424
6024
  const lines = []
5425
6025
  try {
@@ -5436,14 +6036,69 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5436
6036
  // helper compiles a pattern through it, which the boolean generator does for
5437
6037
  // subschemas under not/if/contains; without the binding those threw a
5438
6038
  // ReferenceError on the first document that reached them.
6039
+ if (opts && opts.resultShape) {
6040
+ ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
6041
+ ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
6042
+ }
5439
6043
  const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
5440
6044
  const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
5441
6045
  const closureParams = closureNames.join(',')
5442
- const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
6046
+ // A definition the error generator wrote as a named function inside a
6047
+ // branch (one on a cycle, or referenced from several places) reads the
6048
+ // cycle-guard state the error function declares. Here that state is
6049
+ // declared too, and the checks run as the error function runs them: in a
6050
+ // body of their own, again in the guarded mode when data turns out to
6051
+ // point back at itself. This used to decline, which left every schema
6052
+ // whose oneOf or anyOf branches reach a shared definition without one
6053
+ // pass.
6054
+ const guardList = [...(ctx.defFns ? ctx.defFns.values() : []), ...(ctx.cycFnsC || [])]
6055
+ const guardDefs = guardList.length ? guardList : null
6056
+ if (guardDefs) ctx.helperCode.unshift(emitGuardState())
6057
+ // Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
6058
+ // the branch functions (this function collects everything), and the
6059
+ // runtime shape the error generator gives its literals, without code and
6060
+ // docUrl. The collapsed error itself keeps its code, as everywhere.
6061
+ if (ctx.usesBranchCollapse) ctx.helperCode.push(require('./branch-collapse').embedSource())
6062
+ if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
6063
+ const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
6064
+ for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
6065
+ for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
6066
+ }
5443
6067
  // Lazy error array: no allocation for valid data (the common case)
5444
- const inner = `let _e;\n ` +
5445
- lines.join('\n ') +
5446
- `\n return _e?{valid:false,errors:_e}:R`
6068
+ // resultShape: validate() itself for a validator whose errors are being read
6069
+ // (see validator-core): the result objects built here, so one pass decides,
6070
+ // collects and answers, with nothing between the caller and the checks.
6071
+ const rs = opts && opts.resultShape
6072
+ // Counts shared between a node's unevaluatedProperties and its anyOf and
6073
+ // oneOf (genUnevaluatedDynamicC), visible to every line of the function.
6074
+ let hoistDecl = ctx.hoisted.length ? `let ${ctx.hoisted.map((n) => n + '=null').join(',')};` : ''
6075
+ let checks = lines.join('\n ')
6076
+ let allDecl = ctx.usesBranchCollapse ? 'const _all=true;' : ''
6077
+ if (guardDefs) {
6078
+ const clears = ['_stk.clear()'].concat(guardDefs.map((n) => `${n}_s.clear()`)).join(';')
6079
+ ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
6080
+ hoistDecl = ''
6081
+ allDecl = ''
6082
+ checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};try{_e=_bodyC(d)}finally{_sg=false}}`
6083
+ }
6084
+ // In the result shape this function is validate() itself, so a document
6085
+ // that makes it throw is answered by the caller's previous path instead
6086
+ // (`_FB`). A thrown error reaching the caller is the one thing this form
6087
+ // must not add; a try that does not throw costs nothing measurable.
6088
+ const inner = rs
6089
+ // With collapsed branches, collecting runs every branch of every oneOf
6090
+ // and anyOf in full, which an accepted document does not need: the
6091
+ // verdict answers it first.
6092
+ // Not when every collapse is guarded by a count (emitBranchCollapse): an
6093
+ // accepted document then runs no branch twice, and a rejected one would
6094
+ // pay for the verdict for nothing.
6095
+ ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;\n ` + checks +
6096
+ // A collapsed branch error is presented even alone (its ordinal and
6097
+ // raw branch errors stay inside); otherwise only a list of two or more
6098
+ // needs ordering.
6099
+ `\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)}`
6100
+ : hoistDecl + allDecl + `let _e;\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6101
+ const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5447
6102
 
5448
6103
  try {
5449
6104
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
@@ -5459,9 +6114,102 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5459
6114
  // Combined code generator: type-aware like genCode, error-collecting like genCodeE.
5460
6115
  // After type check passes → use optimizations (destructuring, no guards).
5461
6116
  // If type check fails → push error, skip property checks (they'd crash).
6117
+ // What of a node still applies to a value that failed its `type`, for the
6118
+ // combined generator: the node without `type`, without what it emits outside
6119
+ // the type block anyway (enum, const, anyOf, oneOf), and without the keywords
6120
+ // of the declared types, which no value of another type meets. Null when
6121
+ // nothing that validates is left.
6122
+ const TYPE_KEYWORDS = {
6123
+ object: ['properties', 'patternProperties', 'additionalProperties', 'required', 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired', 'dependentSchemas', 'dependencies', 'unevaluatedProperties'],
6124
+ array: ['items', 'prefixItems', 'additionalItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems'],
6125
+ string: ['minLength', 'maxLength', 'pattern'],
6126
+ number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
6127
+ }
6128
+ TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
6129
+ const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
6130
+ function typeMismatchRest(schema, types) {
6131
+ const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
6132
+ for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
6133
+ // A non-integer number reaches the type block already (numberToo).
6134
+ let rest = null
6135
+ for (const k of Object.keys(schema)) {
6136
+ if (drop.has(k)) continue
6137
+ if (rest === null) rest = {}
6138
+ rest[k] = schema[k]
6139
+ }
6140
+ if (rest === null) return null
6141
+ // A node whose reference was already written (genCodeCNode) keeps that
6142
+ // mark, or the reference is written again here and its errors twice.
6143
+ if (schema[REF_DONE]) Object.defineProperty(rest, REF_DONE, { value: true })
6144
+ for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
6145
+ return null
6146
+ }
6147
+
5462
6148
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5463
6149
  return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
5464
6150
  }
6151
+ // A definition referenced from several places, written once as a function
6152
+ // and called at each reference, instead of its whole code at every one: the
6153
+ // combined function of SchemaStore's cloud-run (48 KB of schema) came to
6154
+ // 1.1 MB of source and sarif's (79 KB) to 2.8 MB, and compiling those was most
6155
+ // of the 50 to 300 ms a first rejection with its errors read took. The body is
6156
+ // generated against a placeholder schema path that becomes the `_sp` argument,
6157
+ // as the error generator does (genCodeENode), so an error names the path it
6158
+ // was reached by. It collects into its own list, returned to the caller, and
6159
+ // declares its own branch counts (genUnevaluatedDynamicC). The document root
6160
+ // comes in as `d` for the rich errors that read it. Not for a definition on a
6161
+ // reference cycle, which keeps its existing path. False when not shared.
6162
+ function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
6163
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
6164
+ if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
6165
+ const cyclic = ctx.cyclicDefs.has(defName)
6166
+ if (!cyclic && !ctx.sharedDefs.has(defName)) return false
6167
+ emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6168
+ return true
6169
+ }
6170
+
6171
+ // One schema as a function of its own in the combined function (see
6172
+ // emitSharedDefC). The name is registered before the body is written, so a
6173
+ // reference back to it inside becomes a call. One that can re-enter itself,
6174
+ // a definition on a cycle or the root through `$ref: "#"`, goes through the
6175
+ // cycle guard the error generator's definitions use: depth counted, and in
6176
+ // the guarded pass a value already being checked is not checked again.
6177
+ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix) {
6178
+ if (!ctx.defFnsC) ctx.defFnsC = new Map()
6179
+ let fnName = ctx.defFnsC.get(key)
6180
+ if (!fnName) {
6181
+ fnName = '_defC' + ctx.defFnsC.size + '_' + (key === '#' ? 'root' : key.replace(/[^A-Za-z0-9_]/g, '_'))
6182
+ ctx.defFnsC.set(key, fnName)
6183
+ const outerHoisted = ctx.hoisted
6184
+ ctx.hoisted = []
6185
+ const bodyLines = []
6186
+ let hoisted
6187
+ try {
6188
+ genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
6189
+ } finally {
6190
+ hoisted = ctx.hoisted
6191
+ ctx.hoisted = outerHoisted
6192
+ }
6193
+ let body = bodyLines.join('\n ')
6194
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
6195
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
6196
+ const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
6197
+ const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6198
+ if (cyclic) {
6199
+ if (!ctx.cycFnsC) ctx.cycFnsC = []
6200
+ ctx.cycFnsC.push(fnName)
6201
+ ctx.helperCode.push(
6202
+ `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d){\n ` +
6203
+ `if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d)}finally{${fnName}_s.delete(_dv)}}\n ` +
6204
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d)\n _sd--\n return _r\n }\n ` + fn)
6205
+ } else {
6206
+ ctx.helperCode.push(fn)
6207
+ }
6208
+ }
6209
+ lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
6210
+ }
6211
+
6212
+ const REF_DONE = Symbol('ata.refDone')
5465
6213
  function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5466
6214
  if (!schemaPrefix) schemaPrefix = '#'
5467
6215
  // Per-node, for the reason spelled out in genCode.
@@ -5469,19 +6217,41 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5469
6217
  if (schema === false) {
5470
6218
  // Leading semicolon: the previous generated line may end in an
5471
6219
  // expression, and a line starting with `(` would be parsed as a call.
5472
- lines.push(`;(_e||(_e=[])).push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'})`)
6220
+ lines.push(';' + errPushC(ctx, 'not', schemaPrefix, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'}`))
5473
6221
  return
5474
6222
  }
5475
6223
  if (schema === true) return
5476
6224
  if (typeof schema !== 'object' || schema === null) return
5477
6225
 
5478
6226
  // $ref — resolve local, anchor, and cross-schema refs
5479
- if (schema.$ref) {
5480
- // Self-reference "#" — no-op (permissive) to avoid infinite recursion
5481
- if (schema.$ref === '#') return
6227
+ if (schema.$ref && !schema[REF_DONE]) {
6228
+ // A keyword beside $ref that validates applies too (2020-12; draft 7
6229
+ // drops such siblings when the schema is read). The reference is written
6230
+ // on its own, then the node again with the reference marked done, on a
6231
+ // fresh copy so the mark cannot reach the same schema object met through
6232
+ // another path; the copy keeps `$ref` for unevaluatedProperties and
6233
+ // unevaluatedItems, which read its annotations. Dropping the siblings
6234
+ // once accepted what they reject, which is why this declined. Not in a
6235
+ // branch function, which the error generator writes.
6236
+ if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
6237
+ if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
6238
+ genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
6239
+ const rest = Object.assign({}, schema)
6240
+ Object.defineProperty(rest, REF_DONE, { value: true })
6241
+ genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
6242
+ return
6243
+ }
6244
+ // A reference back into what is being expanded is a call to that
6245
+ // schema's function (emitDefFnC), never nothing: an expansion that stops
6246
+ // and emits no lines reads as valid.
6247
+ if (schema.$ref === '#') {
6248
+ if (ctx.inCombined && !ctx.inBranchFn && ctx.rootC) { emitDefFnC('#', ctx.rootC, true, v, pathExpr, lines, ctx, schemaPrefix); return }
6249
+ throw DECLINE
6250
+ }
5482
6251
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5483
6252
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
5484
- if (ctx.refStack.has(schema.$ref)) return
6253
+ if (ctx.inCombined && !ctx.inBranchFn && emitSharedDefC(m[1], v, pathExpr, lines, ctx, schemaPrefix)) return
6254
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5485
6255
  ctx.refStack.add(schema.$ref)
5486
6256
  genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
5487
6257
  ctx.refStack.delete(schema.$ref)
@@ -5492,7 +6262,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5492
6262
  const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
5493
6263
  const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
5494
6264
  if (anchorTarget) {
5495
- if (ctx.refStack.has(schema.$ref)) return
6265
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5496
6266
  ctx.refStack.add(schema.$ref)
5497
6267
  genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
5498
6268
  ctx.refStack.delete(schema.$ref)
@@ -5500,7 +6270,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5500
6270
  }
5501
6271
  }
5502
6272
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
5503
- if (ctx.refStack.has(schema.$ref)) return
6273
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5504
6274
  ctx.refStack.add(schema.$ref)
5505
6275
  genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
5506
6276
  ctx.refStack.delete(schema.$ref)
@@ -5510,7 +6280,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5510
6280
  if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
5511
6281
  const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
5512
6282
  if (r) {
5513
- if (ctx.refStack.has(schema.$ref)) return
6283
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5514
6284
  ctx.refStack.add(schema.$ref)
5515
6285
  genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
5516
6286
  ctx.refStack.delete(schema.$ref)
@@ -5530,7 +6300,26 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5530
6300
  const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5531
6301
  const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
5532
6302
  const sp = schemaPrefix + '/' + schemaSuffix
5533
- if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
6303
+ if (ctx.rich) {
6304
+ // A static site shares one frozen params object between calls, as its
6305
+ // hoisted literal does below; a dynamic one builds its params per call.
6306
+ let shared = null
6307
+ if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
6308
+ let pv
6309
+ try { pv = Function('return ' + paramsCode)() } catch { pv = undefined }
6310
+ if (pv !== undefined) {
6311
+ shared = `_P${ctx.varCounter++}`
6312
+ ctx.closureVars.push(shared); ctx.closureVals.push(Object.freeze(pv))
6313
+ }
6314
+ }
6315
+ return errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, v, null, shared)
6316
+ }
6317
+ // Hoisting a static error as one shared literal keeps a function that
6318
+ // also answers accepted documents small. In the runtime shape this
6319
+ // function only runs for documents being rejected (the verdict is its own
6320
+ // function), and a shared literal has to be copied for the caller on
6321
+ // every read, so there it is written fresh.
6322
+ if (isStaticPath && !ctx.noOrdinal && msgCode.startsWith("'") && !msgCode.includes('+')) {
5534
6323
  // Try to evaluate paramsCode as a static constant
5535
6324
  let paramsVal
5536
6325
  try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
@@ -5557,7 +6346,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5557
6346
  const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
5558
6347
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
5559
6348
  if (o !== null) lit._o = o
5560
- ctx.closureVals.push(Object.freeze(lit))
6349
+ ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
5561
6350
  return `(_e||(_e=[])).push(${errVar})`
5562
6351
  }
5563
6352
  }
@@ -5582,7 +6371,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5582
6371
  if (cv === null || typeof cv !== 'object') {
5583
6372
  lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5584
6373
  } else {
5585
- lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
6374
+ lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
5586
6375
  }
5587
6376
  }
5588
6377
  }
@@ -5601,7 +6390,9 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5601
6390
  }
5602
6391
  })
5603
6392
  const expected = types.join(', ')
5604
- const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
6393
+ // As written: a one-element array stays an array, as the interpreted engine
6394
+ // reports it.
6395
+ const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
5605
6396
  // Type check: push error but continue — wrap remaining in type-success block
5606
6397
  const typeOk = `_tok${ctx.varCounter++}`
5607
6398
  lines.push(`const ${typeOk}=${conds.join('||')}`)
@@ -5614,7 +6405,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5614
6405
  isNum = types[0] === 'number' || types[0] === 'integer'
5615
6406
  }
5616
6407
  emitEnumConst()
5617
- lines.push(`if(${typeOk}){`)
6408
+ // A number that is not an integer fails `integer` and is still a number:
6409
+ // minimum, multipleOf and the rest apply to it, and the other engines
6410
+ // report them next to the type error. The block opens for any number.
6411
+ const numberToo = types.includes('integer') && !types.includes('number')
6412
+ lines.push(numberToo ? `if(${typeOk}||typeof ${v}==='number'){` : `if(${typeOk}){`)
5618
6413
  } else {
5619
6414
  emitEnumConst()
5620
6415
  }
@@ -5641,29 +6436,29 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5641
6436
  }
5642
6437
  for (const key of schema.required) {
5643
6438
  const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
5644
- if (isStaticPath) {
6439
+ if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
5645
6440
  const ei = ctx.varCounter++
5646
6441
  const errVar = `_E${ei}`
5647
6442
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5648
6443
  ctx.closureVars.push(errVar)
5649
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
6444
+ ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
5650
6445
  lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
5651
6446
  } else {
5652
- lines.push(`if(${check}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
6447
+ lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
5653
6448
  }
5654
6449
  }
5655
6450
  } else if (schema.required) {
5656
6451
  for (const key of schema.required) {
5657
6452
  const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5658
- if (isStatic) {
6453
+ if (isStatic && !ctx.rich && !ctx.noOrdinal) {
5659
6454
  const ei = ctx.varCounter++
5660
6455
  const errVar = `_E${ei}`
5661
6456
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5662
6457
  ctx.closureVars.push(errVar)
5663
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
6458
+ ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
5664
6459
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
5665
6460
  } else {
5666
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
6461
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
5667
6462
  }
5668
6463
  }
5669
6464
  }
@@ -5701,7 +6496,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5701
6496
  const ri = ctx.varCounter++
5702
6497
  const reVar = `_re${ri}`
5703
6498
  ctx.closureVars.push(reVar)
5704
- ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
6499
+ ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern)))
5705
6500
  const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
5706
6501
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
5707
6502
  }
@@ -5786,27 +6581,36 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5786
6581
  // unevaluatedProperties: false where it is provably additionalProperties
5787
6582
  // (the entry bail guaranteed that), keeping the keyword's own identity so
5788
6583
  // the two engines report the same error.
5789
- if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
6584
+ if (schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true) {
6585
+ if (unevalLocalOk(schema, 'unevaluatedProperties')) {
6586
+ if (schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
6587
+ } else {
6588
+ genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
6589
+ }
6590
+ }
5790
6591
 
5791
6592
  // unevaluatedItems: false under the same rule: one error for the array,
5792
6593
  // the first uncovered position as the limit. Adjacent schema-form `items`
5793
6594
  // evaluates everything; the draft-07 array form acts as the prefix.
5794
- if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6595
+ if (schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true) {
6596
+ if (unevalLocalOk(schema, 'unevaluatedItems')) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6597
+ else genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6598
+ }
5795
6599
 
5796
6600
  // dependentRequired
5797
6601
  if (schema.dependentRequired) {
5798
6602
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
5799
6603
  for (const dep of deps) {
5800
6604
  const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
5801
- if (isStatic) {
6605
+ if (isStatic && !ctx.rich && !ctx.noOrdinal) {
5802
6606
  const ei = ctx.varCounter++
5803
6607
  const errVar = `_E${ei}`
5804
6608
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
5805
6609
  ctx.closureVars.push(errVar)
5806
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
6610
+ ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
5807
6611
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
5808
6612
  } else {
5809
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
6613
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
5810
6614
  }
5811
6615
  }
5812
6616
  }
@@ -5839,6 +6643,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5839
6643
  if (schema.patternProperties) {
5840
6644
  if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
5841
6645
  }
6646
+ // additionalProperties as a schema: each key that is neither declared nor
6647
+ // matched by a pattern, validated where it is (see genAdditionalSchemaC).
6648
+ if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
6649
+
5842
6650
 
5843
6651
  // dependentSchemas
5844
6652
  if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
@@ -5868,11 +6676,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5868
6676
  // allOf
5869
6677
  if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
5870
6678
 
5871
- // anyOf
5872
- if (schema.anyOf) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5873
-
5874
- // oneOf
5875
- if (schema.oneOf) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6679
+ // anyOf, oneOf. A collapsed branch applies to a value of any type, as enum
6680
+ // and const do: the other engines report it next to a failed `type`, so
6681
+ // in the combined function it goes after the type-success block below.
6682
+ if (schema.anyOf && !ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6683
+ if (schema.oneOf && !ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5876
6684
 
5877
6685
  // not
5878
6686
  if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
@@ -5883,7 +6691,22 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5883
6691
  // Close type-success block if opened
5884
6692
  if (types) {
5885
6693
  lines.push(`}`)
6694
+ // A value of another type still meets every keyword that applies to its
6695
+ // own type, and the applicators (allOf, not, if/then/else, $ref): the
6696
+ // other engines report those next to the type error, and the block above
6697
+ // skipped them all. The keywords of the declared types cannot apply to
6698
+ // it and are left out, so the else is small and does not repeat the
6699
+ // declared type's subtree.
6700
+ const rest = typeMismatchRest(schema, types)
6701
+ if (rest !== null) {
6702
+ lines.push(`else{`)
6703
+ genCodeC(rest, v, pathExpr, lines, ctx, schemaPrefix)
6704
+ lines.push(`}`)
6705
+ }
5886
6706
  }
6707
+ if (schema.anyOf && ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6708
+ if (schema.oneOf && ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6709
+
5887
6710
  }
5888
6711
 
5889
6712
  // Rarely used keyword families of genCodeCNode, kept out of it so their compile is
@@ -5909,20 +6732,376 @@ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5909
6732
 
5910
6733
  function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
5911
6734
  const si = ctx.varCounter++
5912
- const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
5913
- const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
5914
- const maxItems = schema.maxItems
5915
6735
  const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
5916
- let inner
5917
- if (isPrim && maxItems && maxItems <= 16) {
5918
- // Small primitive arrays: O(n²) nested loop, zero allocation
5919
- inner = `for(let _i=1;_i<${v}.length;_i++){for(let _k=0;_k<_i;_k++){if(${v}[_i]===${v}[_k]){${failExpr('_k', '_i')};break}}}`
5920
- } else if (isPrim) {
5921
- inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
6736
+ // Only reached once the verdict has rejected, or when errors are wanted, so
6737
+ // one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
6738
+ const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
6739
+ lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
6740
+ }
6741
+
6742
+ // The verdict form of genUnevaluatedDynamicC: the same set, built the same
6743
+ // way, and the answer false at the first key outside it.
6744
+ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6745
+ if (schema.additionalProperties !== undefined) return
6746
+ const ui = ctx.varCounter++
6747
+ const S = `_ev${ui}`
6748
+ const body = [`const ${S}=[]`]
6749
+ addLocalAnnotations(schema, v, S, body, ctx)
6750
+ addTopApplicatorAnnotations(schema, v, S, body, ctx)
6751
+ const kVar = `_uk${ui}`
6752
+ const up = schema.unevaluatedProperties
6753
+ if (up === false) {
6754
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
6755
+ } else if (typeof up === 'object' && up !== null) {
6756
+ const sub = []
6757
+ genCode(up, `${v}[${kVar}]`, sub, ctx)
6758
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
5922
6759
  } else {
5923
- inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
6760
+ throw DECLINE
5924
6761
  }
5925
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
6762
+ lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
6763
+ }
6764
+
6765
+ // unevaluatedProperties where what is evaluated depends on which branches
6766
+ // pass: the set is built at run time with the interpreted engine's rules.
6767
+ // The node's own properties, patternProperties and additionalProperties
6768
+ // count whether or not their values pass; an allOf, anyOf or dependentSchemas
6769
+ // branch, an if, then or else, or a $ref target counts only when it passes,
6770
+ // a oneOf only when exactly one branch passes, and `not` never. Each branch is
6771
+ // asked through an annotation function (emitAnnot).
6772
+ function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6773
+ // additionalProperties evaluates every key the others do not, pass or fail.
6774
+ if (schema.additionalProperties !== undefined) return
6775
+ const ui = ctx.varCounter++
6776
+ const S = `_ev${ui}`
6777
+ const body = [`const ${S}=[]`]
6778
+ addLocalAnnotations(schema, v, S, body, ctx)
6779
+ // The anyOf and oneOf counts are handed to the keywords' own error code
6780
+ // (emitBranchCollapse), with the object they were counted for: they are
6781
+ // declared once at the top of the combined function, where every later
6782
+ // line sees them, and read only when that object is the value at hand, so
6783
+ // a count left from another value, another loop iteration or another place
6784
+ // is never taken for this one. Only in the combined function proper.
6785
+ const share = ctx.inCombined && !ctx.inBranchFn && ctx.hoisted !== undefined &&
6786
+ (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf))
6787
+ const counts = share ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: `_om${ui}`, of: `_ao${ui}` } : null
6788
+ if (share) {
6789
+ ctx.hoisted.push(counts.anyOf, counts.oneOf, counts.oneOfMask, counts.of)
6790
+ body.push(`${counts.of}=${v};${counts.oneOfMask}=0`)
6791
+ if (!ctx.branchCounts) ctx.branchCounts = new Map()
6792
+ if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
6793
+ if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
6794
+ }
6795
+ addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
6796
+ const kVar = `_uk${ui}`
6797
+ const up = schema.unevaluatedProperties
6798
+ if (up === false) {
6799
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
6800
+ } else if (typeof up === 'object' && up !== null) {
6801
+ const pe = emitPtrEsc(ctx)
6802
+ const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
6803
+ const sub = []
6804
+ genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
6805
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
6806
+ } else {
6807
+ throw DECLINE
6808
+ }
6809
+ lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
6810
+ }
6811
+
6812
+ // The names a node's own keywords evaluate, pushed onto the array `S`:
6813
+ // present declared properties, keys matching a pattern, and every key when
6814
+ // additionalProperties is there.
6815
+ function addLocalAnnotations(node, v, S, out, ctx) {
6816
+ if (node.additionalProperties !== undefined) {
6817
+ out.push(`for(const _ka in ${v})${S}.push(_ka)`)
6818
+ return
6819
+ }
6820
+ for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
6821
+ const pats = Object.keys(node.patternProperties || {})
6822
+ if (pats.length) {
6823
+ const tests = []
6824
+ const kv = `_pk${ctx.varCounter++}`
6825
+ for (const pat of pats) {
6826
+ const fast = fastPrefixCheck(pat, kv)
6827
+ if (fast) { tests.push(fast); continue }
6828
+ const ri = ctx.varCounter++
6829
+ ctx.closureVars.push(`_re${ri}`)
6830
+ ctx.closureVals.push(safeReClosure(ctx, pat))
6831
+ tests.push(`_re${ri}.test(${kv})`)
6832
+ }
6833
+ out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
6834
+ }
6835
+ }
6836
+
6837
+ // What the applicators of the node that owns the unevaluatedProperties add.
6838
+ // Their validity is checked by the code around this; here a branch only
6839
+ // contributes, through its annotation function, when it passes, and a oneOf
6840
+ // only with exactly one passing branch. An annotation function that fails
6841
+ // leaves `S` as it found it.
6842
+ function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
6843
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
6844
+ for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6845
+ // With `counts`, the passing branches of anyOf and oneOf are counted into
6846
+ // variables the caller declared, so the keyword's own error code further
6847
+ // down can skip its branches when the count already says it holds (see
6848
+ // emitBranchCollapse).
6849
+ if (Array.isArray(node.anyOf)) {
6850
+ if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
6851
+ else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6852
+ }
6853
+ if (Array.isArray(node.oneOf)) {
6854
+ const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
6855
+ // Which branches passed, as bits, for a collapse that has to name them.
6856
+ const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
6857
+ out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
6858
+ }
6859
+ if (node.if !== undefined) {
6860
+ const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
6861
+ const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
6862
+ out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
6863
+ }
6864
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
6865
+ 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})`)
6866
+ }
6867
+ if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
6868
+ }
6869
+
6870
+ function refTargetFor(ref, ctx) {
6871
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
6872
+ const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
6873
+ if (t === undefined) throw DECLINE
6874
+ return t
6875
+ }
6876
+
6877
+ // The keywords an annotation function evaluates itself, through its children;
6878
+ // the rest of a subschema is checked by a plain verdict.
6879
+ const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
6880
+
6881
+ // A function `(value, S) => valid` for one subschema: whether it passes, and
6882
+ // when it does, what it evaluated pushed onto S; when it does not, S is left
6883
+ // as it was. Validity is worked out bottom up: the subschema's own keywords
6884
+ // by a verdict of the subschema without its applicators, each applicator from
6885
+ // its branches' annotation functions. Asking each branch for a full verdict
6886
+ // and then again for its annotations repeated every nested branch once per
6887
+ // level, which made nested references the slowest shape there was.
6888
+ function emitAnnot(sub, ctx) {
6889
+ if (sub === true) return '_anT'
6890
+ if (sub === false) return '_anF'
6891
+ if (sub === null || typeof sub !== 'object') throw DECLINE
6892
+ if (!ctx.annFns) {
6893
+ ctx.annFns = new Map()
6894
+ ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
6895
+ }
6896
+ const hit = ctx.annFns.get(sub)
6897
+ if (hit !== undefined) {
6898
+ if (hit === null) throw DECLINE // a reference cycle: not modelled here
6899
+ return hit
6900
+ }
6901
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined || sub.dependencies !== undefined) throw DECLINE
6902
+ // unevaluatedItems reads what the applicators evaluate too; the local
6903
+ // verdict would see it without them.
6904
+ if (sub.unevaluatedItems !== undefined) throw DECLINE
6905
+ ctx.annFns.set(sub, null)
6906
+ const name = `_an${ctx.varCounter++}`
6907
+ const local = {}
6908
+ for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) local[k] = sub[k]
6909
+ const vl = []
6910
+ nestedGenCode(local, '_av', vl, ctx)
6911
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
6912
+ const body = [`if(!${name}_v(d))return false`]
6913
+ if (sub.not !== undefined) {
6914
+ const nl = []
6915
+ nestedGenCode(sub.not, '_nv', nl, ctx)
6916
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
6917
+ body.push(`if(${name}_n(d))return false`)
6918
+ }
6919
+ body.push(`const _m=S.length;const _o=typeof d==='object'&&d!==null&&!Array.isArray(d)`)
6920
+ const locals = []
6921
+ addLocalAnnotations(sub, 'd', 'S', locals, ctx)
6922
+ if (locals.length) body.push(`if(_o){${locals.join(';')}}`)
6923
+ const failOut = '{_rb(S,_m);return false}'
6924
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnot(b, ctx)}(d,S))${failOut}`)
6925
+ if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_a=true;`).join('')}if(!_a)${failOut}}`)
6926
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_c++;`).join('')}if(_c!==1)${failOut}}`)
6927
+ if (sub.if !== undefined) {
6928
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnot(sub.then, ctx)}(d,S))${failOut}` : ''
6929
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnot(sub.else, ctx)}(d,S))${failOut}` : ''
6930
+ body.push(`if(${emitAnnot(sub.if, ctx)}(d,S)){${thenCheck}}else{${elseCheck}}`)
6931
+ }
6932
+ if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
6933
+ for (const [k, ds] of Object.entries(sub.dependentSchemas)) body.push(`if(_o&&Object.prototype.hasOwnProperty.call(d,${JSON.stringify(k)})&&!${emitAnnot(ds, ctx)}(d,S))${failOut}`)
6934
+ }
6935
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnot(refTargetFor(sub.$ref, ctx), ctx)}(d,S))${failOut}`)
6936
+ // An unevaluatedProperties of its own: the keys this subschema evaluated
6937
+ // are the ones pushed since _m; what is left must pass it, and then every
6938
+ // key is evaluated.
6939
+ const up = sub.unevaluatedProperties
6940
+ if (up !== undefined && up !== true && sub.additionalProperties === undefined) {
6941
+ if (up === false) {
6942
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0)${failOut}}`)
6943
+ } else if (typeof up === 'object' && up !== null) {
6944
+ const ul = []
6945
+ nestedGenCode(up, '_uv', ul, ctx)
6946
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
6947
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0&&!${name}_u(d[_k]))${failOut}}`)
6948
+ } else {
6949
+ throw DECLINE
6950
+ }
6951
+ }
6952
+ if (up !== undefined && sub.additionalProperties === undefined) body.push(`if(_o)for(const _ka in d)S.push(_ka)`)
6953
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
6954
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
6955
+ ctx.annFns.set(sub, name)
6956
+ return name
6957
+ }
6958
+
6959
+ // unevaluatedItems worked out at run time, for the combined function. The
6960
+ // evaluated positions of an array are kept as the interpreter keeps them
6961
+ // (lib/plan-compiler.js): `n`, every index below it evaluated, and `x`, the
6962
+ // indexes past it that `contains` matched. This node's own prefixItems,
6963
+ // items and contains mark them whatever their own verdict, and each in-place
6964
+ // subschema marks them only when it passes (emitAnnotI); what is left must
6965
+ // pass unevaluatedItems. `false` reports once, at the first position left,
6966
+ // as the interpreter does.
6967
+ function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6968
+ if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
6969
+ if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
6970
+ annItemHelpers(ctx)
6971
+ const ui = ctx.varCounter++
6972
+ const S = `_es${ui}`
6973
+ const body = [`const ${S}={n:0,x:null}`]
6974
+ addLocalItemAnnotations(schema, v, S, body, ctx)
6975
+ for (const b of schema.allOf || []) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
6976
+ if (Array.isArray(schema.anyOf)) for (const b of schema.anyOf) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
6977
+ if (Array.isArray(schema.oneOf)) {
6978
+ body.push(`{const _n0=${S}.n,_x0=${S}.x===null?0:${S}.x.length;let _c=0;${schema.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(${v},${S}))_c++;`).join('')}if(_c!==1)_rbI(${S},_n0,_x0)}`)
6979
+ }
6980
+ if (schema.if !== undefined) {
6981
+ const thenCall = schema.then !== undefined ? `${emitAnnotI(schema.then, ctx)}(${v},${S})` : ''
6982
+ const elseCall = schema.else !== undefined ? `${emitAnnotI(schema.else, ctx)}(${v},${S})` : ''
6983
+ body.push(`if(${emitAnnotI(schema.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
6984
+ }
6985
+ if (typeof schema.$ref === 'string') body.push(`${emitAnnotI(refTargetFor(schema.$ref, ctx), ctx)}(${v},${S})`)
6986
+ const iv = `_ix${ui}`
6987
+ const ui2 = schema.unevaluatedItems
6988
+ if (ui2 === false) {
6989
+ const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)
6990
+ body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${failI};break}`)
6991
+ } else if (typeof ui2 === 'object' && ui2 !== null) {
6992
+ const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
6993
+ const sub = []
6994
+ genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
6995
+ if (sub.length) body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${sub.join('\n ')}\n }`)
6996
+ } else {
6997
+ throw DECLINE
6998
+ }
6999
+ lines.push(`if(Array.isArray(${v})){${body.join(';')}}`)
7000
+ }
7001
+
7002
+ function annItemHelpers(ctx) {
7003
+ if (ctx.annItemHelpers) return
7004
+ ctx.annItemHelpers = true
7005
+ ;(ctx.preamble || ctx.helperCode).push(
7006
+ 'function _aI(S,i){if(i<S.n)return;if(i===S.n){S.n=i+1;return}(S.x||(S.x=[])).push(i)}' +
7007
+ 'function _mrI(S,f,t){if(f>=t)return;if(f<=S.n){if(t>S.n)S.n=t;return}const x=S.x||(S.x=[]);for(let i=f;i<t;i++)x.push(i)}' +
7008
+ 'function _hI(S,i){if(i<S.n)return true;const x=S.x;if(x===null)return false;for(let j=0;j<x.length;j++)if(x[j]===i)return true;return false}' +
7009
+ 'function _rbI(S,n,l){S.n=n;if(S.x!==null)S.x.length=l}' +
7010
+ 'function _anIT(){return true}function _anIF(){return false}')
7011
+ }
7012
+
7013
+ // The positions a node's own keywords evaluate, marked on `S`, for an array.
7014
+ function addLocalItemAnnotations(node, v, S, out, ctx) {
7015
+ if (Array.isArray(node.items) || node.additionalItems !== undefined) throw DECLINE
7016
+ const prefix = Array.isArray(node.prefixItems) ? node.prefixItems.length : 0
7017
+ if (prefix) out.push(`_mrI(${S},0,Math.min(${v}.length,${prefix}))`)
7018
+ if (node.items !== undefined) out.push(`_mrI(${S},${prefix},${v}.length)`)
7019
+ if (node.contains !== undefined) {
7020
+ const cl = []
7021
+ nestedGenCode(node.contains, '_cv', cl, ctx)
7022
+ const ci = ctx.varCounter++
7023
+ ;(ctx.preamble || ctx.helperCode).push(`function _cI${ci}(_cv){${cl.join('\n ')}\n return true}`)
7024
+ out.push(`for(let _i=0;_i<${v}.length;_i++)if(_cI${ci}(${v}[_i]))_aI(${S},_i)`)
7025
+ }
7026
+ }
7027
+
7028
+ // The keywords an item annotation function evaluates through its children;
7029
+ // the rest of a subschema is checked by a plain verdict.
7030
+ const ANNOT_ITEM_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', '$ref', 'not', 'unevaluatedItems'])
7031
+
7032
+ // A function `(value, S) => valid` for one subschema, marking on S the array
7033
+ // positions it evaluated when it passes, and leaving S as it was when it does
7034
+ // not. A subschema with an unevaluatedItems of its own evaluates every
7035
+ // position once that passes, so it works on a record of its own and then
7036
+ // marks them all.
7037
+ function emitAnnotI(sub, ctx) {
7038
+ if (sub === true) return '_anIT'
7039
+ if (sub === false) return '_anIF'
7040
+ if (sub === null || typeof sub !== 'object') throw DECLINE
7041
+ if (!ctx.annFnsI) ctx.annFnsI = new Map()
7042
+ const hit = ctx.annFnsI.get(sub)
7043
+ if (hit !== undefined) {
7044
+ if (hit === null) throw DECLINE
7045
+ return hit
7046
+ }
7047
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) throw DECLINE
7048
+ if (Array.isArray(sub.items) || sub.additionalItems !== undefined) throw DECLINE
7049
+ // unevaluatedProperties reads the applicators too, which the local verdict
7050
+ // below leaves out.
7051
+ if (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== true) throw DECLINE
7052
+ ctx.annFnsI.set(sub, null)
7053
+ annItemHelpers(ctx)
7054
+ const name = `_ai${ctx.varCounter++}`
7055
+ const local = {}
7056
+ for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) local[k] = sub[k]
7057
+ const vl = []
7058
+ nestedGenCode(local, '_av', vl, ctx)
7059
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
7060
+ const body = [`if(!${name}_v(d))return false`]
7061
+ if (sub.not !== undefined) {
7062
+ const nl = []
7063
+ nestedGenCode(sub.not, '_nv', nl, ctx)
7064
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
7065
+ body.push(`if(${name}_n(d))return false`)
7066
+ }
7067
+ const own = sub.unevaluatedItems !== undefined && sub.unevaluatedItems !== true
7068
+ const R = own ? '_R' : 'S'
7069
+ body.push(own ? `const _R={n:0,x:null}` : `const _n0=S.n,_x0=S.x===null?0:S.x.length`)
7070
+ const failOut = own ? 'return false' : '{_rbI(S,_n0,_x0);return false}'
7071
+ const locals = []
7072
+ addLocalItemAnnotations(sub, 'd', R, locals, ctx)
7073
+ if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
7074
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
7075
+ if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_a=true;`).join('')}if(!_a)${failOut}}`)
7076
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_c++;`).join('')}if(_c!==1)${failOut}}`)
7077
+ if (sub.if !== undefined) {
7078
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
7079
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
7080
+ body.push(`if(${emitAnnotI(sub.if, ctx)}(d,${R})){${thenCheck}}else{${elseCheck}}`)
7081
+ }
7082
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnotI(refTargetFor(sub.$ref, ctx), ctx)}(d,${R}))${failOut}`)
7083
+ if (own) {
7084
+ const u = sub.unevaluatedItems
7085
+ if (u === false) {
7086
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i))return false}`)
7087
+ } else if (typeof u === 'object' && u !== null) {
7088
+ const ul = []
7089
+ nestedGenCode(u, '_uv', ul, ctx)
7090
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
7091
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i)&&!${name}_u(d[_i]))return false}`)
7092
+ } else {
7093
+ throw DECLINE
7094
+ }
7095
+ body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7096
+ }
7097
+ // `unevaluatedItems: true` evaluates every position once the subschema
7098
+ // has passed, with nothing to check; marked last, so a failure above has
7099
+ // nothing of it to undo.
7100
+ if (sub.unevaluatedItems === true) body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7101
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
7102
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
7103
+ ctx.annFnsI.set(sub, name)
7104
+ return name
5926
7105
  }
5927
7106
 
5928
7107
  function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
@@ -5952,6 +7131,41 @@ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix
5952
7131
  }
5953
7132
  }
5954
7133
 
7134
+ // The error generator's per-key loop (genAdditionalSchemaE), in the combined
7135
+ // generator: the subschema compiled by genCodeC at each additional key, with
7136
+ // that key's path. Patterns go through the same matchers patternProperties
7137
+ // uses here.
7138
+ function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
7139
+ const ki = ctx.varCounter++
7140
+ const kVar = `_ak${ki}`
7141
+ const known = Object.keys(schema.properties || {})
7142
+ const conds = []
7143
+ if (known.length) {
7144
+ const set = `_akn${ki}`
7145
+ ctx.closureVars.push(set)
7146
+ ctx.closureVals.push(new Set(known))
7147
+ conds.push(`!${set}.has(${kVar})`)
7148
+ }
7149
+ const pats = []
7150
+ for (const pat of Object.keys(schema.patternProperties || {})) {
7151
+ const fast = fastPrefixCheck(pat, kVar)
7152
+ if (fast) { pats.push(fast); continue }
7153
+ const ri = ctx.varCounter++
7154
+ ctx.closureVars.push(`_re${ri}`)
7155
+ ctx.closureVals.push(safeReClosure(ctx, pat))
7156
+ pats.push(`_re${ri}.test(${kVar})`)
7157
+ }
7158
+ if (pats.length) conds.push(`!(${pats.join('||')})`)
7159
+ const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
7160
+ const keep = conds.length ? `if(${conds.join('&&')})` : ''
7161
+ const pe = emitPtrEsc(ctx)
7162
+ const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
7163
+ const sub = []
7164
+ genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
7165
+ if (sub.length === 0) return
7166
+ lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
7167
+ }
7168
+
5955
7169
  function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
5956
7170
  let handledPropertyNames = false
5957
7171
  const ppEntries = Object.entries(schema.patternProperties)
@@ -6070,7 +7284,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6070
7284
  function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6071
7285
  const ki = ctx.varCounter++
6072
7286
  const p = pathExpr || '""'
6073
- lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
7287
+ lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
6074
7288
  }
6075
7289
 
6076
7290
  function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
@@ -6124,12 +7338,18 @@ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6124
7338
  }
6125
7339
 
6126
7340
  function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
7341
+ // The collapsed branch error the other engines report (ATA4001-4003),
7342
+ // built by the error generator's emitter; see compileToJSCombined.
7343
+ if (ctx.inCombined) return emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
6127
7344
  const fi = ctx.varCounter++
6128
7345
  const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
6129
7346
  lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
6130
7347
  }
6131
7348
 
6132
7349
  function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
7350
+ // The collapsed branch error the other engines report (ATA4001-4003),
7351
+ // built by the error generator's emitter; see compileToJSCombined.
7352
+ if (ctx.inCombined) return emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
6133
7353
  const fi = ctx.varCounter++
6134
7354
  const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
6135
7355
  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'")}}}`)