ata-validator 1.42.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -1481,7 +1499,6 @@ function codegenSafe(schema, schemaMap) {
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
  }
@@ -1947,8 +1957,20 @@ function hasSameInstanceRefCycle(root) {
1947
1957
  return found
1948
1958
  }
1949
1959
 
1960
+ // The generators look a definition up by name in one table, `$defs` or else
1961
+ // `definitions`, whichever container the pointer named. With both at the root,
1962
+ // `#/definitions/x` read `$defs/x`: a draft 7 schema carrying both, with
1963
+ // different schemas under one name, rejected what it allowed and accepted
1964
+ // what it rejected. The interpreter resolves the pointer as written, so a
1965
+ // schema with both goes there. None of SchemaStore's 980 has both.
1966
+ function bothDefsContainers(schema) {
1967
+ return schema.$defs !== null && typeof schema.$defs === 'object' &&
1968
+ schema.definitions !== null && typeof schema.definitions === 'object'
1969
+ }
1970
+
1950
1971
  function sharedCodegenGate(schema, schemaMap) {
1951
1972
  if (typeof schema !== 'object' || schema === null) return true
1973
+ if (bothDefsContainers(schema)) return false
1952
1974
  if (hasSameInstanceRefCycle(schema)) return false
1953
1975
  if (!codegenSafe(schema, schemaMap)) return false
1954
1976
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
@@ -2276,6 +2298,15 @@ function prepareForCodegen(schema, schemaMap) {
2276
2298
  return normalizePropertyNames(normalizeRefs(schema, schemaMap))
2277
2299
  }
2278
2300
 
2301
+ const VERDICT_SPLIT = 64 * 1024
2302
+ function sourceSize (lines, ctx) {
2303
+ let n = 0
2304
+ for (const l of lines) n += l.length
2305
+ for (const l of ctx.preamble) n += l.length
2306
+ if (ctx.deferredChecks) for (const l of ctx.deferredChecks) n += l.length
2307
+ return n
2308
+ }
2309
+
2279
2310
  function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2280
2311
  const inputSchema = schema
2281
2312
  schema = prepareForCodegen(schema, schemaMap)
@@ -2300,7 +2331,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2300
2331
  // patternProperties: bail only on boolean sub-schemas or unicode property escapes
2301
2332
  if (schema.patternProperties) {
2302
2333
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
2303
- if (/\\[pP]\{/.test(pat)) return null
2304
2334
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
2305
2335
  }
2306
2336
  }
@@ -2349,20 +2379,28 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2349
2379
  if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
2350
2380
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
2351
2381
 
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
2382
+ // Macro keywords expand into a schema of their own, which this generator
2383
+ // does not apply; such a schema stays on the interpreted engine.
2384
+ if (opts && opts.keywords && usesMacroKeyword(schema, schemaMap, opts.keywords)) return null
2385
+ // Every reference is first expanded where it is used, which is the fastest
2386
+ // form for the checks. Past VERDICT_SPLIT of source the function is
2387
+ // generated again with each definition referenced from several places
2388
+ // written once, as a function in the preamble: expanded, SchemaStore's sarif
2389
+ // (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
2390
+ // half of the first validate() call.
2391
+ let ctx, lines
2392
+ for (const share of [false, true]) {
2393
+ ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
2394
+ keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
2395
+ if (ctx.keywords) ctx.kwEmitted = new Set()
2396
+ lines = []
2397
+ try {
2398
+ genCode(schema, 'd', lines, ctx)
2399
+ } catch (e) {
2400
+ if (e === DECLINE) return null
2401
+ throw e
2402
+ }
2403
+ if (share || sourceSize(lines, ctx) <= VERDICT_SPLIT) break
2366
2404
  }
2367
2405
  // Every schema node that carries a custom keyword must have had its check
2368
2406
  // written. A path that inlined a node without going through genCodeNode
@@ -2396,10 +2434,10 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2396
2434
  closureValues.push(compileSafe(JSON.parse(safeMatch[2])))
2397
2435
  continue
2398
2436
  }
2399
- const match = code.match(/^const (_re\d+)=new RegExp\((.+)\)$/)
2437
+ const match = code.match(/^const (_re\d+)=new RegExp\((.+?)(?:,'(u)')?\)$/)
2400
2438
  if (match) {
2401
2439
  closureNames.push(match[1])
2402
- closureValues.push(new RegExp(JSON.parse(match[2])))
2440
+ closureValues.push(new RegExp(JSON.parse(match[2]), match[3] || ''))
2403
2441
  }
2404
2442
  }
2405
2443
 
@@ -2913,7 +2951,9 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2913
2951
  // $ref — guard against circular references
2914
2952
  // In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
2915
2953
  // Only when THIS schema has unevaluated keywords directly (not via $ref target)
2916
- const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
2954
+ // Any keyword beside $ref that validates is compiled after the reference
2955
+ // (2020-12 applies both; draft 7 drops such siblings when it is read).
2956
+ const hasSiblings = !!schema.$ref && Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
2917
2957
  if (schema.$ref) {
2918
2958
  if (genRefNode(schema, v, lines, ctx, knownType, hasSiblings)) return
2919
2959
  }
@@ -3121,7 +3161,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3121
3161
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
3122
3162
  ctx.usesSafeRe = true
3123
3163
  } else {
3124
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
3164
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
3125
3165
  }
3126
3166
  }
3127
3167
  const ri = ctx.regExpMap.get(pattern);
@@ -3643,6 +3683,31 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3643
3683
  }
3644
3684
  lines.push(`if(!${fnName}(${v}))return false`)
3645
3685
  if (!hasSiblings) return true
3686
+ } else if (ctx.shareDefs && !ctx.refStack.has(schema.$ref) && (ctx.sharedDefs || (ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs))).has(defName)) {
3687
+ // A shared definition off any cycle, as a plain function in the
3688
+ // preamble (see compileToJSCodegen): no guard, since it cannot re-enter
3689
+ // itself. Generated over its own `d` like the cyclic ones above.
3690
+ if (!ctx.sharedDefFns) ctx.sharedDefFns = new Map()
3691
+ let fnName = ctx.sharedDefFns.get(defName)
3692
+ if (!fnName) {
3693
+ fnName = '_dv' + ctx.sharedDefFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
3694
+ ctx.sharedDefFns.set(defName, fnName)
3695
+ const bodyLines = []
3696
+ const outerDeferred = ctx.deferredChecks
3697
+ ctx.deferredChecks = null
3698
+ ctx.refStack.add(schema.$ref)
3699
+ try {
3700
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
3701
+ } finally {
3702
+ ctx.refStack.delete(schema.$ref)
3703
+ }
3704
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
3705
+ ctx.deferredChecks = outerDeferred
3706
+ if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
3707
+ hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
3708
+ }
3709
+ lines.push(`if(!${fnName}(${v}))return false`)
3710
+ if (!hasSiblings) return true
3646
3711
  } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
