ata-validator 1.52.0 → 1.54.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.
@@ -1,5 +1,14 @@
1
1
  'use strict'
2
2
  const { compileFunction } = require('./compile-fn')
3
+ // A comment that differs at every compile, put in front of every program
4
+ // built at run time. V8 keeps one compiled function per source text of a
5
+ // `new Function` call, with one feedback vector: two schemas that generate
6
+ // the same code (two copies of a record schema under different `$id`s) then
7
+ // shared their inline caches, and each other's data shapes, so both ran
8
+ // megamorphic. The emitted (standalone) sources carry no tag, so a build is
9
+ // the same text every time.
10
+ let _compileSeq = 0
11
+ const seqTag = () => `/*${++_compileSeq}*/`
3
12
 
4
13
  // uniqueItems helpers for the boolean generator, hoisted once per compiled
5
14
  // function (and carried into standalone output through the preamble).
@@ -9,7 +18,7 @@ const { compileFunction } = require('./compile-fn')
9
18
  // `enum` compare instances by value: two objects with the same members are
10
19
  // equal whatever order their keys were written in. Comparing serialized
11
20
  // forms answers a different question and costs a string per call.
12
- const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i;if(aa){if(a.length!==b.length)return false;for(i=0;i<a.length;i++)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=0;i<ka.length;i++){var k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k)||!_deq(a[k],b[k]))return false}return true}"
21
+ const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i,k,x,y;if(aa){if(a.length!==b.length)return false;for(i=a.length;i--!==0;)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=ka.length;i--!==0;){k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k))return false;x=a[k];y=b[k];if(x!==y&&(x===null||y===null||typeof x!=='object'||typeof y!=='object'))return false}for(i=ka.length;i--!==0;){k=ka[i];x=a[k];if(x!==null&&typeof x==='object'&&!_deq(x,b[k]))return false}return true}"
13
22
 
14
23
  // uniqueItems helpers, emitted from the functions the interpreted engine
15
24
  // calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
@@ -152,7 +161,26 @@ function enumOperand(ctx, vals, v, lines) {
152
161
 
153
162
  // One `enum` test: primitives compare with `===`, which needs no call, and
154
163
  // object members go through structural equality.
164
+ // An enum of 64 or more strings is a Set in the preamble, looked up once: the
165
+ // chain of comparisons grows with the list and the lookup does not. Below
166
+ // that the chain wins on real documents, whose values tend to sit early in
167
+ // the list: on SchemaStore's samples a Set made jscsrc (62 values) 16%
168
+ // slower and bigquery-table (18) 19% slower, while from 77 values up it
169
+ // made every schema faster (chrome-manifest 77, tsconfig 108, jscpd 224 at
170
+ // 6x, cryproj 2461 at 28x).
171
+ const ENUM_SET_MIN = 64
155
172
  function enumCondition(ctx, vals, v) {
173
+ if (vals.length >= ENUM_SET_MIN && ctx && Array.isArray(ctx.preamble) && vals.every((x) => typeof x === 'string')) {
174
+ if (!ctx.enumSets) ctx.enumSets = new Map()
175
+ const key = JSON.stringify(vals)
176
+ let name = ctx.enumSets.get(key)
177
+ if (name === undefined) {
178
+ name = '_es' + ctx.varCounter++
179
+ ctx.enumSets.set(key, name)
180
+ hoist(ctx, `const ${name}=new Set(${key})`)
181
+ }
182
+ return `${name}.has(${v})`
183
+ }
156
184
  const parts = []
157
185
  const prims = vals.filter((x) => x === null || typeof x !== 'object')
158
186
  const objs = vals.filter((x) => x !== null && typeof x === 'object')
@@ -365,7 +393,7 @@ function compileToJS(schema, defs, schemaMap) {
365
393
  // required applies to objects only; other types are ignored.
366
394
  if (schema.required && Array.isArray(schema.required)) {
367
395
  for (const key of schema.required) {
368
- checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || hasOwnKey(d, key))
396
+ checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || (hasOwnKey(d, key) && d[key] !== undefined))
369
397
  }
370
398
  }
371
399
 
@@ -376,7 +404,9 @@ function compileToJS(schema, defs, schemaMap) {
376
404
  if (!propCheck) return null // bail if sub-schema too complex
377
405
  checks.push((d) => {
378
406
  if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
379
- return propCheck(d[key])
407
+ // An undefined value is not checked, as the generated code does not.
408
+ const v = d[key]
409
+ return v === undefined || propCheck(v)
380
410
  })
381
411
  }
382
412
  }
@@ -539,7 +569,7 @@ function compileToJS(schema, defs, schemaMap) {
539
569
  // format — hand-written fast checks. A format the code generator asserts
540
570
  // but this path cannot is a decline, not a pass.
541
571
  if (schema.format) {
542
- const fc = FORMAT_CHECKS[schema.format]
572
+ const fc = formatChecks()[schema.format]
543
573
  if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
544
574
  else if (FORMAT_CODEGEN[schema.format]) return null
545
575
  }
@@ -837,14 +867,26 @@ function _canonical(x) {
837
867
  // schema with a `format` is compiled; loading it with this module cost every
838
868
  // process 0.8 ms of its cold start.
839
869
  let _formatsCache = null
840
- function getFormats () { return _formatsCache || (_formatsCache = { ...require('./formats'), ...require('./formats-source') }) }
870
+ // The format source generators, and the few runtime values from
871
+ // lib/formats.js they are written with, read from that file on first use.
872
+ function getFormats () {
873
+ if (_formatsCache !== null) return _formatsCache
874
+ const out = { ...require('./formats-source') }
875
+ for (const k of ['IPV4', 'IPV6_FULL', 'ipv4Range']) Object.defineProperty(out, k, { get: () => require('./formats')[k], enumerable: true })
876
+ return (_formatsCache = out)
877
+ }
841
878
  // The closure path's formats. It carried its own idea of an email and knew
842
879
  // five formats in all, so a schema that reached it was told "valid" for
843
880
  // anything the other engines turned away; the first call on a fresh validator
844
881
  // is the one that lands here. Every entry now names the same function the
845
882
  // other two paths use, and the list is the list they implement.
846
- const _fmtFns = require('./formats')
847
- const FORMAT_CHECKS = {
883
+ // Built on first use: lib/formats.js loads only for a schema with a format
884
+ // this path checks, not with the compiler.
885
+ let _formatChecks = null
886
+ function formatChecks () {
887
+ if (_formatChecks !== null) return _formatChecks
888
+ const _fmtFns = require('./formats')
889
+ _formatChecks = {
848
890
  email: _fmtFns.email,
849
891
  'idn-email': _fmtFns.idnEmail,
850
892
  date: _fmtFns.date,
@@ -863,6 +905,8 @@ const FORMAT_CHECKS = {
863
905
  'json-pointer': _fmtFns.jsonPointer,
864
906
  'relative-json-pointer': _fmtFns.relativeJsonPointer,
865
907
  regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
908
+ }
909
+ return _formatChecks
866
910
  }
867
911
 
868
912
  // Dangerous JS property names that exist on Object.prototype
@@ -1236,6 +1280,51 @@ function emitPropFn (prop, ctx) {
1236
1280
  hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
1237
1281
  return fnName
1238
1282
  }
1283
+ // An array's item schema that gets two item functions (see the items case in
1284
+ // genCodeNode): an object of many optional properties and nothing else that
1285
+ // reads keys. 32 or more optional properties, no names on Object.prototype
1286
+ // (the key loop reads `v[k]`, which for `__proto__` is not the data), and no
1287
+ // keyword that needs every key or the evaluated set.
1288
+ const KEY_LOOP_MIN = 32
1289
+ const KEY_LOOP_KEYS = new Set(['type', 'properties', 'required', 'title', 'description', '$comment', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
1290
+ function keyLoopEligible (item) {
1291
+ if (item === null || typeof item !== 'object' || Array.isArray(item) || item.type !== 'object') return false
1292
+ const props = item.properties
1293
+ if (props === null || typeof props !== 'object' || Array.isArray(props)) return false
1294
+ for (const k of Object.keys(item)) if (!KEY_LOOP_KEYS.has(k)) return false
1295
+ const req = new Set(Array.isArray(item.required) ? item.required : [])
1296
+ let optional = 0
1297
+ for (const k of Object.keys(props)) {
1298
+ if (PROTO_NAMES.has(k) || k === '__proto__') return false
1299
+ if (!req.has(k)) optional++
1300
+ }
1301
+ return optional >= KEY_LOOP_MIN
1302
+ }
1303
+ // Whether two values list the same keys in the same order, which objects of
1304
+ // one shape do. Anything that is not two objects counts as alike.
1305
+ const SAME_KEYS_HELPER = 'function _sameKeys(a,b){if(a===null||b===null||typeof a!==\'object\'||typeof b!==\'object\')return true;const ka=[];for(const k in a)ka.push(k);let i=0;for(const k in b){if(ka[i++]!==k)return false}return i===ka.length}'
1306
+ // One item function: the schema's ordinary code, or (keyLoop) the same with
1307
+ // its optional properties checked from the item's own keys. Null when either
1308
+ // cannot be written as a function of its own.
1309
+ function emitItemFn (item, ctx, keyLoop) {
1310
+ if (!ctx.preamble) return null
1311
+ const bodyLines = []
1312
+ const outerDeferred = ctx.deferredChecks
1313
+ ctx.deferredChecks = null
1314
+ const outerReuse = ctx.plainReuse
1315
+ ctx.plainReuse = undefined
1316
+ const outerBytes = ctx.fnBytes
1317
+ ctx.fnBytes = 0
1318
+ const outerNode = ctx.keyLoopNode
1319
+ ctx.keyLoopNode = keyLoop ? item : null
1320
+ try { genIsolated(ctx, () => genCode(item, 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes; ctx.keyLoopNode = outerNode }
1321
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
1322
+ ctx.deferredChecks = outerDeferred
1323
+ if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
1324
+ const fnName = '_it' + (ctx.itemFns = (ctx.itemFns || 0) + 1)
1325
+ hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
1326
+ return fnName
1327
+ }
1239
1328
  function countInline (lines, from, ctx) {
1240
1329
  let n = 0
1241
1330
  for (let i = from; i < lines.length; i++) n += lines[i].length
@@ -2003,6 +2092,75 @@ function sameInstanceEdges(root, node) {
2003
2092
  if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
2004
2093
  return out
2005
2094
  }
2095
+ // A definition that reaches a `$ref` to itself without moving to another value
2096
+ // and through no other reference (`X: { allOf: [{ $ref: X }, ...] }`, as
2097
+ // SchemaStore's cloudify has) re-enters itself on the same value. The
2098
+ // interpreted engine evaluates X once more there and answers the next
2099
+ // re-entry with true, its fixed point: X comes to f(f(true)), where f is X
2100
+ // with the reference as its argument. Reached through allOf and anyOf alone,
2101
+ // f is monotone, a conjunction with a disjunction, and then f(f(true)) is
2102
+ // f(true), so the `$ref` can be taken as true. The copy given to the
2103
+ // generators drops it, and the same-instance gate below lets the schema
2104
+ // through. Through oneOf, not, if, then or else the two differ, and those are
2105
+ // left as they are, as is a cycle through two or more references or a
2106
+ // definition some `$ref` points inside of: they still decline.
2107
+ const MONOTONE_LISTS = ['allOf', 'anyOf']
2108
+ const _selfLoopCache = new WeakMap()
2109
+ function breakSelfLoops(root) {
2110
+ if (root === null || typeof root !== 'object') return root
2111
+ const hit = _selfLoopCache.get(root)
2112
+ if (hit !== undefined) return hit
2113
+ let out = root
2114
+ const key = root.$defs && typeof root.$defs === 'object' ? '$defs' : root.definitions && typeof root.definitions === 'object' ? 'definitions' : null
2115
+ // The document's text is read only once a self-reference is found: almost
2116
+ // no schema has one, and serializing every schema with definitions on every
2117
+ // compile cost a large one a third of a millisecond for nothing.
2118
+ let text = null
2119
+ const doc = () => (text === null ? (text = JSON.stringify(root)) : text)
2120
+ if (key !== null) {
2121
+ const defs = root[key]
2122
+ let copies = null
2123
+ for (const name of Object.keys(defs)) {
2124
+ const def = defs[name]
2125
+ if (def === null || typeof def !== 'object' || Array.isArray(def)) continue
2126
+ // Nodes of X reached on the same value without a reference, and those
2127
+ // among them whose `$ref` names X.
2128
+ const back = new Set()
2129
+ const seen = new Set()
2130
+ const walk = (n) => {
2131
+ if (n === null || typeof n !== 'object' || seen.has(n)) return
2132
+ seen.add(n)
2133
+ if (typeof n.$ref === 'string' && localPointerTarget(root, n.$ref) === def) back.add(n)
2134
+ for (const k of MONOTONE_LISTS) if (Array.isArray(n[k])) for (const x of n[k]) walk(x)
2135
+ }
2136
+ walk(def)
2137
+ if (back.size === 0) continue
2138
+ if (doc().includes('"unevaluated')) { copies = null; break }
2139
+ // A reference into X's inside could enter one of those nodes some other way.
2140
+ const enc = name.replace(/~/g, '~0').replace(/\//g, '~1')
2141
+ const inside = ['#/' + key + '/' + enc + '/', '#/' + key + '/' + encodeURIComponent(enc) + '/']
2142
+ if (inside.some((p) => doc().includes('"$ref":"' + p))) continue
2143
+ const copy = (n, inNames) => {
2144
+ if (n === null || typeof n !== 'object') return n
2145
+ if (Array.isArray(n)) return n.map((x) => copy(x, false))
2146
+ const o = {}
2147
+ for (const k of Object.keys(n)) {
2148
+ if (k === '$ref' && back.has(n)) continue
2149
+ const v = !inNames && SKIP_VALUE_KEYS.has(k) ? n[k] : copy(n[k], !inNames && NAME_MAPS.has(k))
2150
+ if (k === '__proto__') Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
2151
+ else o[k] = v
2152
+ }
2153
+ return o
2154
+ }
2155
+ if (copies === null) copies = { ...defs }
2156
+ copies[name] = copy(def)
2157
+ }
2158
+ if (copies !== null) out = { ...root, [key]: copies }
2159
+ }
2160
+ _selfLoopCache.set(root, out)
2161
+ return out
2162
+ }
2163
+
2006
2164
  const _sameInstanceCycleCache = new WeakMap()
2007
2165
  function hasSameInstanceRefCycle(root) {
2008
2166
  if (root === null || typeof root !== 'object') return false
@@ -2344,6 +2502,13 @@ function normalizeRefs(root, schemaMap) {
2344
2502
  return out
2345
2503
  }
2346
2504
  const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
2505
+ // The keywords whose value is a map from names to schemas. Inside one, a key
2506
+ // is a name, so `default` or `examples` there is a property with a schema,
2507
+ // not a value: the walks below skip SKIP_VALUE_KEYS only in a schema node.
2508
+ // OpenAPI 3.1's own schema has a property named `examples` whose schema is a
2509
+ // `$ref`, and the dynamic-scope copy kept it unrenamed, so the whole schema
2510
+ // fell to the interpreter.
2511
+ const NAME_MAPS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'])
2347
2512
  // A schema whose every reference is `#` or a plain `#/$defs/name` (or
2348
2513
  // `definitions`), with no `$id` or `$anchor` below the root to move the base
2349
2514
  // they resolve against, needs nothing from the resolver: each reference
@@ -2429,6 +2594,9 @@ function expandDynamicScopes(root, schemaMap) {
2429
2594
  }
2430
2595
  const copies = new Map() // node (or a boolean sentinel) -> Map(signature -> entry)
2431
2596
  const order = []
2597
+ // Values kept as they are (an enum's list, a default): not copies, so the
2598
+ // naming pass below does not walk them.
2599
+ const keptRaw = new WeakSet()
2432
2600
  let fail = false
2433
2601
  // The entry for a node in a scope: its copy, made on the first ask, and
2434
2602
  // later its name. A `$ref` slot holds the entry until names are given.
@@ -2471,7 +2639,10 @@ function expandDynamicScopes(root, schemaMap) {
2471
2639
  continue
2472
2640
  }
2473
2641
  if (isSchema && DYN_DROP.has(k) && !(k === '$id' && node === root)) continue
2474
- setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? v : copyValue(v, base, chain))
2642
+ if (isSchema && SKIP_VALUE_KEYS.has(k)) {
2643
+ if (v !== null && typeof v === 'object') keptRaw.add(v)
2644
+ setOwn(out, k, v)
2645
+ } else setOwn(out, k, copyValue(v, base, chain))
2475
2646
  }
2476
2647
  return e
2477
2648
  }
@@ -2506,7 +2677,7 @@ function expandDynamicScopes(root, schemaMap) {
2506
2677
  o.$ref = v.name === '#' ? '#' : '#/' + defsKey + '/' + v.name
2507
2678
  continue
2508
2679
  }
2509
- if (!SKIP_VALUE_KEYS.has(k)) fix(v)
2680
+ if (!(v !== null && typeof v === 'object' && keptRaw.has(v))) fix(v)
2510
2681
  }
2511
2682
  }
2512
2683
  fix(rootCopy)
@@ -2540,10 +2711,10 @@ function normalizeRefsWalk(root, schemaMap) {
2540
2711
  const visited = new Set()
2541
2712
  let fail = false
2542
2713
  let dynamic = false
2543
- const visit = (node) => {
2714
+ const visit = (node, inNames) => {
2544
2715
  if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
2545
2716
  visited.add(node)
2546
- if (Array.isArray(node)) { for (const n of node) visit(n); return }
2717
+ if (Array.isArray(node)) { for (const n of node) visit(n, false); return }
2547
2718
  if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
2548
2719
  // A dynamic reference reached through another document is unrolled by
2549
2720
  // scope the same way one in the root is (expandDynamicScopes, below).
@@ -2560,13 +2731,13 @@ function normalizeRefsWalk(root, schemaMap) {
2560
2731
  // The root, however it is spelled, is `#`: a copy of it under its own
2561
2732
  // definitions would contain itself.
2562
2733
  if (t === root) { /* named '#' below */ }
2563
- else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
2734
+ else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t, false) }
2564
2735
  if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
2565
2736
  }
2566
2737
  }
2567
- for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
2738
+ for (const k of Object.keys(node)) if (inNames || !SKIP_VALUE_KEYS.has(k)) visit(node[k], !inNames && NAME_MAPS.has(k))
2568
2739
  }
2569
- visit(root)
2740
+ visit(root, false)
2570
2741
  if (fail) return null
2571
2742
  if (dynamic) return expandDynamicScopes(root, schemaMap)
2572
2743
  if (refTarget.size === 0) return root
@@ -2580,13 +2751,13 @@ function normalizeRefsWalk(root, schemaMap) {
2580
2751
  }
2581
2752
  // The copy: references renamed, nested `$id`s dropped.
2582
2753
  const memo = new Map()
2583
- const walk = (node) => {
2754
+ const walk = (node, inNames) => {
2584
2755
  if (node === null || typeof node !== 'object') return node
2585
2756
  if (memo.has(node)) return memo.get(node)
2586
2757
  if (Array.isArray(node)) {
2587
2758
  const out = []
2588
2759
  memo.set(node, out)
2589
- for (const x of node) out.push(walk(x))
2760
+ for (const x of node) out.push(walk(x, false))
2590
2761
  return out
2591
2762
  }
2592
2763
  const out = {}
@@ -2598,13 +2769,13 @@ function normalizeRefsWalk(root, schemaMap) {
2598
2769
  out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
2599
2770
  continue
2600
2771
  }
2601
- setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k]))
2772
+ setOwn(out, k, !inNames && SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k], !inNames && NAME_MAPS.has(k)))
2602
2773
  }
2603
2774
  return out
2604
2775
  }
