ata-validator 1.37.1 → 1.39.1

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.
@@ -135,12 +135,26 @@ function enumCondition(ctx, vals, v) {
135
135
  const { codeFor } = require('./error-codes')
136
136
  const { ordinalFor } = require('./schema-order')
137
137
  const { compileSafe, patternIsSafe } = require('./safe-regex')
138
+ // Whether a pattern goes to the linear-time engine. A pattern the platform
139
+ // RegExp also runs in linear time (nativeIsLinear) takes the RegExp: it answers
140
+ // the same, runs faster, and a compiled module that needs no other pattern
141
+ // does not carry the engine. Patterns the engine cannot represent keep the
142
+ // RegExp, as before.
143
+ // Loaded on the first pattern, so a schema without one does not pay for it.
144
+ let _nativeIsLinear = null
145
+ // The linearity check only parses; it runs first so a pattern bound for the
146
+ // RegExp is never compiled for the engine just to be told it could have been.
147
+ const useSafeEngine = (src) => {
148
+ if (_nativeIsLinear === null) _nativeIsLinear = require('./regex-linear').nativeIsLinear
149
+ if (_nativeIsLinear(src)) return false
150
+ return patternIsSafe(src)
151
+ }
138
152
 
139
153
  // Closure value for a patternProperties/propertyNames regex: the linear-time
140
154
  // safe matcher when the pattern is in the supported subset (and flag the ctx so
141
155
  // standalone embeds the engine), else a plain RegExp. Both expose `.test`.
142
156
  function safeReClosure (ctx, src) {
143
- if (patternIsSafe(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
157
+ if (useSafeEngine(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
144
158
  return new RegExp(src)
145
159
  }
146
160
 
@@ -272,6 +286,9 @@ function compileToJS(schema, defs, schemaMap) {
272
286
 
273
287
  if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
274
288
  if (!defs && externalDocsNeedInterpreter(schema, schemaMap)) return null
289
+ // A $ref cycle on one value recurses here as it does in generated code; see
290
+ // hasSameInstanceRefCycle.
291
+ if (!defs && hasSameInstanceRefCycle(schema)) return null
275
292
 
276
293
  // unevaluatedProperties / unevaluatedItems need annotation tracking this
277
294
  // path does not do. Decline so the schema reaches an engine that does.
@@ -523,7 +540,7 @@ function compileToJS(schema, defs, schemaMap) {
523
540
  }
524
541
  if (schema.pattern) {
525
542
  try {
526
- const re = patternIsSafe(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern)
543
+ const re = useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern)
527
544
  checks.push((d) => typeof d !== 'string' || re.test(d))
528
545
  } catch {
529
546
  return null
@@ -1188,6 +1205,31 @@ function emitGuardedRun (attempt, reset, defSets) {
1188
1205
  )
1189
1206
  }
1190
1207
 
1208
+ // Definitions referenced from more than one place whose body is large enough
1209
+ // that repeating its error code at every reference costs more than a call.
1210
+ function sharedDefNames (root, defs) {
1211
+ const out = new Set()
1212
+ if (!defs || typeof defs !== 'object') return out
1213
+ const counts = new Map()
1214
+ const seen = new WeakSet()
1215
+ const walk = (node) => {
1216
+ if (node === null || typeof node !== 'object' || seen.has(node)) return
1217
+ seen.add(node)
1218
+ if (typeof node.$ref === 'string') {
1219
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
1220
+ if (m) counts.set(m[1], (counts.get(m[1]) || 0) + 1)
1221
+ }
1222
+ for (const v of Object.values(node)) walk(v)
1223
+ }
1224
+ walk(root)
1225
+ for (const [name, n] of counts) {
1226
+ const def = defs[name]
1227
+ if (n >= 2 && def && typeof def === 'object' && JSON.stringify(def).length >= SHARED_DEF_MIN_CHARS) out.add(name)
1228
+ }
1229
+ return out
1230
+ }
1231
+ const SHARED_DEF_MIN_CHARS = 120
1232
+
1191
1233
  function cyclicDefNames (defs) {
1192
1234
  const out = new Set()
1193
1235
  if (!defs || typeof defs !== 'object') return out
@@ -1261,6 +1303,76 @@ function aliasChainEnds(def, defs) {
1261
1303
  return true
1262
1304
  }
1263
1305
 
1306
+ // When the properties a schema evaluates depend on which subschemas pass,
1307
+ // genUnevaluatedPropertiesNode picks one of a few runtime models: if/then/else,
1308
+ // one anyOf or oneOf, dependentSchemas, or patterns (root, allOf and a lone
1309
+ // if). Each model reads only the keywords it was written for. A schema whose
1310
+ // evaluated set comes from anything else was compiled with that part ignored,
1311
+ // and a lone `if` with properties emitted no check at all, which accepted every
1312
+ // object. This says whether the model the emitter will pick covers every
1313
+ // source of evaluated properties the schema has; when it does not, the
1314
+ // schema is not generated and the interpreter answers.
1315
+ function unevalPropsModeled(schema, schemaMap) {
1316
+ const own = Object.assign({}, schema)
1317
+ delete own.$defs
1318
+ delete own.definitions
1319
+ if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
1320
+ // A subschema whose evaluated names are a fixed list, which is all the
1321
+ // branch, dependentSchemas and if/then/else models can represent.
1322
+ const fixed = (s) => {
1323
+ if (typeof s === 'boolean') return true
1324
+ if (typeof s !== 'object' || s === null) return false
1325
+ const r = collectEvaluated(s, schemaMap)
1326
+ const j = JSON.stringify(s)
1327
+ return !r.dynamic && !r.allProps && !j.includes('"patternProperties"') && !j.includes('"unevaluatedProperties"')
1328
+ }
1329
+ const allOf = Array.isArray(schema.allOf) ? schema.allOf : []
1330
+ for (const sub of allOf) {
1331
+ if (typeof sub === 'boolean') continue
1332
+ if (typeof sub !== 'object' || sub === null) return false
1333
+ const r = collectEvaluated(sub, schemaMap)
1334
+ if (r.dynamic || r.allProps) return false
1335
+ const rest = Object.assign({}, sub)
1336
+ delete rest.patternProperties
1337
+ const j = JSON.stringify(rest)
1338
+ if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
1339
+ }
1340
+ const patterns = !!schema.patternProperties || allOf.some((s) => s && typeof s === 'object' && s.patternProperties)
1341
+ const branch = schema.anyOf || schema.oneOf
1342
+ if (schema.anyOf && schema.oneOf) return false
1343
+ const hasIf = schema.if !== undefined
1344
+ const hasTE = schema.then !== undefined || schema.else !== undefined
1345
+ const dep = schema.dependentSchemas
1346
+ if (schema.unevaluatedProperties !== false) {
1347
+ // The schema-valued model reads the fixed names and one branch keyword.
1348
+ if (hasIf || hasTE || dep || patterns) return false
1349
+ return !branch || branch.every(fixed)
1350
+ }
1351
+ if (hasIf && hasTE && !branch && !schema.patternProperties && !dep) {
1352
+ if (patterns) return false
1353
+ if (!fixed(schema.if) || (schema.then !== undefined && !fixed(schema.then)) || (schema.else !== undefined && !fixed(schema.else))) return false
1354
+ // The model credits the if's own `properties` names, and only those.
1355
+ const ifNames = typeof schema.if === 'object' ? collectEvaluated(schema.if, schemaMap).props : []
1356
+ const declared = typeof schema.if === 'object' && schema.if.properties ? Object.keys(schema.if.properties) : []
1357
+ return ifNames.length === declared.length && ifNames.every((k) => declared.includes(k))
1358
+ }
1359
+ if (branch) return !hasIf && !hasTE && !dep && !patterns && branch.every(fixed)
1360
+ if (dep) return !hasIf && !hasTE && !patterns && Object.values(dep).every(fixed)
1361
+ if (hasTE) return false
1362
+ if (hasIf) {
1363
+ // A lone if contributes through its top-level patternProperties only.
1364
+ if (typeof schema.if === 'boolean') return true
1365
+ if (typeof schema.if !== 'object' || schema.if === null) return false
1366
+ const r = collectEvaluated(schema.if, schemaMap)
1367
+ if (r.dynamic || r.allProps || r.props.length) return false
1368
+ const rest = Object.assign({}, schema.if)
1369
+ delete rest.patternProperties
1370
+ const j = JSON.stringify(rest)
1371
+ if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
1372
+ }
1373
+ return true
1374
+ }
1375
+
1264
1376
  function codegenSafe(schema, schemaMap) {
1265
1377
  if (typeof schema === 'boolean') return true
1266
1378
  if (typeof schema !== 'object' || schema === null) return true
@@ -1377,6 +1489,7 @@ function codegenSafe(schema, schemaMap) {
1377
1489
  if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
1378
1490
  const evalResult = collectEvaluated(schema, schemaMap)
1379
1491
  if (evalResult.dynamic && evalResult.allProps) return false
1492
+ if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
1380
1493
  }
1381
1494
  }
1382
1495
  // unevaluatedItems: allow boolean and schema values
@@ -1715,8 +1828,85 @@ function externalDocsNeedInterpreter(schema, schemaMap) {
1715
1828
  return false
1716
1829
  }
1717
1830
 
1831
+ // A local $ref cycle that returns to a schema without moving to another value,
1832
+ // such as `A: { oneOf: [{ $ref: '#/$defs/A' }, ...] }`, never terminates on its
1833
+ // own. The interpreter stops it and answers; generated code recursed until the
1834
+ // stack ran out and threw a RangeError into the caller, on primitives, where
1835
+ // the cycle guard has no object to recognise. Cycles that pass through
1836
+ // properties, items or any other keyword that moves to a child value are
1837
+ // ordinary recursion and are not affected.
1838
+ const SAME_INSTANCE_LISTS = ['allOf', 'anyOf', 'oneOf']
1839
+ const SAME_INSTANCE_ONE = ['not', 'if', 'then', 'else']
1840
+ function localPointerTarget(root, ref) {
1841
+ if (typeof ref !== 'string' || ref.charCodeAt(0) !== 35) return null
1842
+ if (ref === '#') return root
1843
+ if (ref.charCodeAt(1) !== 47) return null
1844
+ let t = root
1845
+ for (let seg of ref.slice(2).split('/')) {
1846
+ if (seg.indexOf('%') >= 0) { try { seg = decodeURIComponent(seg) } catch { return null } }
1847
+ seg = seg.replace(/~1/g, '/').replace(/~0/g, '~')
1848
+ if (t === null || typeof t !== 'object') return null
1849
+ if (!Object.prototype.hasOwnProperty.call(t, seg)) {
1850
+ const alias = seg === 'definitions' ? '$defs' : seg === '$defs' ? 'definitions' : null
1851
+ if (alias === null || !Object.prototype.hasOwnProperty.call(t, alias)) return null
1852
+ seg = alias
1853
+ }
1854
+ t = t[seg]
1855
+ }
1856
+ return t !== null && typeof t === 'object' ? t : null
1857
+ }
1858
+ function sameInstanceEdges(root, node) {
1859
+ const out = []
1860
+ for (const k of SAME_INSTANCE_LISTS) if (Array.isArray(node[k])) for (const x of node[k]) if (x && typeof x === 'object') out.push(x)
1861
+ for (const k of SAME_INSTANCE_ONE) if (node[k] && typeof node[k] === 'object') out.push(node[k])
1862
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const x of Object.values(node.dependentSchemas)) if (x && typeof x === 'object') out.push(x)
1863
+ if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
1864
+ return out
1865
+ }
1866
+ const _sameInstanceCycleCache = new WeakMap()
1867
+ function hasSameInstanceRefCycle(root) {
1868
+ if (root === null || typeof root !== 'object') return false
1869
+ const hit = _sameInstanceCycleCache.get(root)
1870
+ if (hit !== undefined) return hit
1871
+ let found = false
1872
+ if (JSON.stringify(root).includes('"$ref"')) {
1873
+ // Only what validation can reach: definitions are entered through the
1874
+ // references that name them, so a broken definition nothing uses (a real
1875
+ // SchemaStore schema has one that includes itself) does not cost the
1876
+ // whole schema its generated code.
1877
+ const nodes = []
1878
+ const seen = new Set()
1879
+ const collect = (n) => {
1880
+ if (n === null || typeof n !== 'object' || seen.has(n)) return
1881
+ seen.add(n)
1882
+ if (Array.isArray(n)) { for (const v of n) collect(v); return }
1883
+ nodes.push(n)
1884
+ for (const [k, v] of Object.entries(n)) {
1885
+ if (k === '$defs' || k === 'definitions' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
1886
+ collect(v)
1887
+ }
1888
+ if (typeof n.$ref === 'string') collect(localPointerTarget(root, n.$ref))
1889
+ }
1890
+ collect(root)
1891
+ const state = new Map() // 1 on the stack, 2 done
1892
+ const visit = (n) => {
1893
+ const st = state.get(n)
1894
+ if (st === 1) return true
1895
+ if (st === 2) return false
1896
+ state.set(n, 1)
1897
+ for (const m of sameInstanceEdges(root, n)) if (visit(m)) return true
1898
+ state.set(n, 2)
1899
+ return false
1900
+ }
1901
+ for (const n of nodes) if (visit(n)) { found = true; break }
1902
+ }
1903
+ _sameInstanceCycleCache.set(root, found)
1904
+ return found
1905
+ }
1906
+
1718
1907
  function sharedCodegenGate(schema, schemaMap) {
1719
1908
  if (typeof schema !== 'object' || schema === null) return true
1909
+ if (hasSameInstanceRefCycle(schema)) return false
1720
1910
  if (!codegenSafe(schema, schemaMap)) return false
1721
1911
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
1722
1912
  if (externalDocsNeedInterpreter(schema, schemaMap)) return false
@@ -1725,16 +1915,10 @@ function sharedCodegenGate(schema, schemaMap) {
1725
1915
 
1726
1916
  // --- Codegen mode: generates a single Function (NOT CSP-safe) ---
1727
1917
  // This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
1728
- // `propertyNames` as the generators read it. They express maxLength,
1729
- // minLength, pattern, const and enum, and the gate declines anything else, so
1730
- // a schema that writes `propertyNames: { $ref: '#/definitions/Key' }` or adds
1731
- // `type: 'string'` went to the interpreted engine. Both are rewritten here, on
1732
- // a copy of the nodes that change: a local reference, through aliases, is
1733
- // replaced by what it names, `type: 'string'` is dropped since every property
1734
- // name is a string, and annotations go. Where what is left is still not all
1735
- // supported keys, the node is left as written and the gate declines as before.
1736
- // Errors keep the path the interpreted engine reports, which runs through the
1737
- // reference without naming it.
1918
+ // `propertyNames` as the generators read it (maxLength, minLength, pattern,
1919
+ // const, enum): a local $ref, through aliases, becomes what it names, and
1920
+ // `type: 'string'` and annotations go, on a copy. Anything else is left as
1921
+ // written and the gate declines. Errors keep the interpreted engine's path.
1738
1922
  const PN_SUPPORTED = new Set(['maxLength', 'minLength', 'pattern', 'const', 'enum'])
1739
1923
  const PN_ANNOTATIONS = new Set(['$schema', '$comment', 'title', 'description', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
1740
1924
  function simplePropertyNames(pn, defs) {
@@ -2476,7 +2660,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2476
2660
  if (!ctx.regExpMap.has(pattern)) {
2477
2661
  const ri = ctx.varCounter++
2478
2662
  ctx.regExpMap.set(pattern, ri)
2479
- if (patternIsSafe(schema.pattern)) {
2663
+ if (useSafeEngine(schema.pattern)) {
2480
2664
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
2481
2665
  ctx.usesSafeRe = true
2482
2666
  } else {
@@ -2529,8 +2713,14 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2529
2713
  // No `properties` means nothing is declared, so every key is additional.
2530
2714
  // The generators used to emit nothing for that shape, and the safety gate
2531
2715
  // declined it everywhere; at any depth it now compiles.
2532
- const propCount = Object.keys(schema.properties || {}).length
2533
- const allRequired = schema.required && schema.required.length === propCount
2716
+ const declaredKeys = Object.keys(schema.properties || {})
2717
+ const propCount = declaredKeys.length
2718
+ // Counting keys proves there is no extra key only when every declared
2719
+ // name is required. Comparing the list lengths was not that: with
2720
+ // `properties: {c}` and `required: ['b']` the counts matched, and `{b: 1}`
2721
+ // was accepted although `b` is an additional property.
2722
+ const req = new Set(schema.required || [])
2723
+ const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
2534
2724
  const inner = allRequired
2535
2725
  ? (propCount <= 15
2536
2726
  ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
@@ -2637,8 +2827,12 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2637
2827
  }
2638
2828
 
2639
2829
  // anyOf — branch fns hoisted when safe, inline fallback when recursive.
2640
- // Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
2641
- if (schema.anyOf && schema.unevaluatedProperties === undefined) genAnyOfNode(schema, v, lines, ctx, knownType)
2830
+ // The anyOf model in genUnevaluatedPropertiesNode checks that a branch
2831
+ // passes while it records what the branches evaluate, so anyOf is emitted
2832
+ // once, there. Only that model does: with unevaluatedProperties true or a
2833
+ // schema, skipping it here left anyOf unchecked and accepted a document no
2834
+ // branch matched.
2835
+ if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, lines, ctx, knownType)
2642
2836
 
2643
2837
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
2644
2838
  // Falls back to inline closures if any branch touches recursive validation.
@@ -3170,6 +3364,12 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3170
3364
  return handledPropertyNames
3171
3365
  }
3172
3366
 
3367
+ function unevalChecksAnyOf(schema, ctx) {
3368
+ if (schema.unevaluatedProperties !== false) return false
3369
+ const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3370
+ return r.dynamic && !r.allProps
3371
+ }
3372
+
3173
3373
  function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
3174
3374
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3175
3375
 
@@ -3448,6 +3648,16 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
3448
3648
  const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
3449
3649
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3450
3650
  }
3651
+ } else {
3652
+ // Nothing dynamic contributes a name (a lone if with no properties
3653
+ // or patterns, say): only the fixed names are evaluated. This branch
3654
+ // used to emit nothing, and nothing read as valid.
3655
+ const ei = ctx.varCounter++
3656
+ const evVar = `_ev${ei}`
3657
+ lines.push(`{const ${evVar}={}`)
3658
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3659
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3660
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3451
3661
  }
3452
3662
  }
3453
3663
  } else if (typeof schema.unevaluatedProperties === 'object') {
@@ -4161,17 +4371,44 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4161
4371
  // function and were rebuilt on every call, which on a product schema with
4162
4372
  // six URLs and one anyOf cost about 450 ns before a single check ran, more
4163
4373
  // than every check and every error together. Here they run once, in a
4164
- // factory, as the verdict function's preamble does. Only for the plain
4165
- // shape: with $defs or recursion the helpers include functions that read
4166
- // the per-call guard state declared in the body, so those keep `body` as
4167
- // it is. `_errSource` below stays `body` either way, for the emitters.
4168
- const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
4169
- if (hoistable) {
4170
- const factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
4374
+ // factory, as the verdict function's preamble does; with $defs or
4375
+ // recursion too, below. `_errSource` stays `body` either way, for the
4376
+ // emitters.
4377
+ // The factory builds the helpers once and returns the error function.
4378
+ // With $defs or recursion the helpers are the definitions' functions and
4379
+ // the cycle guard's state lives beside them; the guarded run resets that
4380
+ // state at its start, and a call made while one is running (a custom
4381
+ // keyword validating with the same validator) gets a function from a fresh
4382
+ // factory call instead of sharing it. `_errFactory` carries the same text
4383
+ // to the standalone emitter.
4384
+ let factoryBody
4385
+ if (needsGuardE) {
4386
+ const attempt = ctx.usesRecursion ? '_validateE(d,_all,_e)' : '_bodyE(d,_all,_e)'
4387
+ const runner = ctx.usesRecursion
4388
+ ? emitRootGuard('_validateE', 'd,_all,_e', '') + `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n `
4389
+ : `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n `
4390
+ factoryBody = guardStrE + helpersE + runner + `return function(d,_all){const _e=[];\n ` +
4391
+ emitGuardedRun(attempt, '_e.length=0;', defSetsE) + `return{valid:_e.length===0,errors:_e}}`
4392
+ } else {
4393
+ factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
4171
4394
  '\n return{valid:_e.length===0,errors:_e}}'
4172
- const params = ctx.usesSafeRe ? [...cvars, '__ataSafeRe'] : [...cvars]
4173
- const args = ctx.usesSafeRe ? [...cvals, compileSafe] : [...cvals]
4174
- fn = new Function(...params, factoryBody)(...args)
4395
+ }
4396
+ const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
4397
+ if (hoistable || needsGuardE) {
4398
+ const params = [...cvars, '__ataSafeRe']
4399
+ const args = [...cvals, compileSafe]
4400
+ const make = new Function(...params, factoryBody)
4401
+ const main = make(...args)
4402
+ if (needsGuardE) {
4403
+ let busy = false
4404
+ fn = (d, _all) => {
4405
+ if (busy) return make(...args)(d, _all)
4406
+ busy = true
4407
+ try { return main(d, _all) } finally { busy = false }
4408
+ }
4409
+ } else {
4410
+ fn = main
4411
+ }
4175
4412
  } else if (ctx.usesSafeRe) {
4176
4413
  // Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
4177
4414
  // Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
@@ -4186,6 +4423,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4186
4423
  fn = new Function('d', '_all', body)
4187
4424
  }
4188
4425
  fn._errSource = body
4426
+ fn._errFactory = factoryBody
4427
+ fn._errGuarded = needsGuardE
4189
4428
  fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
4190
4429
  fn._usesSafeRe = !!ctx.usesSafeRe
4191
4430
  if (cvars.length) {
@@ -4216,32 +4455,65 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4216
4455
  const branchKw = keyword
4217
4456
  const branchSp = schemaPrefix + '/' + branchKw
4218
4457
  // Per-branch error-collecting functions, each returning {valid,errors}.
4458
+ // They take the value, its instance path, `_all` and, inside a definition
4459
+ // written once for all its references, that reference's schema path, so
4460
+ // they can be built once per call of the error function rather than each
4461
+ // time the keyword is reached: on a list of 1723 tokens that is thousands
4462
+ // of closure arrays per validation.
4219
4463
  const fns = []
4220
4464
  const titles = []
4465
+ let usesSp = false
4221
4466
  for (let i = 0; i < branches.length; i++) {
4222
4467
  const sub = branches[i]
4223
4468
  const subLines = []
4224
4469
  const subSp = branchSp + '/' + i
4225
4470
  if (typeof sub === 'object' && sub !== null) {
4226
- genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
4471
+ genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
4227
4472
  } else if (sub === false) {
4228
4473
  // A false branch never matches; it reports the same error the
4229
4474
  // interpreter attaches to a false schema.
4230
- subLines.push(`_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
4475
+ subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
4231
4476
  }
4232
4477
  const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
4233
4478
  titles.push(title)
4234
- const body = subLines.length === 0
4235
- ? `function(_bv){return{valid:true,errors:[]}}`
4236
- : `function(_bv){const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}}`
4479
+ let body = subLines.length === 0 ? '' : `const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}`
4480
+ if (body.includes(SP_PLACEHOLDER)) {
4481
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
4482
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
4483
+ usesSp = true
4484
+ }
4237
4485
  fns.push(body)
4238
4486
  }
4487
+ const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
4239
4488
  const fnArr = `_brf${fi}`
4240
4489
  const resArr = `_brr${fi}`
4241
4490
  const titleArr = JSON.stringify(titles)
4242
4491
  const collapsed = `_brc${fi}`
4243
4492
  const pp = pathExpr || '""'
4244
- lines.push(`{const ${fnArr}=[${fns.join(',')}];const _bt${fi}=${titleArr};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${fnArr}[_bi](${v});${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}const ${collapsed}=__ataCollapse('${branchKw}',${resArr},${pp},'${branchSp}',${(() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}}`)
4493
+ ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
4494
+ const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
4495
+ const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
4496
+ const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
4497
+ if (keyword === 'anyOf') {
4498
+ // anyOf reports nothing once a branch passes, so the branches are first
4499
+ // only asked whether they pass. They are called with `_all` set to 0, a
4500
+ // third value next to true (collect everything) and false (stop at the
4501
+ // first error, for abortEarly): every check reads it as `!_all` and stops
4502
+ // at its first error, and an anyOf reached that way that fails returns
4503
+ // at once instead of collecting its branches and collapsing them, since
4504
+ // the caller only wants to know. An object tried against a branch that is
4505
+ // an anyOf of primitive types used to collapse that anyOf in full and then
4506
+ // pass the next branch, which on a list of 1723 tokens was 2353 collapses
4507
+ // no one read. Only when no branch passes do they run again with the
4508
+ // caller's `_all`, and the collapse sees them all, as before.
4509
+ const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
4510
+ lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors,title:_bt${fi}[_bi]})}${report}}}`)
4511
+ } else {
4512
+ // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
4513
+ // the passing branches and answers; the branch errors of a probe are not
4514
+ // errors anyone reads and are not collapsed.
4515
+ lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
4516
+ }
4245
4517
  }
4246
4518
 
4247
4519
  // Error-collecting code generator.
@@ -4277,9 +4549,14 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4277
4549
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
4278
4550
  const defName = m[1]
4279
4551
  if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
4280
- if (ctx.cyclicDefs.has(defName)) {
4281
- // Named helper for a def on a cycle; the path comes in as an
4282
- // argument because every call site has its own.
4552
+ if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
4553
+ if (ctx.cyclicDefs.has(defName) || ctx.sharedDefs.has(defName)) {
4554
+ // Named helper for a def on a cycle, and for one referenced from
4555
+ // several places, which inlined would repeat its whole error code at
4556
+ // each: Uniswap's token-list schema has 16 references to 10
4557
+ // definitions. The path comes in as an argument because every call
4558
+ // site has its own. Only the error path runs these; the verdict
4559
+ // function, which answers valid documents, is unchanged.
4283
4560
  if (!ctx.defFns) ctx.defFns = new Map()
4284
4561
  let fnName = ctx.defFns.get(defName)
4285
4562
  if (!fnName) {
@@ -4459,7 +4736,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4459
4736
  if (!ctx.regExpMap.has(pattern)) {
4460
4737
  const ri = ctx.varCounter++
4461
4738
  ctx.regExpMap.set(pattern, ri)
4462
- if (patternIsSafe(schema.pattern)) {
4739
+ if (useSafeEngine(schema.pattern)) {
4463
4740
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4464
4741
  ctx.usesSafeRe = true
4465
4742
  } else {
@@ -4534,7 +4811,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4534
4811
  if (!ctx.regExpMap.has(pattern)) {
4535
4812
  const ri = ctx.varCounter++
4536
4813
  ctx.regExpMap.set(pattern, ri)
4537
- if (patternIsSafe(pat)) {
4814
+ if (useSafeEngine(pat)) {
4538
4815
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4539
4816
  ctx.usesSafeRe = true
4540
4817
  } else {
@@ -4551,7 +4828,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4551
4828
  const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
4552
4829
  const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
4553
4830
  const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
4554
- const inner = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
4831
+ // The name set is built once per call of the error function, not at every
4832
+ // object it checks: on a list of 1723 tokens it was 1723 Sets.
4833
+ ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
4834
+ const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
4555
4835
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4556
4836
  }
4557
4837
 
@@ -4697,7 +4977,7 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
4697
4977
  if (!ctx.regExpMap.has(pattern)) {
4698
4978
  const ri = ctx.varCounter++
4699
4979
  ctx.regExpMap.set(pattern, ri)
4700
- if (patternIsSafe(pat)) {
4980
+ if (useSafeEngine(pat)) {
4701
4981
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4702
4982
  ctx.usesSafeRe = true
4703
4983
  } else {
@@ -4707,7 +4987,8 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
4707
4987
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
4708
4988
  }
4709
4989
  const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
4710
- const innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
4990
+ ctx.helperCode.push(`const _a${ui}=new Set([${allowedU}])`)
4991
+ const innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
4711
4992
  lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4712
4993
  }
4713
4994
 
@@ -4728,7 +5009,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4728
5009
  if (!ctx.regExpMap.has(pattern)) {
4729
5010
  const ri = ctx.varCounter++
4730
5011
  ctx.regExpMap.set(pattern, ri)
4731
- if (patternIsSafe(pat)) {
5012
+ if (useSafeEngine(pat)) {
4732
5013
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4733
5014
  ctx.usesSafeRe = true
4734
5015
  } else {
@@ -4749,7 +5030,8 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4749
5030
  function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4750
5031
  const ki = ctx.varCounter++
4751
5032
  const known = Object.keys(schema.properties || {})
4752
- const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
5033
+ if (known.length) ctx.helperCode.push(`const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}])`)
5034
+ const guard = ''
4753
5035
  // A key matched by any patternProperties entry is not additional.
4754
5036
  const patTests = []
4755
5037
  for (const pat of Object.keys(schema.patternProperties || {})) {
@@ -4757,7 +5039,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4757
5039
  if (!ctx.regExpMap.has(pattern)) {
4758
5040
  const ri = ctx.varCounter++
4759
5041
  ctx.regExpMap.set(pattern, ri)
4760
- if (patternIsSafe(pat)) {
5042
+ if (useSafeEngine(pat)) {
4761
5043
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
4762
5044
  ctx.usesSafeRe = true
4763
5045
  } else {
@@ -4809,7 +5091,7 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
4809
5091
  if (!ctx.regExpMap.has(pattern)) {
4810
5092
  const ri = ctx.varCounter++
4811
5093
  ctx.regExpMap.set(pattern, ri)
4812
- if (patternIsSafe(pn.pattern)) {
5094
+ if (useSafeEngine(pn.pattern)) {
4813
5095
  ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
4814
5096
  ctx.usesSafeRe = true
4815
5097
  } else {
@@ -5122,6 +5404,27 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5122
5404
  return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
5123
5405
  }
5124
5406
 
5407
+ // enum and const apply to a value of any type, so a value that fails
5408
+ // `type` is still checked against them, as the other engines do. They sat
5409
+ // inside the type-success block, and `{type: 'number', enum: [1]}` reported
5410
+ // only `type` for 'x' where the interpreter reports both.
5411
+ const emitEnumConst = () => {
5412
+ // enum
5413
+ if (schema.enum) {
5414
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5415
+ }
5416
+
5417
+ // const
5418
+ if (schema.const !== undefined) {
5419
+ const cv = schema.const
5420
+ if (cv === null || typeof cv !== 'object') {
5421
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5422
+ } else {
5423
+ lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
5424
+ }
5425
+ }
5426
+ }
5427
+
5125
5428
  if (types) {
5126
5429
  const conds = types.map(t => {
5127
5430
  switch (t) {
@@ -5148,22 +5451,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5148
5451
  isStr = types[0] === 'string'
5149
5452
  isNum = types[0] === 'number' || types[0] === 'integer'
5150
5453
  }
5454
+ emitEnumConst()
5151
5455
  lines.push(`if(${typeOk}){`)
5152
- }
5153
-
5154
- // enum
5155
- if (schema.enum) {
5156
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
5157
- }
5158
-
5159
- // const
5160
- if (schema.const !== undefined) {
5161
- const cv = schema.const
5162
- if (cv === null || typeof cv !== 'object') {
5163
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5164
- } else {
5165
- lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
5166
- }
5456
+ } else {
5457
+ emitEnumConst()
5167
5458
  }
5168
5459
 
5169
5460
  // required — use destructuring when type is object (SAFE because type check already passed)
@@ -5248,7 +5539,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5248
5539
  const ri = ctx.varCounter++
5249
5540
  const reVar = `_re${ri}`
5250
5541
  ctx.closureVars.push(reVar)
5251
- ctx.closureVals.push(patternIsSafe(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
5542
+ ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
5252
5543
  const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
5253
5544
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
5254
5545
  }