ata-validator 1.42.0 → 1.44.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.
@@ -176,7 +176,24 @@ const { compileSafe, patternIsSafe } = require('./safe-regex')
176
176
  let _nativeIsLinear = null
177
177
  // The linearity check only parses; it runs first so a pattern bound for the
178
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
+ }
179
195
  const useSafeEngine = (src) => {
196
+ if (PROP_ESCAPE.test(src)) return false
180
197
  if (_nativeIsLinear === null) _nativeIsLinear = require('./regex-linear').nativeIsLinear
181
198
  if (_nativeIsLinear(src)) return false
182
199
  return patternIsSafe(src)
@@ -187,7 +204,7 @@ const useSafeEngine = (src) => {
187
204
  // standalone embeds the engine), else a plain RegExp. Both expose `.test`.
188
205
  function safeReClosure (ctx, src) {
189
206
  if (useSafeEngine(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
190
- return new RegExp(src)
207
+ return new RegExp(src, reFlags(src))
191
208
  }
192
209
 
193
210
  // Compile a JSON Schema into a pure JS validator function.
@@ -228,7 +245,7 @@ function literalValue (lit) {
228
245
  }
229
246
 
230
247
  function unescapeSp (sp) {
231
- 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))
232
249
  }
233
250
 
234
251
  const FRAME_INLINE_MAX = 120
@@ -338,6 +355,7 @@ function compileToJS(schema, defs, schemaMap) {
338
355
  // A $ref cycle on one value recurses here as it does in generated code; see
339
356
  // hasSameInstanceRefCycle.
340
357
  if (!defs && hasSameInstanceRefCycle(schema)) return null
358
+ if (!defs && bothDefsContainers(schema)) return null
341
359
 
342
360
  // unevaluatedProperties / unevaluatedItems need annotation tracking this
343
361
  // path does not do. Decline so the schema reaches an engine that does.
@@ -589,7 +607,7 @@ function compileToJS(schema, defs, schemaMap) {
589
607
  }
590
608
  if (schema.pattern) {
591
609
  try {
592
- 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))
593
611
  checks.push((d) => typeof d !== 'string' || re.test(d))
594
612
  } catch {
595
613
  return null
@@ -1047,6 +1065,16 @@ function hasOwnKey (d, key) {
1047
1065
  return _hop.call(d, key)
1048
1066
  }
1049
1067
 
1068
+ // Writes an own key. A schema can name a property, a definition or even a
1069
+ // keyword "__proto__", and assigning that key sets the copy's prototype
1070
+ // instead: the property was gone from the copy, so a constraint on it was
1071
+ // generated as nothing and accepted anything (a schema with such a property
1072
+ // and a reference the resolver rewrites, 1.40.1 to 1.43.0).
1073
+ function setOwn (o, k, v) {
1074
+ if (k === '__proto__') Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
1075
+ else o[k] = v
1076
+ }
1077
+
1050
1078
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
1051
1079
  'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
1052
1080
 
@@ -1362,7 +1390,7 @@ function aliasChainEnds(def, defs) {
1362
1390
  // source of evaluated properties the schema has; when it does not, the
1363
1391
  // schema is not generated and the interpreter answers.
1364
1392
  function unevalPropsModeled(schema, schemaMap) {
1365
- const own = Object.assign({}, schema)
1393
+ const own = { ...schema }
1366
1394
  delete own.$defs
1367
1395
  delete own.definitions
1368
1396
  if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
@@ -1381,7 +1409,7 @@ function unevalPropsModeled(schema, schemaMap) {
1381
1409
  if (typeof sub !== 'object' || sub === null) return false
1382
1410
  const r = collectEvaluated(sub, schemaMap)
1383
1411
  if (r.dynamic || r.allProps) return false
1384
- const rest = Object.assign({}, sub)
1412
+ const rest = { ...sub }
1385
1413
  delete rest.patternProperties
1386
1414
  const j = JSON.stringify(rest)
1387
1415
  if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
@@ -1414,7 +1442,7 @@ function unevalPropsModeled(schema, schemaMap) {
1414
1442
  if (typeof schema.if !== 'object' || schema.if === null) return false
1415
1443
  const r = collectEvaluated(schema.if, schemaMap)
1416
1444
  if (r.dynamic || r.allProps || r.props.length) return false
1417
- const rest = Object.assign({}, schema.if)
1445
+ const rest = { ...schema.if }
1418
1446
  delete rest.patternProperties
1419
1447
  const j = JSON.stringify(rest)
1420
1448
  if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
@@ -1468,20 +1496,9 @@ function codegenSafe(schema, schemaMap) {
1468
1496
  }
1469
1497
 
1470
1498
  // Keys that collide with Object.prototype
1471
- if (schema.required) {
1472
- for (const k of schema.required) {
1473
- if (UNSAFE_KEYS.has(k)) return false
1474
- }
1475
- }
1476
- if (schema.properties) {
1477
- for (const k of Object.keys(schema.properties)) {
1478
- if (UNSAFE_KEYS.has(k)) return false
1479
- if (k === '$ref') return false // property named "$ref" — confusing
1480
- }
1481
- }
1499
+
1482
1500
 
1483
1501
  // Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
1484
- if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
1485
1502
 
1486
1503
  // $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
1487
1504
  if (schema.$ref) {
@@ -1516,17 +1533,11 @@ function codegenSafe(schema, schemaMap) {
1516
1533
  !resolvedTarget.propertyNames
1517
1534
  if (!targetSimple && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
1518
1535
  }
1519
- // In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
1520
- // (unevaluated schemas need $ref + siblings to work properly): without one, the
1521
- // generators emit the $ref and skip the rest of the node, so a sibling that
1522
- // validates would be dropped. Schema-organization keywords and annotations
1523
- // validate nothing and do not count. Annotations are listed rather than
1524
- // inferred from what is not a validation keyword, so a keyword this list
1525
- // does not know still declines instead of being skipped. Measured on the
1526
- // 977 SchemaStore schemas: a $ref with a description or title beside it sent
1527
- // 223 of them to the interpreted engine.
1528
- const siblings = Object.keys(schema).filter(k => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
1529
- 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.
1530
1541
  }
1531
1542
 
1532
1543
  // additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
@@ -1608,7 +1619,6 @@ function codegenSafe(schema, schemaMap) {
1608
1619
  // compiled to code that ignored it.
1609
1620
  if (schema.patternProperties) {
1610
1621
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
1611
- if (/\\[pP]\{/.test(pat)) return false
1612
1622
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
1613
1623
  }
1614
1624
  }
@@ -1617,10 +1627,8 @@ function codegenSafe(schema, schemaMap) {
1617
1627
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
1618
1628
  }
1619
1629
  }
1620
- if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null) {
1621
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
1622
- if (Object.keys(schema.propertyNames).some(k => k !== '$schema' && !supported.includes(k))) return false
1623
- }
1630
+ if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null &&
1631
+ !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return false
1624
1632
 
1625
1633
  // Recurse into sub-schemas — bail on boolean schemas in any position
1626
1634
  // A boolean subschema is a constant that every generator emits at its
@@ -1947,8 +1955,33 @@ function hasSameInstanceRefCycle(root) {
1947
1955
  return found
1948
1956
  }
1949
1957
 
1958
+ // The generators look a definition up by name in one table, `$defs` or else
1959
+ // `definitions`, whichever container the pointer named. With both at the root,
1960
+ // `#/definitions/x` read `$defs/x`: a draft 7 schema carrying both, with
1961
+ // different schemas under one name, rejected what it allowed and accepted
1962
+ // what it rejected. The interpreter resolves the pointer as written, so a
1963
+ // schema with both goes there. None of SchemaStore's 980 has both.
1964
+ function bothDefsContainers(schema) {
1965
+ return schema.$defs !== null && typeof schema.$defs === 'object' &&
1966
+ schema.definitions !== null && typeof schema.definitions === 'object'
1967
+ }
1968
+
1969
+ // The answer depends on the schema and the map alone, and the three
1970
+ // generators ask it about the same prepared schema in turn, so the last
1971
+ // answer is kept: each check walks the whole schema, and the second and third
1972
+ // asked again what the first had found. One entry, compared by identity,
1973
+ // because a WeakMap entry per schema cost more than the walk it saved on a
1974
+ // small schema that only ever gets its verdict generated.
1975
+ let _gateSchema = null, _gateMap = null, _gateOk = false
1950
1976
  function sharedCodegenGate(schema, schemaMap) {
1951
1977
  if (typeof schema !== 'object' || schema === null) return true
1978
+ if (schema === _gateSchema && schemaMap === _gateMap) return _gateOk
1979
+ const ok = sharedCodegenGateUncached(schema, schemaMap)
1980
+ _gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
1981
+ return ok
1982
+ }
1983
+ function sharedCodegenGateUncached(schema, schemaMap) {
1984
+ if (bothDefsContainers(schema)) return false
1952
1985
  if (hasSameInstanceRefCycle(schema)) return false
1953
1986
  if (!codegenSafe(schema, schemaMap)) return false
1954
1987
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
@@ -1963,6 +1996,15 @@ function sharedCodegenGate(schema, schemaMap) {
1963
1996
  // `type: 'string'` and annotations go, on a copy. Anything else is left as
1964
1997
  // written and the gate declines. Errors keep the interpreted engine's path.
1965
1998
  const PN_SUPPORTED = new Set(['maxLength', 'minLength', 'pattern', 'const', 'enum'])
1999
+ // Whether the generators can write `propertyNames` as checks on the key in the
2000
+ // object's own key loop. Any other subschema is applied to each key as a
2001
+ // value of its own (genPropertyNamesNode and its error and combined
2002
+ // counterparts), which is what the interpreter does.
2003
+ function isSimplePN (pn) {
2004
+ if (pn === null || typeof pn !== 'object' || Array.isArray(pn)) return false
2005
+ for (const k of Object.keys(pn)) if (k !== '$schema' && !PN_SUPPORTED.has(k)) return false
2006
+ return true
2007
+ }
1966
2008
  const PN_ANNOTATIONS = new Set(['$schema', '$comment', 'title', 'description', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
1967
2009
  function simplePropertyNames(pn, defs) {
1968
2010
  const seen = new Set()
@@ -1984,7 +2026,7 @@ function simplePropertyNames(pn, defs) {
1984
2026
  return null
1985
2027
  }
1986
2028
  if (!PN_SUPPORTED.has(k)) return null
1987
- out[k] = val
2029
+ setOwn(out, k, val)
1988
2030
  }
1989
2031
  return out
1990
2032
  }
@@ -2015,14 +2057,14 @@ function normalizePropertyNamesWalk(root) {
2015
2057
  return out
2016
2058
  }
2017
2059
  let out = node
2018
- const set = (k, v) => { if (out === node) out = { ...node }; out[k] = v }
2060
+ const set = (k, v) => { if (out === node) out = { ...node }; setOwn(out, k, v) }
2019
2061
  for (const k of SUBSCHEMA_MAPS) {
2020
2062
  const map = node[k]
2021
2063
  if (!map || typeof map !== 'object') continue
2022
2064
  let copy = map
2023
2065
  for (const [name, sub] of Object.entries(map)) {
2024
2066
  const w = walk(sub)
2025
- if (w !== sub) { if (copy === map) copy = { ...map }; copy[name] = w }
2067
+ if (w !== sub) { if (copy === map) copy = { ...map }; setOwn(copy, name, w) }
2026
2068
  }
2027
2069
  if (copy !== map) set(k, copy)
2028
2070
  }
@@ -2140,7 +2182,7 @@ function normalizeDeepRefsWalk(root) {
2140
2182
  if (inner) return node
2141
2183
  let out = node
2142
2184
  memo.set(node, out)
2143
- const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
2185
+ const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } setOwn(out, k, v) }
2144
2186
  if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
2145
2187
  for (const k of Object.keys(node)) {
2146
2188
  if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
@@ -2189,10 +2231,39 @@ function normalizeRefs(root, schemaMap) {
2189
2231
  return out
2190
2232
  }
2191
2233
  const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
2234
+ // A schema whose every reference is `#` or a plain `#/$defs/name` (or
2235
+ // `definitions`), with no `$id` or `$anchor` below the root to move the base
2236
+ // they resolve against, needs nothing from the resolver: each reference
2237
+ // already reads as the generators read it. Telling that from the text skips
2238
+ // loading the interpreted engine and indexing the schema, which was most of
2239
+ // what preparing a small schema for code generation cost.
2240
+ const LOCAL_ONLY_REF = /^#(?:\/(?:\$defs|definitions)\/[^/~%]+)?$/
2241
+ function refsAllPlainLocal (root, text) {
2242
+ const ids = text.split('"$id":').length - 1
2243
+ if (ids > (typeof root.$id === 'string' ? 1 : 0) || text.includes('"$anchor":')) return false
2244
+ const re = /"\$ref":("(?:[^"\\]|\\.)*")/g
2245
+ let m
2246
+ while ((m = re.exec(text)) !== null) {
2247
+ let ref
2248
+ try { ref = JSON.parse(m[1]) } catch { return false }
2249
+ if (!LOCAL_ONLY_REF.test(ref)) return false
2250
+ }
2251
+ return true
2252
+ }
2253
+
2254
+ // What the text of a schema prepared for code generation says about its size,
2255
+ // kept from the stringify normalizeRefsWalk makes anyway: its length when it
2256
+ // holds no `$ref`, -1 when it does. expandedChars counts at most 8 for each
2257
+ // character of a schema without references, so a short one is known not to
2258
+ // reach EXPANDED_SHARE without walking it again at every compile.
2259
+ const _refFreeLength = new WeakMap()
2192
2260
  function normalizeRefsWalk(root, schemaMap) {
2193
2261
  const text = JSON.stringify(root)
2194
- if (!text.includes('"$ref"')) return root
2262
+ const hasRef = text.includes('"$ref"')
2263
+ _refFreeLength.set(root, hasRef ? -1 : text.length)
2264
+ if (!hasRef) return root
2195
2265
  if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
2266
+ if (refsAllPlainLocal(root, text)) return root
2196
2267
  const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
2197
2268
  const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
2198
2269
  const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
@@ -2258,7 +2329,7 @@ function normalizeRefsWalk(root, schemaMap) {
2258
2329
  out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
2259
2330
  continue
2260
2331
  }
2261
- out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
2332
+ setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k]))
2262
2333
  }
2263
2334
  return out
2264
2335
  }
@@ -2276,6 +2347,55 @@ function prepareForCodegen(schema, schemaMap) {
2276
2347
  return normalizePropertyNames(normalizeRefs(schema, schemaMap))
2277
2348
  }
2278
2349
 
2350
+ const VERDICT_SPLIT = 64 * 1024
2351
+ // The schema's size with every local reference followed, each definition
2352
+ // sized once, annotations left out. A rough measure of the expanded verdict
2353
+ // source: on SchemaStore that runs about 1.4 times it at the median, and far
2354
+ // less where allOf merges. Past EXPANDED_SHARE the verdict is generated with
2355
+ // shared definitions from the start, instead of expanded first and thrown
2356
+ // away: SARIF comes to 2.1 million, and generating its 2.8 MB of expanded
2357
+ // source only to discard it was most of a cold first validate() (70 ms of the
2358
+ // 87 a command line run took to report one error). The bound is set where
2359
+ // only schemas that split anyway reach it: SARIF 2.1 measures about 706
2360
+ // thousand, while utcm-monitor (370 thousand) and accelerator (262 thousand)
2361
+ // stay expanded and keep their speed.
2362
+ const EXPANDED_SHARE = 400 * 1024
2363
+ const SIZE_SKIPS = new Set(['$defs', 'definitions', 'description', 'title', 'examples', 'default', '$comment', 'markdownDescription', 'deprecated', 'readOnly', 'writeOnly'])
2364
+ function expandedChars (root, limit) {
2365
+ const defs = (root && (root.$defs || root.definitions)) || {}
2366
+ const memo = new Map()
2367
+ const size = (n, stack) => {
2368
+ if (n === null || typeof n !== 'object') return 8
2369
+ let t = 2
2370
+ if (Array.isArray(n)) { for (const x of n) { t += size(x, stack) + 1; if (t > limit) return t } return t }
2371
+ for (const k of Object.keys(n)) {
2372
+ if (SIZE_SKIPS.has(k)) continue
2373
+ const v = n[k]
2374
+ if (k === '$ref' && typeof v === 'string') {
2375
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(v)
2376
+ if (m && defs[m[1]] !== undefined && !stack.has(m[1])) {
2377
+ let d = memo.get(m[1])
2378
+ if (d === undefined) { stack.add(m[1]); d = size(defs[m[1]], stack); stack.delete(m[1]); memo.set(m[1], d) }
2379
+ t += d
2380
+ if (t > limit) return t
2381
+ continue
2382
+ }
2383
+ }
2384
+ t += k.length + 3 + size(v, stack)
2385
+ if (t > limit) return t
2386
+ }
2387
+ return t
2388
+ }
2389
+ return size(root, new Set())
2390
+ }
2391
+ function sourceSize (lines, ctx) {
2392
+ let n = 0
2393
+ for (const l of lines) n += l.length
2394
+ for (const l of ctx.preamble) n += l.length
2395
+ if (ctx.deferredChecks) for (const l of ctx.deferredChecks) n += l.length
2396
+ return n
2397
+ }
2398
+
2279
2399
  function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2280
2400
  const inputSchema = schema
2281
2401
  schema = prepareForCodegen(schema, schemaMap)
@@ -2300,7 +2420,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2300
2420
  // patternProperties: bail only on boolean sub-schemas or unicode property escapes
2301
2421
  if (schema.patternProperties) {
2302
2422
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
2303
- if (/\\[pP]\{/.test(pat)) return null
2304
2423
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
2305
2424
  }
2306
2425
  }
@@ -2310,13 +2429,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2310
2429
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
2311
2430
  }
2312
2431
  }
2313
- // propertyNames: only codegen simple string constraints
2314
- if (schema.propertyNames) {
2315
- const pn = schema.propertyNames
2316
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
2317
- const keys = Object.keys(pn).filter(k => k !== '$schema')
2318
- if (keys.some(k => !supported.includes(k))) return null
2319
- }
2432
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
2320
2433
 
2321
2434
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
2322
2435
  const anchors = {}
@@ -2349,20 +2462,32 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2349
2462
  if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
2350
2463
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
2351
2464
 
2352
- const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
2353
- keywords: (opts && opts.keywords) || null, kwEmitted: null }
2354
- if (ctx.keywords) {
2355
- // Macro keywords expand into a schema of their own, which this generator
2356
- // does not apply; such a schema stays on the interpreted engine.
2357
- if (usesMacroKeyword(schema, schemaMap, ctx.keywords)) return null
2358
- ctx.kwEmitted = new Set()
2359
- }
2360
- const lines = []
2361
- try {
2362
- genCode(schema, 'd', lines, ctx)
2363
- } catch (e) {
2364
- if (e === DECLINE) return null
2365
- throw e
2465
+ // Macro keywords expand into a schema of their own, which this generator
2466
+ // does not apply; such a schema stays on the interpreted engine.
2467
+ if (opts && opts.keywords && usesMacroKeyword(schema, schemaMap, opts.keywords)) return null
2468
+ // Every reference is first expanded where it is used, which is the fastest
2469
+ // form for the checks. Past VERDICT_SPLIT of source the function is
2470
+ // generated again with each definition referenced from several places
2471
+ // written once, as a function in the preamble: expanded, SchemaStore's sarif
2472
+ // (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
2473
+ // half of the first validate() call.
2474
+ let ctx, lines
2475
+ const refFree = _refFreeLength.get(inputSchema)
2476
+ const shareFirst = refFree !== undefined && refFree >= 0 && refFree * 8 <= EXPANDED_SHARE
2477
+ ? false
2478
+ : expandedChars(schema, EXPANDED_SHARE) > EXPANDED_SHARE
2479
+ for (const share of shareFirst ? [true] : [false, true]) {
2480
+ 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,
2481
+ keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
2482
+ if (ctx.keywords) ctx.kwEmitted = new Set()
2483
+ lines = []
2484
+ try {
2485
+ genCode(schema, 'd', lines, ctx)
2486
+ } catch (e) {
2487
+ if (e === DECLINE) return null
2488
+ throw e
2489
+ }
2490
+ if (share || sourceSize(lines, ctx) <= VERDICT_SPLIT) break
2366
2491
  }
2367
2492
  // Every schema node that carries a custom keyword must have had its check
2368
2493
  // written. A path that inlined a node without going through genCodeNode
@@ -2396,10 +2521,10 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2396
2521
  closureValues.push(compileSafe(JSON.parse(safeMatch[2])))
2397
2522
  continue
2398
2523
  }
2399
- const match = code.match(/^const (_re\d+)=new RegExp\((.+)\)$/)
2524
+ const match = code.match(/^const (_re\d+)=new RegExp\((.+?)(?:,'(u)')?\)$/)
2400
2525
  if (match) {
2401
2526
  closureNames.push(match[1])
2402
- closureValues.push(new RegExp(JSON.parse(match[2])))
2527
+ closureValues.push(new RegExp(JSON.parse(match[2]), match[3] || ''))
2403
2528
  }
2404
2529
  }
2405
2530
 
@@ -2913,7 +3038,9 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2913
3038
  // $ref — guard against circular references
2914
3039
  // In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
2915
3040
  // Only when THIS schema has unevaluated keywords directly (not via $ref target)
2916
- const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
3041
+ // Any keyword beside $ref that validates is compiled after the reference
3042
+ // (2020-12 applies both; draft 7 drops such siblings when it is read).
3043
+ const hasSiblings = !!schema.$ref && Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
2917
3044
  if (schema.$ref) {
2918
3045
  if (genRefNode(schema, v, lines, ctx, knownType, hasSiblings)) return
2919
3046
  }
@@ -3121,7 +3248,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3121
3248
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
3122
3249
  ctx.usesSafeRe = true
3123
3250
  } else {
3124
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
3251
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
3125
3252
  }
3126
3253
  }