3647
3712
  else {
3648
3713
  ctx.refStack.add(schema.$ref)
@@ -4417,7 +4482,11 @@ const FORMAT_CODEGEN = {
4417
4482
  function esc(s) { return JSON.stringify(s).slice(1, -1).replace(/'/g, "\\'") }
4418
4483
  // A schema pointer segment for a pattern: JSON Pointer escaping, then made
4419
4484
  // 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, "\\'") }
4485
+ // A pointer segment as text for a single-quoted literal. Control characters
4486
+ // and the two line separators go in as \u escapes: written raw, a property
4487
+ // named "foo\nbar" broke the literal, the combined function failed to compile
4488
+ // and every schema with such a name lost it.
4489
+ function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/[\u0000-\u001f\u2028\u2029]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')) }
4421
4490
 
4422
4491
  // Resolve child path at codegen time when parent is a static string literal.
4423
4492
  // This enables frozen pre-allocation for ALL nested error objects.
@@ -4744,7 +4813,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4744
4813
  if (!sharedCodegenGate(schema, schemaMap)) return null
4745
4814
  if (schema.patternProperties) {
4746
4815
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
4747
- if (/\\[pP]\{/.test(pat)) return null
4748
4816
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
4749
4817
  }
4750
4818
  }
@@ -4941,6 +5009,50 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4941
5009
  // Emit codegen for oneOf/anyOf that runs each branch with error collection,
4942
5010
  // aggregates results, and uses __ataCollapse to surface a single best-branch
4943
5011
  // error. Wraps the existing per-branch evaluation; does not replace it.
5012
+ const BRANCH_VERDICT_MAX = 4096
5013
+
5014
+ // What undoing a trial emission needs: during one, the context's arrays only
5015
+ // grow and its maps and sets only gain keys (a reference pushed onto refStack
5016
+ // by a declined branch is such a key), so their lengths and sizes are enough,
5017
+ // and what was added is cut off the end, in insertion order. Copying them
5018
+ // instead made the trial quadratic: 297 oneOf and anyOf on apollo-router,
5019
+ // each copying a helper list thousands long.
5020
+ function snapshotCtx (ctx) {
5021
+ const out = new Map()
5022
+ for (const k of Object.keys(ctx)) {
5023
+ const x = ctx[k]
5024
+ if (Array.isArray(x)) out.set(k, { kind: 'a', ref: x, n: x.length })
5025
+ else if (x instanceof Map || x instanceof Set) out.set(k, { kind: 'c', ref: x, n: x.size })
5026
+ else out.set(k, { kind: 'v', ref: x })
5027
+ }
5028
+ return out
5029
+ }
5030
+ function restoreCtx (ctx, saved) {
5031
+ for (const k of Object.keys(ctx)) if (!saved.has(k)) delete ctx[k]
5032
+ for (const [k, e] of saved) {
5033
+ if (e.kind === 'a') { e.ref.length = Math.min(e.ref.length, e.n); ctx[k] = e.ref }
5034
+ else if (e.kind === 'c') {
5035
+ const extra = e.ref.size - e.n
5036
+ if (extra > 0) {
5037
+ const added = []
5038
+ let i = 0
5039
+ for (const key of e.ref.keys()) if (i++ >= e.n) added.push(key)
5040
+ for (const key of added) e.ref.delete(key)
5041
+ }
5042
+ ctx[k] = e.ref
5043
+ } else ctx[k] = e.ref
5044
+ }
5045
+ }
5046
+
5047
+ // Whether a node holds a reference anywhere below it.
5048
+ function holdsRef (node, seen = new Set()) {
5049
+ if (node === null || typeof node !== 'object' || seen.has(node)) return false
5050
+ seen.add(node)
5051
+ if (typeof node.$ref === 'string' || typeof node.$dynamicRef === 'string') return true
5052
+ for (const k of Object.keys(node)) if (holdsRef(node[k], seen)) return true
5053
+ return false
5054
+ }
5055
+
4944
5056
  function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
4945
5057
  ctx.usesBranchCollapse = true
4946
5058
  const fi = ctx.varCounter++
@@ -5004,6 +5116,88 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5004
5116
  const report = ctx.inCombined && !ctx.inBranchFn
5005
5117
  ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5006
5118
  : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5119
+ // The node's runtime unevaluatedProperties already counted how many of
5120
+ // these branches pass, through the annotation functions, which answer the
5121
+ // same validity. A oneOf with exactly one, an anyOf with any, holds and
5122
+ // reports nothing, so its branches are not run a second time to say so.
5123
+ // Only in the combined function proper, where that count is in scope.
5124
+ const counted = ctx.inCombined && !ctx.inBranchFn && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5125
+ const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
5126
+ let guard = counted !== undefined
5127
+ ? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
5128
+ : ''
5129
+ // More than one oneOf branch passed: the error names them and carries no
5130
+ // branch errors, so it is built from the bits the count left.
5131
+ if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5132
+ const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5133
+ guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
5134
+ }
5135
+ // Without such a count, each branch also gets a verdict function, and the
5136
+ // passing branches are counted with those: a oneOf with exactly one and an
5137
+ // anyOf with any hold, and a oneOf with more than one is reported from the
5138
+ // bits, so the error-collecting branches run only when no branch passes
5139
+ // and the collapse needs their errors to pick the closest. Running them to
5140
+ // learn the count built a result object and an error list per branch,
5141
+ // which made a oneOf matched twice cost 69 ns where counting costs about
5142
+ // 12. A branch the verdict generator declines keeps the old path.
5143
+ let vfArr = null
5144
+ // Branches that hold a reference are left to the error branches: their
5145
+ // verdicts would expand every definition again, as a second copy, where the
5146
+ // error branches call the shared one.
5147
+ if (guard === '' && !branches.some((b) => holdsRef(b))) {
5148
+ // A trial: the generators are not exception-safe on the context (a
5149
+ // reference left on refStack by a declined branch made the next error
5150
+ // function write that branch as nothing, which read as valid), so the
5151
+ // context is put back as it was when the verdicts decline.
5152
+ const saved = snapshotCtx(ctx)
5153
+ try {
5154
+ const vfs = branches.map((sub) => {
5155
+ if (sub === true) return 'function(){return true}'
5156
+ if (sub === false) return 'function(){return false}'
5157
+ const bl = []
5158
+ nestedGenCode(sub, '_bv', bl, ctx)
5159
+ return bl.length === 0 ? 'function(){return true}' : `function(_bv){${bl.join(';')};return true}`
5160
+ })
5161
+ // Only while the verdicts are small: they are a second copy of the
5162
+ // branches, with every reference expanded, and on large branches that
5163
+ // copy cost more to compile than counting saves (apollo-router's error
5164
+ // function grew from 1.5 to 2.5 MB of source).
5165
+ if (!vfs.some((b) => b.includes(SP_PLACEHOLDER)) && vfs.reduce((n, b) => n + b.length, 0) <= BRANCH_VERDICT_MAX) {
5166
+ // One constant per branch, called directly: through an array the one
5167
+ // call site sees every branch and none of them is inlined.
5168
+ vfArr = `_bvf${fi}`
5169
+ ctx.helperCode.push(vfs.map((b, i) => `const ${vfArr}_${i}=${b}`).join(';'))
5170
+ } else {
5171
+ restoreCtx(ctx, saved)
5172
+ }
5173
+ } catch (e) {
5174
+ if (e !== DECLINE) throw e
5175
+ restoreCtx(ctx, saved)
5176
+ }
5177
+ }
5178
+ if (vfArr !== null) {
5179
+ const L = branches.length
5180
+ const top = ctx.inCombined && !ctx.inBranchFn
5181
+ // An accepted document is still answered by the verdict first (see the
5182
+ // result shape in compileToJSCombined): that is the fastest path to it,
5183
+ // and dropping it measured 20 ns slower per accepted oneOf.
5184
+ if (top) ctx.unguardedCollapse = true
5185
+ const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
5186
+ if (keyword === 'anyOf') {
5187
+ lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5188
+ } else {
5189
+ const mk = L <= 30
5190
+ const collect = mk
5191
+ ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5192
+ : ''
5193
+ const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5194
+ const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5195
+ const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5196
+ lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
5197
+ }
5198
+ return
5199
+ }
5200
+ if (guard === '' && ctx.inCombined && !ctx.inBranchFn) ctx.unguardedCollapse = true
5007
5201
  if (keyword === 'anyOf') {
5008
5202
  // anyOf reports nothing once a branch passes, so the branches are first
5009
5203
  // only asked whether they pass. They are called with `_all` set to 0, a
@@ -5017,12 +5211,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5017
5211
  // no one read. Only when no branch passes do they run again with the
5018
5212
  // caller's `_all`, and the collapse sees them all, as before.
5019
5213
  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}}}`)
5214
+ lines.push(`${guard}{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
5021
5215
  } else {
5022
5216
  // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
5023
5217
  // the passing branches and answers; the branch errors of a probe are not
5024
5218
  // 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}}}`)
5219
+ lines.push(`${guard}{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
5026
5220
  }
5027
5221
  }
5028
5222
 
@@ -5259,7 +5453,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5259
5453
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5260
5454
  ctx.usesSafeRe = true
5261
5455
  } else {
5262
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5456
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5263
5457
  }
5264
5458
  }
5265
5459
  const ri = ctx.regExpMap.get(pattern);
@@ -5334,7 +5528,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5334
5528
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5335
5529
  ctx.usesSafeRe = true
5336
5530
  } else {
5337
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5531
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5338
5532
  }
5339
5533
  }
5340
5534
  patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
@@ -5365,6 +5559,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5365
5559
  // whose evaluated set depends on its own branches would report the wrong
5366
5560
  // errors, and the collapse would pick the wrong closest branch; declined.
5367
5561
  if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
5562
+ if (ctx.inCombined && schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true && !unevalLocalOk(schema, 'unevaluatedItems')) throw DECLINE
5368
5563
  if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
5369
5564
 
5370
5565
  // unevaluatedItems: false under the same rule. The interpreted engine
@@ -5497,7 +5692,7 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
5497
5692
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5498
5693
  ctx.usesSafeRe = true
5499
5694
  } else {
5500
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5695
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5501
5696
  }
5502
5697
  }
5503
5698
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
@@ -5529,7 +5724,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5529
5724
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5530
5725
  ctx.usesSafeRe = true
5531
5726
  } else {
5532
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5727
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5533
5728
  }
5534
5729
  }
5535
5730
  const ri = ctx.regExpMap.get(pattern);
@@ -5559,7 +5754,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5559
5754
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
5560
5755
  ctx.usesSafeRe = true
5561
5756
  } else {
5562
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
5757
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
5563
5758
  }
5564
5759
  }
5565
5760
  patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
@@ -5611,7 +5806,7 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5611
5806
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
5612
5807
  ctx.usesSafeRe = true
5613
5808
  } else {
5614
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
5809
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
5615
5810
  }
5616
5811
  }
5617
5812
  const ri = ctx.regExpMap.get(pattern);
@@ -5733,8 +5928,11 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
5733
5928
  const { errorSite } = require('./enrich-site')
5734
5929
  const siteVar = `_S${ctx.varCounter++}`
5735
5930
  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))`
5931
+ // Inside a shared definition (emitSharedDefC) the site holds the path past
5932
+ // the definition and the reference's own comes in as `_sp`.
5933
+ const rel = sp.startsWith(SP_PLACEHOLDER)
5934
+ ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
5935
+ return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
5738
5936
  }
5739
5937
 
5740
5938
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -5743,15 +5941,12 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5743
5941
  // Same provably-local rule as the error generator above.
5744
5942
  if (typeof schema === 'object' && schema !== null) {
5745
5943
  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
5944
+ // unevaluatedProperties and unevaluatedItems that are not provably local
5945
+ // are worked out at run time (genUnevaluatedDynamicC,
5946
+ // genUnevaluatedItemsDynamicC).
5947
+ // A schema that references itself, through `$ref: "#"` or a definition on
5948
+ // a cycle, has those references written as calls under the cycle guard
5949
+ // (emitDefFnC).
5755
5950
  // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
5756
5951
  // Letting genCodeC emit the legacy "must match exactly one" placeholder
5757
5952
  // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
@@ -5775,7 +5970,6 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5775
5970
  if (!sharedCodegenGate(schema, schemaMap)) return null
5776
5971
  if (schema.patternProperties) {
5777
5972
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
5778
- if (/\\[pP]\{/.test(pat)) return null
5779
5973
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5780
5974
  }
5781
5975
  }
@@ -5825,7 +6019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5825
6019
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5826
6020
 
5827
6021
  // 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],
6022
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5829
6023
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5830
6024
  const lines = []
5831
6025
  try {
@@ -5851,10 +6045,15 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5851
6045
  const closureParams = closureNames.join(',')
5852
6046
  // A definition the error generator wrote as a named function inside a
5853
6047
  // 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
6048
+ // cycle-guard state the error function declares. Here that state is
6049
+ // declared too, and the checks run as the error function runs them: in a
6050
+ // body of their own, again in the guarded mode when data turns out to
6051
+ // point back at itself. This used to decline, which left every schema
6052
+ // whose oneOf or anyOf branches reach a shared definition without one
6053
+ // pass.
6054
+ const guardList = [...(ctx.defFns ? ctx.defFns.values() : []), ...(ctx.cycFnsC || [])]
6055
+ const guardDefs = guardList.length ? guardList : null
6056
+ if (guardDefs) ctx.helperCode.unshift(emitGuardState())
5858
6057
  // Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
5859
6058
  // the branch functions (this function collects everything), and the
5860
6059
  // runtime shape the error generator gives its literals, without code and
@@ -5865,12 +6064,23 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5865
6064
  for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
5866
6065
  for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
5867
6066
  }
5868
- const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5869
6067
  // Lazy error array: no allocation for valid data (the common case)
5870
6068
  // resultShape: validate() itself for a validator whose errors are being read
5871
6069
  // (see validator-core): the result objects built here, so one pass decides,
5872
6070
  // collects and answers, with nothing between the caller and the checks.
5873
6071
  const rs = opts && opts.resultShape
6072
+ // Counts shared between a node's unevaluatedProperties and its anyOf and
6073
+ // oneOf (genUnevaluatedDynamicC), visible to every line of the function.
6074
+ let hoistDecl = ctx.hoisted.length ? `let ${ctx.hoisted.map((n) => n + '=null').join(',')};` : ''
6075
+ let checks = lines.join('\n ')
6076
+ let allDecl = ctx.usesBranchCollapse ? 'const _all=true;' : ''
6077
+ if (guardDefs) {
6078
+ const clears = ['_stk.clear()'].concat(guardDefs.map((n) => `${n}_s.clear()`)).join(';')
6079
+ ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
6080
+ hoistDecl = ''
6081
+ allDecl = ''
6082
+ checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};try{_e=_bodyC(d)}finally{_sg=false}}`
6083
+ }
5874
6084
  // In the result shape this function is validate() itself, so a document
5875
6085
  // that makes it throw is answered by the caller's previous path instead
5876
6086
  // (`_FB`). A thrown error reaching the caller is the one thing this form
@@ -5879,12 +6089,16 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5879
6089
  // With collapsed branches, collecting runs every branch of every oneOf
5880
6090
  // and anyOf in full, which an accepted document does not need: the
5881
6091
  // 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 ') +
6092
+ // Not when every collapse is guarded by a count (emitBranchCollapse): an
6093
+ // accepted document then runs no branch twice, and a rejected one would
6094
+ // pay for the verdict for nothing.
6095
+ ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;\n ` + checks +
5883
6096
  // A collapsed branch error is presented even alone (its ordinal and
5884
6097
  // raw branch errors stay inside); otherwise only a list of two or more
5885
6098
  // needs ordering.
5886
6099
  `\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
5887
- : (ctx.usesBranchCollapse ? 'const _all=true;' : '') + `let _e;\n ` + lines.join('\n ') + `\n return _e?{valid:false,errors:_e}:R`
6100
+ : hoistDecl + allDecl + `let _e;\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6101
+ const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5888
6102
 
5889
6103
  try {
5890
6104
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
@@ -5924,6 +6138,9 @@ function typeMismatchRest(schema, types) {
5924
6138
  rest[k] = schema[k]
5925
6139
  }
5926
6140
  if (rest === null) return null
6141
+ // A node whose reference was already written (genCodeCNode) keeps that
6142
+ // mark, or the reference is written again here and its errors twice.
6143
+ if (schema[REF_DONE]) Object.defineProperty(rest, REF_DONE, { value: true })
5927
6144
  for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
5928
6145
  return null
5929
6146
  }
@@ -5931,6 +6148,68 @@ function typeMismatchRest(schema, types) {
5931
6148
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5932
6149
  return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
5933
6150
  }
6151
+ // A definition referenced from several places, written once as a function
6152
+ // and called at each reference, instead of its whole code at every one: the
6153
+ // combined function of SchemaStore's cloud-run (48 KB of schema) came to
6154
+ // 1.1 MB of source and sarif's (79 KB) to 2.8 MB, and compiling those was most
6155
+ // of the 50 to 300 ms a first rejection with its errors read took. The body is
6156
+ // generated against a placeholder schema path that becomes the `_sp` argument,
6157
+ // as the error generator does (genCodeENode), so an error names the path it
6158
+ // was reached by. It collects into its own list, returned to the caller, and
6159
+ // declares its own branch counts (genUnevaluatedDynamicC). The document root
6160
+ // comes in as `d` for the rich errors that read it. Not for a definition on a
6161
+ // reference cycle, which keeps its existing path. False when not shared.
6162
+ function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
6163
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
6164
+ if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
6165
+ const cyclic = ctx.cyclicDefs.has(defName)
6166
+ if (!cyclic && !ctx.sharedDefs.has(defName)) return false
6167
+ emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6168
+ return true
6169
+ }
6170
+
6171
+ // One schema as a function of its own in the combined function (see
6172
+ // emitSharedDefC). The name is registered before the body is written, so a
6173
+ // reference back to it inside becomes a call. One that can re-enter itself,
6174
+ // a definition on a cycle or the root through `$ref: "#"`, goes through the
6175
+ // cycle guard the error generator's definitions use: depth counted, and in
6176
+ // the guarded pass a value already being checked is not checked again.
6177
+ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix) {
6178
+ if (!ctx.defFnsC) ctx.defFnsC = new Map()
6179
+ let fnName = ctx.defFnsC.get(key)
6180
+ if (!fnName) {
6181
+ fnName = '_defC' + ctx.defFnsC.size + '_' + (key === '#' ? 'root' : key.replace(/[^A-Za-z0-9_]/g, '_'))
6182
+ ctx.defFnsC.set(key, fnName)
6183
+ const outerHoisted = ctx.hoisted
6184
+ ctx.hoisted = []
6185
+ const bodyLines = []
6186
+ let hoisted
6187
+ try {
6188
+ genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
6189
+ } finally {
6190
+ hoisted = ctx.hoisted
6191
+ ctx.hoisted = outerHoisted
6192
+ }
6193
+ let body = bodyLines.join('\n ')
6194
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
6195
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
6196
+ const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
6197
+ const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6198
+ if (cyclic) {
6199
+ if (!ctx.cycFnsC) ctx.cycFnsC = []
6200
+ ctx.cycFnsC.push(fnName)
6201
+ ctx.helperCode.push(
6202
+ `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d){\n ` +
6203
+ `if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d)}finally{${fnName}_s.delete(_dv)}}\n ` +
6204
+ `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d)\n _sd--\n return _r\n }\n ` + fn)
6205
+ } else {
6206
+ ctx.helperCode.push(fn)
6207
+ }
6208
+ }
6209
+ lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
6210
+ }
6211
+
6212
+ const REF_DONE = Symbol('ata.refDone')
5934
6213
  function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5935
6214
  if (!schemaPrefix) schemaPrefix = '#'
5936
6215
  // Per-node, for the reason spelled out in genCode.
@@ -5945,18 +6224,34 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5945
6224
  if (typeof schema !== 'object' || schema === null) return
5946
6225
 
5947
6226
  // $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
6227
+ if (schema.$ref && !schema[REF_DONE]) {
6228
+ // A keyword beside $ref that validates applies too (2020-12; draft 7
6229
+ // drops such siblings when the schema is read). The reference is written
6230
+ // on its own, then the node again with the reference marked done, on a
6231
+ // fresh copy so the mark cannot reach the same schema object met through
6232
+ // another path; the copy keeps `$ref` for unevaluatedProperties and
6233
+ // unevaluatedItems, which read its annotations. Dropping the siblings
6234
+ // once accepted what they reject, which is why this declined. Not in a
6235
+ // branch function, which the error generator writes.
6236
+ if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
6237
+ if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
6238
+ genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
6239
+ const rest = Object.assign({}, schema)
6240
+ Object.defineProperty(rest, REF_DONE, { value: true })
6241
+ genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
6242
+ return
6243
+ }
6244
+ // A reference back into what is being expanded is a call to that
6245
+ // schema's function (emitDefFnC), never nothing: an expansion that stops
6246
+ // and emits no lines reads as valid.
6247
+ if (schema.$ref === '#') {
6248
+ if (ctx.inCombined && !ctx.inBranchFn && ctx.rootC) { emitDefFnC('#', ctx.rootC, true, v, pathExpr, lines, ctx, schemaPrefix); return }
6249
+ throw DECLINE
6250
+ }
5957
6251
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5958
6252
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
5959
- if (ctx.refStack.has(schema.$ref)) return
6253
+ if (ctx.inCombined && !ctx.inBranchFn && emitSharedDefC(m[1], v, pathExpr, lines, ctx, schemaPrefix)) return
6254
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5960
6255
  ctx.refStack.add(schema.$ref)
5961
6256
  genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
5962
6257
  ctx.refStack.delete(schema.$ref)
@@ -5967,7 +6262,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5967
6262
  const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
5968
6263
  const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
5969
6264
  if (anchorTarget) {
5970
- if (ctx.refStack.has(schema.$ref)) return
6265
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5971
6266
  ctx.refStack.add(schema.$ref)
5972
6267
  genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
5973
6268
  ctx.refStack.delete(schema.$ref)
@@ -5975,7 +6270,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5975
6270
  }
5976
6271
  }
5977
6272
  if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
5978
- if (ctx.refStack.has(schema.$ref)) return
6273
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5979
6274
  ctx.refStack.add(schema.$ref)
5980
6275
  genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
5981
6276
  ctx.refStack.delete(schema.$ref)
@@ -5985,7 +6280,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5985
6280
  if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
5986
6281
  const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
5987
6282
  if (r) {
5988
- if (ctx.refStack.has(schema.$ref)) return
6283
+ if (ctx.refStack.has(schema.$ref)) throw DECLINE
5989
6284
  ctx.refStack.add(schema.$ref)
5990
6285
  genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
5991
6286
  ctx.refStack.delete(schema.$ref)
@@ -6201,7 +6496,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6201
6496
  const ri = ctx.varCounter++
6202
6497
  const reVar = `_re${ri}`
6203
6498
  ctx.closureVars.push(reVar)
6204
- ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
6499
+ ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern)))
6205
6500
  const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
6206
6501
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
6207
6502
  }
@@ -6297,7 +6592,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6297
6592
  // unevaluatedItems: false under the same rule: one error for the array,
6298
6593
  // the first uncovered position as the limit. Adjacent schema-form `items`
6299
6594
  // evaluates everything; the draft-07 array form acts as the prefix.
6300
- if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6595
+ if (schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true) {
6596
+ if (unevalLocalOk(schema, 'unevaluatedItems')) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6597
+ else genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6598
+ }
6301
6599
 
6302
6600
  // dependentRequired
6303
6601
  if (schema.dependentRequired) {
@@ -6447,17 +6745,17 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6447
6745
  if (schema.additionalProperties !== undefined) return
6448
6746
  const ui = ctx.varCounter++
6449
6747
  const S = `_ev${ui}`
6450
- const body = [`const ${S}=new Set()`]
6748
+ const body = [`const ${S}=[]`]
6451
6749
  addLocalAnnotations(schema, v, S, body, ctx)
6452
- addApplicatorAnnotations(schema, v, S, body, ctx, true)
6750
+ addTopApplicatorAnnotations(schema, v, S, body, ctx)
6453
6751
  const kVar = `_uk${ui}`
6454
6752
  const up = schema.unevaluatedProperties
6455
6753
  if (up === false) {
6456
- body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar}))return false`)
6754
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
6457
6755
  } else if (typeof up === 'object' && up !== null) {
6458
6756
  const sub = []
6459
6757
  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 }`)
6758
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
6461
6759
  } else {
6462
6760
  throw DECLINE
6463
6761
  }
@@ -6470,36 +6768,56 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
6470
6768
  // count whether or not their values pass; an allOf, anyOf or dependentSchemas
6471
6769
  // branch, an if, then or else, or a $ref target counts only when it passes,
6472
6770
  // 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.
6771
+ // asked through an annotation function (emitAnnot).
6475
6772
  function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6476
6773
  // additionalProperties evaluates every key the others do not, pass or fail.
6477
6774
  if (schema.additionalProperties !== undefined) return
6478
6775
  const ui = ctx.varCounter++
6479
6776
  const S = `_ev${ui}`
6480
- const body = [`const ${S}=new Set()`]
6777
+ const body = [`const ${S}=[]`]
6481
6778
  addLocalAnnotations(schema, v, S, body, ctx)
6482
- addApplicatorAnnotations(schema, v, S, body, ctx, true)
6779
+ // The anyOf and oneOf counts are handed to the keywords' own error code
6780
+ // (emitBranchCollapse), with the object they were counted for: they are
6781
+ // declared once at the top of the combined function, where every later
6782
+ // line sees them, and read only when that object is the value at hand, so
6783
+ // a count left from another value, another loop iteration or another place
6784
+ // is never taken for this one. Only in the combined function proper.
6785
+ const share = ctx.inCombined && !ctx.inBranchFn && ctx.hoisted !== undefined &&
6786
+ (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf))
6787
+ const counts = share ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: `_om${ui}`, of: `_ao${ui}` } : null
6788
+ if (share) {
6789
+ ctx.hoisted.push(counts.anyOf, counts.oneOf, counts.oneOfMask, counts.of)
6790
+ body.push(`${counts.of}=${v};${counts.oneOfMask}=0`)
6791
+ if (!ctx.branchCounts) ctx.branchCounts = new Map()
6792
+ if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
6793
+ if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
6794
+ }
6795
+ addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
6483
6796
  const kVar = `_uk${ui}`
6484
6797
  const up = schema.unevaluatedProperties
6485
6798
  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'")}}`)
6799
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
6487
6800
  } else if (typeof up === 'object' && up !== null) {
6488
6801
  const pe = emitPtrEsc(ctx)
6489
6802
  const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
6490
6803
  const sub = []
6491
6804
  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 }`)
6805
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
6493
6806
  } else {
6494
6807
  throw DECLINE
6495
6808
  }
6496
6809
  lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
6497
6810
  }
6498
6811
 
6499
- // The names a node's own keywords evaluate, added to `S`: present declared
6500
- // properties, keys matching a pattern.
6812
+ // The names a node's own keywords evaluate, pushed onto the array `S`:
6813
+ // present declared properties, keys matching a pattern, and every key when
6814
+ // additionalProperties is there.
6501
6815
  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)})`)
6816
+ if (node.additionalProperties !== undefined) {
6817
+ out.push(`for(const _ka in ${v})${S}.push(_ka)`)
6818
+ return
6819
+ }
6820
+ for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
6503
6821
  const pats = Object.keys(node.patternProperties || {})
6504
6822
  if (pats.length) {
6505
6823
  const tests = []
@@ -6512,24 +6830,31 @@ function addLocalAnnotations(node, v, S, out, ctx) {
6512
6830
  ctx.closureVals.push(safeReClosure(ctx, pat))
6513
6831
  tests.push(`_re${ri}.test(${kv})`)
6514
6832
  }
6515
- out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.add(${kv})`)
6833
+ out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
6516
6834
  }
6517
6835
  }
6518
6836
 
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) {
6837
+ // What the applicators of the node that owns the unevaluatedProperties add.
6838
+ // Their validity is checked by the code around this; here a branch only
6839
+ // contributes, through its annotation function, when it passes, and a oneOf
6840
+ // only with exactly one passing branch. An annotation function that fails
6841
+ // leaves `S` as it found it.
6842
+ function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
6843
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
6523
6844
  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})`)
6845
+ // With `counts`, the passing branches of anyOf and oneOf are counted into
6846
+ // variables the caller declared, so the keyword's own error code further
6847
+ // down can skip its branches when the count already says it holds (see
6848
+ // emitBranchCollapse).
6849
+ if (Array.isArray(node.anyOf)) {
6850
+ if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
6851
+ else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
6852
+ }
6525
6853
  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
- }
6854
+ const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
6855
+ // Which branches passed, as bits, for a collapse that has to name them.
6856
+ const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
6857
+ out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
6533
6858
  }
6534
6859
  if (node.if !== undefined) {
6535
6860
  const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
@@ -6540,7 +6865,6 @@ function addApplicatorAnnotations(node, v, S, out, ctx, top) {
6540
6865
  for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
6541
6866
  }
6542
6867
  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
6868
  }
6545
6869
 
6546
6870
  function refTargetFor(ref, ctx) {
@@ -6550,44 +6874,236 @@ function refTargetFor(ref, ctx) {
6550
6874
  return t
6551
6875
  }
6552
6876
 
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.
6877
+ // The keywords an annotation function evaluates itself, through its children;
6878
+ // the rest of a subschema is checked by a plain verdict.
6879
+ const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
6880
+
6881
+ // A function `(value, S) => valid` for one subschema: whether it passes, and
6882
+ // when it does, what it evaluated pushed onto S; when it does not, S is left
6883
+ // as it was. Validity is worked out bottom up: the subschema's own keywords
6884
+ // by a verdict of the subschema without its applicators, each applicator from
6885
+ // its branches' annotation functions. Asking each branch for a full verdict
6886
+ // and then again for its annotations repeated every nested branch once per
6887
+ // level, which made nested references the slowest shape there was.
6555
6888
  function emitAnnot(sub, ctx) {
6556
6889
  if (sub === true) return '_anT'
6557
6890
  if (sub === false) return '_anF'
6558
6891
  if (sub === null || typeof sub !== 'object') throw DECLINE
6559
6892
  if (!ctx.annFns) {
6560
6893
  ctx.annFns = new Map()
6561
- ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}')
6894
+ ;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
6562
6895
  }
6563
6896
  const hit = ctx.annFns.get(sub)
6564
6897
  if (hit !== undefined) {
6565
6898
  if (hit === null) throw DECLINE // a reference cycle: not modelled here
6566
6899
  return hit
6567
6900
  }
6901
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined || sub.dependencies !== undefined) throw DECLINE
6902
+ // unevaluatedItems reads what the applicators evaluate too; the local
6903
+ // verdict would see it without them.
6904
+ if (sub.unevaluatedItems !== undefined) throw DECLINE
6568
6905
  ctx.annFns.set(sub, null)
6569
6906
  const name = `_an${ctx.varCounter++}`
6907
+ const local = {}
6908
+ for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) local[k] = sub[k]
6570
6909
  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)
6910
+ nestedGenCode(local, '_av', vl, ctx)
6911
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
6912
+ const body = [`if(!${name}_v(d))return false`]
6913
+ if (sub.not !== undefined) {
6914
+ const nl = []
6915
+ nestedGenCode(sub.not, '_nv', nl, ctx)
6916
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
6917
+ body.push(`if(${name}_n(d))return false`)
6918
+ }
6919
+ body.push(`const _m=S.length;const _o=typeof d==='object'&&d!==null&&!Array.isArray(d)`)
6920
+ const locals = []
6921
+ addLocalAnnotations(sub, 'd', 'S', locals, ctx)
6922
+ if (locals.length) body.push(`if(_o){${locals.join(';')}}`)
6923
+ const failOut = '{_rb(S,_m);return false}'
6924
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnot(b, ctx)}(d,S))${failOut}`)
6925
+ if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_a=true;`).join('')}if(!_a)${failOut}}`)
6926
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_c++;`).join('')}if(_c!==1)${failOut}}`)
6927
+ if (sub.if !== undefined) {
6928
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnot(sub.then, ctx)}(d,S))${failOut}` : ''
6929
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnot(sub.else, ctx)}(d,S))${failOut}` : ''
6930
+ body.push(`if(${emitAnnot(sub.if, ctx)}(d,S)){${thenCheck}}else{${elseCheck}}`)
6931
+ }
6932
+ if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
6933
+ for (const [k, ds] of Object.entries(sub.dependentSchemas)) body.push(`if(_o&&Object.prototype.hasOwnProperty.call(d,${JSON.stringify(k)})&&!${emitAnnot(ds, ctx)}(d,S))${failOut}`)
6934
+ }
6935
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnot(refTargetFor(sub.$ref, ctx), ctx)}(d,S))${failOut}`)
6936
+ // An unevaluatedProperties of its own: the keys this subschema evaluated
6937
+ // are the ones pushed since _m; what is left must pass it, and then every
6938
+ // key is evaluated.
6939
+ const up = sub.unevaluatedProperties
6940
+ if (up !== undefined && up !== true && sub.additionalProperties === undefined) {
6941
+ if (up === false) {
6942
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0)${failOut}}`)
6943
+ } else if (typeof up === 'object' && up !== null) {
6944
+ const ul = []
6945
+ nestedGenCode(up, '_uv', ul, ctx)
6946
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
6947
+ body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0&&!${name}_u(d[_k]))${failOut}}`)
6948
+ } else {
6949
+ throw DECLINE
6950
+ }
6582
6951
  }
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}`)
6952
+ if (up !== undefined && sub.additionalProperties === undefined) body.push(`if(_o)for(const _ka in d)S.push(_ka)`)
6953
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
6954
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
6587
6955
  ctx.annFns.set(sub, name)
6588
6956
  return name
6589
6957
  }
6590
6958
 
6959
+ // unevaluatedItems worked out at run time, for the combined function. The
6960
+ // evaluated positions of an array are kept as the interpreter keeps them
6961
+ // (lib/plan-compiler.js): `n`, every index below it evaluated, and `x`, the
6962
+ // indexes past it that `contains` matched. This node's own prefixItems,
6963
+ // items and contains mark them whatever their own verdict, and each in-place
6964
+ // subschema marks them only when it passes (emitAnnotI); what is left must
6965
+ // pass unevaluatedItems. `false` reports once, at the first position left,
6966
+ // as the interpreter does.
6967
+ function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6968
+ if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
6969
+ if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
6970
+ annItemHelpers(ctx)
6971
+ const ui = ctx.varCounter++
6972
+ const S = `_es${ui}`
6973
+ const body = [`const ${S}={n:0,x:null}`]
6974
+ addLocalItemAnnotations(schema, v, S, body, ctx)
6975
+ for (const b of schema.allOf || []) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
6976
+ if (Array.isArray(schema.anyOf)) for (const b of schema.anyOf) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
6977
+ if (Array.isArray(schema.oneOf)) {
6978
+ body.push(`{const _n0=${S}.n,_x0=${S}.x===null?0:${S}.x.length;let _c=0;${schema.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(${v},${S}))_c++;`).join('')}if(_c!==1)_rbI(${S},_n0,_x0)}`)
6979
+ }
6980
+ if (schema.if !== undefined) {
6981
+ const thenCall = schema.then !== undefined ? `${emitAnnotI(schema.then, ctx)}(${v},${S})` : ''
6982
+ const elseCall = schema.else !== undefined ? `${emitAnnotI(schema.else, ctx)}(${v},${S})` : ''
6983
+ body.push(`if(${emitAnnotI(schema.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
6984
+ }
6985
+ if (typeof schema.$ref === 'string') body.push(`${emitAnnotI(refTargetFor(schema.$ref, ctx), ctx)}(${v},${S})`)
6986
+ const iv = `_ix${ui}`
6987
+ const ui2 = schema.unevaluatedItems
6988
+ if (ui2 === false) {
6989
+ const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)
6990
+ body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${failI};break}`)
6991
+ } else if (typeof ui2 === 'object' && ui2 !== null) {
6992
+ const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
6993
+ const sub = []
6994
+ genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
6995
+ if (sub.length) body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${sub.join('\n ')}\n }`)
6996
+ } else {
6997
+ throw DECLINE
6998
+ }
6999
+ lines.push(`if(Array.isArray(${v})){${body.join(';')}}`)
7000
+ }
7001
+
7002
+ function annItemHelpers(ctx) {
7003
+ if (ctx.annItemHelpers) return
7004
+ ctx.annItemHelpers = true
7005
+ ;(ctx.preamble || ctx.helperCode).push(
7006
+ 'function _aI(S,i){if(i<S.n)return;if(i===S.n){S.n=i+1;return}(S.x||(S.x=[])).push(i)}' +
7007
+ 'function _mrI(S,f,t){if(f>=t)return;if(f<=S.n){if(t>S.n)S.n=t;return}const x=S.x||(S.x=[]);for(let i=f;i<t;i++)x.push(i)}' +
7008
+ 'function _hI(S,i){if(i<S.n)return true;const x=S.x;if(x===null)return false;for(let j=0;j<x.length;j++)if(x[j]===i)return true;return false}' +
7009
+ 'function _rbI(S,n,l){S.n=n;if(S.x!==null)S.x.length=l}' +
7010
+ 'function _anIT(){return true}function _anIF(){return false}')
7011
+ }
7012
+
7013
+ // The positions a node's own keywords evaluate, marked on `S`, for an array.
7014
+ function addLocalItemAnnotations(node, v, S, out, ctx) {
7015
+ if (Array.isArray(node.items) || node.additionalItems !== undefined) throw DECLINE
7016
+ const prefix = Array.isArray(node.prefixItems) ? node.prefixItems.length : 0
7017
+ if (prefix) out.push(`_mrI(${S},0,Math.min(${v}.length,${prefix}))`)
7018
+ if (node.items !== undefined) out.push(`_mrI(${S},${prefix},${v}.length)`)
7019
+ if (node.contains !== undefined) {
7020
+ const cl = []
7021
+ nestedGenCode(node.contains, '_cv', cl, ctx)
7022
+ const ci = ctx.varCounter++
7023
+ ;(ctx.preamble || ctx.helperCode).push(`function _cI${ci}(_cv){${cl.join('\n ')}\n return true}`)
7024
+ out.push(`for(let _i=0;_i<${v}.length;_i++)if(_cI${ci}(${v}[_i]))_aI(${S},_i)`)
7025
+ }
7026
+ }
7027
+
7028
+ // The keywords an item annotation function evaluates through its children;
7029
+ // the rest of a subschema is checked by a plain verdict.
7030
+ const ANNOT_ITEM_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', '$ref', 'not', 'unevaluatedItems'])
7031
+
7032
+ // A function `(value, S) => valid` for one subschema, marking on S the array
7033
+ // positions it evaluated when it passes, and leaving S as it was when it does
7034
+ // not. A subschema with an unevaluatedItems of its own evaluates every
7035
+ // position once that passes, so it works on a record of its own and then
7036
+ // marks them all.
7037
+ function emitAnnotI(sub, ctx) {
7038
+ if (sub === true) return '_anIT'
7039
+ if (sub === false) return '_anIF'
7040
+ if (sub === null || typeof sub !== 'object') throw DECLINE
7041
+ if (!ctx.annFnsI) ctx.annFnsI = new Map()
7042
+ const hit = ctx.annFnsI.get(sub)
7043
+ if (hit !== undefined) {
7044
+ if (hit === null) throw DECLINE
7045
+ return hit
7046
+ }
7047
+ if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) throw DECLINE
7048
+ if (Array.isArray(sub.items) || sub.additionalItems !== undefined) throw DECLINE
7049
+ // unevaluatedProperties reads the applicators too, which the local verdict
7050
+ // below leaves out.
7051
+ if (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== true) throw DECLINE
7052
+ ctx.annFnsI.set(sub, null)
7053
+ annItemHelpers(ctx)
7054
+ const name = `_ai${ctx.varCounter++}`
7055
+ const local = {}
7056
+ for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) local[k] = sub[k]
7057
+ const vl = []
7058
+ nestedGenCode(local, '_av', vl, ctx)
7059
+ const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
7060
+ const body = [`if(!${name}_v(d))return false`]
7061
+ if (sub.not !== undefined) {
7062
+ const nl = []
7063
+ nestedGenCode(sub.not, '_nv', nl, ctx)
7064
+ decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
7065
+ body.push(`if(${name}_n(d))return false`)
7066
+ }
7067
+ const own = sub.unevaluatedItems !== undefined && sub.unevaluatedItems !== true
7068
+ const R = own ? '_R' : 'S'
7069
+ body.push(own ? `const _R={n:0,x:null}` : `const _n0=S.n,_x0=S.x===null?0:S.x.length`)
7070
+ const failOut = own ? 'return false' : '{_rbI(S,_n0,_x0);return false}'
7071
+ const locals = []
7072
+ addLocalItemAnnotations(sub, 'd', R, locals, ctx)
7073
+ if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
7074
+ for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
7075
+ if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_a=true;`).join('')}if(!_a)${failOut}}`)
7076
+ if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_c++;`).join('')}if(_c!==1)${failOut}}`)
7077
+ if (sub.if !== undefined) {
7078
+ const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
7079
+ const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
7080
+ body.push(`if(${emitAnnotI(sub.if, ctx)}(d,${R})){${thenCheck}}else{${elseCheck}}`)
7081
+ }
7082
+ if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnotI(refTargetFor(sub.$ref, ctx), ctx)}(d,${R}))${failOut}`)
7083
+ if (own) {
7084
+ const u = sub.unevaluatedItems
7085
+ if (u === false) {
7086
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i))return false}`)
7087
+ } else if (typeof u === 'object' && u !== null) {
7088
+ const ul = []
7089
+ nestedGenCode(u, '_uv', ul, ctx)
7090
+ decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
7091
+ body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i)&&!${name}_u(d[_i]))return false}`)
7092
+ } else {
7093
+ throw DECLINE
7094
+ }
7095
+ body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7096
+ }
7097
+ // `unevaluatedItems: true` evaluates every position once the subschema
7098
+ // has passed, with nothing to check; marked last, so a failure above has
7099
+ // nothing of it to undo.
7100
+ if (sub.unevaluatedItems === true) body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
7101
+ decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
7102
+ ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
7103
+ ctx.annFnsI.set(sub, name)
7104
+ return name
7105
+ }
7106
+
6591
7107
  function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
6592
7108
  const propKeysU = Object.keys(schema.properties || {})
6593
7109
  const ui = ctx.varCounter++