ata-validator 1.38.0 → 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.
package/README.md CHANGED
@@ -32,13 +32,13 @@ npm install --save-dev ata-validator
32
32
  npx ata build 'schemas/*.json' --out-dir src/generated
33
33
  ```
34
34
 
35
- The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
35
+ The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc (2.17 or later, below the 2.28 Node itself needs) and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
36
36
 
37
37
  ```bash
38
38
  npm install ata-validator --omit=optional
39
39
  ```
40
40
 
41
- or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
41
+ or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
42
42
 
43
43
  Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
44
44
 
package/index.d.ts CHANGED
@@ -439,7 +439,8 @@ export interface ValidatorOptions {
439
439
  * answers `validate()`, `isValidObject()` and `validateJSON()`. For a schema
440
440
  * that arrives from outside the trust boundary. The verdict is the same on
441
441
  * every engine; the cost is not. The buffer APIs (`isValid`, `countValid`,
442
- * `batchIsValid`) are native-only and unaffected.
442
+ * `batchIsValid`) use the native addon where it loads, which this option
443
+ * does not affect; without it they answer through the engine it picks.
443
444
  */
444
445
  engine?: 'auto' | 'interpreter';
445
446
  }
package/lib/aot-impl.js CHANGED
@@ -17,7 +17,7 @@ const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions }
17
17
  const { buildTargetedPositionMap } = require('./data-positions');
18
18
  const { schemaHash } = require('./schema-hash');
19
19
  const ATA_VERSION = require('./version');
20
- const SAFE_REGEX_SOURCE = require('./safe-regex-source');
20
+ const { engineSource: safeRegexEngineSource } = require('./safe-regex');
21
21
 
22
22
  // Embedded verbatim in standalone modules so the output file has no runtime
23
23
  // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
@@ -31,21 +31,16 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
31
31
  return len;
32
32
  }`;
33
33
 
34
- // The linear-time regex engine, inlined verbatim into standalone output so a
35
- // compiled module that uses safe `pattern` matchers has no runtime dependency
36
- // on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
37
- // at build time (see `scripts/regen-safe-regex-source.js`), so this path has
38
- // no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
39
- // strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
40
- // alias the emitted code calls. The engine has no eval/new Function, so the
34
+ // The linear-time regex engine, embedded in standalone modules whose patterns
35
+ // need it so the output does not depend on ata-validator at run time. The text
36
+ // is the engine's own function (lib/safe-regex.js), called once for the
37
+ // `__ataSafeRe` the emitted code uses. No fs, path or __dirname, so this is
38
+ // safe in browser bundles, and the engine has no eval or new Function, so the
41
39
  // embed is CSP-safe.
42
40
  let _safeRegexEmbed = null;
43
41
  function getSafeRegexEmbed() {
44
42
  if (_safeRegexEmbed === null) {
45
- const body = SAFE_REGEX_SOURCE
46
- .replace(/^'use strict'\s*\n/, '')
47
- .replace(/\nmodule\.exports[^\n]*\n?/, '\n');
48
- _safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
43
+ _safeRegexEmbed = 'const __ataSafeRe = (' + safeRegexEngineSource() + ')().compileSafe;';
49
44
  }
50
45
  return _safeRegexEmbed;
51
46
  }
@@ -213,7 +208,18 @@ function toStandaloneModule(validator, opts) {
213
208
  (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
214
209
  );
215
210
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
216
- if (errSrc) {
211
+ if (errSrc && jsErrFn._errFactory) {
212
+ // The helpers (patterns, name sets, definition and branch functions) are
213
+ // built once, not on every call. With $defs the
214
+ // cycle guard's state sits beside them, so a call made while one is
215
+ // running takes a function from a fresh factory call.
216
+ // Built on the first error read rather than at import, so a page that
217
+ // only ever accepts pays nothing for them at load.
218
+ const fac = jsErrFn._errFactory;
219
+ errCore = jsErrFn._errGuarded
220
+ ? `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nlet _errBusy = false;\nconst errFn = function(d, _all) { if (_errBusy) return _mkErr()(d, _all); if (_errMain === null) _errMain = _mkErr(); _errBusy = true; try { return _errMain(d, _all); } finally { _errBusy = false; } };\n`
221
+ : `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nconst errFn = function(d, _all) { return (_errMain || (_errMain = _mkErr()))(d, _all); };\n`;
222
+ } else if (errSrc) {
217
223
  errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
218
224
  } else if (opts && typeof opts.onWarning === 'function') {
219
225
  // The caller asked for error detail and is not getting it: the error
@@ -122,14 +122,13 @@ function toText(input, name) {
122
122
  throw new TypeError(`${name}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
123
123
  }
124
124
 
125
- // Replaces the instance's buffer APIs with versions that parse and call
126
- // validate(). Installed after compilation, so `validator.validate` is final.
125
+ // Replaces the instance's buffer APIs with versions that decode the bytes and
126
+ // ask isValidJSON(), which answers as validate() does on the parsed value and
127
+ // reads text the scanner covers without parsing it (a small document went from
128
+ // 350 to 155 ns without the addon). Its own native route is behind this same
129
+ // gate, so a schema routed here never reaches the walker through it.
127
130
  function installSlowBufferApis(validator) {
128
- const isValidText = (text) => {
129
- let value;
130
- try { value = JSON.parse(text); } catch { return false; }
131
- return validator.validate(value).valid;
132
- };
131
+ const isValidText = (text) => validator.isValidJSON(text);
133
132
  validator.isValid = (input) => isValidText(toText(input, 'isValid'));
134
133
  validator.isValidPrepadded = (paddedBuffer, jsonLength) =>
135
134
  isValidText(Buffer.from(paddedBuffer.buffer, paddedBuffer.byteOffset, jsonLength).toString('utf8'));
@@ -233,8 +233,10 @@ function enrich (rawErr, opts) {
233
233
  if ('schema' in rawErr) out.schema = rawErr.schema;
234
234
 
235
235
  // oneOf/anyOf collapse: preserve the nested branch errors so the pretty
236
- // renderer can surface the closest variant's diagnostics.
237
- if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors;
236
+ // renderer can surface the closest variant's diagnostics. They are enriched
237
+ // like any other error: passed through raw, they carried whatever the engine
238
+ // that answered wrote, the generator's internal ordering key included.
239
+ if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
238
240
 
239
241
  if (positions && positions[path]) {
240
242
  const p = positions[path];
@@ -286,6 +286,9 @@ function compileToJS(schema, defs, schemaMap) {
286
286
 
287
287
  if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
288
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
289
292
 
290
293
  // unevaluatedProperties / unevaluatedItems need annotation tracking this
291
294
  // path does not do. Decline so the schema reaches an engine that does.
@@ -1300,6 +1303,76 @@ function aliasChainEnds(def, defs) {
1300
1303
  return true
1301
1304
  }
1302
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
+
1303
1376
  function codegenSafe(schema, schemaMap) {
1304
1377
  if (typeof schema === 'boolean') return true
1305
1378
  if (typeof schema !== 'object' || schema === null) return true
@@ -1416,6 +1489,7 @@ function codegenSafe(schema, schemaMap) {
1416
1489
  if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
1417
1490
  const evalResult = collectEvaluated(schema, schemaMap)
1418
1491
  if (evalResult.dynamic && evalResult.allProps) return false
1492
+ if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
1419
1493
  }
1420
1494
  }
1421
1495
  // unevaluatedItems: allow boolean and schema values
@@ -1754,8 +1828,85 @@ function externalDocsNeedInterpreter(schema, schemaMap) {
1754
1828
  return false
1755
1829
  }
1756
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
+
1757
1907
  function sharedCodegenGate(schema, schemaMap) {
1758
1908
  if (typeof schema !== 'object' || schema === null) return true
1909
+ if (hasSameInstanceRefCycle(schema)) return false
1759
1910
  if (!codegenSafe(schema, schemaMap)) return false
1760
1911
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
1761
1912
  if (externalDocsNeedInterpreter(schema, schemaMap)) return false
@@ -2562,8 +2713,14 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2562
2713
  // No `properties` means nothing is declared, so every key is additional.
2563
2714
  // The generators used to emit nothing for that shape, and the safety gate
2564
2715
  // 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
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))
2567
2724
  const inner = allRequired
2568
2725
  ? (propCount <= 15
2569
2726
  ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
@@ -2670,8 +2827,12 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
2670
2827
  }
2671
2828
 
2672
2829
  // 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)
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)
2675
2836
 
2676
2837
  // oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
2677
2838
  // Falls back to inline closures if any branch touches recursive validation.
@@ -3203,6 +3364,12 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
3203
3364
  return handledPropertyNames
3204
3365
  }
3205
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
+
3206
3373
  function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
3207
3374
  const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
3208
3375
 
@@ -3481,6 +3648,16 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
3481
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}`
3482
3649
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
3483
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}}}`)
3484
3661
  }
3485
3662
  }
3486
3663
  } else if (typeof schema.unevaluatedProperties === 'object') {
@@ -4194,17 +4371,44 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4194
4371
  // function and were rebuilt on every call, which on a product schema with
4195
4372
  // six URLs and one anyOf cost about 450 ns before a single check ran, more
4196
4373
  // 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 +
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 +
4204
4394
  '\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)
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
+ }
4208
4412
  } else if (ctx.usesSafeRe) {
4209
4413
  // Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
4210
4414
  // Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
@@ -4219,6 +4423,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4219
4423
  fn = new Function('d', '_all', body)
4220
4424
  }
4221
4425
  fn._errSource = body
4426
+ fn._errFactory = factoryBody
4427
+ fn._errGuarded = needsGuardE
4222
4428
  fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
4223
4429
  fn._usesSafeRe = !!ctx.usesSafeRe
4224
4430
  if (cvars.length) {
@@ -4249,32 +4455,65 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
4249
4455
  const branchKw = keyword
4250
4456
  const branchSp = schemaPrefix + '/' + branchKw
4251
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.
4252
4463
  const fns = []
4253
4464
  const titles = []
4465
+ let usesSp = false
4254
4466
  for (let i = 0; i < branches.length; i++) {
4255
4467
  const sub = branches[i]
4256
4468
  const subLines = []
4257
4469
  const subSp = branchSp + '/' + i
4258
4470
  if (typeof sub === 'object' && sub !== null) {
4259
- genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
4471
+ genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
4260
4472
  } else if (sub === false) {
4261
4473
  // A false branch never matches; it reports the same error the
4262
4474
  // 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'})`)
4475
+ subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
4264
4476
  }