2605
- const out = walk(root)
2776
+ const out = walk(root, false)
2606
2777
  const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
2607
- for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
2778
+ for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t, false)
2608
2779
  out[defsKey] = defs
2609
2780
  return out
2610
2781
  }
@@ -2667,7 +2838,14 @@ function sourceSize (lines, ctx) {
2667
2838
 
2668
2839
  function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2669
2840
  const inputSchema = schema
2841
+ // The verdict alone takes a self-reference on the same value as true (see
2842
+ // breakSelfLoops): the interpreted engine reports the errors of the second
2843
+ // visit as well, so the error and combined generators keep declining such a
2844
+ // schema and its errors come from that engine.
2670
2845
  schema = prepareForCodegen(schema, schemaMap)
2846
+ // Only a schema with definitions can hold a self-reference; the others do
2847
+ // not pay for compiling the function on their first schema.
2848
+ if (schema !== null && typeof schema === 'object' && (schema.$defs || schema.definitions)) schema = breakSelfLoops(schema)
2671
2849
  if (typeof schema === 'boolean') return schema ? () => true : () => false
2672
2850
  if (typeof schema !== 'object' || schema === null) return null
2673
2851
  // removeAdditional: a verdict function that deletes the keys the remover
@@ -2805,13 +2983,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2805
2983
  // validateJSON or get extended, and rewriting and compiling the source twice
2806
2984
  // more on every schema was about a third of what a first call cost.
2807
2985
  let body, hybridThunk, tailThunk, resultThunk
2986
+ // The live verdict of a guarded schema declares its functions once, beside
2987
+ // the preamble, and returns the entry: written in the body they were made
2988
+ // again on every call, three closures per document (jsonld's recursive root
2989
+ // spent a third of its 46 ns on them). The source the emitters read, `body`
2990
+ // and the forms below, is unchanged.
2991
+ let liveTail = null, liveDecl = null
2808
2992
  if (ctx.usesRecursion) {
2809
2993
  // Self-recursive: a named body reached through the cycle guard, so data
2810
2994
  // that points back at itself settles instead of exhausting the stack.
2811
- const decl = emitRootGuard('_validate', 'd', 'true') +
2995
+ const decl = (ctx.rootFwd ? `${ctx.rootFwd}_f=_validate;` : '') + emitRootGuard('_validate', 'd', 'true') +
2812
2996
  `function _validate_b(d){\n ${checkStr}\n return true\n }\n `
2813
2997
  const run = emitGuardedRun('return _validate(d)', '', defSets)
2814
2998
  body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
2999
+ liveDecl = `${decl}function _run(d){\n ${run}\n }\n `
3000
+ liveTail = `${liveDecl}return _run`
2815
3001
  // Hybrid: keep the boolean answer, wrap only the outer call
2816
3002
  hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
2817
3003
  resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
@@ -2821,6 +3007,8 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2821
3007
  // re-entered with the same value; the guarded functions need their state.
2822
3008
  const run = emitGuardedRun(`return _body(d)`, '', defSets)
2823
3009
  body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
3010
+ liveDecl = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n `
3011
+ liveTail = `${liveDecl}return _run`
2824
3012
  hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
2825
3013
  resultThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
2826
3014
  tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
@@ -2839,17 +3027,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2839
3027
  const guardStr = needsGuard ? emitGuardState() : ''
2840
3028
  const preambleStr = guardStr +
2841
3029
  (ctx.preamble && ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '')
3030
+ // The tag goes in front of what is compiled here, never into the sources
3031
+ // the emitters read (_preambleSource, _source).
3032
+ const tag = seqTag()
2842
3033
 
2843
3034
  try {
2844
3035
  let boolFn
2845
- if (closureNames.length > 0) {
2846
- const factory = compileFunction(...closureNames, `${preambleStr}return function(d){${body}}`)
3036
+ if (liveTail !== null) {
3037
+ const factory = compileFunction(...closureNames, `${tag}${preambleStr}${liveTail}`)
3038
+ boolFn = factory(...closureValues)
3039
+ } else if (closureNames.length > 0) {
3040
+ const factory = compileFunction(...closureNames, `${tag}${preambleStr}return function(d){${body}}`)
2847
3041
  boolFn = factory(...closureValues)
2848
- } else if (preambleStr) {
2849
- const factory = compileFunction(`${preambleStr}return function(d){${body}}`)
2850
- boolFn = factory()
2851
3042
  } else {
2852
- boolFn = compileFunction('d', body)
3043
+ const factory = compileFunction(`${tag}${preambleStr}return function(d){${body}}`)
3044
+ boolFn = factory()
2853
3045
  }
2854
3046
 
2855
3047
  // Light by default: the pieces the tiering and standalone paths need
@@ -2869,7 +3061,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2869
3061
  attachLazyHeavy(boolFn, inputSchema, schemaMap, userFormats, opts, !!ctx.usesSafeRe)
2870
3062
  return boolFn
2871
3063
  }
2872
- attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body })
3064
+ attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl })
2873
3065
  return boolFn
2874
3066
  } catch {
2875
3067
  return null
@@ -2891,7 +3083,13 @@ function attachLazyHeavy (boolFn, schema, schemaMap, userFormats, opts, usesSafe
2891
3083
  }
2892
3084
  // The factories and the source, as the full compile attaches them.
2893
3085
  function attachHeavy (boolFn, P) {
2894
- const { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body } = P
3086
+ const { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl } = P
3087
+ // These factories are compiled later than the verdict, so they take a tag
3088
+ // of their own (seqTag).
3089
+ const tag = seqTag()
3090
+ // A guarded schema's functions are declared once in each factory, as in the
3091
+ // live verdict (see compileToJSCodegen), and each form only calls `_run`.
3092
+ const live = (ret, thunk) => (liveDecl ? `${tag}${preambleStr}${liveDecl}return function(d){${ret}}` : `${tag}${preambleStr}return function(d){${thunk()}}`)
2895
3093
  // The hybrid: same body, return R instead of true, return E(d) instead of
2896
3094
  // false. Compiled on first request; null when that compile is refused, and
2897
3095
  // the caller then assembles from the error functions instead.
@@ -2899,7 +3097,7 @@ function attachHeavy (boolFn, P) {
2899
3097
  boolFn._hybridFactory = (R, E) => {
2900
3098
  if (hybridFactory === undefined) {
2901
3099
  try {
2902
- hybridFactory = compileFunction(...closureNames, 'R', 'E', `${preambleStr}return function(d){${hybridBody()}}`)
3100
+ hybridFactory = compileFunction(...closureNames, 'R', 'E', live('return _run(d)?R:E(d)', hybridBody))
2903
3101
  } catch {
2904
3102
  hybridFactory = null
2905
3103
  }
@@ -2918,7 +3116,7 @@ function attachHeavy (boolFn, P) {
2918
3116
  boolFn._resultFactory = (LR, B, W, EE) => {
2919
3117
  if (resultFactory === undefined) {
2920
3118
  try {
2921
- resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
3119
+ resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', live(`return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`, resultThunk))
2922
3120
  } catch {
2923
3121
  resultFactory = null
2924
3122
  }
@@ -2938,7 +3136,7 @@ function attachHeavy (boolFn, P) {
2938
3136
  boolFn._withTail = (tail) => {
2939
3137
  if (tailFactory === undefined) {
2940
3138
  try {
2941
- tailFactory = compileFunction(...closureNames, '__ataTail', `${preambleStr}return function(d){${tailThunk()}}`)
3139
+ tailFactory = compileFunction(...closureNames, '__ataTail', live('return _run(d)&&__ataTail(d)', tailThunk))
2942
3140
  } catch {
2943
3141
  tailFactory = null
2944
3142
  }
@@ -3437,7 +3635,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3437
3635
  // object: its type check fails on undefined. A name on Object.prototype
3438
3636
  // is never absent that way, and it is checked explicitly.
3439
3637
  if (!hasTypeCheck || PROTO_NAMES.has(key)) {
3440
- reqChecks.push(`!${ownKeyExpr(ctx, v, key)}`)
3638
+ reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
3441
3639
  } else {
3442
3640
  typed.push(key)
3443
3641
  }
@@ -3458,7 +3656,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3458
3656
  lines.push(`if(!${pk || protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
3459
3657
  }
3460
3658
  } else if (schema.required && schema.required.length > 0) {
3461
- const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
3659
+ const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
3462
3660
  if (isObj) {
3463
3661
  lines.push(`if(${checks.join('||')})return false`)
3464
3662
  } else {
@@ -3548,7 +3746,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3548
3746
 
3549
3747
  if (schema.pattern) {
3550
3748
  // Try inline charCode compilation for simple patterns (avoids RegExp engine)
3551
- const inlineCheck = compilePatternInline(schema.pattern, v)
3749
+ const inlineCheck = classRunCheck(ctx, schema.pattern, v) || compilePatternInline(schema.pattern, v)
3552
3750
  if (inlineCheck) {
3553
3751
  lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
3554
3752
  } else {
@@ -3659,13 +3857,32 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3659
3857
 
3660
3858
  // properties — use hoisted vars for required props, hoist optional to locals too
3661
3859
  if (schema.properties) {
3860
+ // Without a known object type, one test that the value is an object
3861
+ // covers every property below, instead of one test per property: a
3862
+ // definition with many optional properties stays small enough for V8 to
3863
+ // optimize (tslint's rules, 200 of them, did not).
3864
+ // Not an array: `properties` applies to objects only, and an array's own
3865
+ // `length` was checked against a property of that name, rejecting a list
3866
+ // that every other engine accepts. Only `length` and index names can be an
3867
+ // array's own key (an inherited one fails the own-key test), so the array
3868
+ // test is written only for those: on every other node it cost a reject
3869
+ // path 18% in its first calls (github-issue-forms).
3870
+ const arrayOwnable = Object.keys(schema.properties).some((k) => k === 'length' || /^(?:0|[1-9][0-9]*)$/.test(k))
3871
+ if (!isObj) lines.push(`if(typeof ${v}==='object'&&${v}!==null${arrayOwnable ? `&&!Array.isArray(${v})` : ''}){`)
3872
+ // The key-loop form (see emitItemFn): the optional properties are checked
3873
+ // from the object's own keys in one loop instead of one read each.
3874
+ const keyLoop = isObj && ctx.keyLoopNode === schema
3875
+ const keyCases = keyLoop ? [] : null
3662
3876
  for (const [key, prop] of Object.entries(schema.properties)) {
3877
+ if (keyLoop && !requiredSet.has(key)) { keyCases.push([key, prop]); continue }
3663
3878
  const from = lines.length
3664
3879
  // A large subschema as a function of its own (emitPropFn); the call
3665
3880
  // takes the place of the inline code in each of the three shapes below.
3666
3881
  const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
3667
3882
  if (requiredSet.has(key) && isObj) {
3668
- // Required + type:object — hoist to local to reduce repeated property lookups
3883
+ // Required + type:object — hoist to local to reduce repeated property lookups.
3884
+ // A key holding undefined is missing, as in the default validator: a
3885
+ // typed key's check fails on undefined, the others are tested above.
3669
3886
  const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
3670
3887
  const combined = fn ? null : tryGenCombined(prop, access, ctx)
3671
3888
  if (fn) {
@@ -3681,7 +3898,21 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3681
3898
  } else {
3682
3899
  genCode(prop, access, lines, ctx)
3683
3900
  }
3684
- } else if (isObj) {
3901
+ } else if (!isObj) {
3902
+ // A value not known to be an object (tested once above): the key test
3903
+ // first, as before, then the value, read once. An absent key costs
3904
+ // what it did; a key holding undefined is skipped. A required key is
3905
+ // there on any object that got this far, so its value is read first
3906
+ // and only then asked whether it is the object's own: one test fewer,
3907
+ // and the read an absent key would make expensive does not happen.
3908
+ const oi = ctx.varCounter++
3909
+ const local = `_o${oi}`
3910
+ if (requiredSet.has(key)) lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
3911
+ else lines.push(`if(${ownKeyExpr(ctx, v, key)}){const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
3912
+ if (fn) lines.push(`if(!${fn}(${local}))return false`)
3913
+ else genCode(prop, local, lines, ctx)
3914
+ lines.push(`}}`)
3915
+ } else {
3685
3916
  // Optional — hoist to local, check undefined
3686
3917
  const oi = ctx.varCounter++
3687
3918
  const local = `_o${oi}`
@@ -3695,14 +3926,28 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3695
3926
  genCode(prop, local, lines, ctx)
3696
3927
  }
3697
3928
  lines.push(`}}`)
3698
- } else {
3699
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
3700
- if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
3701
- else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
3702
- lines.push(`}`)
3703
3929
  }
3704
3930
  countInline(lines, from, ctx)
3705
3931
  }
3932
+ if (keyCases !== null && keyCases.length > 0) {
3933
+ const kk = `_kk${ctx.varCounter++}`
3934
+ const kv = `_kv${ctx.varCounter++}`
3935
+ // The switch is one line: callers join lines with `;`, and a `;`
3936
+ // before a `case` is a syntax error (the error generator's guard did
3937
+ // that, and the rich error program silently fell back to the plain one).
3938
+ let cases = ''
3939
+ for (const [key, prop] of keyCases) {
3940
+ const caseLines = []
3941
+ const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
3942
+ const combined = fn ? null : tryGenCombined(prop, kv, ctx)
3943
+ if (fn) caseLines.push(`if(!${fn}(${kv}))return false`)
3944
+ else if (combined) caseLines.push(combined)
3945
+ else genCode(prop, kv, caseLines, ctx)
3946
+ cases += `case ${JSON.stringify(key)}:{${caseLines.join(';')};break}`
3947
+ }
3948
+ lines.push(`{${ownHead(ctx, v, kk, 'const')}{const ${kv}=${v}[${kk}];if(${kv}!==undefined){switch(${kk}){${cases}}}}}`)
3949
+ }
3950
+ if (!isObj) lines.push(`}`)
3706
3951
  }
3707
3952
 
3708
3953
  // items — pass known type info to children
@@ -3712,11 +3957,26 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3712
3957
  ctx.varCounter++
3713
3958
  // items applies after the prefixItems positions (Draft 2020-12).
3714
3959
  const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
3715
- lines.push(isArr
3716
- ? `for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
3717
- : `if(Array.isArray(${v})){for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
3718
- genCode(schema.items, elem, lines, ctx)
3719
- lines.push(isArr ? `}` : `}}`)
3960
+ const fk = keyLoopEligible(schema.items) ? emitItemFn(schema.items, ctx, true) : null
3961
+ if (fk !== null) {
3962
+ // Records with many optional properties: when three of them (first,
3963
+ // middle, last) list their keys alike, they likely share one shape and
3964
+ // reading each declared property, written in the loop as always, is
3965
+ // cheapest; when they do not, each record walks its own keys instead
3966
+ // (fk), which costs the same whatever their shapes. Decided per call,
3967
+ // from three items.
3968
+ hoistOnce(ctx, 'sameKeysHelper', SAME_KEYS_HELPER)
3969
+ const n = `_n${ctx.varCounter++}`
3970
+ lines.push(`${isArr ? '' : `if(Array.isArray(${v}))`}{const ${n}=${v}.length;if(${n}-${start}>2&&(!_sameKeys(${v}[${start}],${v}[(${n}+${start})>>1])||!_sameKeys(${v}[${start}],${v}[${n}-1]))){for(let ${idx}=${start};${idx}<${n};${idx}++)if(!${fk}(${v}[${idx}]))return false}else{for(let ${idx}=${start};${idx}<${n};${idx}++){const ${elem}=${v}[${idx}]`)
3971
+ genCode(schema.items, elem, lines, ctx)
3972
+ lines.push(`}}}`)
3973
+ } else {
3974
+ lines.push(isArr
3975
+ ? `for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
3976
+ : `if(Array.isArray(${v})){for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
3977
+ genCode(schema.items, elem, lines, ctx)
3978
+ lines.push(isArr ? `}` : `}}`)
3979
+ }
3720
3980
  }
3721
3981
 
3722
3982
  // prefixItems
@@ -3751,7 +4011,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3751
4011
 
3752
4012
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
3753
4013
  // Falls back to inline closures if any branch touches recursive validation.
3754
- if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, knownType)
4014
+ if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, effectiveType)
3755
4015
 
3756
4016
  // not
3757
4017
  if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
@@ -3920,7 +4180,42 @@ function genContainsNode(schema, v, lines, ctx, knownType, isArr) {
3920
4180
  }
3921
4181
  }
3922
4182
 
4183
+ // A branch of anyOf or oneOf that cannot be a function in the preamble (it
4184
+ // calls `_validate`, the recursive entry, which is not in scope there) was a
4185
+ // closure in an array built on every call. Written in place instead, as a
4186
+ // labelled block whose `return false` becomes the break out of it: jsonld's
4187
+ // root anyOf, one branch of which recurses, took 61 ns a document against 30
4188
+ // for the default validator. Only a body with no function of its own, whose
4189
+ // returns are all `return false`; the parameter becomes a local of the block,
4190
+ // and `\b` leaves longer names that start the same alone.
4191
+ function inlineBranchBody (body, param, label, local) {
4192
+ if (/\bfunction\b|=>|\bthis\b|\barguments\b/.test(body)) return null
4193
+ let core
4194
+ if (body === 'return true') core = ''
4195
+ else if (body.endsWith(';return true')) core = body.slice(0, -';return true'.length)
4196
+ else return null
4197
+ if (/\breturn\b(?! false\b)/.test(core)) return null
4198
+ const re = new RegExp('\\b' + param + '\\b', 'g')
4199
+ return core.replace(/\breturn false\b/g, 'break ' + label).replace(re, local)
4200
+ }
4201
+
4202
+ // A single-branch anyOf or oneOf holds exactly when its branch does, so the
4203
+ // branch is written in place. When the branch is only a $ref that an allOf
4204
+ // beside it also applies, it is already checked and nothing is written:
4205
+ // devinit's items are `{ allOf: [{ $ref: R }], anyOf: [{ $ref: R }] }`, and
4206
+ // each run object was checked twice. The verdict only; errors keep the
4207
+ // keyword's own shape.
4208
+ const refOnly = (b) => b !== null && typeof b === 'object' && typeof b.$ref === 'string' && Object.keys(b).every((k) => k === '$ref' || REF_NEUTRAL_SIBLINGS.has(k))
4209
+ function singleBranch(schema, branches, v, lines, ctx, knownType) {
4210
+ if (branches.length !== 1) return false
4211
+ const b = branches[0]
4212
+ if (refOnly(b) && Array.isArray(schema.allOf) && schema.allOf.some((x) => refOnly(x) && x.$ref === b.$ref)) return true
4213
+ genCode(b, v, lines, ctx, knownType)
4214
+ return true
4215
+ }
4216
+
3923
4217
  function genAnyOfNode(schema, v, lines, ctx, knownType) {
4218
+ if (singleBranch(schema, schema.anyOf, v, lines, ctx, knownType)) return
3924
4219
  const fi = ctx.varCounter++
3925
4220
  const branchBodies = []
3926
4221
  const reused = []
@@ -3956,6 +4251,13 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
3956
4251
  const checks = names.map(n => `${n}(${v})`).join('||')
3957
4252
  lines.push(`if(!(${checks}))return false`)
3958
4253
  } else {
4254
+ const inl = branchBodies.map((body, i) => inlineBranchBody(body, '_av', `_al${fi}_${i}`, `_ai${fi}_${i}`))
4255
+ if (inl.every((x) => x !== null)) {
4256
+ const m = `_am${fi}`
4257
+ const blocks = inl.map((core, i) => `_al${fi}_${i}:{const _ai${fi}_${i}=${v};${core};${m}=true}`)
4258
+ lines.push(`{let ${m}=false;${blocks.map((b, i) => (i === 0 ? b : `if(!${m}){${b}}`)).join('')}if(!${m})return false}`)
4259
+ return
4260
+ }
3959
4261
  const fns = branchBodies.map(body => `function(_av){${body}}`)
3960
4262
  lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi})return false}`)
3961
4263
  }
@@ -4007,6 +4309,7 @@ function keyedBranches (branches) {
4007
4309
  }
4008
4310
 
4009
4311
  function genOneOfNode(schema, v, lines, ctx, knownType) {
4312
+ if (singleBranch(schema, schema.oneOf, v, lines, ctx, knownType)) return
4010
4313
  const fi = ctx.varCounter++
4011
4314
  const branchBodies = []
4012
4315
  const reused = []
@@ -4018,19 +4321,24 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4018
4321
  // 86919 branch functions and 197 MB of source, and took seconds to build.
4019
4322
  // A hoisted branch takes its input as a parameter and reads only
4020
4323
  // module-level names, so the same schema object gives the same function.
4021
- if (canHoist && !ctx.hoistedBranch) ctx.hoistedBranch = new Map()
4324
+ // A branch reached with an object already checked is written for one, a
4325
+ // different function, so it is reused only for the same known type.
4326
+ const kt = knownType === 'object' ? 'object' : undefined
4327
+ const mapName = kt ? 'hoistedBranchObj' : 'hoistedBranch'
4328
+ if (canHoist && !ctx[mapName]) ctx[mapName] = new Map()
4329
+ const branchMap = canHoist ? ctx[mapName] : null
4022
4330
  for (let i = 0; i < schema.oneOf.length; i++) {
4023
4331
  const sub = schema.oneOf[i]
4024
4332
  const key = canHoist ? branchKey(sub, ctx) : undefined
4025
- const known = key !== undefined ? ctx.hoistedBranch.get(key) : undefined
4333
+ const known = key !== undefined ? branchMap.get(key) : undefined
4026
4334
  if (known !== undefined) {
4027
4335
  reused[i] = known.name
4028
4336
  branchBodies.push(known.body)
4029
4337
  continue
4030
4338
  }
4031
4339
  const subLines = []
4032
- if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
4033
- else genCode(sub, '_ov', subLines, ctx)
4340
+ if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx, kt))
4341
+ else genCode(sub, '_ov', subLines, ctx, kt)
4034
4342
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
4035
4343
  // _validate is the recursive entry — hoisting branches above it breaks scope.
4036
4344
  if (/\b_validate\b/.test(body)) canHoist = false
@@ -4042,7 +4350,7 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4042
4350
  const name = `_of${fi}_b${i}`
4043
4351
  hoist(ctx, `function ${name}(_ov){${body}}`)
4044
4352
  const key = branchKey(schema.oneOf[i], ctx)
4045
- if (key !== undefined) ctx.hoistedBranch.set(key, { name, body })
4353
+ if (key !== undefined) branchMap.set(key, { name, body })
4046
4354
  return name
4047
4355
  })
4048
4356
  const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
@@ -4059,12 +4367,77 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4059
4367
  }
4060
4368
  lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
4061
4369
  } else {
4370
+ const inl = branchBodies.map((body, i) => inlineBranchBody(body, '_ov', `_ol${fi}_${i}`, `_oj${fi}_${i}`))
4371
+ if (inl.every((x) => x !== null)) {
4372
+ const c = `_oc${fi}`
4373
+ const blocks = inl.map((core, i) => `_ol${fi}_${i}:{const _oj${fi}_${i}=${v};${core};if(++${c}>1)return false}`)
4374
+ lines.push(`{let ${c}=0;${blocks.join('')}if(${c}!==1)return false}`)
4375
+ return
4376
+ }
4062
4377
  const fns = branchBodies.map(body => `function(_ov){${body}}`)
4063
4378
  lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)return false}if(_oc${fi}!==1)return false}`)
4064
4379
  }
4065
4380
  }
4066
4381
 
4382
+ // A subschema checked on the same value (`not`, `if`, `then`, `else`) as a
4383
+ // named function in the preamble, so the check allocates nothing: written in
4384
+ // the body, each was a closure created on every call, and SchemaStore's
4385
+ // cloudify, about 3000 if/then pairs, spent most of its time making them.
4386
+ // Generated over its own parameter, like a hoisted oneOf branch. Null where
4387
+ // it cannot be hoisted, and the caller writes the closure as before.
4388
+ // The function also takes whether the value is an ordinary object (see
4389
+ // withPlain), which the caller has usually worked out already: each of
4390
+ // minecraft-predicate's 25 if/then pairs read the object's prototype again.
4391
+ function hoistedCheck (ctx, sub, param, prefix) {
4392
+ if (!ctx.preamble) return null
4393
+ const bodyLines = []
4394
+ const outerDeferred = ctx.deferredChecks
4395
+ ctx.deferredChecks = null
4396
+ const outerReuse = ctx.plainReuse
4397
+ const flag = param + 'pk'
4398
+ ctx.plainReuse = param
4399
+ try {
4400
+ genIsolated(ctx, () => {
4401
+ ctx.plainOf = new Map([[param, flag]])
4402
+ genCode(sub, param, bodyLines, ctx)
4403
+ })
4404
+ } finally { ctx.plainReuse = outerReuse }
4405
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4406
+ ctx.deferredChecks = outerDeferred
4407
+ const body = bodyLines.length === 0 ? 'return true' : `${bodyLines.join(';')};return true`
4408
+ // _validate is the recursive entry, not in scope above it.
4409
+ if (/\b_validate\b/.test(body)) return null
4410
+ const name = prefix + ctx.varCounter++
4411
+ // A body that never asks takes no flag, so the caller does not work it out.
4412
+ if (new RegExp('\\b' + flag + '\\b').test(body)) {
4413
+ hoist(ctx, `function ${name}(${param},${flag}){${body}}`)
4414
+ } else {
4415
+ if (!ctx.flaglessChecks) ctx.flaglessChecks = new Set()
4416
+ ctx.flaglessChecks.add(name)
4417
+ hoist(ctx, `function ${name}(${param}){${body}}`)
4418
+ }
4419
+ return name
4420
+ }
4421
+ // A call to a function from hoistedCheck, with the flag argument only where
4422
+ // the function takes one.
4423
+ function callHoisted (ctx, name, v, pk) {
4424
+ return ctx.flaglessChecks && ctx.flaglessChecks.has(name) ? `${name}(${v})` : `${name}(${v},${pk})`
4425
+ }
4426
+ // The second argument to a function from hoistedCheck: the caller's flag for
4427
+ // the value when it has one, else the test itself. Not inside the boolean
4428
+ // code the error and combined generators nest in their own functions, where a
4429
+ // flag recorded for the value may belong to a scope the call is not in.
4430
+ function plainArg (ctx, v) {
4431
+ const pk = ctx.nestedBoolean ? undefined : plainFlag(ctx, v)
4432
+ return pk || `(typeof ${v}==='object'&&${v}!==null&&${protoIs(ctx, v)})`
4433
+ }
4434
+
4067
4435
  function genNotNode(schema, v, lines, ctx, knownType) {
4436
+ const hoisted = hoistedCheck(ctx, schema.not, '_nv', '_nh')
4437
+ if (hoisted !== null) {
4438
+ lines.push(`if(${callHoisted(ctx, hoisted, v, plainArg(ctx, v))})return false`)
4439
+ return
4440
+ }
4068
4441
  const subLines = []
4069
4442
  genCode(schema.not, '_nv', subLines, ctx)
4070
4443
  if (subLines.length === 0) {
@@ -4076,6 +4449,29 @@ function genNotNode(schema, v, lines, ctx, knownType) {
4076
4449
  }
4077
4450
 
4078
4451
  function genIfNode(schema, v, lines, ctx, knownType) {
4452
+ if (ctx.preamble) {
4453
+ const ifName = hoistedCheck(ctx, schema.if, '_iv', '_ih')
4454
+ const thenName = ifName !== null && schema.then !== undefined ? hoistedCheck(ctx, schema.then, '_tv', '_th') : null
4455
+ const elseName = ifName !== null && schema.else !== undefined ? hoistedCheck(ctx, schema.else, '_ev', '_eh') : null
4456
+ if (ifName !== null && (schema.then === undefined || thenName !== null) && (schema.else === undefined || elseName !== null)) {
4457
+ if (thenName === null && elseName === null) return
4458
+ // The flag is worked out once: from the caller when it has one, else in
4459
+ // a local when more than one of the functions takes it.
4460
+ const takers = [ifName, thenName, elseName].filter((n) => n !== null && !(ctx.flaglessChecks && ctx.flaglessChecks.has(n))).length
4461
+ let pk = plainArg(ctx, v)
4462
+ let open = '', close = ''
4463
+ if (takers > 1 && !/^[\w$]+$/.test(pk)) {
4464
+ const local = `_ipk${ctx.varCounter++}`
4465
+ open = `{const ${local}=${pk};`
4466
+ close = '}'
4467
+ pk = local
4468
+ }
4469
+ const thenCheck = thenName !== null ? `if(!${callHoisted(ctx, thenName, v, pk)})return false` : ''
4470
+ const elseCheck = elseName !== null ? `if(!${callHoisted(ctx, elseName, v, pk)})return false` : ''
4471
+ lines.push(`${open}if(${callHoisted(ctx, ifName, v, pk)}){${thenCheck}}else{${elseCheck}}${close}`)
4472
+ return
4473
+ }
4474
+ }
4079
4475
  const ifLines = []
4080
4476
  genCode(schema.if, '_iv', ifLines, ctx)
4081
4477
  const fi = ctx.varCounter++
@@ -4102,12 +4498,30 @@ function genIfNode(schema, v, lines, ctx, knownType) {
4102
4498
  lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
4103
4499
  }
4104
4500
 
4501
+ // The root as a definition function in the preamble reaches it: a function
4502
+ // declared there that calls whatever the entry stored in its slot, which the
4503
+ // entry's declarations fill in (see compileToJSCodegen). Named per compile,
4504
+ // and written with hoist, not hoistOnce, so a bundle never shares one slot
4505
+ // between two schemas. SchemaStore's jsone, whose definitions refer back to
4506
+ // the root, ran on the interpreter for want of it.
4507
+ function rootForwarder (ctx) {
4508
+ if (!ctx.rootFwd) {
4509
+ const name = `_root${ctx.varCounter++}`
4510
+ ctx.rootFwd = name
4511
+ hoist(ctx, `let ${name}_f=null;function ${name}(d){return ${name}_f(d)}`)
4512
+ }
4513
+ return ctx.rootFwd
4514
+ }
4515
+
4105
4516
  function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4106
4517
  // Self-reference "#" — recursive call to root validator
4107
4518
  if (schema.$ref === '#') {
4108
4519
  if (ctx.nestedBoolean) throw DECLINE
4109
4520
  ctx.usesRecursion = true
4110
- lines.push(`if(!_validate(${v}))return false`)
4521
+ // Inside a definition function, which sits in the preamble where the
4522
+ // entry is not in scope, the root is reached through a forwarder the
4523
+ // entry fills in (rootForwarder).
4524
+ lines.push(`if(!${ctx.inDefFn ? rootForwarder(ctx) : '_validate'}(${v}))return false`)
4111
4525
  if (!hasSiblings) return true
4112
4526
  }
4113
4527
  // 1. Local ref
@@ -4134,14 +4548,14 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4134
4548
  ctx.deferredChecks = null
4135
4549
  const outerReuse = ctx.plainReuse
4136
4550
  ctx.plainReuse = undefined
4137
- try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse }
4551
+ ctx.inDefFn = (ctx.inDefFn || 0) + 1
4552
+ try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.inDefFn-- }
4138
4553
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4139
4554
  ctx.deferredChecks = outerDeferred
4140
4555
  // The def is hoisted above the entry function, where the recursive
4141
- // entry `_validate` is not in scope; a body that calls it (a `$ref: '#'`
4142
- // inside the def) threw a ReferenceError at the first document that
4143
- // reached it. SchemaStore's jsone schema is such a case. Decline, as
4144
- // oneOf and anyOf do not hoist such a branch.
4556
+ // entry `_validate` is not in scope: a `$ref: '#'` inside it goes
4557
+ // through the forwarder (rootForwarder). Anything else naming the
4558
+ // entry still declines, as oneOf and anyOf do not hoist such a branch.
4145
4559
  if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
4146
4560
  // Entered through the cycle guard: see emitGuardState. In the fast
4147
4561
  // pass the wrapper only counts depth; the set is used by the second,
@@ -4331,8 +4745,15 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4331
4745
  }
4332
4746
  lines.push(`}}`)
4333
4747
  } else {
4334
- // No additionalProperties: validate matching keys + propertyNames
4748
+ // No additionalProperties: validate matching keys + propertyNames.
4749
+ // A pattern whose subschema checks nothing (`{}`, `true`), as OpenAPI's
4750
+ // `^x-` extensions are on nearly every object, needs no matching here:
4751
+ // the loop read every key's value and did nothing with it. With no such
4752
+ // check left and no propertyNames, there is no loop at all.
4335
4753
  handledPropertyNames = !!pn
4754
+ const live = []
4755
+ for (let i = 0; i < ppEntries.length; i++) if (subChecks[i] !== '') live.push(i)
4756
+ if (!pn && live.length === 0) return handledPropertyNames
4336
4757
  lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4337
4758
  // propertyNames checks (merged)
4338
4759
  if (pn) {
@@ -4357,7 +4778,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4357
4778
  lines.push(`if(!_es${ei}.has(${kVar}))return false`)
4358
4779
  }
4359
4780
  }
4360
- for (let i = 0; i < ppEntries.length; i++) {
4781
+ for (const i of live) {
4361
4782
  lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
4362
4783
  }
4363
4784
  lines.push(`}}`)
@@ -4918,8 +5339,19 @@ const FORMAT_CODEGEN = {
4918
5339
  'json-pointer': (...a) => getFormats().jsonPointerSource(...a),
4919
5340
  'relative-json-pointer': (...a) => getFormats().relativeJsonPointerSource(...a),
4920
5341
  'uri-template': (...a) => getFormats().uriTemplateSource(...a),
4921
- iri: (...a) => getFormats().iriSource(...a),
4922
- 'iri-reference': (...a) => getFormats().iriReferenceSource(...a),
5342
+ // The URI family shares one hoisted walk (uriHelper): `_uri` is the uri
5343
+ // check with its regex in front, `_urif(s, mode)` the other three with
5344
+ // theirs, `_uriw(s, mode)` the walk itself.
5345
+ iri: (v, isStr, ctx) => {
5346
+ if (!ctx) return getFormats().iriSource(v, isStr)
5347
+ hoistOnce(ctx, '_uriHoisted', uriHelper())
5348
+ return isStr ? `if(!_urif(${v},3))return false` : `if(typeof ${v}==='string'&&!_urif(${v},3))return false`
5349
+ },
5350
+ 'iri-reference': (v, isStr, ctx) => {
5351
+ if (!ctx) return getFormats().iriReferenceSource(v, isStr)
5352
+ hoistOnce(ctx, '_uriHoisted', uriHelper())
5353
+ return isStr ? `if(!_urif(${v},2))return false` : `if(typeof ${v}==='string'&&!_urif(${v},2))return false`
5354
+ },
4923
5355
  'idn-email': (...a) => getFormats().idnEmailSource(...a),
4924
5356
  regex: (v, isStr) => {
4925
5357
  const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
@@ -4935,9 +5367,11 @@ const FORMAT_CODEGEN = {
4935
5367
  hoistOnce(ctx, '_uriHoisted', uriHelper())
4936
5368
  return isStr ? `if(!_uri(${v}))return false` : `if(typeof ${v}==='string'&&!_uri(${v}))return false`
4937
5369
  },
4938
- 'uri-reference': (v, isStr) => isStr
4939
- ? `{${getFormats().uriCharsSource(v, '0')}}`
4940
- : `if(typeof ${v}==='string'){${getFormats().uriCharsSource(v, '0')}}`,
5370
+ 'uri-reference': (v, isStr, ctx) => {
5371
+ if (!ctx) return getFormats().uriReferenceSource(v, isStr)
5372
+ hoistOnce(ctx, '_uriHoisted', uriHelper())
5373
+ return isStr ? `if(!_urif(${v},0))return false` : `if(typeof ${v}==='string'&&!_urif(${v},0))return false`
5374
+ },
4941
5375
  ipv4: (v, isStr, ctx) => {
4942
5376
  if (!ctx) return getFormats().ipv4Source(v, isStr)
4943
5377
  // Hoisted: a regex literal in the body would build a new RegExp per call.
@@ -4955,6 +5389,38 @@ const FORMAT_CODEGEN = {
4955
5389
  hostname: (...a) => getFormats().hostnameSource(...a),
4956
5390
  }
4957
5391
 
5392
+ // `^[class]+$`, `^[class]{m,n}$` and `^[class]{n}$` for a class of ASCII
5393
+ // letters, digits, `_` and `-`: a function in the preamble that reads each
5394
+ // character once and looks it up in a 128-entry table. The inline form below
5395
+ // read it again for every range it compared and built an arrow function on
5396
+ // every call; component.json's `^[0-9a-z-_]+$` cost the default validator's
5397
+ // RegExp the same. Null without a preamble, or for any other shape.
5398
+ function classRunCheck (ctx, pattern, v) {
5399
+ if (!ctx || !ctx.preamble) return null
5400
+ let m = pattern.match(/^\^(\[[\w\-]+\])(\+|\{(\d+)\}|\{(\d+),(\d+)\})\$$/)
5401
+ if (!m) return null
5402
+ const cls = m[1]
5403
+ let min, max
5404
+ if (m[2] === '+') { min = 1; max = Infinity }
5405
+ else if (m[3] !== undefined) { min = max = +m[3]; if (max <= 16) return null }
5406
+ else { min = +m[4]; max = +m[5] }
5407
+ if (!(min <= max)) return null
5408
+ let re
5409
+ try { re = new RegExp('^' + cls + '$') } catch { return null }
5410
+ if (!ctx.classRuns) ctx.classRuns = new Map()
5411
+ const key = cls + '|' + min + '|' + max
5412
+ let name = ctx.classRuns.get(key)
5413
+ if (name === undefined) {
5414
+ name = '_cr' + ctx.varCounter++
5415
+ ctx.classRuns.set(key, name)
5416
+ const bits = []
5417
+ for (let c = 0; c < 128; c++) bits.push(re.test(String.fromCharCode(c)) ? 1 : 0)
5418
+ const lenCheck = max === Infinity ? `n<${min}` : min === max ? `n!==${min}` : `n<${min}||n>${max}`
5419
+ hoist(ctx, `const ${name}_t=new Uint8Array([${bits.join(',')}])\n function ${name}(s){const n=s.length;if(${lenCheck})return false;for(let i=0;i<n;i++){const c=s.charCodeAt(i);if(c>127||${name}_t[c]===0)return false}return true}`)
5420
+ }
5421
+ return `${name}(${v})`
5422
+ }
5423
+
4958
5424
  // Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
4959
5425
  // Returns null if pattern is too complex for inline compilation.
4960
5426
  // Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
@@ -5141,6 +5607,14 @@ function unevalContributions (sub, kind, names, state, depth) {
5141
5607
  // Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
5142
5608
  // Returns a JS expression string or null if pattern is too complex
5143
5609
  function fastPrefixCheck(pattern, keyVar) {
5610
+ // ^name$ or ^(a|b)$ of plain characters is string equality (`$` without
5611
+ // the m flag ends the input); `.*` matches every string.
5612
+ const ex = pattern.match(/^\^(?:\(([a-zA-Z0-9_-]+(?:\|[a-zA-Z0-9_-]+)*)\)|([a-zA-Z0-9_-]+))\$$/)
5613
+ if (ex) {
5614
+ const alts = (ex[1] || ex[2]).split('|')
5615
+ if (alts.length <= 8) return `(${alts.map((a) => `${keyVar}===${JSON.stringify(a)}`).join('||')})`
5616
+ }
5617
+ if (pattern === '.*') return 'true'
5144
5618
  // Match patterns like ^literal (no regex metacharacters after ^)
5145
5619
  const m = pattern.match(/^\^([a-zA-Z0-9_\-./]+)$/)
5146
5620
  if (!m) return null
@@ -5568,4 +6042,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5568
6042
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5569
6043
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5570
6044
 
5571
- module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
6045
+ module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag }