ata-validator 1.53.0 → 1.55.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
 
@@ -541,7 +569,7 @@ function compileToJS(schema, defs, schemaMap) {
541
569
  // format — hand-written fast checks. A format the code generator asserts
542
570
  // but this path cannot is a decline, not a pass.
543
571
  if (schema.format) {
544
- const fc = FORMAT_CHECKS[schema.format]
572
+ const fc = formatChecks()[schema.format]
545
573
  if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
546
574
  else if (FORMAT_CODEGEN[schema.format]) return null
547
575
  }
@@ -839,14 +867,26 @@ function _canonical(x) {
839
867
  // schema with a `format` is compiled; loading it with this module cost every
840
868
  // process 0.8 ms of its cold start.
841
869
  let _formatsCache = null
842
- 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
+ }
843
878
  // The closure path's formats. It carried its own idea of an email and knew
844
879
  // five formats in all, so a schema that reached it was told "valid" for
845
880
  // anything the other engines turned away; the first call on a fresh validator
846
881
  // is the one that lands here. Every entry now names the same function the
847
882
  // other two paths use, and the list is the list they implement.
848
- const _fmtFns = require('./formats')
849
- 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 = {
850
890
  email: _fmtFns.email,
851
891
  'idn-email': _fmtFns.idnEmail,
852
892
  date: _fmtFns.date,
@@ -865,6 +905,8 @@ const FORMAT_CHECKS = {
865
905
  'json-pointer': _fmtFns.jsonPointer,
866
906
  'relative-json-pointer': _fmtFns.relativeJsonPointer,
867
907
  regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
908
+ }
909
+ return _formatChecks
868
910
  }
869
911
 
870
912
  // Dangerous JS property names that exist on Object.prototype
@@ -1238,6 +1280,51 @@ function emitPropFn (prop, ctx) {
1238
1280
  hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
1239
1281
  return fnName
1240
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
+ }
1241
1328
  function countInline (lines, from, ctx) {
1242
1329
  let n = 0
1243
1330
  for (let i = from; i < lines.length; i++) n += lines[i].length
@@ -2005,6 +2092,75 @@ function sameInstanceEdges(root, node) {
2005
2092
  if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
2006
2093
  return out
2007
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
+
2008
2164
  const _sameInstanceCycleCache = new WeakMap()
2009
2165
  function hasSameInstanceRefCycle(root) {
2010
2166
  if (root === null || typeof root !== 'object') return false
@@ -2346,6 +2502,13 @@ function normalizeRefs(root, schemaMap) {
2346
2502
  return out
2347
2503
  }
2348
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'])
2349
2512
  // A schema whose every reference is `#` or a plain `#/$defs/name` (or
2350
2513
  // `definitions`), with no `$id` or `$anchor` below the root to move the base
2351
2514
  // they resolve against, needs nothing from the resolver: each reference
@@ -2431,6 +2594,9 @@ function expandDynamicScopes(root, schemaMap) {
2431
2594
  }
2432
2595
  const copies = new Map() // node (or a boolean sentinel) -> Map(signature -> entry)
2433
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()
2434
2600
  let fail = false
2435
2601
  // The entry for a node in a scope: its copy, made on the first ask, and
2436
2602
  // later its name. A `$ref` slot holds the entry until names are given.
@@ -2473,7 +2639,10 @@ function expandDynamicScopes(root, schemaMap) {
2473
2639
  continue
2474
2640
  }
2475
2641
  if (isSchema && DYN_DROP.has(k) && !(k === '$id' && node === root)) continue
2476
- 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))
2477
2646
  }
2478
2647
  return e
2479
2648
  }
@@ -2508,7 +2677,7 @@ function expandDynamicScopes(root, schemaMap) {
2508
2677
  o.$ref = v.name === '#' ? '#' : '#/' + defsKey + '/' + v.name
2509
2678
  continue
2510
2679
  }
2511
- if (!SKIP_VALUE_KEYS.has(k)) fix(v)
2680
+ if (!(v !== null && typeof v === 'object' && keptRaw.has(v))) fix(v)
2512
2681
  }
2513
2682
  }
2514
2683
  fix(rootCopy)
@@ -2542,10 +2711,10 @@ function normalizeRefsWalk(root, schemaMap) {
2542
2711
  const visited = new Set()
2543
2712
  let fail = false
2544
2713
  let dynamic = false
2545
- const visit = (node) => {
2714
+ const visit = (node, inNames) => {
2546
2715
  if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
2547
2716
  visited.add(node)
2548
- 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 }
2549
2718
  if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
2550
2719
  // A dynamic reference reached through another document is unrolled by
2551
2720
  // scope the same way one in the root is (expandDynamicScopes, below).
@@ -2562,13 +2731,13 @@ function normalizeRefsWalk(root, schemaMap) {
2562
2731
  // The root, however it is spelled, is `#`: a copy of it under its own
2563
2732
  // definitions would contain itself.
2564
2733
  if (t === root) { /* named '#' below */ }
2565
- 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) }
2566
2735
  if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
2567
2736
  }
2568
2737
  }
2569
- 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))
2570
2739
  }
2571
- visit(root)
2740
+ visit(root, false)
2572
2741
  if (fail) return null
2573
2742
  if (dynamic) return expandDynamicScopes(root, schemaMap)
2574
2743
  if (refTarget.size === 0) return root
@@ -2582,13 +2751,13 @@ function normalizeRefsWalk(root, schemaMap) {
2582
2751
  }
2583
2752
  // The copy: references renamed, nested `$id`s dropped.
2584
2753
  const memo = new Map()
2585
- const walk = (node) => {
2754
+ const walk = (node, inNames) => {
2586
2755
  if (node === null || typeof node !== 'object') return node
2587
2756
  if (memo.has(node)) return memo.get(node)
2588
2757
  if (Array.isArray(node)) {
2589
2758
  const out = []
2590
2759
  memo.set(node, out)
2591
- for (const x of node) out.push(walk(x))
2760
+ for (const x of node) out.push(walk(x, false))
2592
2761
  return out
2593
2762
  }
2594
2763
  const out = {}
@@ -2600,13 +2769,13 @@ function normalizeRefsWalk(root, schemaMap) {
2600
2769
  out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
2601
2770
  continue
2602
2771
  }
2603
- 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)))
2604
2773
  }
2605
2774
  return out
2606
2775
  }
2607
- const out = walk(root)
2776
+ const out = walk(root, false)
2608
2777
  const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
2609
- 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)
2610
2779
  out[defsKey] = defs
2611
2780
  return out
2612
2781
  }
@@ -2669,7 +2838,14 @@ function sourceSize (lines, ctx) {
2669
2838
 
2670
2839
  function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2671
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.
2672
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)
2673
2849
  if (typeof schema === 'boolean') return schema ? () => true : () => false
2674
2850
  if (typeof schema !== 'object' || schema === null) return null
2675
2851
  // removeAdditional: a verdict function that deletes the keys the remover
@@ -2807,13 +2983,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2807
2983
  // validateJSON or get extended, and rewriting and compiling the source twice
2808
2984
  // more on every schema was about a third of what a first call cost.
2809
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
2810
2992
  if (ctx.usesRecursion) {
2811
2993
  // Self-recursive: a named body reached through the cycle guard, so data
2812
2994
  // that points back at itself settles instead of exhausting the stack.
2813
- const decl = emitRootGuard('_validate', 'd', 'true') +
2995
+ const decl = (ctx.rootFwd ? `${ctx.rootFwd}_f=_validate;` : '') + emitRootGuard('_validate', 'd', 'true') +
2814
2996
  `function _validate_b(d){\n ${checkStr}\n return true\n }\n `
2815
2997
  const run = emitGuardedRun('return _validate(d)', '', defSets)
2816
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`
2817
3001
  // Hybrid: keep the boolean answer, wrap only the outer call
2818
3002
  hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
2819
3003
  resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
@@ -2823,6 +3007,8 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2823
3007
  // re-entered with the same value; the guarded functions need their state.
2824
3008
  const run = emitGuardedRun(`return _body(d)`, '', defSets)
2825
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`
2826
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)`
2827
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}`
2828
3014
  tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
@@ -2841,17 +3027,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2841
3027
  const guardStr = needsGuard ? emitGuardState() : ''
2842
3028
  const preambleStr = guardStr +
2843
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()
2844
3033
 
2845
3034
  try {
2846
3035
  let boolFn
2847
- if (closureNames.length > 0) {
2848
- 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}}`)
2849
3041
  boolFn = factory(...closureValues)
2850
- } else if (preambleStr) {
2851
- const factory = compileFunction(`${preambleStr}return function(d){${body}}`)
2852
- boolFn = factory()
2853
3042
  } else {
2854
- boolFn = compileFunction('d', body)
3043
+ const factory = compileFunction(`${tag}${preambleStr}return function(d){${body}}`)
3044
+ boolFn = factory()
2855
3045
  }
2856
3046
 
2857
3047
  // Light by default: the pieces the tiering and standalone paths need
@@ -2871,7 +3061,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2871
3061
  attachLazyHeavy(boolFn, inputSchema, schemaMap, userFormats, opts, !!ctx.usesSafeRe)
2872
3062
  return boolFn
2873
3063
  }
2874
- attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body })
3064
+ attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl })
2875
3065
  return boolFn
2876
3066
  } catch {
2877
3067
  return null
@@ -2893,7 +3083,13 @@ function attachLazyHeavy (boolFn, schema, schemaMap, userFormats, opts, usesSafe
2893
3083
  }
2894
3084
  // The factories and the source, as the full compile attaches them.
2895
3085
  function attachHeavy (boolFn, P) {
2896
- 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()}}`)
2897
3093
  // The hybrid: same body, return R instead of true, return E(d) instead of
2898
3094
  // false. Compiled on first request; null when that compile is refused, and
2899
3095
  // the caller then assembles from the error functions instead.
@@ -2901,7 +3097,7 @@ function attachHeavy (boolFn, P) {
2901
3097
  boolFn._hybridFactory = (R, E) => {
2902
3098
  if (hybridFactory === undefined) {
2903
3099
  try {
2904
- hybridFactory = compileFunction(...closureNames, 'R', 'E', `${preambleStr}return function(d){${hybridBody()}}`)
3100
+ hybridFactory = compileFunction(...closureNames, 'R', 'E', live('return _run(d)?R:E(d)', hybridBody))
2905
3101
  } catch {
2906
3102
  hybridFactory = null
2907
3103
  }
@@ -2920,7 +3116,7 @@ function attachHeavy (boolFn, P) {
2920
3116
  boolFn._resultFactory = (LR, B, W, EE) => {
2921
3117
  if (resultFactory === undefined) {
2922
3118
  try {
2923
- 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))
2924
3120
  } catch {
2925
3121
  resultFactory = null
2926
3122
  }
@@ -2940,7 +3136,7 @@ function attachHeavy (boolFn, P) {
2940
3136
  boolFn._withTail = (tail) => {
2941
3137
  if (tailFactory === undefined) {
2942
3138
  try {
2943
- tailFactory = compileFunction(...closureNames, '__ataTail', `${preambleStr}return function(d){${tailThunk()}}`)
3139
+ tailFactory = compileFunction(...closureNames, '__ataTail', live('return _run(d)&&__ataTail(d)', tailThunk))
2944
3140
  } catch {
2945
3141
  tailFactory = null
2946
3142
  }
@@ -3439,7 +3635,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3439
3635
  // object: its type check fails on undefined. A name on Object.prototype
3440
3636
  // is never absent that way, and it is checked explicitly.
3441
3637
  if (!hasTypeCheck || PROTO_NAMES.has(key)) {
3442
- reqChecks.push(`!${ownKeyExpr(ctx, v, key)}`)
3638
+ reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
3443
3639
  } else {
3444
3640
  typed.push(key)
3445
3641
  }
@@ -3460,7 +3656,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3460
3656
  lines.push(`if(!${pk || protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
3461
3657
  }
3462
3658
  } else if (schema.required && schema.required.length > 0) {
3463
- 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)}`)
3464
3660
  if (isObj) {
3465
3661
  lines.push(`if(${checks.join('||')})return false`)
3466
3662
  } else {
@@ -3550,7 +3746,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3550
3746
 
3551
3747
  if (schema.pattern) {
3552
3748
  // Try inline charCode compilation for simple patterns (avoids RegExp engine)
3553
- const inlineCheck = compilePatternInline(schema.pattern, v)
3749
+ const inlineCheck = classRunCheck(ctx, schema.pattern, v) || compilePatternInline(schema.pattern, v)
3554
3750
  if (inlineCheck) {
3555
3751
  lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
3556
3752
  } else {
@@ -3661,13 +3857,32 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3661
3857
 
3662
3858
  // properties — use hoisted vars for required props, hoist optional to locals too
3663
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
3664
3876
  for (const [key, prop] of Object.entries(schema.properties)) {
3877
+ if (keyLoop && !requiredSet.has(key)) { keyCases.push([key, prop]); continue }
3665
3878
  const from = lines.length
3666
3879
  // A large subschema as a function of its own (emitPropFn); the call
3667
3880
  // takes the place of the inline code in each of the three shapes below.
3668
3881
  const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
3669
3882
  if (requiredSet.has(key) && isObj) {
3670
- // 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.
3671
3886
  const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
3672
3887
  const combined = fn ? null : tryGenCombined(prop, access, ctx)
3673
3888
  if (fn) {
@@ -3683,7 +3898,21 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3683
3898
  } else {
3684
3899
  genCode(prop, access, lines, ctx)
3685
3900
  }
3686
- } 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 {
3687
3916
  // Optional — hoist to local, check undefined
3688
3917
  const oi = ctx.varCounter++
3689
3918
  const local = `_o${oi}`
@@ -3697,14 +3926,28 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3697
3926
  genCode(prop, local, lines, ctx)
3698
3927
  }
3699
3928
  lines.push(`}}`)
3700
- } else {
3701
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
3702
- if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
3703
- else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
3704
- lines.push(`}`)
3705
3929
  }
3706
3930
  countInline(lines, from, ctx)
3707
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(`}`)
3708
3951
  }
3709
3952
 
3710
3953
  // items — pass known type info to children
@@ -3714,11 +3957,26 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3714
3957
  ctx.varCounter++
3715
3958
  // items applies after the prefixItems positions (Draft 2020-12).
3716
3959
  const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
3717
- lines.push(isArr
3718
- ? `for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
3719
- : `if(Array.isArray(${v})){for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
3720
- genCode(schema.items, elem, lines, ctx)
3721
- 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
+ }
3722
3980
  }
3723
3981
 
3724
3982
  // prefixItems
@@ -3753,7 +4011,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3753
4011
 
3754
4012
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
3755
4013
  // Falls back to inline closures if any branch touches recursive validation.
3756
- if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, knownType)
4014
+ if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, effectiveType)
3757
4015
 
3758
4016
  // not
3759
4017
  if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
@@ -3922,7 +4180,42 @@ function genContainsNode(schema, v, lines, ctx, knownType, isArr) {
3922
4180
  }
3923
4181
  }
3924
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
+
3925
4217
  function genAnyOfNode(schema, v, lines, ctx, knownType) {
4218
+ if (singleBranch(schema, schema.anyOf, v, lines, ctx, knownType)) return
3926
4219
  const fi = ctx.varCounter++
3927
4220
  const branchBodies = []
3928
4221
  const reused = []
@@ -3958,6 +4251,13 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
3958
4251
  const checks = names.map(n => `${n}(${v})`).join('||')
3959
4252
  lines.push(`if(!(${checks}))return false`)
3960
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
+ }
3961
4261
  const fns = branchBodies.map(body => `function(_av){${body}}`)
3962
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}`)
3963
4263
  }
@@ -4009,6 +4309,7 @@ function keyedBranches (branches) {
4009
4309
  }
4010
4310
 
4011
4311
  function genOneOfNode(schema, v, lines, ctx, knownType) {
4312
+ if (singleBranch(schema, schema.oneOf, v, lines, ctx, knownType)) return
4012
4313
  const fi = ctx.varCounter++
4013
4314
  const branchBodies = []
4014
4315
  const reused = []
@@ -4020,19 +4321,24 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4020
4321
  // 86919 branch functions and 197 MB of source, and took seconds to build.
4021
4322
  // A hoisted branch takes its input as a parameter and reads only
4022
4323
  // module-level names, so the same schema object gives the same function.
4023
- 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
4024
4330
  for (let i = 0; i < schema.oneOf.length; i++) {
4025
4331
  const sub = schema.oneOf[i]
4026
4332
  const key = canHoist ? branchKey(sub, ctx) : undefined
4027
- const known = key !== undefined ? ctx.hoistedBranch.get(key) : undefined
4333
+ const known = key !== undefined ? branchMap.get(key) : undefined
4028
4334
  if (known !== undefined) {
4029
4335
  reused[i] = known.name
4030
4336
  branchBodies.push(known.body)
4031
4337
  continue
4032
4338
  }
4033
4339
  const subLines = []
4034
- if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
4035
- 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)
4036
4342
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
4037
4343
  // _validate is the recursive entry — hoisting branches above it breaks scope.
4038
4344
  if (/\b_validate\b/.test(body)) canHoist = false
@@ -4044,7 +4350,7 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4044
4350
  const name = `_of${fi}_b${i}`
4045
4351
  hoist(ctx, `function ${name}(_ov){${body}}`)
4046
4352
  const key = branchKey(schema.oneOf[i], ctx)
4047
- if (key !== undefined) ctx.hoistedBranch.set(key, { name, body })
4353
+ if (key !== undefined) branchMap.set(key, { name, body })
4048
4354
  return name
4049
4355
  })
4050
4356
  const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
@@ -4061,12 +4367,77 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
4061
4367
  }
4062
4368
  lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
4063
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
+ }
4064
4377
  const fns = branchBodies.map(body => `function(_ov){${body}}`)
4065
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}`)
4066
4379
  }
4067
4380
  }
4068
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
+
4069
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
+ }
4070
4441
  const subLines = []
4071
4442
  genCode(schema.not, '_nv', subLines, ctx)
4072
4443
  if (subLines.length === 0) {
@@ -4078,6 +4449,29 @@ function genNotNode(schema, v, lines, ctx, knownType) {
4078
4449
  }
4079
4450
 
4080
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
+ }
4081
4475
  const ifLines = []
4082
4476
  genCode(schema.if, '_iv', ifLines, ctx)
4083
4477
  const fi = ctx.varCounter++
@@ -4104,12 +4498,30 @@ function genIfNode(schema, v, lines, ctx, knownType) {
4104
4498
  lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
4105
4499
  }
4106
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
+
4107
4516
  function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4108
4517
  // Self-reference "#" — recursive call to root validator
4109
4518
  if (schema.$ref === '#') {
4110
4519
  if (ctx.nestedBoolean) throw DECLINE
4111
4520
  ctx.usesRecursion = true
4112
- 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`)
4113
4525
  if (!hasSiblings) return true
4114
4526
  }
4115
4527
  // 1. Local ref
@@ -4136,14 +4548,14 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4136
4548
  ctx.deferredChecks = null
4137
4549
  const outerReuse = ctx.plainReuse
4138
4550
  ctx.plainReuse = undefined
4139
- 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-- }
4140
4553
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4141
4554
  ctx.deferredChecks = outerDeferred
4142
4555
  // The def is hoisted above the entry function, where the recursive
4143
- // entry `_validate` is not in scope; a body that calls it (a `$ref: '#'`
4144
- // inside the def) threw a ReferenceError at the first document that
4145
- // reached it. SchemaStore's jsone schema is such a case. Decline, as
4146
- // 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.
4147
4559
  if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
4148
4560
  // Entered through the cycle guard: see emitGuardState. In the fast
4149
4561
  // pass the wrapper only counts depth; the set is used by the second,
@@ -4333,8 +4745,15 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4333
4745
  }
4334
4746
  lines.push(`}}`)
4335
4747
  } else {
4336
- // 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.
4337
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
4338
4757
  lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4339
4758
  // propertyNames checks (merged)
4340
4759
  if (pn) {
@@ -4359,7 +4778,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4359
4778
  lines.push(`if(!_es${ei}.has(${kVar}))return false`)
4360
4779
  }
4361
4780
  }
4362
- for (let i = 0; i < ppEntries.length; i++) {
4781
+ for (const i of live) {
4363
4782
  lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
4364
4783
  }
4365
4784
  lines.push(`}}`)
@@ -4920,8 +5339,19 @@ const FORMAT_CODEGEN = {
4920
5339
  'json-pointer': (...a) => getFormats().jsonPointerSource(...a),
4921
5340
  'relative-json-pointer': (...a) => getFormats().relativeJsonPointerSource(...a),
4922
5341
  'uri-template': (...a) => getFormats().uriTemplateSource(...a),
4923
- iri: (...a) => getFormats().iriSource(...a),
4924
- '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
+ },
4925
5355
  'idn-email': (...a) => getFormats().idnEmailSource(...a),
4926
5356
  regex: (v, isStr) => {
4927
5357
  const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
@@ -4937,9 +5367,11 @@ const FORMAT_CODEGEN = {
4937
5367
  hoistOnce(ctx, '_uriHoisted', uriHelper())
4938
5368
  return isStr ? `if(!_uri(${v}))return false` : `if(typeof ${v}==='string'&&!_uri(${v}))return false`
4939
5369
  },
4940
- 'uri-reference': (v, isStr) => isStr
4941
- ? `{${getFormats().uriCharsSource(v, '0')}}`
4942
- : `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
+ },
4943
5375
  ipv4: (v, isStr, ctx) => {
4944
5376
  if (!ctx) return getFormats().ipv4Source(v, isStr)
4945
5377
  // Hoisted: a regex literal in the body would build a new RegExp per call.
@@ -4957,6 +5389,38 @@ const FORMAT_CODEGEN = {
4957
5389
  hostname: (...a) => getFormats().hostnameSource(...a),
4958
5390
  }
4959
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
+
4960
5424
  // Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
4961
5425
  // Returns null if pattern is too complex for inline compilation.
4962
5426
  // Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
@@ -5143,6 +5607,14 @@ function unevalContributions (sub, kind, names, state, depth) {
5143
5607
  // Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
5144
5608
  // Returns a JS expression string or null if pattern is too complex
5145
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'
5146
5618
  // Match patterns like ^literal (no regex metacharacters after ^)
5147
5619
  const m = pattern.match(/^\^([a-zA-Z0-9_\-./]+)$/)
5148
5620
  if (!m) return null
@@ -5570,4 +6042,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5570
6042
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5571
6043
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5572
6044
 
5573
- 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 }