3127
3254
  const ri = ctx.regExpMap.get(pattern);
@@ -3398,6 +3525,13 @@ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
3398
3525
  const pn = schema.propertyNames
3399
3526
  const ki = ctx.varCounter++
3400
3527
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
3528
+ if (!isSimplePN(pn)) {
3529
+ const sub = []
3530
+ genCode(pn, '_pv', sub, ctx)
3531
+ if (sub.length === 0) return
3532
+ lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};for(const _k${ki} in ${v}){if(!_pnf${ki}(_k${ki}))return false}}`)
3533
+ return
3534
+ }
3401
3535
  lines.push(`${guard}{for(const _k${ki} in ${v}){`)
3402
3536
  if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
3403
3537
  if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
@@ -3643,6 +3777,31 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3643
3777
  }
3644
3778
  lines.push(`if(!${fnName}(${v}))return false`)
3645
3779
  if (!hasSiblings) return true
3780
+ } else if (ctx.shareDefs && !ctx.refStack.has(schema.$ref) && (ctx.sharedDefs || (ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs))).has(defName)) {
3781
+ // A shared definition off any cycle, as a plain function in the
3782
+ // preamble (see compileToJSCodegen): no guard, since it cannot re-enter
3783
+ // itself. Generated over its own `d` like the cyclic ones above.
3784
+ if (!ctx.sharedDefFns) ctx.sharedDefFns = new Map()
3785
+ let fnName = ctx.sharedDefFns.get(defName)
3786
+ if (!fnName) {
3787
+ fnName = '_dv' + ctx.sharedDefFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
3788
+ ctx.sharedDefFns.set(defName, fnName)
3789
+ const bodyLines = []
3790
+ const outerDeferred = ctx.deferredChecks
3791
+ ctx.deferredChecks = null
3792
+ ctx.refStack.add(schema.$ref)
3793
+ try {
3794
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
3795
+ } finally {
3796
+ ctx.refStack.delete(schema.$ref)
3797
+ }
3798
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
3799
+ ctx.deferredChecks = outerDeferred
3800
+ if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
3801
+ hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
3802
+ }
3803
+ lines.push(`if(!${fnName}(${v}))return false`)
3804
+ if (!hasSiblings) return true
3646
3805
  } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
3647
3806
  else {
3648
3807
  ctx.refStack.add(schema.$ref)
@@ -3695,7 +3854,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3695
3854
  function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3696
3855
  let handledPropertyNames = false
3697
3856
  const ppEntries = Object.entries(schema.patternProperties)
3698
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
3857
+ const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
3699
3858
  const pi = ctx.varCounter++
3700
3859
  const kVar = `_ppk${pi}`
3701
3860
 
@@ -4417,7 +4576,11 @@ const FORMAT_CODEGEN = {
4417
4576
  function esc(s) { return JSON.stringify(s).slice(1, -1).replace(/'/g, "\\'") }
4418
4577
  // A schema pointer segment for a pattern: JSON Pointer escaping, then made
4419
4578
  // safe for the single-quoted literal the error emitters use.
4420
- function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'") }
4579
+ // A pointer segment as text for a single-quoted literal. Control characters
4580
+ // and the two line separators go in as \u escapes: written raw, a property
4581
+ // named "foo\nbar" broke the literal, the combined function failed to compile
4582
+ // and every schema with such a name lost it.
4583
+ 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')) }
4421
4584
 
4422
4585
  // Resolve child path at codegen time when parent is a static string literal.
4423
4586
  // This enables frozen pre-allocation for ALL nested error objects.
@@ -4744,7 +4907,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4744
4907
  if (!sharedCodegenGate(schema, schemaMap)) return null
4745
4908
  if (schema.patternProperties) {
4746
4909
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
4747
- if (/\\[pP]\{/.test(pat)) return null
4748
4910
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
4749
4911
  }
4750
4912
  }
@@ -4753,12 +4915,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4753
4915
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
4754
4916
  }
4755
4917
  }
4756
- if (schema.propertyNames) {
4757
- const pn = schema.propertyNames
4758
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
4759
- const keys = Object.keys(pn).filter(k => k !== '$schema')
4760
- if (keys.some(k => !supported.includes(k))) return null
4761
- }
4918
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
4762
4919
 
4763
4920
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
4764
4921
  const eRootDefs = schema.$defs || schema.definitions || null
@@ -4941,6 +5098,50 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4941
5098
  // Emit codegen for oneOf/anyOf that runs each branch with error collection,
4942
5099
  // aggregates results, and uses __ataCollapse to surface a single best-branch
4943
5100
  // error. Wraps the existing per-branch evaluation; does not replace it.
5101
+ const BRANCH_VERDICT_MAX = 4096
5102
+
5103
+ // What undoing a trial emission needs: during one, the context's arrays only
5104
+ // grow and its maps and sets only gain keys (a reference pushed onto refStack
5105
+ // by a declined branch is such a key), so their lengths and sizes are enough,
5106
+ // and what was added is cut off the end, in insertion order. Copying them
5107
+ // instead made the trial quadratic: 297 oneOf and anyOf on apollo-router,
5108
+ // each copying a helper list thousands long.
5109
+ function snapshotCtx (ctx) {
5110
+ const out = new Map()
5111
+ for (const k of Object.keys(ctx)) {
5112
+ const x = ctx[k]
5113
+ if (Array.isArray(x)) out.set(k, { kind: 'a', ref: x, n: x.length })
5114
+ else if (x instanceof Map || x instanceof Set) out.set(k, { kind: 'c', ref: x, n: x.size })
5115
+ else out.set(k, { kind: 'v', ref: x })
5116
+ }
5117
+ return out
5118
+ }
5119
+ function restoreCtx (ctx, saved) {
5120
+ for (const k of Object.keys(ctx)) if (!saved.has(k)) delete ctx[k]
5121
+ for (const [k, e] of saved) {
5122
+ if (e.kind === 'a') { e.ref.length = Math.min(e.ref.length, e.n); ctx[k] = e.ref }
5123
+ else if (e.kind === 'c') {
5124
+ const extra = e.ref.size - e.n
5125
+ if (extra > 0) {
5126
+ const added = []
5127
+ let i = 0
5128
+ for (const key of e.ref.keys()) if (i++ >= e.n) added.push(key)
5129
+ for (const key of added) e.ref.delete(key)
5130
+ }
5131
+ ctx[k] = e.ref
5132
+ } else ctx[k] = e.ref
5133
+ }
5134
+ }
5135
+
5136
+ // Whether a node holds a reference anywhere below it.
5137
+ function holdsRef (node, seen = new Set()) {
5138
+ if (node === null || typeof node !== 'object' || seen.has(node)) return false
5139
+ seen.add(node)
5140
+ if (typeof node.$ref === 'string' || typeof node.$dynamicRef === 'string') return true
5141
+ for (const k of Object.keys(node)) if (holdsRef(node[k], seen)) return true
5142
+ return false
5143
+ }
5144
+
4944
5145
  function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
4945
5146
  ctx.usesBranchCollapse = true
4946
5147
  const fi = ctx.varCounter++
@@ -5004,6 +5205,88 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5004
5205
  const report = ctx.inCombined && !ctx.inBranchFn
5005
5206
  ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5006
5207
  : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5208
+ // The node's runtime unevaluatedProperties already counted how many of
5209
+ // these branches pass, through the annotation functions, which answer the
5210
+ // same validity. A oneOf with exactly one, an anyOf with any, holds and
5211
+ // reports nothing, so its branches are not run a second time to say so.
5212
+ // Only in the combined function proper, where that count is in scope.
5213
+ const counted = ctx.inCombined && !ctx.inBranchFn && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5214
+ const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
5215
+ let guard = counted !== undefined
5216
+ ? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
5217
+ : ''
5218
+ // More than one oneOf branch passed: the error names them and carries no
5219
+ // branch errors, so it is built from the bits the count left.
5220
+ if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5221
+ const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5222
+ 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))`
5223
+ }
5224
+ // Without such a count, each branch also gets a verdict function, and the
5225
+ // passing branches are counted with those: a oneOf with exactly one and an
5226
+ // anyOf with any hold, and a oneOf with more than one is reported from the
5227
+ // bits, so the error-collecting branches run only when no branch passes
5228
+ // and the collapse needs their errors to pick the closest. Running them to
5229
+ // learn the count built a result object and an error list per branch,
5230
+ // which made a oneOf matched twice cost 69 ns where counting costs about
5231
+ // 12. A branch the verdict generator declines keeps the old path.
5232
+ let vfArr = null
5233
+ // Branches that hold a reference are left to the error branches: their
5234
+ // verdicts would expand every definition again, as a second copy, where the
5235
+ // error branches call the shared one.
5236
+ if (guard === '' && !branches.some((b) => holdsRef(b))) {
5237
+ // A trial: the generators are not exception-safe on the context (a
5238
+ // reference left on refStack by a declined branch made the next error
5239
+ // function write that branch as nothing, which read as valid), so the
5240
+ // context is put back as it was when the verdicts decline.
5241
+ const saved = snapshotCtx(ctx)
5242
+ try {
5243
+ const vfs = branches.map((sub) => {
5244
+ if (sub === true) return 'function(){return true}'
5245
+ if (sub === false) return 'function(){return false}'
5246
+ const bl = []
5247
+ nestedGenCode(sub, '_bv', bl, ctx)
5248
+ return bl.length === 0 ? 'function(){return true}' : `function(_bv){${bl.join(';')};return true}`
5249
+ })
5250
+ // Only while the verdicts are small: they are a second copy of the
5251
+ // branches, with every reference expanded, and on large branches that
5252
+ // copy cost more to compile than counting saves (apollo-router's error
5253
+ // function grew from 1.5 to 2.5 MB of source).
5254
+ if (!vfs.some((b) => b.includes(SP_PLACEHOLDER)) && vfs.reduce((n, b) => n + b.length, 0) <= BRANCH_VERDICT_MAX) {
5255
+ // One constant per branch, called directly: through an array the one
5256
+ // call site sees every branch and none of them is inlined.
5257
+ vfArr = `_bvf${fi}`
5258
+ ctx.helperCode.push(vfs.map((b, i) => `const ${vfArr}_${i}=${b}`).join(';'))
5259
+ } else {
5260
+ restoreCtx(ctx, saved)
5261
+ }
5262
+ } catch (e) {
5263
+ if (e !== DECLINE) throw e
5264
+ restoreCtx(ctx, saved)
5265
+ }
5266
+ }
5267
+ if (vfArr !== null) {
5268
+ const L = branches.length
5269
+ const top = ctx.inCombined && !ctx.inBranchFn
5270
+ // An accepted document is still answered by the verdict first (see the
5271
+ // result shape in compileToJSCombined): that is the fastest path to it,
5272
+ // and dropping it measured 20 ns slower per accepted oneOf.
5273
+ if (top) ctx.unguardedCollapse = true
5274
+ const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
5275
+ if (keyword === 'anyOf') {
5276
+ 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}}}`)
5277
+ } else {
5278
+ const mk = L <= 30
5279
+ const collect = mk
5280
+ ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5281
+ : ''
5282
+ const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5283
+ const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5284
+ const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5285
+ 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}}}}`)
5286
+ }
5287
+ return
5288
+ }
5289
+ if (guard === '' && ctx.inCombined && !ctx.inBranchFn) ctx.unguardedCollapse = true
5007
5290
  if (keyword === 'anyOf') {
5008
5291
  // anyOf reports nothing once a branch passes, so the branches are first
5009
5292
  // only asked whether they pass. They are called with `_all` set to 0, a
@@ -5017,12 +5300,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5017
5300
  // no one read. Only when no branch passes do they run again with the
5018
5301
  // caller's `_all`, and the collapse sees them all, as before.
5019
5302
  const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
5020
- lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5303
+ 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}}}`)
5021
5304
  } else {
5022
5305
  // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
5023
5306
  // the passing branches and answers; the branch errors of a probe are not
5024
5307
  // errors anyone reads and are not collapsed.
5025
- lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
5308
+ 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}}}`)
5026
5309
  }
5027
5310
  }
5028
5311
 
@@ -5259,7 +5542,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5259
5542
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5260
5543
  ctx.usesSafeRe = true
5261
5544
  } else {
5262
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5545
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5263
5546
  }
5264
5547
  }
5265
5548
  const ri = ctx.regExpMap.get(pattern);
@@ -5334,7 +5617,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5334
5617
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5335
5618
  ctx.usesSafeRe = true
5336
5619
  } else {
5337
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5620
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5338
5621
  }
5339
5622
  }
5340
5623
  patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
@@ -5365,6 +5648,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5365
5648
  // whose evaluated set depends on its own branches would report the wrong
5366
5649
  // errors, and the collapse would pick the wrong closest branch; declined.
5367
5650
  if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
5651
+ if (ctx.inCombined && schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true && !unevalLocalOk(schema, 'unevaluatedItems')) throw DECLINE
5368
5652
  if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
5369
5653
 
5370
5654
  // unevaluatedItems: false under the same rule. The interpreted engine
@@ -5497,7 +5781,7 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
5497
5781
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5498
5782
  ctx.usesSafeRe = true
5499
5783
  } else {
5500
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5784
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5501
5785
  }
5502
5786
  }
5503
5787
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
@@ -5529,7 +5813,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5529
5813
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5530
5814
  ctx.usesSafeRe = true
5531
5815
  } else {
5532
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5816
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5533
5817
  }
5534
5818
  }
5535
5819
  const ri = ctx.regExpMap.get(pattern);
@@ -5559,7 +5843,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5559
5843
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5560
5844
  ctx.usesSafeRe = true
5561
5845
  } else {
5562
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5846
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5563
5847
  }
5564
5848
  }
5565
5849
  patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
@@ -5596,6 +5880,13 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5596
5880
  const pn = schema.propertyNames
5597
5881
  const ki = ctx.varCounter++
5598
5882
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
5883
+ // The subschema checks the key as a value, and its errors are reported at
5884
+ // the object, as the interpreter reports them.
5885
+ if (!isSimplePN(pn)) {
5886
+ genCodeE(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
5887
+ lines.push(`}}`)
5888
+ return
5889
+ }
5599
5890
  if (pn.minLength !== undefined) {
5600
5891
  lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5601
5892
  }
@@ -5611,7 +5902,7 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5611
5902
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5612
5903
  ctx.usesSafeRe = true
5613
5904
  } else {
5614
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5905
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5615
5906
  }
5616
5907
  }
5617
5908
  const ri = ctx.regExpMap.get(pattern);
@@ -5733,8 +6024,11 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
5733
6024
  const { errorSite } = require('./enrich-site')
5734
6025
  const siteVar = `_S${ctx.varCounter++}`
5735
6026
  ctx.closureVars.push(siteVar)
5736
- ctx.closureVals.push(errorSite(keyword, unescapeSp(sp), paramsVal, fmt))
5737
- return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d))`
6027
+ // Inside a shared definition (emitSharedDefC) the site holds the path past
6028
+ // the definition and the reference's own comes in as `_sp`.
6029
+ const rel = sp.startsWith(SP_PLACEHOLDER)
6030
+ ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
6031
+ return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
5738
6032
  }
5739
6033
 
5740
6034
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -5743,15 +6037,12 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5743
6037
  // Same provably-local rule as the error generator above.
5744
6038
  if (typeof schema === 'object' && schema !== null) {
5745
6039
  const s = JSON.stringify(schema)
5746
- // unevaluatedProperties that is not provably local is built at run time
5747
- // (genUnevaluatedDynamicC); unevaluatedItems still has to be local.
5748
- if (s.includes('unevaluatedItems') && !unevalAllProvablyLocal(schema, 'items')) return null
5749
- // Bail on self-referencing schemas — combined codegen doesn't support recursion
5750
- if (s.includes('"$ref":"#"')) return null
5751
- // Defs on a $ref cycle compile as named functions in the boolean and
5752
- // errors generators; this generator has no preamble for them, so it
5753
- // declines and the router uses the errors generator.
5754
- if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
6040
+ // unevaluatedProperties and unevaluatedItems that are not provably local
6041
+ // are worked out at run time (genUnevaluatedDynamicC,
6042
+ // genUnevaluatedItemsDynamicC).
6043
+ // A schema that references itself, through `$ref: "#"` or a definition on
6044
+ // a cycle, has those references written as calls under the cycle guard
6045
+ // (emitDefFnC).
5755
6046
  // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
5756
6047
  // Letting genCodeC emit the legacy "must match exactly one" placeholder
5757
6048
  // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
@@ -5775,7 +6066,6 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5775
6066
  if (!sharedCodegenGate(schema, schemaMap)) return null
5776
6067
  if (schema.patternProperties) {
5777
6068
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
5778
- if (/\\[pP]\{/.test(pat)) return null
5779
6069
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5780
6070
  }
5781
6071
  }
@@ -5784,12 +6074,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5784
6074
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5785
6075
  }
5786
6076
  }
5787
- if (schema.propertyNames) {
5788
- const pn = schema.propertyNames
5789
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
5790
- const keys = Object.keys(pn).filter(k => k !== '$schema')
5791
- if (keys.some(k => !supported.includes(k))) return null
5792
- }
6077
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
5793
6078
 
5794
6079
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
5795
6080
  const cRootDefs = schema.$defs || schema.definitions || null
@@ -5825,7 +6110,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5825
6110
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5826
6111
 
5827
6112
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
5828
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6113
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5829
6114
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5830
6115
  const lines = []
5831
6116
  try {
@@ -5851,10 +6136,15 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5851
6136
  const closureParams = closureNames.join(',')
5852
6137
  // A definition the error generator wrote as a named function inside a
5853
6138
  // branch (one on a cycle, or referenced from several places) reads the
5854
- // cycle-guard state its own function declares, which this one does not:
5855
- // the first document to reach it threw a ReferenceError. Declined; the
5856
- // error function, which has that state, answers instead.
5857
- if (ctx.usesBranchCollapse && ctx.defFns && ctx.defFns.size) return null
6139
+ // cycle-guard state the error function declares. Here that state is
6140
+ // declared too, and the checks run as the error function runs them: in a
6141
+ // body of their own, again in the guarded mode when data turns out to
6142
+ // point back at itself. This used to decline, which left every schema
6143
+ // whose oneOf or anyOf branches reach a shared definition without one
6144
+ // pass.
6145
+ const guardList = [...(ctx.defFns ? ctx.defFns.values() : []), ...(ctx.cycFnsC || [])]
6146
+ const guardDefs = guardList.length ? guardList : null
6147
+ if (guardDefs) ctx.helperCode.unshift(emitGuardState())
5858
6148
  // Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
5859
6149
  // the branch functions (this function collects everything), and the
5860
6150
  // runtime shape the error generator gives its literals, without code and
@@ -5865,12 +6155,23 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5865
6155
  for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
5866
6156
  for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
5867
6157
  }
5868
- const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5869
6158
  // Lazy error array: no allocation for valid data (the common case)
5870
6159
  // resultShape: validate() itself for a validator whose errors are being read
5871
6160
  // (see validator-core): the result objects built here, so one pass decides,
5872
6161
  // collects and answers, with nothing between the caller and the checks.
5873
6162
  const rs = opts && opts.resultShape
6163
+ // Counts shared between a node's unevaluatedProperties and its anyOf and
6164
+ // oneOf (genUnevaluatedDynamicC), visible to every line of the function.
6165
+ let hoistDecl = ctx.hoisted.length ? `let ${ctx.hoisted.map((n) => n + '=null').join(',')};` : ''
6166
+ let checks = lines.join('\n ')
6167
+ let allDecl = ctx.usesBranchCollapse ? 'const _all=true;' : ''
6168
+ if (guardDefs) {
6169
+ const clears = ['_stk.clear()'].concat(guardDefs.map((n) => `${n}_s.clear()`)).join(';')
6170
+ ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
6171
+ hoistDecl = ''
6172
+ allDecl = ''
6173
+ checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};try{_e=_bodyC(d)}finally{_sg=false}}`
6174
+ }
5874
6175
  // In the result shape this function is validate() itself, so a document
5875
6176
  // that makes it throw is answered by the caller's previous path instead
5876
6177
  // (`_FB`). A thrown error reaching the caller is the one thing this form
@@ -5879,12 +6180,16 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5879
6180
  // With collapsed branches, collecting runs every branch of every oneOf
5880
6181
  // and anyOf in full, which an accepted document does not need: the
5881
6182
  // verdict answers it first.
5882
- ? `try{${ctx.usesBranchCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};const _all=true;' : ''}let _e;\n ` + lines.join('\n ') +
6183
+ // Not when every collapse is guarded by a count (emitBranchCollapse): an
6184
+ // accepted document then runs no branch twice, and a rejected one would
6185
+ // pay for the verdict for nothing.
6186
+ ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;\n ` + checks +
5883
6187
  // A collapsed branch error is presented even alone (its ordinal and
5884
6188
  // raw branch errors stay inside); otherwise only a list of two or more
5885
6189
  // needs ordering.
5886
6190
  `\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
5887
- : (ctx.usesBranchCollapse ? 'const _all=true;' : '') + `let _e;\n ` + lines.join('\n ') + `\n return _e?{valid:false,errors:_e}:R`
6191
+ : hoistDecl + allDecl + `let _e;\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6192
+ const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5888
6193
 
5889
6194
  try {
5890
6195
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
@@ -5921,9 +6226,12 @@ function typeMismatchRest(schema, types) {
5921
6226
  for (const k of Object.keys(schema)) {
5922
6227
  if (drop.has(k)) continue
5923
6228
  if (rest === null) rest = {}
5924
- rest[k] = schema[k]
6229
+ setOwn(rest, k, schema[k])
5925
6230
  }
5926
6231
  if (rest === null) return null
6232
+ // A node whose reference was already written (genCodeCNode) keeps that
6233
+ // mark, or the reference is written again here and its errors twice.
6234
+ if (schema[REF_DONE]) Object.defineProperty(rest, REF_DONE, { value: true })
5927
6235
  for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
5928
6236
  return null
5929
6237
  }
@@ -5931,6 +6239,68 @@ function typeMismatchRest(schema, types) {
5931
6239
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5932
6240
  return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
5933
6241
  }
6242
+ // A definition referenced from several places, written once as a function
6243
+ // and called at each reference, instead of its whole code at every one: the
6244
+ // combined function of SchemaStore's cloud-run (48 KB of schema) came to
6245
+ // 1.1 MB of source and sarif's (79 KB) to 2.8 MB, and compiling those was most
6246
+ // of the 50 to 300 ms a first rejection with its errors read took. The body is
6247
+ // generated against a placeholder schema path that becomes the `_sp` argument,
6248
+ // as the error generator does (genCodeENode), so an error names the path it
6249
+ // was reached by. It collects into its own list, returned to the caller, and
6250
+ // declares its own branch counts (genUnevaluatedDynamicC). The document root
6251
+ // comes in as `d` for the rich errors that read it. Not for a definition on a
6252
+ // reference cycle, which keeps its existing path. False when not shared.
6253
+ function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
6254
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
6255
+ if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
6256
+ const cyclic = ctx.cyclicDefs.has(defName)
6257
+ if (!cyclic && !ctx.sharedDefs.has(defName)) return false
6258
+ emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6259
+ return true
6260
+ }
6261
+
6262
+ // One schema as a function of its own in the combined function (see
6263
+ // emitSharedDefC). The name is registered before the body is written, so a
6264
+ // reference back to it inside becomes a call. One that can re-enter itself,
6265
+ // a definition on a cycle or the root through `$ref: "#"`, goes through the
6266
+ // cycle guard the error generator's definitions use: depth counted, and in
6267
+ // the guarded pass a value already being checked is not checked again.
6268
+ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix) {
6269
+ if (!ctx.defFnsC) ctx.defFnsC = new Map()
6270
+ let fnName = ctx.defFnsC.get(key)
6271
+ if (!fnName) {
6272
+ fnName = '_defC' + ctx.defFnsC.size + '_' + (key === '#' ? 'root' : key.replace(/[^A-Za-z0-9_]/g, '_'))
6273
+ ctx.defFnsC.set(key, fnName)
6274
+ const outerHoisted = ctx.hoisted
6275
+ ctx.hoisted = []
6276
+ const bodyLines = []
6277
+ let hoisted
6278
+ try {
6279
+ genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
6280
+ } finally {
6281
+ hoisted = ctx.hoisted
6282
+ ctx.hoisted = outerHoisted
6283
+ }
6284
+ let body = bodyLines.join('\n ')
6285
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
6286
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
6287
+ const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
6288
+ const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6289
+ if (cyclic) {
6290
+ if (!ctx.cycFnsC) ctx.cycFnsC = []
6291
+ ctx.cycFnsC.push(fnName)
6292
+ ctx.helperCode.push(
6293
+ `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d){\n ` +
6294
+ `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 ` +
6295
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d)\n _sd--\n return _r\n }\n ` + fn)
6296
+ } else {
6297
+ ctx.helperCode.push(fn)
6298
+ }
6299
+ }
6300
+ 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])}}`)
6301
+ }
6302
+
6303
+ const REF_DONE = Symbol('ata.refDone')
5934
6304
  function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5935
6305
  if (!schemaPrefix) schemaPrefix = '#'
5936
6306
  // Per-node, for the reason spelled out in genCode.
@@ -5945,18 +6315,34 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5945
6315
  if (typeof schema !== 'object' || schema === null) return
5946
6316
 
5947
6317
  // $ref — resolve local, anchor, and cross-schema refs
5948
- if (schema.$ref) {
5949
- // The reference is followed and the rest of the node is not compiled, so
5950
- // a node with a keyword beside its $ref that validates (2020-12 applies
5951
- // both) declines: compiled, the sibling was dropped and a document it
5952
- // rejects was accepted. Only reachable since unevaluatedProperties next
5953
- // to a $ref stopped declining the whole function.
5954
- for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
5955
- // Self-reference "#" — no-op (permissive) to avoid infinite recursion
5956
- if (schema.$ref === '#') return
6318
+ if (schema.$ref && !schema[REF_DONE]) {
6319
+ // A keyword beside $ref that validates applies too (2020-12; draft 7
6320
+ // drops such siblings when the schema is read). The reference is written
6321
+ // on its own, then the node again with the reference marked done, on a
6322
+ // fresh copy so the mark cannot reach the same schema object met through
6323
+ // another path; the copy keeps `$ref` for unevaluatedProperties and
6324
+ // unevaluatedItems, which read its annotations. Dropping the siblings
6325
+ // once accepted what they reject, which is why this declined. Not in a
6326
+ // branch function, which the error generator writes.
6327
+ if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
6328
+ if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
6329
+ genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
6330
+ const rest = { ...schema }
6331
+ Object.defineProperty(rest, REF_DONE, { value: true })
6332
+ genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
6333
+ return
6334
+ }
6335
+ // A reference back into what is being expanded is a call to that
6336
+ // schema's function (emitDefFnC), never nothing: an expansion that stops
6337
+ // and emits no lines reads as valid.
6338
+ if (schema.$ref === '#') {
6339
+ if (ctx.inCombined && !ctx.inBranchFn && ctx.rootC) { emitDefFnC('#', ctx.rootC, true, v, pathExpr, lines, ctx, schemaPrefix); return }
6340
+ throw DECLINE
6341
+ }
5957
6342
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5958
6343
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
5959
- if (ctx.refStack.has(schema.$ref)) return
6344
+ if (ctx.inCombined && !ctx.inBranchFn && emitSharedDefC(m[1], v, pathExpr, lines, ctx, schemaPrefix)) return
6345
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5960
6346
  ctx.refStack.add(schema.$ref)
5961
6347
  genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
5962
6348
  ctx.refStack.delete(schema.$ref)
@@ -5967,7 +6353,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5967
6353
  const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
5968
6354
  const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
5969
6355
  if (anchorTarget) {
5970
- if (ctx.refStack.has(schema.$ref)) return
6356
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5971
6357
  ctx.refStack.add(schema.$ref)
5972
6358
  genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
5973
6359
  ctx.refStack.delete(schema.$ref)
@@ -5975,7 +6361,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5975
6361
  }
5976
6362
  }
5977
6363
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
5978
- if (ctx.refStack.has(schema.$ref)) return
6364
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5979
6365
  ctx.refStack.add(schema.$ref)
5980
6366
  genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
5981
6367
  ctx.refStack.delete(schema.$ref)
@@ -5985,7 +6371,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5985
6371
  if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
5986
6372
  const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
5987
6373
  if (r) {
5988
- if (ctx.refStack.has(schema.$ref)) return
6374
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5989
6375
  ctx.refStack.add(schema.$ref)
5990
6376
  genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
5991
6377
  ctx.refStack.delete(schema.$ref)
@@ -6201,7 +6587,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6201
6587
  const ri = ctx.varCounter++
6202
6588
  const reVar = `_re${ri}`
6203
6589
  ctx.closureVars.push(reVar)
6204
- ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
6590
+ ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern)))
6205
6591
  const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
6206
6592
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
6207
6593
  }
@@ -6297,7 +6683,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6297
6683
  // unevaluatedItems: false under the same rule: one error for the array,
6298
6684
  // the first uncovered position as the limit. Adjacent schema-form `items`
6299
6685
  // evaluates everything; the draft-07 array form acts as the prefix.
6300
- if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6686
+ if (schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true) {
6687
+ if (unevalLocalOk(schema, 'unevaluatedItems')) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6688
+ else genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6689
+ }
6301
6690
 
6302
6691
  // dependentRequired
6303
6692
  if (schema.dependentRequired) {
@@ -6447,17 +6836,17 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6447
6836
  if (schema.additionalProperties !== undefined) return
6448
6837
  const ui = ctx.varCounter++
6449
6838
  const S = `_ev${ui}`
6450
- const body = [`const ${S}=new Set()`]
6839
+ const body = [`const ${S}=[]`]
6451
6840
  addLocalAnnotations(schema, v, S, body, ctx)
6452
- addApplicatorAnnotations(schema, v, S, body, ctx, true)
6841
+ addTopApplicatorAnnotations(schema, v, S, body, ctx)
6453
6842
  const kVar = `_uk${ui}`
6454
6843
  const up = schema.unevaluatedProperties
6455
6844
  if (up === false) {
6456
- body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar}))return false`)
6845
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
6457
6846
  } else if (typeof up === 'object' && up !== null) {
6458
6847
  const sub = []
6459
6848
  genCode(up, `${v}[${kVar}]`, sub, ctx)
6460
- if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
6849
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
6461
6850
  } else {
6462
6851
  throw DECLINE
6463
6852
  }
@@ -6470,36 +6859,56 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6470
6859
  // count whether or not their values pass; an allOf, anyOf or dependentSchemas
6471
6860
  // branch, an if, then or else, or a $ref target counts only when it passes,
6472
6861
  // a oneOf only when exactly one branch passes, and `not` never. Each branch is
6473
- // asked through an annotation function (emitAnnot), which validates first and
6474
- // only then adds what it evaluated, so nothing has to be rolled back.
6862
+ // asked through an annotation function (emitAnnot).
6475
6863
  function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6476
6864
  // additionalProperties evaluates every key the others do not, pass or fail.
6477
6865
  if (schema.additionalProperties !== undefined) return
6478
6866
  const ui = ctx.varCounter++
6479
6867
  const S = `_ev${ui}`
6480
- const body = [`const ${S}=new Set()`]
6868
+ const body = [`const ${S}=[]`]
6481
6869
  addLocalAnnotations(schema, v, S, body, ctx)
6482
- addApplicatorAnnotations(schema, v, S, body, ctx, true)
6870
+ // The anyOf and oneOf counts are handed to the keywords' own error code
6871
+ // (emitBranchCollapse), with the object they were counted for: they are
6872
+ // declared once at the top of the combined function, where every later
6873
+ // line sees them, and read only when that object is the value at hand, so
6874
+ // a count left from another value, another loop iteration or another place
6875
+ // is never taken for this one. Only in the combined function proper.
6876
+ const share = ctx.inCombined && !ctx.inBranchFn && ctx.hoisted !== undefined &&
6877
+ (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf))
6878
+ const counts = share ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: `_om${ui}`, of: `_ao${ui}` } : null
6879
+ if (share) {
6880
+ ctx.hoisted.push(counts.anyOf, counts.oneOf, counts.oneOfMask, counts.of)
6881
+ body.push(`${counts.of}=${v};${counts.oneOfMask}=0`)
6882
+ if (!ctx.branchCounts) ctx.branchCounts = new Map()
6883
+ if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
6884
+ if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
6885
+ }
6886
+ addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
6483
6887
  const kVar = `_uk${ui}`
6484
6888
  const up = schema.unevaluatedProperties
6485
6889
  if (up === false) {
6486
- body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
6890
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
6487
6891
  } else if (typeof up === 'object' && up !== null) {
6488
6892
  const pe = emitPtrEsc(ctx)
6489
6893
  const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
6490
6894
  const sub = []
6491
6895
  genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
6492
- if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
6896
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
6493
6897
  } else {
6494
6898
  throw DECLINE
6495
6899
  }
6496
6900
  lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
6497
6901
  }
6498
6902
 
6499
- // The names a node's own keywords evaluate, added to `S`: present declared
6500
- // properties, keys matching a pattern.
6903
+ // The names a node's own keywords evaluate, pushed onto the array `S`:
6904
+ // present declared properties, keys matching a pattern, and every key when
6905
+ // additionalProperties is there.
6501
6906
  function addLocalAnnotations(node, v, S, out, ctx) {
6502
- for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.add(${JSON.stringify(k)})`)
6907
+ if (node.additionalProperties !== undefined) {
6908
+ out.push(`for(const _ka in ${v})${S}.push(_ka)`)
6909
+ return
6910
+ }
6911
+ for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
6503
6912
  const pats = Object.keys(node.patternProperties || {})
6504
6913
  if (pats.length) {
6505
6914
  const tests = []
@@ -6512,24 +6921,31 @@ function addLocalAnnotations(node, v, S, out, ctx) {
6512
6921
  ctx.closureVals.push(safeReClosure(ctx, pat))
6513
6922
  tests.push(`_re${ri}.test(${kv})`)
6514
6923
  }
6515
- out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.add(${kv})`)
6924
+ out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
6516
6925
  }
6517
6926
  }
6518
6927
 
6519
- // What a node's applicators add to `S`. At the node that owns the
6520
- // unevaluatedProperties (`top`), a oneOf counts only with exactly one passing
6521
- // branch; inside a branch that passed, its oneOf has exactly one.
6522
- function addApplicatorAnnotations(node, v, S, out, ctx, top) {
6928
+ // What the applicators of the node that owns the unevaluatedProperties add.
6929
+ // Their validity is checked by the code around this; here a branch only
6930
+ // contributes, through its annotation function, when it passes, and a oneOf
6931
+ // only with exactly one passing branch. An annotation function that fails
6932
+ // leaves `S` as it found it.
6933
+ function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
6934
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
6523
6935
  for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6524
- for (const b of node.anyOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6936
+ // With `counts`, the passing branches of anyOf and oneOf are counted into
6937
+ // variables the caller declared, so the keyword's own error code further
6938
+ // down can skip its branches when the count already says it holds (see
6939
+ // emitBranchCollapse).
6940
+ if (Array.isArray(node.anyOf)) {
6941
+ if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
6942
+ else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6943
+ }
6525
6944
  if (Array.isArray(node.oneOf)) {
6526
- if (top) {
6527
- const t = `_one${ctx.varCounter++}`
6528
- const calls = node.oneOf.map((b, i) => `{const _s=new Set();if(${emitAnnot(b, ctx)}(${v},_s)){${t}c++;${t}s=_s}}`).join('')
6529
- out.push(`{let ${t}c=0,${t}s=null;${calls}if(${t}c===1)for(const _x of ${t}s)${S}.add(_x)}`)
6530
- } else {
6531
- for (const b of node.oneOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6532
- }
6945
+ const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
6946
+ // Which branches passed, as bits, for a collapse that has to name them.
6947
+ const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
6948
+ 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)}`)
6533
6949
  }
6534
6950
  if (node.if !== undefined) {
6535
6951
  const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
@@ -6540,7 +6956,6 @@ function addApplicatorAnnotations(node, v, S, out, ctx, top) {
6540
6956
  for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
6541
6957
  }
6542
6958
  if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
6543
- if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
6544
6959
  }
6545
6960
 
6546
6961
  function refTargetFor(ref, ctx) {
@@ -6550,44 +6965,236 @@ function refTargetFor(ref, ctx) {
6550
6965
  return t
6551
6966
  }
6552
6967
 
6553
- // A function `(value, S) => valid` for one subschema: the subschema's verdict,
6554
- // and when it passes, what it evaluated added to S. One per subschema object.
6968
+ // The keywords an annotation function evaluates itself, through its children;
6969
+ // the rest of a subschema is checked by a plain verdict.
6970
+ const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
6971
+
6972
+ // A function `(value, S) => valid` for one subschema: whether it passes, and
6973
+ // when it does, what it evaluated pushed onto S; when it does not, S is left
6974
+ // as it was. Validity is worked out bottom up: the subschema's own keywords
6975
+ // by a verdict of the subschema without its applicators, each applicator from
6976
+ // its branches' annotation functions. Asking each branch for a full verdict
6977
+ // and then again for its annotations repeated every nested branch once per
6978
+ // level, which made nested references the slowest shape there was.
6555
6979
  function emitAnnot(sub, ctx) {
6556
6980
  if (sub === true) return '_anT'
6557
6981
  if (sub === false) return '_anF'
6558
6982
  if (sub === null || typeof sub !== 'object') throw DECLINE
6559
6983
  if (!ctx.annFns) {
6560
6984
  ctx.annFns = new Map()
6561
- ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}')
6985
+ ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
6562
6986
  }
6563
6987
  const hit = ctx.annFns.get(sub)
6564
6988
  if (hit !== undefined) {
6565
6989
  if (hit === null) throw DECLINE // a reference cycle: not modelled here
6566
6990
  return hit
6567
6991
  }
6992
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined || sub.dependencies !== undefined) throw DECLINE
6993
+ // unevaluatedItems reads what the applicators evaluate too; the local
6994
+ // verdict would see it without them.
6995
+ if (sub.unevaluatedItems !== undefined) throw DECLINE
6568
6996
  ctx.annFns.set(sub, null)
6569
6997
  const name = `_an${ctx.varCounter++}`
6998
+ const local = {}
6999
+ for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) setOwn(local, k, sub[k])
6570
7000
  const vl = []
6571
- nestedGenCode(sub, '_av', vl, ctx)
6572
- const body = []
6573
- // additionalProperties, or an unevaluatedProperties that passed, evaluate
6574
- // every remaining key.
6575
- if (sub.additionalProperties !== undefined || (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== false)) {
6576
- body.push(`for(const _ka in d)S.add(_ka)`)
6577
- } else if (sub.unevaluatedProperties === false) {
6578
- body.push(`for(const _ka in d)S.add(_ka)`)
6579
- } else {
6580
- addLocalAnnotations(sub, 'd', 'S', body, ctx)
6581
- addApplicatorAnnotations(sub, 'd', 'S', body, ctx, false)
7001
+ nestedGenCode(local, '_av', vl, ctx)
7002
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
7003
+ const body = [`if(!${name}_v(d))return false`]
7004
+ if (sub.not !== undefined) {
7005
+ const nl = []
7006
+ nestedGenCode(sub.not, '_nv', nl, ctx)
7007
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
7008
+ body.push(`if(${name}_n(d))return false`)
7009
+ }
7010
+ body.push(`const _m=S.length;const _o=typeof d==='object'&&d!==null&&!Array.isArray(d)`)
7011
+ const locals = []
7012
+ addLocalAnnotations(sub, 'd', 'S', locals, ctx)
7013
+ if (locals.length) body.push(`if(_o){${locals.join(';')}}`)
7014
+ const failOut = '{_rb(S,_m);return false}'
7015
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnot(b, ctx)}(d,S))${failOut}`)
7016
+ 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}}`)
7017
+ 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}}`)
7018
+ if (sub.if !== undefined) {
7019
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnot(sub.then, ctx)}(d,S))${failOut}` : ''
7020
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnot(sub.else, ctx)}(d,S))${failOut}` : ''
7021
+ body.push(`if(${emitAnnot(sub.if, ctx)}(d,S)){${thenCheck}}else{${elseCheck}}`)
7022
+ }
7023
+ if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
7024
+ 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}`)
7025
+ }
7026
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnot(refTargetFor(sub.$ref, ctx), ctx)}(d,S))${failOut}`)
7027
+ // An unevaluatedProperties of its own: the keys this subschema evaluated
7028
+ // are the ones pushed since _m; what is left must pass it, and then every
7029
+ // key is evaluated.
7030
+ const up = sub.unevaluatedProperties
7031
+ if (up !== undefined && up !== true && sub.additionalProperties === undefined) {
7032
+ if (up === false) {
7033
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0)${failOut}}`)
7034
+ } else if (typeof up === 'object' && up !== null) {
7035
+ const ul = []
7036
+ nestedGenCode(up, '_uv', ul, ctx)
7037
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
7038
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0&&!${name}_u(d[_k]))${failOut}}`)
7039
+ } else {
7040
+ throw DECLINE
7041
+ }
6582
7042
  }
6583
- // The verdict generator keeps its declarations in the preamble (its
6584
- // helperCode is only scanned for patterns); the error generators in
6585
- // helperCode.
6586
- ;(ctx.preamble || ctx.helperCode).push(`function ${name}_v(_av){${vl.join('\n ')}\n return true}function ${name}(d,S){if(!${name}_v(d))return false;if(typeof d!=='object'||d===null||Array.isArray(d))return true;${body.join(';')};return true}`)
7043
+ if (up !== undefined && sub.additionalProperties === undefined) body.push(`if(_o)for(const _ka in d)S.push(_ka)`)
7044
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
7045
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
6587
7046
  ctx.annFns.set(sub, name)
6588
7047
  return name
6589
7048
  }
6590
7049
 
7050
+ // unevaluatedItems worked out at run time, for the combined function. The
7051
+ // evaluated positions of an array are kept as the interpreter keeps them
7052
+ // (lib/plan-compiler.js): `n`, every index below it evaluated, and `x`, the
7053
+ // indexes past it that `contains` matched. This node's own prefixItems,
7054
+ // items and contains mark them whatever their own verdict, and each in-place
7055
+ // subschema marks them only when it passes (emitAnnotI); what is left must
7056
+ // pass unevaluatedItems. `false` reports once, at the first position left,
7057
+ // as the interpreter does.
7058
+ function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
7059
+ if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
7060
+ if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
7061
+ annItemHelpers(ctx)
7062
+ const ui = ctx.varCounter++
7063
+ const S = `_es${ui}`
7064
+ const body = [`const ${S}={n:0,x:null}`]
7065
+ addLocalItemAnnotations(schema, v, S, body, ctx)
7066
+ for (const b of schema.allOf || []) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
7067
+ if (Array.isArray(schema.anyOf)) for (const b of schema.anyOf) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
7068
+ if (Array.isArray(schema.oneOf)) {
7069
+ 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)}`)
7070
+ }
7071
+ if (schema.if !== undefined) {
7072
+ const thenCall = schema.then !== undefined ? `${emitAnnotI(schema.then, ctx)}(${v},${S})` : ''
7073
+ const elseCall = schema.else !== undefined ? `${emitAnnotI(schema.else, ctx)}(${v},${S})` : ''
7074
+ body.push(`if(${emitAnnotI(schema.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
7075
+ }
7076
+ if (typeof schema.$ref === 'string') body.push(`${emitAnnotI(refTargetFor(schema.$ref, ctx), ctx)}(${v},${S})`)
7077
+ const iv = `_ix${ui}`
7078
+ const ui2 = schema.unevaluatedItems
7079
+ if (ui2 === false) {
7080
+ const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)
7081
+ body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${failI};break}`)
7082
+ } else if (typeof ui2 === 'object' && ui2 !== null) {
7083
+ const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
7084
+ const sub = []
7085
+ genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
7086
+ if (sub.length) body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${sub.join('\n ')}\n }`)
7087
+ } else {
7088
+ throw DECLINE
7089
+ }
7090
+ lines.push(`if(Array.isArray(${v})){${body.join(';')}}`)
7091
+ }
7092
+
7093
+ function annItemHelpers(ctx) {
7094
+ if (ctx.annItemHelpers) return
7095
+ ctx.annItemHelpers = true
7096
+ ;(ctx.preamble || ctx.helperCode).push(
7097
+ 'function _aI(S,i){if(i<S.n)return;if(i===S.n){S.n=i+1;return}(S.x||(S.x=[])).push(i)}' +
7098
+ '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)}' +
7099
+ '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}' +
7100
+ 'function _rbI(S,n,l){S.n=n;if(S.x!==null)S.x.length=l}' +
7101
+ 'function _anIT(){return true}function _anIF(){return false}')
7102
+ }
7103
+
7104
+ // The positions a node's own keywords evaluate, marked on `S`, for an array.
7105
+ function addLocalItemAnnotations(node, v, S, out, ctx) {
7106
+ if (Array.isArray(node.items) || node.additionalItems !== undefined) throw DECLINE
7107
+ const prefix = Array.isArray(node.prefixItems) ? node.prefixItems.length : 0
7108
+ if (prefix) out.push(`_mrI(${S},0,Math.min(${v}.length,${prefix}))`)
7109
+ if (node.items !== undefined) out.push(`_mrI(${S},${prefix},${v}.length)`)
7110
+ if (node.contains !== undefined) {
7111
+ const cl = []
7112
+ nestedGenCode(node.contains, '_cv', cl, ctx)
7113
+ const ci = ctx.varCounter++
7114
+ ;(ctx.preamble || ctx.helperCode).push(`function _cI${ci}(_cv){${cl.join('\n ')}\n return true}`)
7115
+ out.push(`for(let _i=0;_i<${v}.length;_i++)if(_cI${ci}(${v}[_i]))_aI(${S},_i)`)
7116
+ }
7117
+ }
7118
+
7119
+ // The keywords an item annotation function evaluates through its children;
7120
+ // the rest of a subschema is checked by a plain verdict.
7121
+ const ANNOT_ITEM_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', '$ref', 'not', 'unevaluatedItems'])
7122
+
7123
+ // A function `(value, S) => valid` for one subschema, marking on S the array
7124
+ // positions it evaluated when it passes, and leaving S as it was when it does
7125
+ // not. A subschema with an unevaluatedItems of its own evaluates every
7126
+ // position once that passes, so it works on a record of its own and then
7127
+ // marks them all.
7128
+ function emitAnnotI(sub, ctx) {
7129
+ if (sub === true) return '_anIT'
7130
+ if (sub === false) return '_anIF'
7131
+ if (sub === null || typeof sub !== 'object') throw DECLINE
7132
+ if (!ctx.annFnsI) ctx.annFnsI = new Map()
7133
+ const hit = ctx.annFnsI.get(sub)
7134
+ if (hit !== undefined) {
7135
+ if (hit === null) throw DECLINE
7136
+ return hit
7137
+ }
7138
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) throw DECLINE
7139
+ if (Array.isArray(sub.items) || sub.additionalItems !== undefined) throw DECLINE
7140
+ // unevaluatedProperties reads the applicators too, which the local verdict
7141
+ // below leaves out.
7142
+ if (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== true) throw DECLINE
7143
+ ctx.annFnsI.set(sub, null)
7144
+ annItemHelpers(ctx)
7145
+ const name = `_ai${ctx.varCounter++}`
7146
+ const local = {}
7147
+ for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7148
+ const vl = []
7149
+ nestedGenCode(local, '_av', vl, ctx)
7150
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
7151
+ const body = [`if(!${name}_v(d))return false`]
7152
+ if (sub.not !== undefined) {
7153
+ const nl = []
7154
+ nestedGenCode(sub.not, '_nv', nl, ctx)
7155
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
7156
+ body.push(`if(${name}_n(d))return false`)
7157
+ }
7158
+ const own = sub.unevaluatedItems !== undefined && sub.unevaluatedItems !== true
7159
+ const R = own ? '_R' : 'S'
7160
+ body.push(own ? `const _R={n:0,x:null}` : `const _n0=S.n,_x0=S.x===null?0:S.x.length`)
7161
+ const failOut = own ? 'return false' : '{_rbI(S,_n0,_x0);return false}'
7162
+ const locals = []
7163
+ addLocalItemAnnotations(sub, 'd', R, locals, ctx)
7164
+ if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
7165
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
7166
+ 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}}`)
7167
+ 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}}`)
7168
+ if (sub.if !== undefined) {
7169
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
7170
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
7171
+ body.push(`if(${emitAnnotI(sub.if, ctx)}(d,${R})){${thenCheck}}else{${elseCheck}}`)
7172
+ }
7173
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnotI(refTargetFor(sub.$ref, ctx), ctx)}(d,${R}))${failOut}`)
7174
+ if (own) {
7175
+ const u = sub.unevaluatedItems
7176
+ if (u === false) {
7177
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i))return false}`)
7178
+ } else if (typeof u === 'object' && u !== null) {
7179
+ const ul = []
7180
+ nestedGenCode(u, '_uv', ul, ctx)
7181
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
7182
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i)&&!${name}_u(d[_i]))return false}`)
7183
+ } else {
7184
+ throw DECLINE
7185
+ }
7186
+ body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7187
+ }
7188
+ // `unevaluatedItems: true` evaluates every position once the subschema
7189
+ // has passed, with nothing to check; marked last, so a failure above has
7190
+ // nothing of it to undo.
7191
+ if (sub.unevaluatedItems === true) body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7192
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
7193
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
7194
+ ctx.annFnsI.set(sub, name)
7195
+ return name
7196
+ }
7197
+
6591
7198
  function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6592
7199
  const propKeysU = Object.keys(schema.properties || {})
6593
7200
  const ui = ctx.varCounter++
@@ -6653,7 +7260,7 @@ function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
6653
7260
  function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6654
7261
  let handledPropertyNames = false
6655
7262
  const ppEntries = Object.entries(schema.patternProperties)
6656
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
7263
+ const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
6657
7264
  const pi = ctx.varCounter++
6658
7265
 
6659
7266
  // Build pattern matchers: prefer charCodeAt for simple prefixes
@@ -6774,7 +7381,16 @@ function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6774
7381
  function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6775
7382
  const pn = schema.propertyNames
6776
7383
  const ki = ctx.varCounter++
7384
+ // In the rich function an error carries the value it was raised on, and
7385
+ // here that would be the key where the interpreter's error points at the
7386
+ // object, so that one is left to the plain function and enrichment.
7387
+ if (!isSimplePN(pn) && ctx.rich) throw DECLINE
6777
7388
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
7389
+ if (!isSimplePN(pn)) {
7390
+ genCodeC(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
7391
+ lines.push(`}}`)
7392
+ return
7393
+ }
6778
7394
  if (pn.minLength !== undefined) {
6779
7395
  lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
6780
7396
  }
@@ -6992,6 +7608,6 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
6992
7608
  // not → contributes nothing (spec: annotations from not are discarded)
6993
7609
  }
6994
7610
 
6995
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES, _setCodeBudget }
7611
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget }
6996
7612
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
6997
7613
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })