ata-validator 1.43.0 → 1.45.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.
@@ -10,12 +10,20 @@
10
10
  // forms answers a different question and costs a string per call.
11
11
  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}"
12
12
 
13
- // uniqueItems helpers for the boolean generator. `_uq` answers whether every
14
- // item is distinct: short arrays compare pairwise and allocate nothing,
15
- // longer ones use a Set of primitives and fall back to sorted-key canonical
16
- // strings only once an object actually appears.
17
- const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn(x[k])}).join(',')+'}'}" +
18
- "function _uq(a){var n=a.length,i,k;if(n<2)return true;if(n<=12){for(i=1;i<n;i++)for(k=0;k<i;k++)if(_deq(a[i],a[k]))return false;return true}var s=new Set();for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object')break;if(s.has(x))return false;s.add(x)}if(i===n)return true;s=new Set();for(i=0;i<n;i++){var c=_cn(a[i]);if(s.has(c))return false;s.add(c)}return true}"
13
+ // uniqueItems helpers, emitted from the functions the interpreted engine
14
+ // calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
15
+ // Loaded at the first schema with uniqueItems, not on every cold start.
16
+ // Comment lines and indentation are dropped: the text lands in every
17
+ // generated function and ahead-of-time module that checks uniqueItems.
18
+ let _uqSources = null
19
+ function uqSources () {
20
+ if (_uqSources === null) {
21
+ const u = require('./unique-items')
22
+ const compact = (fn) => String(fn).replace(/\n\s*\/\/[^\n]*/g, '').replace(/\n\s*/g, '\n')
23
+ _uqSources = { uq: compact(u._uqh) + compact(u._uq), uqp: 'var _uqpI=0;' + compact(u._uqp) }
24
+ }
25
+ return _uqSources
26
+ }
19
27
 
20
28
  // Where a generator hoists a declaration so it runs once per compiled
21
29
  // function rather than once per call. The boolean generator has a preamble
@@ -97,7 +105,7 @@ function emitPtrEsc(ctx) {
97
105
  // `_uq` compares items with `_deq`, so it can never be hoisted alone.
98
106
  function emitUq(ctx) {
99
107
  emitDeq(ctx)
100
- hoistOnce(ctx, '_uqHoisted', UQ_HELPERS)
108
+ hoistOnce(ctx, '_uqHoisted', uqSources().uq)
101
109
  return '_uq'
102
110
  }
103
111
 
@@ -107,15 +115,11 @@ function emitUq(ctx) {
107
115
  // a later equal, paired with its first later equal. That is the pair the
108
116
  // interpreted engine reports, and the generators used to report a different
109
117
  // one (the earliest repeat, paired with what it repeats), so the same document
110
- // named a different pair depending on the engine that answered. Short arrays
111
- // compare pairwise and allocate nothing; longer ones key a Map by value, or by
112
- // canonical string once an object appears. Building a canonical string for
113
- // every item on every call, as the generators did, made reading a uniqueItems
114
- // error up to ten times the cost of the verdict.
115
- const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
118
+ // named a different pair depending on the engine that answered. See
119
+ // lib/unique-items.js for how it finds the pair.
116
120
  function emitUqp(ctx) {
117
121
  emitUq(ctx)
118
- hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
122
+ hoistOnce(ctx, '_uqpHoisted', uqSources().uqp)
119
123
  return '_uqp'
120
124
  }
121
125
 
@@ -1065,6 +1069,16 @@ function hasOwnKey (d, key) {
1065
1069
  return _hop.call(d, key)
1066
1070
  }
1067
1071
 
1072
+ // Writes an own key. A schema can name a property, a definition or even a
1073
+ // keyword "__proto__", and assigning that key sets the copy's prototype
1074
+ // instead: the property was gone from the copy, so a constraint on it was
1075
+ // generated as nothing and accepted anything (a schema with such a property
1076
+ // and a reference the resolver rewrites, 1.40.1 to 1.43.0).
1077
+ function setOwn (o, k, v) {
1078
+ if (k === '__proto__') Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
1079
+ else o[k] = v
1080
+ }
1081
+
1068
1082
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
1069
1083
  'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
1070
1084
 
@@ -1380,7 +1394,7 @@ function aliasChainEnds(def, defs) {
1380
1394
  // source of evaluated properties the schema has; when it does not, the
1381
1395
  // schema is not generated and the interpreter answers.
1382
1396
  function unevalPropsModeled(schema, schemaMap) {
1383
- const own = Object.assign({}, schema)
1397
+ const own = { ...schema }
1384
1398
  delete own.$defs
1385
1399
  delete own.definitions
1386
1400
  if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
@@ -1399,7 +1413,7 @@ function unevalPropsModeled(schema, schemaMap) {
1399
1413
  if (typeof sub !== 'object' || sub === null) return false
1400
1414
  const r = collectEvaluated(sub, schemaMap)
1401
1415
  if (r.dynamic || r.allProps) return false
1402
- const rest = Object.assign({}, sub)
1416
+ const rest = { ...sub }
1403
1417
  delete rest.patternProperties
1404
1418
  const j = JSON.stringify(rest)
1405
1419
  if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
@@ -1432,7 +1446,7 @@ function unevalPropsModeled(schema, schemaMap) {
1432
1446
  if (typeof schema.if !== 'object' || schema.if === null) return false
1433
1447
  const r = collectEvaluated(schema.if, schemaMap)
1434
1448
  if (r.dynamic || r.allProps || r.props.length) return false
1435
- const rest = Object.assign({}, schema.if)
1449
+ const rest = { ...schema.if }
1436
1450
  delete rest.patternProperties
1437
1451
  const j = JSON.stringify(rest)
1438
1452
  if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
@@ -1486,17 +1500,7 @@ function codegenSafe(schema, schemaMap) {
1486
1500
  }
1487
1501
 
1488
1502
  // Keys that collide with Object.prototype
1489
- if (schema.required) {
1490
- for (const k of schema.required) {
1491
- if (UNSAFE_KEYS.has(k)) return false
1492
- }
1493
- }
1494
- if (schema.properties) {
1495
- for (const k of Object.keys(schema.properties)) {
1496
- if (UNSAFE_KEYS.has(k)) return false
1497
- if (k === '$ref') return false // property named "$ref" — confusing
1498
- }
1499
- }
1503
+
1500
1504
 
1501
1505
  // Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
1502
1506
 
@@ -1627,10 +1631,8 @@ function codegenSafe(schema, schemaMap) {
1627
1631
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
1628
1632
  }
1629
1633
  }
1630
- if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null) {
1631
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
1632
- if (Object.keys(schema.propertyNames).some(k => k !== '$schema' && !supported.includes(k))) return false
1633
- }
1634
+ if (typeof schema.propertyNames === 'object' && schema.propertyNames !== null &&
1635
+ !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return false
1634
1636
 
1635
1637
  // Recurse into sub-schemas — bail on boolean schemas in any position
1636
1638
  // A boolean subschema is a constant that every generator emits at its
@@ -1968,8 +1970,21 @@ function bothDefsContainers(schema) {
1968
1970
  schema.definitions !== null && typeof schema.definitions === 'object'
1969
1971
  }
1970
1972
 
1973
+ // The answer depends on the schema and the map alone, and the three
1974
+ // generators ask it about the same prepared schema in turn, so the last
1975
+ // answer is kept: each check walks the whole schema, and the second and third
1976
+ // asked again what the first had found. One entry, compared by identity,
1977
+ // because a WeakMap entry per schema cost more than the walk it saved on a
1978
+ // small schema that only ever gets its verdict generated.
1979
+ let _gateSchema = null, _gateMap = null, _gateOk = false
1971
1980
  function sharedCodegenGate(schema, schemaMap) {
1972
1981
  if (typeof schema !== 'object' || schema === null) return true
1982
+ if (schema === _gateSchema && schemaMap === _gateMap) return _gateOk
1983
+ const ok = sharedCodegenGateUncached(schema, schemaMap)
1984
+ _gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
1985
+ return ok
1986
+ }
1987
+ function sharedCodegenGateUncached(schema, schemaMap) {
1973
1988
  if (bothDefsContainers(schema)) return false
1974
1989
  if (hasSameInstanceRefCycle(schema)) return false
1975
1990
  if (!codegenSafe(schema, schemaMap)) return false
@@ -1985,6 +2000,15 @@ function sharedCodegenGate(schema, schemaMap) {
1985
2000
  // `type: 'string'` and annotations go, on a copy. Anything else is left as
1986
2001
  // written and the gate declines. Errors keep the interpreted engine's path.
1987
2002
  const PN_SUPPORTED = new Set(['maxLength', 'minLength', 'pattern', 'const', 'enum'])
2003
+ // Whether the generators can write `propertyNames` as checks on the key in the
2004
+ // object's own key loop. Any other subschema is applied to each key as a
2005
+ // value of its own (genPropertyNamesNode and its error and combined
2006
+ // counterparts), which is what the interpreter does.
2007
+ function isSimplePN (pn) {
2008
+ if (pn === null || typeof pn !== 'object' || Array.isArray(pn)) return false
2009
+ for (const k of Object.keys(pn)) if (k !== '$schema' && !PN_SUPPORTED.has(k)) return false
2010
+ return true
2011
+ }
1988
2012
  const PN_ANNOTATIONS = new Set(['$schema', '$comment', 'title', 'description', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
1989
2013
  function simplePropertyNames(pn, defs) {
1990
2014
  const seen = new Set()
@@ -2006,7 +2030,7 @@ function simplePropertyNames(pn, defs) {
2006
2030
  return null
2007
2031
  }
2008
2032
  if (!PN_SUPPORTED.has(k)) return null
2009
- out[k] = val
2033
+ setOwn(out, k, val)
2010
2034
  }
2011
2035
  return out
2012
2036
  }
@@ -2037,14 +2061,14 @@ function normalizePropertyNamesWalk(root) {
2037
2061
  return out
2038
2062
  }
2039
2063
  let out = node
2040
- const set = (k, v) => { if (out === node) out = { ...node }; out[k] = v }
2064
+ const set = (k, v) => { if (out === node) out = { ...node }; setOwn(out, k, v) }
2041
2065
  for (const k of SUBSCHEMA_MAPS) {
2042
2066
  const map = node[k]
2043
2067
  if (!map || typeof map !== 'object') continue
2044
2068
  let copy = map
2045
2069
  for (const [name, sub] of Object.entries(map)) {
2046
2070
  const w = walk(sub)
2047
- if (w !== sub) { if (copy === map) copy = { ...map }; copy[name] = w }
2071
+ if (w !== sub) { if (copy === map) copy = { ...map }; setOwn(copy, name, w) }
2048
2072
  }
2049
2073
  if (copy !== map) set(k, copy)
2050
2074
  }
@@ -2162,7 +2186,7 @@ function normalizeDeepRefsWalk(root) {
2162
2186
  if (inner) return node
2163
2187
  let out = node
2164
2188
  memo.set(node, out)
2165
- const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
2189
+ const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } setOwn(out, k, v) }
2166
2190
  if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
2167
2191
  for (const k of Object.keys(node)) {
2168
2192
  if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
@@ -2211,10 +2235,39 @@ function normalizeRefs(root, schemaMap) {
2211
2235
  return out
2212
2236
  }
2213
2237
  const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
2238
+ // A schema whose every reference is `#` or a plain `#/$defs/name` (or
2239
+ // `definitions`), with no `$id` or `$anchor` below the root to move the base
2240
+ // they resolve against, needs nothing from the resolver: each reference
2241
+ // already reads as the generators read it. Telling that from the text skips
2242
+ // loading the interpreted engine and indexing the schema, which was most of
2243
+ // what preparing a small schema for code generation cost.
2244
+ const LOCAL_ONLY_REF = /^#(?:\/(?:\$defs|definitions)\/[^/~%]+)?$/
2245
+ function refsAllPlainLocal (root, text) {
2246
+ const ids = text.split('"$id":').length - 1
2247
+ if (ids > (typeof root.$id === 'string' ? 1 : 0) || text.includes('"$anchor":')) return false
2248
+ const re = /"\$ref":("(?:[^"\\]|\\.)*")/g
2249
+ let m
2250
+ while ((m = re.exec(text)) !== null) {
2251
+ let ref
2252
+ try { ref = JSON.parse(m[1]) } catch { return false }
2253
+ if (!LOCAL_ONLY_REF.test(ref)) return false
2254
+ }
2255
+ return true
2256
+ }
2257
+
2258
+ // What the text of a schema prepared for code generation says about its size,
2259
+ // kept from the stringify normalizeRefsWalk makes anyway: its length when it
2260
+ // holds no `$ref`, -1 when it does. expandedChars counts at most 8 for each
2261
+ // character of a schema without references, so a short one is known not to
2262
+ // reach EXPANDED_SHARE without walking it again at every compile.
2263
+ const _refFreeLength = new WeakMap()
2214
2264
  function normalizeRefsWalk(root, schemaMap) {
2215
2265
  const text = JSON.stringify(root)
2216
- if (!text.includes('"$ref"')) return root
2266
+ const hasRef = text.includes('"$ref"')
2267
+ _refFreeLength.set(root, hasRef ? -1 : text.length)
2268
+ if (!hasRef) return root
2217
2269
  if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
2270
+ if (refsAllPlainLocal(root, text)) return root
2218
2271
  const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
2219
2272
  const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
2220
2273
  const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
@@ -2280,7 +2333,7 @@ function normalizeRefsWalk(root, schemaMap) {
2280
2333
  out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
2281
2334
  continue
2282
2335
  }
2283
- out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
2336
+ setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k]))
2284
2337
  }
2285
2338
  return out
2286
2339
  }
@@ -2299,6 +2352,46 @@ function prepareForCodegen(schema, schemaMap) {
2299
2352
  }
2300
2353
 
2301
2354
  const VERDICT_SPLIT = 64 * 1024
2355
+ // The schema's size with every local reference followed, each definition
2356
+ // sized once, annotations left out. A rough measure of the expanded verdict
2357
+ // source: on SchemaStore that runs about 1.4 times it at the median, and far
2358
+ // less where allOf merges. Past EXPANDED_SHARE the verdict is generated with
2359
+ // shared definitions from the start, instead of expanded first and thrown
2360
+ // away: SARIF comes to 2.1 million, and generating its 2.8 MB of expanded
2361
+ // source only to discard it was most of a cold first validate() (70 ms of the
2362
+ // 87 a command line run took to report one error). The bound is set where
2363
+ // only schemas that split anyway reach it: SARIF 2.1 measures about 706
2364
+ // thousand, while utcm-monitor (370 thousand) and accelerator (262 thousand)
2365
+ // stay expanded and keep their speed.
2366
+ const EXPANDED_SHARE = 400 * 1024
2367
+ const SIZE_SKIPS = new Set(['$defs', 'definitions', 'description', 'title', 'examples', 'default', '$comment', 'markdownDescription', 'deprecated', 'readOnly', 'writeOnly'])
2368
+ function expandedChars (root, limit) {
2369
+ const defs = (root && (root.$defs || root.definitions)) || {}
2370
+ const memo = new Map()
2371
+ const size = (n, stack) => {
2372
+ if (n === null || typeof n !== 'object') return 8
2373
+ let t = 2
2374
+ if (Array.isArray(n)) { for (const x of n) { t += size(x, stack) + 1; if (t > limit) return t } return t }
2375
+ for (const k of Object.keys(n)) {
2376
+ if (SIZE_SKIPS.has(k)) continue
2377
+ const v = n[k]
2378
+ if (k === '$ref' && typeof v === 'string') {
2379
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(v)
2380
+ if (m && defs[m[1]] !== undefined && !stack.has(m[1])) {
2381
+ let d = memo.get(m[1])
2382
+ if (d === undefined) { stack.add(m[1]); d = size(defs[m[1]], stack); stack.delete(m[1]); memo.set(m[1], d) }
2383
+ t += d
2384
+ if (t > limit) return t
2385
+ continue
2386
+ }
2387
+ }
2388
+ t += k.length + 3 + size(v, stack)
2389
+ if (t > limit) return t
2390
+ }
2391
+ return t
2392
+ }
2393
+ return size(root, new Set())
2394
+ }
2302
2395
  function sourceSize (lines, ctx) {
2303
2396
  let n = 0
2304
2397
  for (const l of lines) n += l.length
@@ -2340,13 +2433,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2340
2433
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
2341
2434
  }
2342
2435
  }
2343
- // propertyNames: only codegen simple string constraints
2344
- if (schema.propertyNames) {
2345
- const pn = schema.propertyNames
2346
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
2347
- const keys = Object.keys(pn).filter(k => k !== '$schema')
2348
- if (keys.some(k => !supported.includes(k))) return null
2349
- }
2436
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
2350
2437
 
2351
2438
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
2352
2439
  const anchors = {}
@@ -2389,7 +2476,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2389
2476
  // (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
2390
2477
  // half of the first validate() call.
2391
2478
  let ctx, lines
2392
- for (const share of [false, true]) {
2479
+ const refFree = _refFreeLength.get(inputSchema)
2480
+ const shareFirst = refFree !== undefined && refFree >= 0 && refFree * 8 <= EXPANDED_SHARE
2481
+ ? false
2482
+ : expandedChars(schema, EXPANDED_SHARE) > EXPANDED_SHARE
2483
+ for (const share of shareFirst ? [true] : [false, true]) {
2393
2484
  ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
2394
2485
  keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
2395
2486
  if (ctx.keywords) ctx.kwEmitted = new Set()
@@ -3394,6 +3485,10 @@ function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, ty
3394
3485
  // up. The previous form built the canonical closure and a Set on
3395
3486
  // every call, even for [1, 2, 3].
3396
3487
  inner = `if(!${emitUq(ctx)}(${v}))return false`
3488
+ } else if (ctx.helperCode) {
3489
+ // Inside the error or combined function, whose helper code is emitted
3490
+ // into its source, the same hoisted helper.
3491
+ inner = `if(!${emitUq(ctx)}(${v}))return false`
3397
3492
  } else {
3398
3493
  inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
3399
3494
  }
@@ -3438,6 +3533,13 @@ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
3438
3533
  const pn = schema.propertyNames
3439
3534
  const ki = ctx.varCounter++
3440
3535
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
3536
+ if (!isSimplePN(pn)) {
3537
+ const sub = []
3538
+ genCode(pn, '_pv', sub, ctx)
3539
+ if (sub.length === 0) return
3540
+ lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};for(const _k${ki} in ${v}){if(!_pnf${ki}(_k${ki}))return false}}`)
3541
+ return
3542
+ }
3441
3543
  lines.push(`${guard}{for(const _k${ki} in ${v}){`)
3442
3544
  if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
3443
3545
  if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
@@ -3760,7 +3862,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
3760
3862
  function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3761
3863
  let handledPropertyNames = false
3762
3864
  const ppEntries = Object.entries(schema.patternProperties)
3763
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
3865
+ const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
3764
3866
  const pi = ctx.varCounter++
3765
3867
  const kVar = `_ppk${pi}`
3766
3868
 
@@ -4821,12 +4923,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4821
4923
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
4822
4924
  }
4823
4925
  }
4824
- if (schema.propertyNames) {
4825
- const pn = schema.propertyNames
4826
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
4827
- const keys = Object.keys(pn).filter(k => k !== '$schema')
4828
- if (keys.some(k => !supported.includes(k))) return null
4829
- }
4926
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
4830
4927
 
4831
4928
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
4832
4929
  const eRootDefs = schema.$defs || schema.definitions || null
@@ -5114,7 +5211,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5114
5211
  // In the combined function `_e` is created on the first error and every
5115
5212
  // error is collected (`_all` is true there), so the report only pushes.
5116
5213
  const report = ctx.inCombined && !ctx.inBranchFn
5117
- ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
5214
+ ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e=_ap(_e,${collapsed}))`
5118
5215
  : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5119
5216
  // The node's runtime unevaluatedProperties already counted how many of
5120
5217
  // these branches pass, through the annotation functions, which answer the
@@ -5130,7 +5227,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5130
5227
  // branch errors, so it is built from the bits the count left.
5131
5228
  if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
5132
5229
  const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5133
- guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
5230
+ guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e=_ap(_e,__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0})))}else if(!(${same}&&${counted.count}===1))`
5134
5231
  }
5135
5232
  // Without such a count, each branch also gets a verdict function, and the
5136
5233
  // passing branches are counted with those: a oneOf with exactly one and an
@@ -5191,7 +5288,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
5191
5288
  ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5192
5289
  : ''
5193
5290
  const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5194
- const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5291
+ const pushMulti = top ? `(_e=_ap(_e,${multi}))` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5195
5292
  const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5196
5293
  lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
5197
5294
  }
@@ -5791,6 +5888,13 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
5791
5888
  const pn = schema.propertyNames
5792
5889
  const ki = ctx.varCounter++
5793
5890
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
5891
+ // The subschema checks the key as a value, and its errors are reported at
5892
+ // the object, as the interpreter reports them.
5893
+ if (!isSimplePN(pn)) {
5894
+ genCodeE(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
5895
+ lines.push(`}}`)
5896
+ return
5897
+ }
5794
5898
  if (pn.minLength !== undefined) {
5795
5899
  lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
5796
5900
  }
@@ -5916,7 +6020,7 @@ function sharedErr (lit, rootSchema) {
5916
6020
  // the document. `paramsShared` names a frozen params object hoisted for a
5917
6021
  // static site, so a static error's params stay one shared object as before.
5918
6022
  function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
5919
- if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
6023
+ if (!ctx.rich) return `(_e=_ap(_e,${lean}))`
5920
6024
  if (!ctx.richMk) {
5921
6025
  ctx.richMk = true
5922
6026
  const { makeRich } = require('./enrich-site')
@@ -5932,7 +6036,7 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
5932
6036
  // the definition and the reference's own comes in as `_sp`.
5933
6037
  const rel = sp.startsWith(SP_PLACEHOLDER)
5934
6038
  ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
5935
- return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
6039
+ return `(_e=_ap(_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''})))`
5936
6040
  }
5937
6041
 
5938
6042
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
@@ -5978,12 +6082,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
5978
6082
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5979
6083
  }
5980
6084
  }
5981
- if (schema.propertyNames) {
5982
- const pn = schema.propertyNames
5983
- const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
5984
- const keys = Object.keys(pn).filter(k => k !== '$schema')
5985
- if (keys.some(k => !supported.includes(k))) return null
5986
- }
6085
+ if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
5987
6086
 
5988
6087
  // Build anchors map for $ref/#anchor and $dynamicRef resolution
5989
6088
  const cRootDefs = schema.$defs || schema.definitions || null
@@ -6019,7 +6118,11 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
6019
6118
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
6020
6119
 
6021
6120
  // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
6022
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6121
+ // `_ap` adds an error to a list that may not exist yet: the first one
6122
+ // creates it as a one-element literal. Pushing onto an empty `[]` grew its
6123
+ // backing store on every rejection, about 7% of reading errors across the
6124
+ // official suite.
6125
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: ['function _ap(e,x){if(e===undefined)return[x];e.push(x);return e}'], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
6023
6126
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
6024
6127
  const lines = []
6025
6128
  try {
@@ -6135,7 +6238,7 @@ function typeMismatchRest(schema, types) {
6135
6238
  for (const k of Object.keys(schema)) {
6136
6239
  if (drop.has(k)) continue
6137
6240
  if (rest === null) rest = {}
6138
- rest[k] = schema[k]
6241
+ setOwn(rest, k, schema[k])
6139
6242
  }
6140
6243
  if (rest === null) return null
6141
6244
  // A node whose reference was already written (genCodeCNode) keeps that
@@ -6182,6 +6285,14 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6182
6285
  ctx.defFnsC.set(key, fnName)
6183
6286
  const outerHoisted = ctx.hoisted
6184
6287
  ctx.hoisted = []
6288
+ // The body is a function of its own: a reference the caller was in the
6289
+ // middle of expanding is not on the stack inside it, where reaching it
6290
+ // again is a call (this function, or another one) rather than a loop.
6291
+ // With the caller's stack, a definition expanded once on the way into
6292
+ // the root's function was taken for a cycle inside it, and the schema
6293
+ // (a tree whose nodes hold trees) lost its one-pass function.
6294
+ const outerRefs = ctx.refStack
6295
+ ctx.refStack = new Set()
6185
6296
  const bodyLines = []
6186
6297
  let hoisted
6187
6298
  try {
@@ -6189,6 +6300,7 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6189
6300
  } finally {
6190
6301
  hoisted = ctx.hoisted
6191
6302
  ctx.hoisted = outerHoisted
6303
+ ctx.refStack = outerRefs
6192
6304
  }
6193
6305
  let body = bodyLines.join('\n ')
6194
6306
  body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
@@ -6236,7 +6348,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6236
6348
  if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
6237
6349
  if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
6238
6350
  genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
6239
- const rest = Object.assign({}, schema)
6351
+ const rest = { ...schema }
6240
6352
  Object.defineProperty(rest, REF_DONE, { value: true })
6241
6353
  genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
6242
6354
  return
@@ -6347,12 +6459,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6347
6459
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
6348
6460
  if (o !== null) lit._o = o
6349
6461
  ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
6350
- return `(_e||(_e=[])).push(${errVar})`
6462
+ return `(_e=_ap(_e,${errVar}))`
6351
6463
  }
6352
6464
  }
6353
6465
  }
6354
6466
  // Dynamic path (e.g., array index): inline as before
6355
- return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
6467
+ return `(_e=_ap(_e,{keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}}))`
6356
6468
  }
6357
6469
 
6358
6470
  // enum and const apply to a value of any type, so a value that fails
@@ -6442,7 +6554,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6442
6554
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6443
6555
  ctx.closureVars.push(errVar)
6444
6556
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6445
- lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
6557
+ lines.push(`if(${check}){(_e=_ap(_e,${errVar}))}`)
6446
6558
  } else {
6447
6559
  lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6448
6560
  }
@@ -6456,7 +6568,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6456
6568
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6457
6569
  ctx.closureVars.push(errVar)
6458
6570
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
6459
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
6571
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_e,${errVar}))}`)
6460
6572
  } else {
6461
6573
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
6462
6574
  }
@@ -6608,7 +6720,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6608
6720
  const pathVal = pathExpr ? literalValue(pathExpr) : ''
6609
6721
  ctx.closureVars.push(errVar)
6610
6722
  ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
6611
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
6723
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e=_ap(_e,${errVar}))}`)
6612
6724
  } else {
6613
6725
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
6614
6726
  }
@@ -6905,7 +7017,7 @@ function emitAnnot(sub, ctx) {
6905
7017
  ctx.annFns.set(sub, null)
6906
7018
  const name = `_an${ctx.varCounter++}`
6907
7019
  const local = {}
6908
- for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) local[k] = sub[k]
7020
+ for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) setOwn(local, k, sub[k])
6909
7021
  const vl = []
6910
7022
  nestedGenCode(local, '_av', vl, ctx)
6911
7023
  const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
@@ -7053,7 +7165,7 @@ function emitAnnotI(sub, ctx) {
7053
7165
  annItemHelpers(ctx)
7054
7166
  const name = `_ai${ctx.varCounter++}`
7055
7167
  const local = {}
7056
- for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) local[k] = sub[k]
7168
+ for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7057
7169
  const vl = []
7058
7170
  nestedGenCode(local, '_av', vl, ctx)
7059
7171
  const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
@@ -7169,7 +7281,7 @@ function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
7169
7281
  function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
7170
7282
  let handledPropertyNames = false
7171
7283
  const ppEntries = Object.entries(schema.patternProperties)
7172
- const pn = schema.propertyNames && typeof schema.propertyNames === 'object' ? schema.propertyNames : null
7284
+ const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
7173
7285
  const pi = ctx.varCounter++
7174
7286
 
7175
7287
  // Build pattern matchers: prefer charCodeAt for simple prefixes
@@ -7290,7 +7402,16 @@ function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
7290
7402
  function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
7291
7403
  const pn = schema.propertyNames
7292
7404
  const ki = ctx.varCounter++
7405
+ // In the rich function an error carries the value it was raised on, and
7406
+ // here that would be the key where the interpreter's error points at the
7407
+ // object, so that one is left to the plain function and enrichment.
7408
+ if (!isSimplePN(pn) && ctx.rich) throw DECLINE
7293
7409
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
7410
+ if (!isSimplePN(pn)) {
7411
+ genCodeC(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
7412
+ lines.push(`}}`)
7413
+ return
7414
+ }
7294
7415
  if (pn.minLength !== undefined) {
7295
7416
  lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
7296
7417
  }
@@ -7508,6 +7629,6 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
7508
7629
  // not → contributes nothing (spec: annotations from not are discarded)
7509
7630
  }
7510
7631
 
7511
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES, _setCodeBudget }
7632
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget }
7512
7633
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
7513
7634
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })