ata-validator 1.38.0 → 1.39.2

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.
@@ -173,6 +173,7 @@ const DOC_BASE = 'https://ata-validator.com/e/'
173
173
  // reader sorts by without deriving it from the path. Empty when the path is
174
174
  // not into the root document.
175
175
  function ordinalField (ctx, schemaPath) {
176
+ if (ctx && ctx.noOrdinal) return ''
176
177
  const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
177
178
  return o === null ? '' : `,_o:${o}`
178
179
  }
@@ -198,6 +199,8 @@ function unescapeSp (sp) {
198
199
  return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
199
200
  }
200
201
 
202
+ const FRAME_INLINE_MAX = 120
203
+
201
204
  function buildErrorLiteral (opts) {
202
205
  const { keyword, format, schemaPath, sourceMap } = opts
203
206
  let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
@@ -214,10 +217,24 @@ function buildErrorLiteral (opts) {
214
217
  const line = Array.isArray(hit) ? hit[0] : hit.line
215
218
  const col = Array.isArray(hit) ? hit[1] : hit.col
216
219
  const text = Array.isArray(hit) ? hit[2] : hit.text
217
- frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
218
- ',line:' + line +
219
- ',col:' + col +
220
- ',text:' + JSON.stringify(text) + '})'
220
+ if (sourceMap.frames && text.length > FRAME_INLINE_MAX) {
221
+ // A long source line is written once at module scope (see
222
+ // sourceFrameDecls in aot-impl.js) and the literal names it. Inlined,
223
+ // every error site carried the whole line, and a schema kept on one
224
+ // line is one line of all of it: 181 KB minified for a 2 KB schema.
225
+ // Short lines stay inline: gzip folds the repeats, and measured on
226
+ // the 50-field fixture the table cost 1206 gzipped bytes where the
227
+ // inline frames cost 696.
228
+ const key = line + ':' + col
229
+ let f = sourceMap.frames.get(key)
230
+ if (!f) { f = { line, col, text, index: sourceMap.frames.size }; sourceMap.frames.set(key, f) }
231
+ frame = ',schemaSource:__ataSS[' + f.index + ']'
232
+ } else {
233
+ frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
234
+ ',line:' + line +
235
+ ',col:' + col +
236
+ ',text:' + JSON.stringify(text) + '})'
237
+ }
221
238
  }
222
239
  }
223
240
  return { codeStr: code, docUrl, frame }
@@ -286,6 +303,9 @@ function compileToJS(schema, defs, schemaMap) {
286
303
 
287
304
  if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
288
305
  if (!defs && externalDocsNeedInterpreter(schema, schemaMap)) return null
306
+ // A $ref cycle on one value recurses here as it does in generated code; see
307
+ // hasSameInstanceRefCycle.
308
+ if (!defs && hasSameInstanceRefCycle(schema)) return null
289
309
 
290
310
  // unevaluatedProperties / unevaluatedItems need annotation tracking this
291
311
  // path does not do. Decline so the schema reaches an engine that does.
@@ -1300,6 +1320,76 @@ function aliasChainEnds(def, defs) {
1300
1320
  return true
1301
1321
  }
1302
1322
 
1323
+ // When the properties a schema evaluates depend on which subschemas pass,
1324
+ // genUnevaluatedPropertiesNode picks one of a few runtime models: if/then/else,
1325
+ // one anyOf or oneOf, dependentSchemas, or patterns (root, allOf and a lone
1326
+ // if). Each model reads only the keywords it was written for. A schema whose
1327
+ // evaluated set comes from anything else was compiled with that part ignored,
1328
+ // and a lone `if` with properties emitted no check at all, which accepted every
1329
+ // object. This says whether the model the emitter will pick covers every
1330
+ // source of evaluated properties the schema has; when it does not, the
1331
+ // schema is not generated and the interpreter answers.
1332
+ function unevalPropsModeled(schema, schemaMap) {
1333
+ const own = Object.assign({}, schema)
1334
+ delete own.$defs
1335
+ delete own.definitions
1336
+ if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
1337
+ // A subschema whose evaluated names are a fixed list, which is all the
1338
+ // branch, dependentSchemas and if/then/else models can represent.
1339
+ const fixed = (s) => {
1340
+ if (typeof s === 'boolean') return true
1341
+ if (typeof s !== 'object' || s === null) return false
1342
+ const r = collectEvaluated(s, schemaMap)
1343
+ const j = JSON.stringify(s)
1344
+ return !r.dynamic && !r.allProps && !j.includes('"patternProperties"') && !j.includes('"unevaluatedProperties"')
1345
+ }
1346
+ const allOf = Array.isArray(schema.allOf) ? schema.allOf : []
1347
+ for (const sub of allOf) {
1348
+ if (typeof sub === 'boolean') continue
1349
+ if (typeof sub !== 'object' || sub === null) return false
1350
+ const r = collectEvaluated(sub, schemaMap)
1351
+ if (r.dynamic || r.allProps) return false
1352
+ const rest = Object.assign({}, sub)
1353
+ delete rest.patternProperties
1354
+ const j = JSON.stringify(rest)
1355
+ if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
1356
+ }
1357
+ const patterns = !!schema.patternProperties || allOf.some((s) => s && typeof s === 'object' && s.patternProperties)
1358
+ const branch = schema.anyOf || schema.oneOf
1359
+ if (schema.anyOf && schema.oneOf) return false
1360
+ const hasIf = schema.if !== undefined
1361
+ const hasTE = schema.then !== undefined || schema.else !== undefined
1362
+ const dep = schema.dependentSchemas
1363
+ if (schema.unevaluatedProperties !== false) {
1364
+ // The schema-valued model reads the fixed names and one branch keyword.
1365
+ if (hasIf || hasTE || dep || patterns) return false
1366
+ return !branch || branch.every(fixed)
1367
+ }
1368
+ if (hasIf && hasTE && !branch && !schema.patternProperties && !dep) {
1369
+ if (patterns) return false
1370
+ if (!fixed(schema.if) || (schema.then !== undefined && !fixed(schema.then)) || (schema.else !== undefined && !fixed(schema.else))) return false
1371
+ // The model credits the if's own `properties` names, and only those.
1372
+ const ifNames = typeof schema.if === 'object' ? collectEvaluated(schema.if, schemaMap).props : []
1373
+ const declared = typeof schema.if === 'object' && schema.if.properties ? Object.keys(schema.if.properties) : []
1374
+ return ifNames.length === declared.length && ifNames.every((k) => declared.includes(k))
1375
+ }
1376
+ if (branch) return !hasIf && !hasTE && !dep && !patterns && branch.every(fixed)
1377
+ if (dep) return !hasIf && !hasTE && !patterns && Object.values(dep).every(fixed)
1378
+ if (hasTE) return false
1379
+ if (hasIf) {
1380
+ // A lone if contributes through its top-level patternProperties only.
1381
+ if (typeof schema.if === 'boolean') return true
1382
+ if (typeof schema.if !== 'object' || schema.if === null) return false
1383
+ const r = collectEvaluated(schema.if, schemaMap)
1384
+ if (r.dynamic || r.allProps || r.props.length) return false
1385
+ const rest = Object.assign({}, schema.if)
1386
+ delete rest.patternProperties
1387
+ const j = JSON.stringify(rest)
1388
+ if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
1389
+ }
1390
+ return true
1391
+ }
1392
+
1303
1393
  function codegenSafe(schema, schemaMap) {
1304
1394
  if (typeof schema === 'boolean') return true
1305
1395
  if (typeof schema !== 'object' || schema === null) return true
@@ -1416,6 +1506,7 @@ function codegenSafe(schema, schemaMap) {
1416
1506
  if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
1417
1507
  const evalResult = collectEvaluated(schema, schemaMap)
1418
1508
  if (evalResult.dynamic && evalResult.allProps) return false
1509
+ if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
1419
1510
  }
1420
1511
  }
1421
1512
  // unevaluatedItems: allow boolean and schema values
@@ -1754,8 +1845,85 @@ function externalDocsNeedInterpreter(schema, schemaMap) {
1754
1845
  return false
1755
1846
  }
1756
1847
 
1848
+ // A local $ref cycle that returns to a schema without moving to another value,
1849
+ // such as `A: { oneOf: [{ $ref: '#/$defs/A' }, ...] }`, never terminates on its
1850
+ // own. The interpreter stops it and answers; generated code recursed until the
1851
+ // stack ran out and threw a RangeError into the caller, on primitives, where
1852
+ // the cycle guard has no object to recognise. Cycles that pass through
1853
+ // properties, items or any other keyword that moves to a child value are
1854
+ // ordinary recursion and are not affected.
1855
+ const SAME_INSTANCE_LISTS = ['allOf', 'anyOf', 'oneOf']
1856
+ const SAME_INSTANCE_ONE = ['not', 'if', 'then', 'else']
1857
+ function localPointerTarget(root, ref) {
1858
+ if (typeof ref !== 'string' || ref.charCodeAt(0) !== 35) return null
1859
+ if (ref === '#') return root
1860
+ if (ref.charCodeAt(1) !== 47) return null
1861
+ let t = root
1862
+ for (let seg of ref.slice(2).split('/')) {
1863
+ if (seg.indexOf('%') >= 0) { try { seg = decodeURIComponent(seg) } catch { return null } }
1864
+ seg = seg.replace(/~1/g, '/').replace(/~0/g, '~')
1865
+ if (t === null || typeof t !== 'object') return null
1866
+ if (!Object.prototype.hasOwnProperty.call(t, seg)) {
1867
+ const alias = seg === 'definitions' ? '$defs' : seg === '$defs' ? 'definitions' : null
1868
+ if (alias === null || !Object.prototype.hasOwnProperty.call(t, alias)) return null
1869
+ seg = alias
1870
+ }
1871
+ t = t[seg]
1872
+ }
1873
+ return t !== null && typeof t === 'object' ? t : null
1874
+ }
1875
+ function sameInstanceEdges(root, node) {
1876
+ const out = []
1877
+ 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)
1878
+ for (const k of SAME_INSTANCE_ONE) if (node[k] && typeof node[k] === 'object') out.push(node[k])
1879
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const x of Object.values(node.dependentSchemas)) if (x && typeof x === 'object') out.push(x)
1880
+ if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
1881
+ return out
1882
+ }
1883
+ const _sameInstanceCycleCache = new WeakMap()
1884
+ function hasSameInstanceRefCycle(root) {
1885
+ if (root === null || typeof root !== 'object') return false
1886
+ const hit = _sameInstanceCycleCache.get(root)
1887
+ if (hit !== undefined) return hit
1888
+ let found = false
1889
+ if (JSON.stringify(root).includes('"$ref"')) {
1890
+ // Only what validation can reach: definitions are entered through the
1891
+ // references that name them, so a broken definition nothing uses (a real
1892
+ // SchemaStore schema has one that includes itself) does not cost the
1893
+ // whole schema its generated code.
1894
+ const nodes = []
1895
+ const seen = new Set()
1896
+ const collect = (n) => {
1897
+ if (n === null || typeof n !== 'object' || seen.has(n)) return
1898
+ seen.add(n)
1899
+ if (Array.isArray(n)) { for (const v of n) collect(v); return }
1900
+ nodes.push(n)
1901
+ for (const [k, v] of Object.entries(n)) {
1902
+ if (k === '$defs' || k === 'definitions' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
1903
+ collect(v)
1904
+ }
1905
+ if (typeof n.$ref === 'string') collect(localPointerTarget(root, n.$ref))
1906
+ }
1907
+ collect(root)
1908
+ const state = new Map() // 1 on the stack, 2 done
1909
+ const visit = (n) => {
1910
+ const st = state.get(n)
1911
+ if (st === 1) return true
1912
+ if (st === 2) return false
1913
+ state.set(n, 1)
1914
+ for (const m of sameInstanceEdges(root, n)) if (visit(m)) return true
1915
+ state.set(n, 2)
1916
+ return false
1917
+ }
1918
+ for (const n of nodes) if (visit(n)) { found = true; break }
1919
+ }
1920
+ _sameInstanceCycleCache.set(root, found)
1921
+ return found
1922
+ }
1923
+
1757
1924
  function sharedCodegenGate(schema, schemaMap) {
1758
1925
  if (typeof schema !== 'object' || schema === null) return true
1926
+ if (hasSameInstanceRefCycle(schema)) return false
1759
1927
  if (!codegenSafe(schema, schemaMap)) return false
1760
1928
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
1761
1929
  if (externalDocsNeedInterpreter(schema, schemaMap)) return false
@@ -2562,8 +2730,14 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2562
2730
  // No `properties` means nothing is declared, so every key is additional.
2563
2731
  // The generators used to emit nothing for that shape, and the safety gate
2564
2732
  // declined it everywhere; at any depth it now compiles.
2565
- const propCount = Object.keys(schema.properties || {}).length
2566
- const allRequired = schema.required && schema.required.length === propCount
2733
+ const declaredKeys = Object.keys(schema.properties || {})
2734
+ const propCount = declaredKeys.length
2735
+ // Counting keys proves there is no extra key only when every declared
2736
+ // name is required. Comparing the list lengths was not that: with
2737
+ // `properties: {c}` and `required: ['b']` the counts matched, and `{b: 1}`
2738
+ // was accepted although `b` is an additional property.
2739
+ const req = new Set(schema.required || [])
2740
+ const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
2567
2741
  const inner = allRequired
2568
2742
  ? (propCount <= 15
2569
2743
  ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
@@ -2670,8 +2844,12 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2670
2844
  }
2671
2845
 
2672
2846
  // anyOf — branch fns hoisted when safe, inline fallback when recursive.
2673
- // Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
2674
- if (schema.anyOf && schema.unevaluatedProperties === undefined) genAnyOfNode(schema, v, lines, ctx, knownType)
2847
+ // The anyOf model in genUnevaluatedPropertiesNode checks that a branch
2848
+ // passes while it records what the branches evaluate, so anyOf is emitted
2849
+ // once, there. Only that model does: with unevaluatedProperties true or a
2850
+ // schema, skipping it here left anyOf unchecked and accepted a document no
2851
+ // branch matched.
2852
+ if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, lines, ctx, knownType)
2675
2853
 
2676
2854
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
2677
2855
  // Falls back to inline closures if any branch touches recursive validation.
@@ -3203,6 +3381,12 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3203
3381
  return handledPropertyNames
3204
3382
  }
3205
3383
 
3384
+ function unevalChecksAnyOf(schema, ctx) {
3385
+ if (schema.unevaluatedProperties !== false) return false
3386
+ const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3387
+ return r.dynamic && !r.allProps
3388
+ }
3389
+
3206
3390
  function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
3207
3391
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3208
3392
 
@@ -3481,6 +3665,16 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
3481
3665
  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}`
3482
3666
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3483
3667
  }
3668
+ } else {
3669
+ // Nothing dynamic contributes a name (a lone if with no properties
3670
+ // or patterns, say): only the fixed names are evaluated. This branch
3671
+ // used to emit nothing, and nothing read as valid.
3672
+ const ei = ctx.varCounter++
3673
+ const evVar = `_ev${ei}`
3674
+ lines.push(`{const ${evVar}={}`)
3675
+ for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
3676
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
3677
+ _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3484
3678
  }
3485
3679
  }
3486
3680
  } else if (typeof schema.unevaluatedProperties === 'object') {
@@ -4138,7 +4332,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4138
4332
  // closure parameters below the way the other entry points do.
4139
4333
  closureVars: [], closureVals: [],
4140
4334
  sourceMap: (sourceOpts && sourceOpts.sourceMap && sourceOpts.schemaFile)
4141
- ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap }
4335
+ ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap, frames: sourceOpts.frames || null }
4142
4336
  : null }
4143
4337
  ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
4144
4338
  const lines = []
@@ -4194,17 +4388,56 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4194
4388
  // function and were rebuilt on every call, which on a product schema with
4195
4389
  // six URLs and one anyOf cost about 450 ns before a single check ran, more
4196
4390
  // than every check and every error together. Here they run once, in a
4197
- // factory, as the verdict function's preamble does. Only for the plain
4198
- // shape: with $defs or recursion the helpers include functions that read
4199
- // the per-call guard state declared in the body, so those keep `body` as
4200
- // it is. `_errSource` below stays `body` either way, for the emitters.
4201
- const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
4202
- if (hoistable) {
4203
- const factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
4391
+ // factory, as the verdict function's preamble does; with $defs or
4392
+ // recursion too, below. `_errSource` stays `body` either way, for the
4393
+ // emitters.
4394
+ // The factory builds the helpers once and returns the error function.
4395
+ // With $defs or recursion the helpers are the definitions' functions and
4396
+ // the cycle guard's state lives beside them; the guarded run resets that
4397
+ // state at its start, and a call made while one is running (a custom
4398
+ // keyword validating with the same validator) gets a function from a fresh
4399
+ // factory call instead of sharing it. `_errFactory` carries the same text
4400
+ // to the standalone emitter.
4401
+ let factoryBody
4402
+ if (needsGuardE) {
4403
+ const attempt = ctx.usesRecursion ? '_validateE(d,_all,_e)' : '_bodyE(d,_all,_e)'
4404
+ const runner = ctx.usesRecursion
4405
+ ? emitRootGuard('_validateE', 'd,_all,_e', '') + `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n `
4406
+ : `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n `
4407
+ factoryBody = guardStrE + helpersE + runner + `return function(d,_all){const _e=[];\n ` +
4408
+ emitGuardedRun(attempt, '_e.length=0;', defSetsE) + `return{valid:_e.length===0,errors:_e}}`
4409
+ } else {
4410
+ factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
4204
4411
  '\n return{valid:_e.length===0,errors:_e}}'
4205
- const params = ctx.usesSafeRe ? [...cvars, '__ataSafeRe'] : [...cvars]
4206
- const args = ctx.usesSafeRe ? [...cvals, compileSafe] : [...cvals]
4207
- fn = new Function(...params, factoryBody)(...args)
4412
+ }
4413
+ // For the runtime, the literals take the legacy shape directly. `code` and
4414
+ // `docUrl` are there for standalone modules, which return errors as they
4415
+ // are; the runtime derives both from the keyword when it enriches, and
4416
+ // orders errors by schemaPath, so the literals carried three fields that
4417
+ // every read then copied the error to remove. The patterns are this
4418
+ // generator's own spelling; the collapse helper writes its fields with a
4419
+ // space and keeps them.
4420
+ if (sourceOpts && sourceOpts.runtimeShape) {
4421
+ const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '').replace(/,_o:\d+/g, '')
4422
+ body = shape(body)
4423
+ factoryBody = shape(factoryBody)
4424
+ }
4425
+ const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
4426
+ if (hoistable || needsGuardE) {
4427
+ const params = [...cvars, '__ataSafeRe']
4428
+ const args = [...cvals, compileSafe]
4429
+ const make = new Function(...params, factoryBody)
4430
+ const main = make(...args)
4431
+ if (needsGuardE) {
4432
+ let busy = false
4433
+ fn = (d, _all) => {
4434
+ if (busy) return make(...args)(d, _all)
4435
+ busy = true
4436
+ try { return main(d, _all) } finally { busy = false }
4437
+ }
4438
+ } else {
4439
+ fn = main
4440
+ }
4208
4441
  } else if (ctx.usesSafeRe) {
4209
4442
  // Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
4210
4443
  // Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
@@ -4219,6 +4452,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4219
4452
  fn = new Function('d', '_all', body)
4220
4453
  }
4221
4454
  fn._errSource = body
4455
+ fn._errFactory = factoryBody
4456
+ fn._errGuarded = needsGuardE
4222
4457
  fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
4223
4458
  fn._usesSafeRe = !!ctx.usesSafeRe
4224
4459
  if (cvars.length) {
@@ -4249,32 +4484,65 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4249
4484
  const branchKw = keyword
4250
4485
  const branchSp = schemaPrefix + '/' + branchKw
4251
4486
  // Per-branch error-collecting functions, each returning {valid,errors}.
4487
+ // They take the value, its instance path, `_all` and, inside a definition
4488
+ // written once for all its references, that reference's schema path, so
4489
+ // they can be built once per call of the error function rather than each
4490
+ // time the keyword is reached: on a list of 1723 tokens that is thousands
4491
+ // of closure arrays per validation.
4252
4492
  const fns = []
4253
4493
  const titles = []
4494
+ let usesSp = false
4254
4495
  for (let i = 0; i < branches.length; i++) {
4255
4496
  const sub = branches[i]
4256
4497
  const subLines = []
4257
4498
  const subSp = branchSp + '/' + i
4258
4499
  if (typeof sub === 'object' && sub !== null) {
4259
- genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
4500
+ genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
4260
4501
  } else if (sub === false) {
4261
4502
  // A false branch never matches; it reports the same error the
4262
4503
  // interpreter attaches to a false schema.
4263
- subLines.push(`_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
4504
+ subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
4264
4505
  }
4265
4506
  const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
4266
4507
  titles.push(title)
4267
- const body = subLines.length === 0
4268
- ? `function(_bv){return{valid:true,errors:[]}}`
4269
- : `function(_bv){const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}}`
4508
+ let body = subLines.length === 0 ? '' : `const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}`
4509
+ if (body.includes(SP_PLACEHOLDER)) {
4510
+ body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
4511
+ if (body.includes(SP_PLACEHOLDER)) throw DECLINE
4512
+ usesSp = true
4513
+ }
4270
4514
  fns.push(body)
4271
4515
  }
4516
+ const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
4272
4517
  const fnArr = `_brf${fi}`
4273
4518
  const resArr = `_brr${fi}`
4274
4519
  const titleArr = JSON.stringify(titles)
4275
4520
  const collapsed = `_brc${fi}`
4276
4521
  const pp = pathExpr || '""'
4277
- 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}}}`)
4522
+ ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
4523
+ const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
4524
+ const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
4525
+ const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
4526
+ if (keyword === 'anyOf') {
4527
+ // anyOf reports nothing once a branch passes, so the branches are first
4528
+ // only asked whether they pass. They are called with `_all` set to 0, a
4529
+ // third value next to true (collect everything) and false (stop at the
4530
+ // first error, for abortEarly): every check reads it as `!_all` and stops
4531
+ // at its first error, and an anyOf reached that way that fails returns
4532
+ // at once instead of collecting its branches and collapsing them, since
4533
+ // the caller only wants to know. An object tried against a branch that is
4534
+ // an anyOf of primitive types used to collapse that anyOf in full and then
4535
+ // pass the next branch, which on a list of 1723 tokens was 2353 collapses
4536
+ // no one read. Only when no branch passes do they run again with the
4537
+ // caller's `_all`, and the collapse sees them all, as before.
4538
+ const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
4539
+ 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}}}`)
4540
+ } else {
4541
+ // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
4542
+ // the passing branches and answers; the branch errors of a probe are not
4543
+ // errors anyone reads and are not collapsed.
4544
+ 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}}}`)
4545
+ }
4278
4546
  }
4279
4547
 
4280
4548
  // Error-collecting code generator.
@@ -4589,7 +4857,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4589
4857
  const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
4590
4858
  const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
4591
4859
  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}}}`
4592
- 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}}`
4860
+ // The name set is built once per call of the error function, not at every
4861
+ // object it checks: on a list of 1723 tokens it was 1723 Sets.
4862
+ ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
4863
+ const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
4593
4864
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4594
4865
  }
4595
4866
 
@@ -4745,7 +5016,8 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
4745
5016
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
4746
5017
  }
4747
5018
  const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
4748
- 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}}}`
5019
+ ctx.helperCode.push(`const _a${ui}=new Set([${allowedU}])`)
5020
+ 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}}}`
4749
5021
  lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4750
5022
  }
4751
5023
 
@@ -4787,7 +5059,8 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4787
5059
  function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4788
5060
  const ki = ctx.varCounter++
4789
5061
  const known = Object.keys(schema.properties || {})
4790
- const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
5062
+ if (known.length) ctx.helperCode.push(`const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}])`)
5063
+ const guard = ''
4791
5064
  // A key matched by any patternProperties entry is not additional.
4792
5065
  const patTests = []
4793
5066
  for (const pat of Object.keys(schema.patternProperties || {})) {
@@ -4933,7 +5206,7 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4933
5206
  // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
4934
5207
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
4935
5208
  // Uses type-aware optimizations: after type check passes, skip guards.
4936
- function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
5209
+ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
4937
5210
  const inputSchema = schema
4938
5211
  schema = normalizePropertyNames(schema)
4939
5212
  // Same provably-local rule as the error generator above.
@@ -5013,7 +5286,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
5013
5286
  if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
5014
5287
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5015
5288
 
5016
- const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5289
+ // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
5290
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5017
5291
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5018
5292
  const lines = []
5019
5293
  try {
@@ -5148,7 +5422,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5148
5422
  if (pathVal !== undefined) {
5149
5423
  const spVal = unescapeSp(sp)
5150
5424
  ctx.closureVars.push(errVar)
5151
- const o = ordinalFor(ctx.rootSchema, spVal)
5425
+ const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
5152
5426
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
5153
5427
  if (o !== null) lit._o = o
5154
5428
  ctx.closureVals.push(Object.freeze(lit))
@@ -5160,6 +5434,27 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5160
5434
  return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
5161
5435
  }
5162
5436
 
5437
+ // enum and const apply to a value of any type, so a value that fails
5438
+ // `type` is still checked against them, as the other engines do. They sat
5439
+ // inside the type-success block, and `{type: 'number', enum: [1]}` reported
5440
+ // only `type` for 'x' where the interpreter reports both.
5441
+ const emitEnumConst = () => {
5442
+ // enum
5443
+ if (schema.enum) {
5444
+ 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'")}}`)
5445
+ }
5446
+
5447
+ // const
5448
+ if (schema.const !== undefined) {
5449
+ const cv = schema.const
5450
+ if (cv === null || typeof cv !== 'object') {
5451
+ lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5452
+ } else {
5453
+ 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'")}}`)
5454
+ }
5455
+ }
5456
+ }
5457
+
5163
5458
  if (types) {
5164
5459
  const conds = types.map(t => {
5165
5460
  switch (t) {
@@ -5186,22 +5481,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5186
5481
  isStr = types[0] === 'string'
5187
5482
  isNum = types[0] === 'number' || types[0] === 'integer'
5188
5483
  }
5484
+ emitEnumConst()
5189
5485
  lines.push(`if(${typeOk}){`)
5190
- }
5191
-
5192
- // enum
5193
- if (schema.enum) {
5194
- 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'")}}`)
5195
- }
5196
-
5197
- // const
5198
- if (schema.const !== undefined) {
5199
- const cv = schema.const
5200
- if (cv === null || typeof cv !== 'object') {
5201
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
5202
- } else {
5203
- 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'")}}`)
5204
- }
5486
+ } else {
5487
+ emitEnumConst()
5205
5488
  }
5206
5489
 
5207
5490
  // required — use destructuring when type is object (SAFE because type check already passed)