ata-validator 1.60.0 → 1.61.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.
Files changed (56) hide show
  1. package/README.md +134 -6
  2. package/bin/ata.js +84 -0
  3. package/build.d.ts +29 -0
  4. package/build.mjs +2 -0
  5. package/compiled-bare-rich.d.ts +6 -0
  6. package/compiled-bare.d.ts +7 -0
  7. package/compiled-bare.js +5 -0
  8. package/compiled-bare.mjs +35 -0
  9. package/compiled-object.d.ts +11 -0
  10. package/compiled-object.js +6 -0
  11. package/compiled-object.mjs +3 -0
  12. package/compiled-plain.d.ts +16 -0
  13. package/compiled-plain.js +5 -0
  14. package/compiled-plain.mjs +3 -0
  15. package/compiled.d.ts +3 -1
  16. package/index.browser.mjs +2 -2
  17. package/index.bundle.mjs +2 -2
  18. package/index.d.ts +9 -0
  19. package/index.edge.mjs +2 -2
  20. package/index.js +48 -0
  21. package/index.mjs +1 -1
  22. package/index.node.mjs +1 -1
  23. package/lib/aot-build.js +81 -6
  24. package/lib/aot-impl.js +24 -21
  25. package/lib/bun-target.js +612 -0
  26. package/lib/codegen-engine.js +9 -3
  27. package/lib/combined-runtime.js +1 -1
  28. package/lib/compiled-bare-rich.js +54 -0
  29. package/lib/compiled-bare.js +44 -0
  30. package/lib/compiled-common.js +17 -2
  31. package/lib/compiled-defaults.browser.js +9 -0
  32. package/lib/compiled-defaults.js +8 -0
  33. package/lib/compiled-object.js +123 -0
  34. package/lib/compiled-plain.js +57 -0
  35. package/lib/compiled.js +2 -1
  36. package/lib/email-literal.js +29 -0
  37. package/lib/enrich-error.js +5 -1
  38. package/lib/error-codes.js +6 -76
  39. package/lib/formats-source.js +20 -3
  40. package/lib/formats.js +1 -1
  41. package/lib/js-compiler-errors.js +49 -10
  42. package/lib/js-compiler.js +92 -13
  43. package/lib/json-rejections.js +159 -0
  44. package/lib/native-codes.js +67 -0
  45. package/lib/native-message.js +8 -0
  46. package/lib/native.browser.js +11 -0
  47. package/lib/public-api.js +1 -0
  48. package/lib/rejections.js +13 -152
  49. package/lib/standard-schema.js +1 -1
  50. package/lib/tier0.js +1 -1
  51. package/lib/validator-core.js +46 -4
  52. package/lib/verify.js +386 -0
  53. package/lib/version.js +1 -1
  54. package/lite.js +1 -0
  55. package/lite.mjs +1 -1
  56. package/package.json +44 -9
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
9
9
  // the first call that compiles errors. Everything here was written as part
10
10
  // of js-compiler.js and reads its helpers from that module's _internal.
11
11
  const core = require('./js-compiler')
12
- const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag } = core._internal
12
+ const { ownHead, plainFlag, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag } = core._internal
13
13
 
14
14
 
15
15
  function emitPtrEsc(ctx) {
@@ -1017,7 +1017,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1017
1017
  const bodyLines = []
1018
1018
  const outerStack = ctx.refStack
1019
1019
  ctx.refStack = new Set()
1020
- try { genCodeE(ctx.rootE, 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER) } finally { ctx.refStack = outerStack }
1020
+ try { isolatedE(ctx, () => genCodeE(ctx.rootE, 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER)) } finally { ctx.refStack = outerStack }
1021
1021
  let body = bodyLines.join('\n ')
1022
1022
  body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
1023
1023
  if (body.includes(SP_PLACEHOLDER)) throw DECLINE
@@ -1056,7 +1056,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1056
1056
  // string it starts. One left anywhere else declines, rather than
1057
1057
  // emitting a path the placeholder stands in for.
1058
1058
  const bodyLines = []
1059
- genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER)
1059
+ isolatedE(ctx, () => genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER))
1060
1060
  let body = bodyLines.join('\n ')
1061
1061
  body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
1062
1062
  if (body.includes(SP_PLACEHOLDER)) throw DECLINE
@@ -1321,12 +1321,26 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1321
1321
  // This function walks every node of a document that failed anywhere, so on a
1322
1322
  // large array the clean items were each paying for a key array.
1323
1323
  const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
1324
- const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
1324
+ // The gate names the declared keys outright when there are few of them: a
1325
+ // Set lookup per key of every object of a document that failed somewhere
1326
+ // was a third of the error path on a ten-item order (31% of the samples
1327
+ // on that one line); three string compares are not.
1328
+ const declaredNames = Object.keys(schema.properties || {})
1329
+ const notDeclared = declaredNames.length > 0 && declaredNames.length <= 8
1330
+ ? declaredNames.map((k) => `_f${ci}!==${JSON.stringify(k)}`).join('&&')
1331
+ : `!_a${ci}.has(_f${ci})`
1332
+ const gateTest = gateChecks.length ? `${notDeclared}&&!(${gateChecks.join('||')})` : notDeclared
1325
1333
  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}}}`
1326
1334
  // The name set is built once per call of the error function, not at every
1327
1335
  // object it checks: on a list of 1723 tokens it was 1723 Sets.
1328
1336
  ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
1329
- const inner = `let _x${ci}=0;${ownHead(ctx, v, `_f${ci}`, 'const')}{if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
1337
+ // The walk reads the node's plain-object flag where one is in scope
1338
+ // (withPlain declared it for this value) instead of reading the prototype
1339
+ // again for every object: that read was a tenth of the error path.
1340
+ const stdHead = ownHead(ctx, v, `_f${ci}`, 'const')
1341
+ const pk = plainFlag(ctx, v)
1342
+ const head = pk ? `for(const _f${ci} in ${v})if(!${pk}&&!_hop.call(${v},_f${ci}))continue;else ` : stdHead
1343
+ const inner = `let _x${ci}=0;${head}{if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
1330
1344
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
1331
1345
  }
1332
1346
 
@@ -1368,12 +1382,18 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1368
1382
  const sp = schemaPrefix+'/properties/'+ptrSeg(key)
1369
1383
  const from = lines.length
1370
1384
  // A value that is undefined is not checked (see the verdict generator).
1371
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
1385
+ // The value is read once into a local and the subschema checks that:
1386
+ // read as `${v}[key]` at every keyword below, a nested property was
1387
+ // loaded again for each check (a getter may answer differently, so V8
1388
+ // cannot merge the loads), and a rejected ten-item order cost 378 ns on
1389
+ // this collector against 105 on the one-pass function.
1390
+ const child = `_c${ctx.varCounter++}`
1391
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const ${child}=${v}[${JSON.stringify(key)}];if(${child}!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
1372
1392
  // A large subschema as a function of its own (emitPropFnE); false means inline.
1373
- if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
1374
- genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
1393
+ if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, child, childPath, lines, ctx, sp)) {
1394
+ genCodeE(prop, child, childPath, lines, ctx, sp)
1375
1395
  }
1376
- lines.push(`}`)
1396
+ lines.push(`}}`)
1377
1397
  countInlineE(lines, from, ctx)
1378
1398
  }
1379
1399
  }
@@ -2008,6 +2028,10 @@ const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs'
2008
2028
  function typeMismatchRest(schema, types) {
2009
2029
  const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
2010
2030
  for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
2031
+ // A built-in format is written for strings only (`typeof v==='string'&&`
2032
+ // off the string path), so for a value that failed `type: string` it can
2033
+ // never fire: carried here it was dead code in every module that checks one.
2034
+ if (types.includes('string') && typeof schema.format === 'string' && FORMAT_CODEGEN[schema.format]) drop.add('format')
2011
2035
  // A non-integer number reaches the type block already (numberToo).
2012
2036
  let rest = null
2013
2037
  for (const k of Object.keys(schema)) {
@@ -2080,11 +2104,26 @@ function propFnCandidateE (prop, ctx, schemaPrefix) {
2080
2104
  try { size = JSON.stringify(prop).length } catch { return false }
2081
2105
  return size >= SPLIT_C_SCHEMA || ((ctx.fnBytesE || 0) > SPLIT_C_BUDGET && size >= SPLIT_C_SMALL)
2082
2106
  }
2107
+ // A body that becomes a function of its own (a hoisted property, a
2108
+ // definition, the root through `$ref: "#"`) takes the parameter name `d` as
2109
+ // the node being written does; the ordinary-object flags that node declared
2110
+ // for its own `d` (withPlain) are out of scope in the helper and must not be
2111
+ // read there, as js-compiler's genIsolated keeps them out of the verdict's
2112
+ // helpers. Found by the SchemaStore differential: 24 schemas threw
2113
+ // `ReferenceError: _pk0 is not defined` from validate().
2114
+ function isolatedE (ctx, run) {
2115
+ const outerPlain = ctx.plainOf
2116
+ const outerReuse = ctx.plainReuse
2117
+ ctx.plainOf = undefined
2118
+ ctx.plainReuse = undefined
2119
+ try { return run() } finally { ctx.plainOf = outerPlain; ctx.plainReuse = outerReuse }
2120
+ }
2121
+
2083
2122
  function emitPropFnE (prop, v, pathExpr, lines, ctx, schemaPrefix) {
2084
2123
  const outerBytes = ctx.fnBytesE
2085
2124
  ctx.fnBytesE = 0
2086
2125
  const bodyLines = []
2087
- try { genCodeE(prop, 'd', '_p', bodyLines, ctx, schemaPrefix) } finally { ctx.fnBytesE = outerBytes }
2126
+ try { isolatedE(ctx, () => genCodeE(prop, 'd', '_p', bodyLines, ctx, schemaPrefix)) } finally { ctx.fnBytesE = outerBytes }
2088
2127
  if (bodyLines.length === 0) return false
2089
2128
  const body = bodyLines.join('\n ')
2090
2129
  if (body.includes(SP_PLACEHOLDER)) return false
@@ -105,8 +105,8 @@ function apMembershipCheck (ctx, names, v) {
105
105
  }
106
106
  }
107
107
  // With nothing declared every key is additional; `if()` would not parse.
108
- if (names.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
109
- return `for(var _k in ${v})if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
108
+ if (names.length === 0) return closedKeyLoop(ctx, v, `if(${ownFail(ctx, v)})return false`)
109
+ return closedKeyLoop(ctx, v, `if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`)
110
110
  }
111
111
 
112
112
  function emitDeq(ctx) {
@@ -946,12 +946,26 @@ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '_
946
946
  // every module that asked about presence at all, about a sixth of a small
947
947
  // schema's compiled module.
948
948
  const OWN_HELPER_CODE = {
949
- _PN: 'const _PN=({}).__proto__===Object.prototype;const _OP=Object.prototype;const _gp=Object.getPrototypeOf;',
949
+ // Whether the plain-object test reads `__proto__` (V8: the accessor is
950
+ // the cheaper read) or Object.getPrototypeOf (JavaScriptCore, and Bun's
951
+ // ahead-of-time compiler, where the accessor is a slow path: 212 against
952
+ // 127 ns on a ten-item order). Decided once, where the module loads.
953
+ // The plain-object test reads `__proto__` on V8, where the accessor is the
954
+ // cheaper read, and Object.getPrototypeOf on JavaScriptCore, where the
955
+ // accessor is a slow path under Bun's compiler (212 against 127 ns on a
956
+ // ten-item order). Decided per target engine when the code is generated;
957
+ // the V8 form still tests the accessor at load, since Deno removes it.
958
+ get _PN () { return TARGET_ENGINE !== 'v8' ? 'const _PN=false;const _OP=Object.prototype;const _gp=Object.getPrototypeOf;' : 'const _PN=({}).__proto__===Object.prototype;const _OP=Object.prototype;const _gp=Object.getPrototypeOf;' },
950
959
  _hop: 'const _hop=Object.prototype.hasOwnProperty;',
951
960
  _h: 'function _h(o,k){return _hop.call(o,k)}',
952
961
  _ok: 'function _ok(o,k){return (_PN?o.__proto__:_gp(o))===_OP||_hop.call(o,k)}',
953
962
  _hall: 'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}',
954
963
  _nk: 'function _nk(o,n){return (_PN?o.__proto__:_gp(o))===_OP||Object.keys(o).length!==n}',
964
+ // The own-key count of a closed object, and the walk over its keys, take
965
+ // the form of the engine the code is generated for (targetEngine below):
966
+ // for...in on V8, where it is the cheaper form, Object.keys on
967
+ // JavaScriptCore, where for...in is a slow path. One form is written, so
968
+ // neither engine pays for a branch per object.
955
969
  }
956
970
  const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'], _nk: ['_PN'] }
957
971
  function useOwnHelper (ctx, name) {
@@ -1032,7 +1046,7 @@ function withPlain (schema, v, lines, ctx, run) {
1032
1046
  }
1033
1047
  // The same test at run time, for the closure compiler.
1034
1048
  const _hop = Object.prototype.hasOwnProperty
1035
- const _PN = ({}).__proto__ === Object.prototype
1049
+ const _PN = typeof Bun === 'undefined' && ({}).__proto__ === Object.prototype // JavaScriptCore reads the prototype through Object.getPrototypeOf, see the _PN declaration
1036
1050
  // One hasOwnProperty call. From 1.31.2 to 1.33.0 this first tested the name
1037
1051
  // against Object.prototype's names, then `in`, then read the prototype through
1038
1052
  // the __proto__ accessor, on every property of every object the eval-free
@@ -3644,7 +3658,9 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3644
3658
  lines.push(`if(${reqChecks.join('||')})return false`)
3645
3659
  }
3646
3660
  // On an object with another prototype the typed ones may be inherited.
3647
- if (typed.length > 0) {
3661
+ // Where the node's key set is proved exactly (exactKeysNode), the typed
3662
+ // names are own by that proof and the list is not needed.
3663
+ if (typed.length > 0 && !exactKeysNode(schema, ctx)) {
3648
3664
  useOwnHelper(ctx, '_hall')
3649
3665
  // Read directly: the object type is already established here, and a
3650
3666
  // flag declared for this alone costs more than it saves. The list is
@@ -3677,7 +3693,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3677
3693
  if (allRequired && propCount > 0) {
3678
3694
  // Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
3679
3695
  if (propCount <= 15) {
3680
- lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`)
3696
+ lines.push(exactKeys(ctx, v, knownKeys))
3681
3697
  } else {
3682
3698
  lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
3683
3699
  }
@@ -3817,7 +3833,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3817
3833
  const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
3818
3834
  const inner = allRequired
3819
3835
  ? (propCount <= 15
3820
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
3836
+ ? exactKeys(ctx, v, declaredKeys)
3821
3837
  : `if(Object.keys(${v}).length!==${propCount})return false`)
3822
3838
  : apMembershipCheck(ctx, Object.keys(schema.properties || {}), v)
3823
3839
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
@@ -4833,7 +4849,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4833
4849
  if (!earlyKeyCount) {
4834
4850
  // Adaptive: for-in for <=15 keys, Object.keys for >15
4835
4851
  inner = propCount <= 15
4836
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
4852
+ ? exactKeys(ctx, v, knownKeys)
4837
4853
  : `if(Object.keys(${v}).length!==${propCount})return false`
4838
4854
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4839
4855
  }
@@ -5647,6 +5663,69 @@ function ownFail (ctx, v, k = '_k') {
5647
5663
  useOwnHelper(ctx, '_ok')
5648
5664
  return `_ok(${v},${k})`
5649
5665
  }
5666
+ // The engine the generated code is written for. The runtime compiles for the
5667
+ // engine it runs on; `ata build --target bun` and the plugin's Bun entry write
5668
+ // modules for JavaScriptCore (setTargetEngine). The two engines want opposite
5669
+ // forms for walking a closed object's keys: for...in is the cheaper form on
5670
+ // V8 and a slow path on JavaScriptCore (a ten-item order under Bun's
5671
+ // ahead-of-time compiler: 227 against 108 ns), and Object.keys the reverse. A
5672
+ // branch per object on a load-time flag cost V8 5 to 7% on small schemas, so
5673
+ // one form is written, chosen here.
5674
+ // 'jsc-aot' is JavaScriptCore under Bun's ahead-of-time compiler, where the
5675
+ // cheapest proof that a closed object holds exactly its declared keys is to
5676
+ // read Object.keys once and compare it with the declared list in order (the
5677
+ // order JSON.parse gives), falling back to a membership walk when the order
5678
+ // differs. On Bun's JIT that form lost 5 to 15 ns a ten-item order to the
5679
+ // plain count, and under the compiler it won 4 to 10, which brought the
5680
+ // module level with Bun's own validate<T>; so it is its own target, the one
5681
+ // `ata build --target bun` writes for.
5682
+ let TARGET_ENGINE = typeof Bun !== 'undefined' ? 'jsc' : 'v8'
5683
+ const ENGINES = ['v8', 'jsc', 'jsc-aot']
5684
+ function setTargetEngine (engine) {
5685
+ const prev = TARGET_ENGINE
5686
+ if (ENGINES.includes(engine)) TARGET_ENGINE = engine
5687
+ else if (engine === null || engine === undefined) TARGET_ENGINE = typeof Bun !== 'undefined' ? 'jsc' : 'v8'
5688
+ else throw new Error(`unknown target engine ${JSON.stringify(engine)}; expected "v8", "jsc" or "jsc-aot"`)
5689
+ return prev
5690
+ }
5691
+ function targetEngine () { return TARGET_ENGINE }
5692
+ // `var _n=<own key count of v>;`
5693
+ function keyCount (v) {
5694
+ return TARGET_ENGINE !== 'v8' ? `var _n=Object.keys(${v}).length;` : `var _n=0;for(var _k in ${v})_n++;`
5695
+ }
5696
+ // Whether this node's check under the compiler is the exact key list: a
5697
+ // closed object, no pattern properties, every declared key required, at
5698
+ // most fifteen of them, not one the removing verdict rewrites. The proof then
5699
+ // covers presence and ownership of the typed keys too.
5700
+ function exactKeysNode (schema, ctx) {
5701
+ if (TARGET_ENGINE !== 'jsc-aot') return false
5702
+ if (schema.additionalProperties !== false || schema.patternProperties || !schema.properties || !schema.required) return false
5703
+ if (ctx.removeNodes && ctx.removeNodes.has(schema)) return false
5704
+ const keys = Object.keys(schema.properties)
5705
+ if (keys.length === 0 || keys.length > 15) return false
5706
+ const req = new Set(schema.required)
5707
+ return keys.every((k) => req.has(k))
5708
+ }
5709
+ // The check that a closed object whose declared keys are all required holds
5710
+ // exactly those keys: the count, compared against the own-key count when it
5711
+ // is off (ownCountOff), or under the compiler the key list itself.
5712
+ function exactKeys (ctx, v, keys) {
5713
+ const n = keys.length
5714
+ // The plain-object flag is asked for only on the counting form: asking marks
5715
+ // it used, and the exact key list needs no prototype read at all.
5716
+ if (TARGET_ENGINE !== 'jsc-aot' || n === 0) return `${keyCount(v)}if(_n!==${n}&&${ownCountOff(ctx, v, n, plainFlag(ctx, v))})return false`
5717
+ const i = ctx.varCounter++
5718
+ const names = keys.map((k) => JSON.stringify(k))
5719
+ const inOrder = names.map((k, j) => `_ks${i}[${j}]===${k}`).join('&&')
5720
+ const member = names.map((k) => `_k!==${k}`).join('&&')
5721
+ return `{const _ks${i}=Object.keys(${v});if(_ks${i}.length!==${n})return false;if(!(${inOrder}))for(let _i=0;_i<${n};_i++){const _k=_ks${i}[_i];if(${member})return false}}`
5722
+ }
5723
+ // A loop over the keys of a closed object, `body` reading each as `_k`.
5724
+ function closedKeyLoop (ctx, v, body) {
5725
+ if (TARGET_ENGINE === 'v8') return `for(var _k in ${v}){${body}}`
5726
+ const i = ctx.varCounter++
5727
+ return `{var _ks${i}=Object.keys(${v});for(var _ki${i}=0;_ki${i}<_ks${i}.length;_ki${i}++){var _k=_ks${i}[_ki${i}];${body}}}`
5728
+ }
5650
5729
  // For a key count that came out wrong: whether it is wrong counting own keys
5651
5730
  // only. An ordinary object's count stands; any other object is counted again.
5652
5731
  // A oneOf of closed objects fails its other branches here on every value, so
@@ -5677,10 +5756,10 @@ function ownHead (ctx, v, k = '_k', kind = 'var') {
5677
5756
  // Generate a charCodeAt(0)-based switch tree for fast key validation.
5678
5757
  // V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
5679
5758
  function genCharCodeSwitch(keys, v, ctx) {
5680
- if (keys.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
5759
+ if (keys.length === 0) return closedKeyLoop(ctx, v, `if(${ownFail(ctx, v)})return false`)
5681
5760
  if (keys.length <= 3) {
5682
5761
  // Small set: simple chain is faster than switch overhead
5683
- return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
5762
+ return closedKeyLoop(ctx, v, `if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`)
5684
5763
  }
5685
5764
 
5686
5765
  // Group keys by first charCode
@@ -5697,7 +5776,7 @@ function genCharCodeSwitch(keys, v, ctx) {
5697
5776
  cases += `case ${cc}:if(${cond})continue;break;`
5698
5777
  }
5699
5778
 
5700
- return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}if(${ownFail(ctx, v)})return false}`
5779
+ return closedKeyLoop(ctx, v, `switch(_k.charCodeAt(0)){${cases}default:break}if(${ownFail(ctx, v)})return false`)
5701
5780
  }
5702
5781
 
5703
5782
  // The verdict form of genUnevaluatedDynamicC: the same set, built the same
@@ -6036,10 +6115,10 @@ function compileToJSCodegenWithErrors (schema, schemaMap, userFormats, sourceOpt
6036
6115
  function compileToJSCombined (schema, VALID_RESULT, schemaMap, userFormats, opts) { return errorsModule().compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) }
6037
6116
 
6038
6117
 
6039
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget, _helperSources: { DEQ_HELPER, PTR_ESC_HELPER, OWN_HELPER_CODE } }
6118
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, setTargetEngine, targetEngine, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget, _helperSources: { DEQ_HELPER, PTR_ESC_HELPER, OWN_HELPER_CODE } }
6040
6119
  // For tests: the dynamic-scope unrolling, to hold its output to the interpreter.
6041
6120
  Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDynamicScopes, enumerable: false })
6042
6121
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
6043
6122
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
6044
6123
 
6045
- module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag }
6124
+ module.exports._internal = { ownHead, plainFlag, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag }
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ // The rejection validateJSON() returns, and the position map behind an error's
4
+ // frame in the JSON text. Apart from lib/rejections.js so that code with no
5
+ // JSON entry point (validate() through a compiled wrapper) does not bundle it.
6
+
7
+ const { needsOrdering, sortErrorsBySchemaOrder, attachRelated, _setPositionsFor } = require('./rejections');
8
+ const { setDiagnosticSource: attachDiagnosticSource } = require('./diagnostic-source');
9
+
10
+ // The rejection validateJSON returns. Everything the text path adds over
11
+ // validate(data), the value tree enrichment needs, the position map, the
12
+ // diagnostic payload, happens on first access to `.errors`. Reading `.valid`
13
+ // touches none of it. The JSON text is held here rather than read back off the
14
+ // validator, so the result does not depend on what the instance does next.
15
+ class LazyJsonRejection {
16
+ // `parsed`, when given, is the document the inner path parsed from
17
+ // `jsonStr` and validated, after defaults, coercion and removal: reading the
18
+ // errors uses it instead of parsing the text again, which on a 5 MB
19
+ // document was a third of what reading them cost.
20
+ constructor(result, jsonStr, self, enrich, parsed) {
21
+ this.valid = false;
22
+ this._result = result;
23
+ this._jsonStr = jsonStr;
24
+ this._self = self;
25
+ this._enrich = enrich;
26
+ this._cached = null;
27
+ this._parsed = parsed;
28
+ }
29
+ toJSON() {
30
+ return { valid: false, errors: this.errors };
31
+ }
32
+ }
33
+ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
34
+ enumerable: true,
35
+ configurable: true,
36
+ get() {
37
+ if (this._cached !== null) return this._cached;
38
+ const self = this._self;
39
+ const enrich = this._enrich;
40
+ const jsonStr = this._jsonStr;
41
+ // Reading the inner errors realizes the inner lazy layer, if there was one.
42
+ const raw = this._result.errors || [];
43
+ if (!raw.length) { this._cached = raw; return raw; }
44
+
45
+ // The enrich pass plucks `received` from the value tree and the suggestion
46
+ // engine (required-typo, format hints, coercion nudges) walks it too.
47
+ let parsedData = this._parsed;
48
+ if (parsedData === undefined) {
49
+ try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
50
+ // What was checked is the document after defaults, coercion and removal,
51
+ // so `received` has to come from that, as it does for validate(). A
52
+ // default that failed its own schema has no value in the text at all.
53
+ if (parsedData !== undefined && self._preprocess) self._preprocess(parsedData);
54
+ }
55
+
56
+ // Errors the inner path already enriched carry a `received` key: enrich()
57
+ // always sets one, even when there is no value to show. `code` is not a
58
+ // safe signal, because branch-collapse attaches codes to raw errors, and
59
+ // `docUrl` is not either, because the generated error functions stamp it
60
+ // on raw errors; taking it as the signal left those errors without
61
+ // `received` or a suggestion on the text path while validate() had both.
62
+ if (!raw[0] || !('received' in raw[0])) {
63
+ // Declaration order, as validate() applies it; the text path used to
64
+ // enrich in emission order.
65
+ const ordered = needsOrdering(raw) ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
66
+ // The position map waits for the first read of a frame (lib/enrich-error
67
+ // defineLazyFrames); until then reading the list costs the enrichment alone.
68
+ const lazyPositions = lazyPositionsFor(self, jsonStr, ordered);
69
+ const enrichOpts = {
70
+ data: parsedData,
71
+ positions: null,
72
+ lazyPositions,
73
+ schemaPositions: self._schemaPositions,
74
+ schemaFile: self._source ? self._source.path : undefined,
75
+ };
76
+ const enriched = ordered.map((e) => enrich(e, enrichOpts));
77
+ if (enriched.length > 1) attachRelated(enriched);
78
+ attachDiagnosticSource(enriched, {
79
+ data: parsedData,
80
+ text: jsonStr,
81
+ // Read by the diagnostic renderer only, which resolves the same map.
82
+ get positions () { return lazyPositions(); },
83
+ schema: self._schemaObj,
84
+ mutatesInput: self._mutatesInput === true,
85
+ });
86
+ this._cached = enriched;
87
+ return enriched;
88
+ }
89
+
90
+ // Already enriched, so the frames are usually attached too. Only a gap in
91
+ // them is worth another walk of the document: resolving the map to discover
92
+ // there was nothing to fill cost a second full walk on every rejection.
93
+ if (raw.some((e) => e && !e.dataFrame)) {
94
+ const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
95
+ if (positions) {
96
+ for (const e of raw) {
97
+ if (e && !e.dataFrame) {
98
+ const path = e.path != null ? e.path : (e.instancePath || '');
99
+ const p = positions[path];
100
+ if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
101
+ }
102
+ }
103
+ self._posCache.reset();
104
+ }
105
+ }
106
+ if (raw.length > 1) attachRelated(raw);
107
+ attachDiagnosticSource(raw, {
108
+ data: parsedData,
109
+ text: jsonStr,
110
+ schema: self._schemaObj,
111
+ mutatesInput: self._mutatesInput === true,
112
+ });
113
+ this._cached = raw;
114
+ return raw;
115
+ },
116
+ });
117
+
118
+ // The targeted position map for a list, built on the first call and shared
119
+ // by every error's frame accessor (lib/enrich-error defineLazyFrames).
120
+ function lazyPositionsFor (self, text, errors) {
121
+ let done = false;
122
+ let positions = null;
123
+ return () => {
124
+ if (done) return positions;
125
+ done = true;
126
+ positions = self._pos().targeted(text, wantedPointersFor(errors));
127
+ if (positions) self._posCache.reset();
128
+ return positions;
129
+ };
130
+ }
131
+
132
+ // The pointers a set of errors will ask the position map about. lib/enrich-error
133
+ // looks up the error's own path, and for an additional or unevaluated property
134
+ // the child pointer named in `params`, unescaped, which is the form it asks for.
135
+ // The escaped form goes in too: including a pointer that is never read costs
136
+ // nothing, and the cache answers anything outside this set from the full map
137
+ // rather than reporting no position.
138
+ function wantedPointersFor (errors) {
139
+ const wanted = new Set();
140
+ for (const e of errors) {
141
+ if (!e) continue;
142
+ const path = e.path != null ? e.path : (e.instancePath || '');
143
+ wanted.add(path);
144
+ if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
145
+ const params = e.params;
146
+ const named = params && (params.additionalProperty || params.unevaluatedProperty);
147
+ if (typeof named === 'string') {
148
+ wanted.add(path + '/' + named);
149
+ if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
150
+ wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
151
+ }
152
+ }
153
+ }
154
+ return wanted;
155
+ }
156
+
157
+ _setPositionsFor(lazyPositionsFor);
158
+
159
+ module.exports = { LazyJsonRejection };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ // A module of its own so the browser build, which has no native engine
4
+ // (lib/native-load.browser.js), maps it to lib/native-codes.browser.js and
5
+ // carries no table for ordinals that never reach it.
6
+
7
+ const { codeFor } = require('./error-codes');
8
+
9
+ // The native engine reports its own `error_code` enum from include/ata.h as an
10
+ // ordinal. Nothing outside the addon should ever see one: an error that came
11
+ // from there carries the same public code and keyword as the same failure from
12
+ // the JavaScript engines. Ordinals are positional, so this table follows the
13
+ // enum's declaration order and `tests/test_native_error_codes.js` fails if the
14
+ // two drift apart.
15
+ const NATIVE_KEYWORDS = [
16
+ null, // 0 ok
17
+ '__parse__', // 1 invalid_json
18
+ '__compile__', // 2 invalid_schema
19
+ 'type', // 3 type_mismatch
20
+ 'required', // 4 required_property_missing
21
+ 'additionalProperties', // 5 additional_property_not_allowed
22
+ 'enum', // 6 enum_mismatch
23
+ 'const', // 7 const_mismatch
24
+ 'minimum', // 8 minimum_violation
25
+ 'maximum', // 9 maximum_violation
26
+ 'exclusiveMinimum', // 10 exclusive_minimum_violation
27
+ 'exclusiveMaximum', // 11 exclusive_maximum_violation
28
+ 'minLength', // 12 min_length_violation
29
+ 'maxLength', // 13 max_length_violation
30
+ 'pattern', // 14 pattern_mismatch
31
+ 'format', // 15 format_mismatch
32
+ 'minItems', // 16 min_items_violation
33
+ 'maxItems', // 17 max_items_violation
34
+ 'uniqueItems', // 18 unique_items_violation
35
+ 'minProperties', // 19 min_properties_violation
36
+ 'maxProperties', // 20 max_properties_violation
37
+ 'multipleOf', // 21 multiple_of_violation
38
+ 'allOf', // 22 all_of_failed
39
+ 'anyOf', // 23 any_of_failed
40
+ 'oneOf', // 24 one_of_failed
41
+ 'not', // 25 not_failed
42
+ '$ref', // 26 ref_not_found
43
+ 'if', // 27 if_then_else_failed
44
+ '__depth__', // 28 depth_exceeded
45
+ ]
46
+
47
+ // Keyword and public code for a native ordinal, or null when the ordinal is
48
+ // outside the enum, which would mean the addon is newer than this table.
49
+ function fromNative (ordinal, format) {
50
+ if (typeof ordinal !== 'number' || !Number.isInteger(ordinal)) return null
51
+ const keyword = NATIVE_KEYWORDS[ordinal]
52
+ if (!keyword) return null
53
+ // codeFor('format') answers with the first format code, which names a
54
+ // specific format. Without knowing which format failed, the generic one is
55
+ // the honest answer.
56
+ // The walker stopped at its depth limit and rejected the document. No
57
+ // public code names that: the APIs that ask the walker route schemas deep or
58
+ // recursive enough to reach it to validate() first (lib/buffer-gate.js), so
59
+ // it is the generic failure.
60
+ if (keyword === '__depth__') return { keyword, code: 'ATA9000' }
61
+ const code = keyword === 'format'
62
+ ? (format ? codeFor('format', format) || 'ATA3099' : 'ATA3099')
63
+ : codeFor(keyword) || 'ATA9001'
64
+ return { keyword, code }
65
+ }
66
+
67
+ module.exports = { fromNative, NATIVE_KEYWORDS };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ // The words for an error the native engine reported without a message (it
4
+ // reports numeric codes): the enrich pass knows them. A module of its own so
5
+ // the browser build, which has no native engine (lib/native-load.browser.js),
6
+ // maps it to lib/native-message.browser.js and leaves the enrichment out of
7
+ // bundles that only reach it from here, such as ata-validator/compiled-plain.
8
+ module.exports = (err) => require('./enrich-error').enrich(err, {}).message;
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ // Browser build of lib/native-message.js, lib/native-codes.js and
4
+ // lib/native-positions.js, one file for all three so a page carries one module
5
+ // for them: there is no native engine in a browser (lib/native-load.browser.js
6
+ // returns null), so no error arrives without a message or with a native
7
+ // ordinal, and no position is found by the addon. Kept total all the same.
8
+ const message = (err) => (err && err.message) || 'validation failed';
9
+ message.fromNative = message.nativeTargeted = () => null;
10
+ message.NATIVE_KEYWORDS = [];
11
+ module.exports = message;
package/lib/public-api.js CHANGED
@@ -70,6 +70,7 @@ module.exports = {
70
70
  parseJSON: core.parseJSON,
71
71
  toTypeScript,
72
72
  defineSchema: core.defineSchema,
73
+ schemaOf: core.schemaOf,
73
74
  renderPretty,
74
75
  renderCompact,
75
76
  toOutput,