4265
4477
  const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
4266
4478
  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}}`
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
+ }
4270
4485
  fns.push(body)
4271
4486
  }
4487
+ const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
4272
4488
  const fnArr = `_brf${fi}`
4273
4489
  const resArr = `_brr${fi}`
4274
4490
  const titleArr = JSON.stringify(titles)
4275
4491
  const collapsed = `_brc${fi}`
4276
4492
  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}}}`)
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
+ }
4278
4517
  }
4279
4518
 
4280
4519
  // Error-collecting code generator.
@@ -4589,7 +4828,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4589
4828
  const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
4590
4829
  const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
4591
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}}}`
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}}`
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}}`
4593
4835
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4594
4836
  }
4595
4837
 
@@ -4745,7 +4987,8 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
4745
4987
  patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
4746
4988
  }
4747
4989
  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}}}`
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}}}`
4749
4992
  lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4750
4993
  }
4751
4994
 
@@ -4787,7 +5030,8 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4787
5030
  function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4788
5031
  const ki = ctx.varCounter++
4789
5032
  const known = Object.keys(schema.properties || {})
4790
- 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 = ''
4791
5035
  // A key matched by any patternProperties entry is not additional.
4792
5036
  const patTests = []
4793
5037
  for (const pat of Object.keys(schema.patternProperties || {})) {
@@ -5160,6 +5404,27 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5160
5404
  return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
5161
5405
  }
5162
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
+
5163
5428
  if (types) {
5164
5429
  const conds = types.map(t => {
5165
5430
  switch (t) {
@@ -5186,22 +5451,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5186
5451
  isStr = types[0] === 'string'
5187
5452
  isNum = types[0] === 'number' || types[0] === 'integer'
5188
5453
  }
5454
+ emitEnumConst()
5189
5455
  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
- }
5456
+ } else {
5457
+ emitEnumConst()
5205
5458
  }
5206
5459
 
5207
5460
  // required — use destructuring when type is object (SAFE because type check already passed)