ata-validator 1.57.0 → 1.58.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
@@ -53,8 +53,9 @@ interpreted engine, which gives the same answers, and a validator called a thous
53
53
  a second loads the generator in the background, through a dynamic import that Vite, webpack and
54
54
  Rollup put in a chunk of their own, and is rebuilt on generated code when the page is idle. A
55
55
  form, checked as the user types, never loads it. On a react-jsonschema-form page this takes the validator's
56
- share of the first load from 134.7 KB to about 57 KB gzipped; the generator chunk is about 78 KB
57
- more when it does load. Where `unsafe-eval` is blocked, on a page with a strict
56
+ share of the first load from 134.7 KB to about 55 KB gzipped; the generator chunk is about 78 KB
57
+ more when it does load, and the plan compiler, which speeds up a schema the interpreted engine checks
58
+ often, comes in a 6 KB chunk once the page is idle. Where `unsafe-eval` is blocked, on a page with a strict
58
59
  Content-Security-Policy or in a Manifest V3 extension, it is never fetched and every schema
59
60
  stays on the interpreted engine. To have generated code from the first call, import
60
61
  `ata-validator/codegen` once; it also makes `parse()` available in the browser, which otherwise
package/lib/aot-impl.js CHANGED
@@ -355,9 +355,20 @@ const ABORT_SHAPE = Object.freeze({ valid: false, errors: Object.freeze([Object.
355
355
  // default, or where the pass declines it (a schema object containing itself).
356
356
  function defaultsDecl(validator) {
357
357
  const { resolveSchemaForPreprocess } = require('./validator-core')._internals;
358
+ const schema = resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap);
358
359
  let src = null;
359
- try { src = require('./codegen-engine').preprocessSource(resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap), { useDefaults: true }); } catch {}
360
- return src === null ? '' : `function applyDefaults(d) {\n${src}\n}\n`;
360
+ try { src = require('./codegen-engine').preprocessSource(schema, { useDefaults: true }); } catch {}
361
+ if (src !== null) return `function applyDefaults(d) {\n${src}\n}\n`;
362
+ // The generated pass declines a recursive reference and keys matched by
363
+ // patterns; the runtime fills those with the closure pass, so the module
364
+ // carries it, as its own text (lib/defaults.js), with its schema less the
365
+ // descriptive keywords, `default` kept. Built on the first call.
366
+ const defaults = require('./defaults');
367
+ if (defaults.buildDefaultsApplier(schema) === null) return '';
368
+ const embedded = require('./schema-strip').stripAnnotationsKeepingDefaults(schema);
369
+ const text = JSON.stringify(JSON.stringify(embedded)).replace(/</g, '\\u003c');
370
+ return `const _ataDefaults = (function () {\nconst MAX_DEPTH = ${defaults.MAX_DEPTH};\n${require(AOT_ORDER_MODULE).take('defaults.js', defaults.EMBED)}\nreturn buildDefaultsApplier;\n})();\n` +
371
+ `let _ataFill;\nfunction applyDefaults(d) { (_ataFill || (_ataFill = _ataDefaults(JSON.parse(${text}))))(d); }\n`;
361
372
  }
362
373
 
363
374
  function toStandaloneModule(validator, opts) {
package/lib/aot-order.js CHANGED
@@ -112,4 +112,4 @@ function _ataAfter(a, b) {
112
112
  ${ORDER('rank', '_ataAfter(keys[j], k)')}`;
113
113
  }
114
114
 
115
- module.exports = { orderDecl, wantsOrdinals };
115
+ module.exports = { orderDecl, wantsOrdinals, take };
@@ -61,7 +61,39 @@ function re2Diverges(pattern) {
61
61
  return false;
62
62
  }
63
63
 
64
+ // The walker gives up past a depth of its own (100 schema frames, about 50
65
+ // levels of data under a recursive schema) and rejects the document there. A
66
+ // schema that refers back into itself, or that is nested that deep, can reach
67
+ // it with a document validate() accepts, so it is answered through validate(),
68
+ // which has no such limit.
69
+ const NATIVE_DEPTH = 40;
70
+
71
+ // Whether a `$ref` into this document leads back to a schema it is inside.
72
+ function refersBack(root) {
73
+ const { localRef } = require('./defaults.js');
74
+ const done = new Set();
75
+ const onPath = new Set();
76
+ const visit = (node) => {
77
+ if (node === null || typeof node !== 'object') return false;
78
+ if (onPath.has(node)) return true;
79
+ if (done.has(node)) return false;
80
+ onPath.add(node);
81
+ let back = false;
82
+ for (const key of Object.keys(node)) {
83
+ const v = node[key];
84
+ if (key === '$ref' && typeof v === 'string' && v.startsWith('#')) back = visit(localRef(root, v));
85
+ else if (key !== 'enum' && key !== 'const' && key !== 'default' && key !== 'examples' && v !== null && typeof v === 'object') back = visit(v);
86
+ if (back) break;
87
+ }
88
+ onPath.delete(node);
89
+ done.add(node);
90
+ return back;
91
+ };
92
+ return visit(root);
93
+ }
94
+
64
95
  function walk(schema, depth) {
96
+ if (depth > NATIVE_DEPTH) return true;
65
97
  if (schema === true || schema === false) {
66
98
  // A boolean root is answered wrong by the walker; nested booleans are
67
99
  // handled where they appear (items and prefixItems below).
@@ -110,6 +142,7 @@ function walk(schema, depth) {
110
142
  function bufferNeedsSlowPath(schema, schemaMap, keywords) {
111
143
  if (keywords && schemaUsesKeywords(schema, keywords)) return true;
112
144
  if (walk(schema, 0)) return true;
145
+ if (refersBack(schema)) return true;
113
146
  if (schemaMap && schemaMap.size > 0) {
114
147
  for (const s of schemaMap.values()) if (walk(s, 1)) return true;
115
148
  }
package/lib/clone-emit.js CHANGED
@@ -50,12 +50,13 @@ const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment',
50
50
  const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
51
51
  function inlineRefsForClone(root) {
52
52
  const state = { budget: 512 };
53
- // `inRef` marks content brought in from behind a reference. The runtime's
54
- // useDefaults fills a default written next to the $ref, and does not fill
55
- // defaults written inside the referenced definition, so the inlined copy
56
- // keeps the first and drops the second. parse() must stay exactly as
57
- // generous as validate(); a parse that fills more is a disagreement, not a
58
- // feature.
53
+ // The runtime's useDefaults fills a property's default from the schema
54
+ // written under the property, the $ref node with any default next to it,
55
+ // and then the defaults inside the referenced definition, as it fills them
56
+ // inline. The definition's own top-level default is not the property's, so
57
+ // the inlined copy drops that one and keeps everything below it. parse()
58
+ // must stay exactly as generous as validate(); a parse that fills more, or
59
+ // less, is a disagreement.
59
60
  const walk = (node, active, inRef) => {
60
61
  if (!node || typeof node !== 'object') return node;
61
62
  if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
@@ -73,14 +74,13 @@ function inlineRefsForClone(root) {
73
74
  cur = target;
74
75
  entered = true;
75
76
  }
76
- const strip = inRef || entered;
77
77
  const out = {};
78
78
  for (const k of Object.keys(cur)) {
79
- if (strip && k === 'default') continue;
79
+ if (entered && k === 'default') continue;
80
80
  const v = cur[k];
81
81
  // A property named "__proto__" is an own key of the copy: assigned, it
82
82
  // set the copy's prototype and the property was gone from parse().
83
- const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, strip);
83
+ const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, false);
84
84
  if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
85
85
  else out[k] = w;
86
86
  }
@@ -197,7 +197,7 @@ function emitClone(node, access, depth) {
197
197
  if (apSchema) {
198
198
  const kVar = `_k${depth}`;
199
199
  const vVar = `_v${depth}`;
200
- const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
200
+ const valueExpr = emitValueExpr(apSchema, vVar, depth);
201
201
  if (valueExpr === null) return null;
202
202
  let skip = '';
203
203
  let declSet = '';
@@ -255,20 +255,6 @@ function emitClone(node, access, depth) {
255
255
  // carry keys this code cannot see, and copying the reference would smuggle
256
256
  // them into a value that claims to be sanitised, so the caller declines the
257
257
  // whole clone instead.
258
- // Drop every schema-position `default` in a subtree, leaving data positions
259
- // (const, enum, examples) untouched. Used on a record's value schema, where
260
- // the runtime's useDefaults fills nothing.
261
- function stripDefaultsDeep(node) {
262
- if (!node || typeof node !== 'object') return node;
263
- if (Array.isArray(node)) return node.map(stripDefaultsDeep);
264
- const out = {};
265
- for (const k of Object.keys(node)) {
266
- if (k === 'default') continue;
267
- out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
268
- }
269
- return out;
270
- }
271
-
272
258
  const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
273
259
  // Whether every value this schema accepts is a primitive, so a copy has
274
260
  // nothing inside it to strip and takes the value as it is. A primitive type
@@ -329,4 +315,4 @@ function cloneExprFor(schema) {
329
315
  return emitClone(inlineRefsForClone(schema), 'data', 0);
330
316
  }
331
317
 
332
- module.exports = { cloneExprFor, emitClone, inlineRefsForClone, stripDefaultsDeep, resolveLocalPointer };
318
+ module.exports = { cloneExprFor, emitClone, inlineRefsForClone, resolveLocalPointer };
@@ -236,6 +236,10 @@ function installCodegenPaths (ctx) {
236
236
  // The boolean engine is the verdict authority for these paths; the
237
237
  // final lazy wrapper uses it to skip error construction entirely.
238
238
  if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
239
+ // With a preprocess pass the verdict runs it first, for the lazy layer in
240
+ // lib/validator-core.js, which then builds the errors only when they are
241
+ // read, from the document as the pass left it.
242
+ if ((!hasDynRef || _isCodegen) && preprocess && !fusedRemove) { ctx.preVerdict = (data) => { preprocess(data); return jsFn(data) }; ctx.rawVerdict = jsFn; }
239
243
 
240
244
  if (options.abortEarly && jsFn && !hasDynRef) {
241
245
  // abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
@@ -319,7 +323,7 @@ function installCodegenPaths (ctx) {
319
323
  // deciding again: the lazy layer in lib/validator-core.js has its verdict
320
324
  // and used to call validate() to get the errors, which ran the verdict a
321
325
  // second time and built a second rejection around it.
322
- if (!preprocess) ctx.rejectBase = onReject;
326
+ ctx.rejectBase = onReject;
323
327
  // The rich combined function, for a validator whose errors are being read
324
328
  // with richErrors on. See _ensureCompiled.
325
329
  if (!preprocess) {
@@ -360,7 +364,7 @@ function installCodegenPaths (ctx) {
360
364
  this.validate = preprocess
361
365
  ? (data) => { preprocess(data); return safeCombinedFn(data); }
362
366
  : safeCombinedFn;
363
- if (!preprocess) ctx.rejectBase = (data) => _mustReject(safeCombinedFn(data));
367
+ ctx.rejectBase = (data) => _mustReject(safeCombinedFn(data));
364
368
  } else {
365
369
  this.validate = preprocess
366
370
  ? (data) => {
@@ -368,7 +372,7 @@ function installCodegenPaths (ctx) {
368
372
  return jsFn(data) ? VALID_RESULT : errOnly(data);
369
373
  }
370
374
  : (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
371
- if (!preprocess) ctx.rejectBase = errOnly;
375
+ ctx.rejectBase = errOnly;
372
376
  }
373
377
  }
374
378
  // Verbose mode: populate parentSchema, schema and data on each error, the
@@ -803,18 +807,88 @@ function emitCoercions(node, ov, lines, st, depth) {
803
807
  }
804
808
  st.seen.delete(node);
805
809
  }
806
- function hasDefaultsInside(node, seen) {
807
- if (!node || typeof node !== 'object' || !node.properties || seen.has(node)) return false;
808
- seen.add(node);
809
- let found = false;
810
- for (const prop of Object.values(node.properties)) {
811
- if (prop && typeof prop === 'object' && (prop.default !== undefined || hasDefaultsInside(prop, seen))) { found = true; break; }
810
+ // From the closure pass, on the first reference a defaults walk meets: loaded
811
+ // with this module it joined every process's cold start.
812
+ let _localRef = null;
813
+ const localRef = (root, ref) => (_localRef || (_localRef = require('./defaults').localRef))(root, ref);
814
+ // Whether a default is reachable from a schema through the places the pass
815
+ // goes (lib/defaults.js reachesDefault, which this pass is held to): the
816
+ // same walk, with answers kept per schema object for the whole pass, except
817
+ // one that rests on a schema still being decided (a recursive reference). It
818
+ // started over for every property, quadratic in the depth of the schema.
819
+ function reachState(root) { return { root, memo: new Map() }; }
820
+ function hasDefaultsInside(node, rs) { return reachDefault(node, rs, new Set()) === 1; }
821
+ function reachDefault(node, rs, stack) {
822
+ if (!node || typeof node !== 'object') return 0;
823
+ const known = rs.memo.get(node);
824
+ if (known !== undefined) return known;
825
+ if (stack.has(node)) return 2;
826
+ stack.add(node);
827
+ let open = false;
828
+ const check = (n) => { const r = reachDefault(n, rs, stack); if (r === 2) open = true; return r === 1; };
829
+ let yes = false;
830
+ if (node.properties && typeof node.properties === 'object') {
831
+ for (const prop of Object.values(node.properties)) {
832
+ if (prop && typeof prop === 'object' && (prop.default !== undefined || check(prop))) { yes = true; break; }
833
+ }
812
834
  }
813
- seen.delete(node);
814
- return found;
835
+ if (!yes && typeof node.$ref === 'string') yes = check(localRef(rs.root, node.$ref));
836
+ if (!yes) for (const list of [node.allOf, node.prefixItems]) { if (Array.isArray(list) && list.some(check)) { yes = true; break; } }
837
+ if (!yes) for (const one of [node.items, node.additionalProperties]) { if (check(one)) { yes = true; break; } }
838
+ if (!yes) for (const map of [node.patternProperties, node.dependentSchemas]) { if (map && typeof map === 'object' && Object.keys(map).some((k) => check(map[k]))) { yes = true; break; } }
839
+ stack.delete(node);
840
+ if (yes) { rs.memo.set(node, 1); return 1; }
841
+ if (!open) { rs.memo.set(node, 0); return 0; }
842
+ return 2;
843
+ }
844
+ // Which keys `additionalProperties` covers depends on the patterns of
845
+ // `patternProperties`, which this pass does not test (a RegExp built on
846
+ // every call would cost more than the pass): a schema whose defaults depend
847
+ // on one is left to the closure pass.
848
+ function defaultsNeedPatterns(node, rs) {
849
+ const pp = node.patternProperties;
850
+ if (!pp || typeof pp !== 'object' || Object.keys(pp).length === 0) return false;
851
+ for (const k of Object.keys(pp)) if (hasDefaultsInside(pp[k], rs)) return true;
852
+ return hasDefaultsInside(node.additionalProperties, rs);
853
+ }
854
+ // The defaults of `schema` filled into the value of `expr`, when it holds an
855
+ // object (an array included), as the closure pass fills them.
856
+ function emitDefaultsAt(schema, expr, lines, st) {
857
+ const n = '_d' + st.n++;
858
+ lines.push(`{const ${n}=${expr};if(typeof ${n}==='object'&&${n}!==null){`);
859
+ emitDefaults(schema, n, lines, st);
860
+ lines.push('}}');
861
+ }
862
+ // A pass that would expand to more than this many lines (references written
863
+ // out again at every use) is left to the closure pass.
864
+ const DEFAULTS_MAX_LINES = 4000;
865
+ // How many schema objects emitDefaults would write out, references expanded
866
+ // at every use, or Infinity where it would decline (a cycle, or defaults
867
+ // under patternProperties). Each writes at least one line, so a count over
868
+ // DEFAULTS_MAX_LINES declines where emitDefaults would, before writing
869
+ // anything: SARIF's schema wrote 4000 lines in 2.7 ms and threw them away.
870
+ function defaultsVisits(node, st, stack) {
871
+ const known = st.visits.get(node);
872
+ if (known !== undefined) return known;
873
+ if (stack.has(node)) return Infinity;
874
+ stack.add(node);
875
+ let n = 1;
876
+ const add = (x) => { if (n <= DEFAULTS_MAX_LINES && hasDefaultsInside(x, st.reach)) n += defaultsVisits(x, st, stack); };
877
+ if (defaultsNeedPatterns(node, st.reach)) n = Infinity;
878
+ for (const prop of Object.values(node.properties || {})) if (prop && typeof prop === 'object') add(prop);
879
+ if (Array.isArray(node.prefixItems)) node.prefixItems.forEach(add);
880
+ add(node.items);
881
+ add(node.additionalProperties);
882
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') Object.values(node.dependentSchemas).forEach(add);
883
+ if (typeof node.$ref === 'string') add(localRef(st.root, node.$ref));
884
+ if (Array.isArray(node.allOf)) node.allOf.forEach(add);
885
+ stack.delete(node);
886
+ if (n > DEFAULTS_MAX_LINES) n = Infinity;
887
+ st.visits.set(node, n);
888
+ return n;
815
889
  }
816
890
  function emitDefaults(node, ov, lines, st) {
817
- if (st.seen.has(node)) { st.cycle = true; return; }
891
+ if (st.seen.has(node) || lines.length > DEFAULTS_MAX_LINES) { st.cycle = true; return; }
818
892
  st.seen.add(node);
819
893
  // A default is written only where the key is not an own property, and the
820
894
  // own-property test used to be Object.hasOwn for every default on every
@@ -846,7 +920,7 @@ function emitDefaults(node, ov, lines, st) {
846
920
  // closure pass walks it. Object.prototype's own properties are functions
847
921
  // apart from the __proto__ accessor, so on a plain object a read that
848
922
  // gives an object is an own property for every other key.
849
- if (prop.properties && hasDefaultsInside(prop, new Set())) {
923
+ if (hasDefaultsInside(prop, st.reach)) {
850
924
  const n = '_d' + st.n++;
851
925
  const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
852
926
  lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
@@ -854,6 +928,56 @@ function emitDefaults(node, ov, lines, st) {
854
928
  lines.push('}}');
855
929
  }
856
930
  }
931
+ if (defaultsNeedPatterns(node, st.reach)) { st.cycle = true; st.seen.delete(node); return; }
932
+ // Elements of an array: `prefixItems` by position, `items` after them.
933
+ const has = (x) => hasDefaultsInside(x, st.reach);
934
+ const tuple = Array.isArray(node.prefixItems) ? node.prefixItems : [];
935
+ const tupleHas = tuple.map(has);
936
+ const itemsHas = has(node.items);
937
+ if (tupleHas.some(Boolean) || itemsHas) {
938
+ lines.push(`if(Array.isArray(${ov})){`);
939
+ for (let i = 0; i < tuple.length; i++) {
940
+ if (!tupleHas[i]) continue;
941
+ lines.push(`if(${ov}.length>${i})`);
942
+ emitDefaultsAt(tuple[i], `${ov}[${i}]`, lines, st);
943
+ }
944
+ if (itemsHas) {
945
+ const i = '_i' + st.n++;
946
+ lines.push(`for(let ${i}=${tuple.length};${i}<${ov}.length;${i}++)`);
947
+ emitDefaultsAt(node.items, `${ov}[${i}]`, lines, st);
948
+ }
949
+ lines.push('}');
950
+ }
951
+ // Keys of an object that `properties` does not name (no patterns here,
952
+ // see defaultsNeedPatterns).
953
+ if (has(node.additionalProperties)) {
954
+ const k = '_k' + st.n++;
955
+ const declared = Object.keys(node.properties || {});
956
+ lines.push(`if(!Array.isArray(${ov}))for(const ${k} of Object.keys(${ov})){`);
957
+ if (declared.length) lines.push(`if(${declared.map((d) => `${k}===${JSON.stringify(d)}`).join('||')})continue;`);
958
+ emitDefaultsAt(node.additionalProperties, `${ov}[${k}]`, lines, st);
959
+ lines.push('}');
960
+ }
961
+ // A schema for the same object, for each key present.
962
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
963
+ for (const key of Object.keys(node.dependentSchemas)) {
964
+ const dep = node.dependentSchemas[key];
965
+ if (!has(dep)) continue;
966
+ lines.push(`if(!Array.isArray(${ov})&&Object.hasOwn(${ov},${JSON.stringify(key)})){`);
967
+ emitDefaults(dep, ov, lines, st);
968
+ lines.push('}');
969
+ }
970
+ }
971
+ // The schemas applied to the same value: a `$ref` into the same document,
972
+ // written out here (one back into what is being written is a cycle, left
973
+ // to the closure pass), then each `allOf` member, in the closure pass's order.
974
+ if (typeof node.$ref === 'string') {
975
+ const target = localRef(st.root, node.$ref);
976
+ if (target && hasDefaultsInside(target, st.reach)) emitDefaults(target, ov, lines, st);
977
+ }
978
+ if (Array.isArray(node.allOf)) {
979
+ for (const sub of node.allOf) if (hasDefaultsInside(sub, st.reach)) emitDefaults(sub, ov, lines, st);
980
+ }
857
981
  st.seen.delete(node);
858
982
  }
859
983
 
@@ -861,7 +985,11 @@ function emitDefaults(node, ov, lines, st) {
861
985
  // the closure passes must take the schema. A module written ahead of time
862
986
  // embeds it as applyDefaults (lib/aot-impl.js), so it runs the runtime's code.
863
987
  function preprocessSource(schema, options) {
864
- if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
988
+ if (typeof schema !== 'object' || schema === null) return null;
989
+ // Defaults reach through `$ref` and `allOf` from the root too; the other
990
+ // passes start at `properties`.
991
+ const reach = reachState(schema);
992
+ if (!schema.properties && !(options.useDefaults !== false && hasDefaultsInside(schema, reach))) return null;
865
993
  const lines = [];
866
994
 
867
995
  // removeAdditional: strip unknown keys at every level the schema describes,
@@ -880,11 +1008,14 @@ function preprocessSource(schema, options) {
880
1008
  // used to stop at the top-level properties. A schema object that contains
881
1009
  // itself declines here, and the closure passes, which carry a guard for it,
882
1010
  // take the schema.
883
- const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map() };
1011
+ const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map(), root: schema, reach, visits: new Map() };
884
1012
  if (options.coerceTypes) emitCoercions(schema, 'd', lines, st, 0);
885
1013
  // hasDefaultsInside answers without emitting; most schemas have no default,
886
1014
  // and the emitting walk allocates for every property it visits.
887
- if (options.useDefaults !== false && hasDefaultsInside(schema, new Set())) emitDefaults(schema, 'd', lines, st);
1015
+ if (options.useDefaults !== false && hasDefaultsInside(schema, reach)) {
1016
+ if (defaultsVisits(schema, st, new Set()) > DEFAULTS_MAX_LINES) return null;
1017
+ emitDefaults(schema, 'd', lines, st);
1018
+ }
888
1019
  if (st.cycle) return null;
889
1020
 
890
1021
  if (lines.length === 0) return null;
package/lib/defaults.js CHANGED
@@ -2,19 +2,32 @@
2
2
 
3
3
  // The closure pass that fills `default` values, shared by the validator core
4
4
  // and by the wrapper around a compiled module, so both fill defaults the same
5
- // way. The generated pass in index.js is held to this one by
5
+ // way. The generated pass in lib/codegen-engine.js is held to this one by
6
6
  // tests/test_nested_defaults_engines.js.
7
+ //
8
+ // A default is filled in every schema applied to the value at that place, as
9
+ // ajv's `useDefaults` fills one: the schemas under `properties`, through a
10
+ // `$ref` into the same document (recursive ones included), each `allOf`
11
+ // member, `items` for every element and `prefixItems` by position,
12
+ // `patternProperties` for the keys they match, `additionalProperties` for the
13
+ // rest, and `dependentSchemas` for the keys present. The pass stopped at
14
+ // `properties`, so a default behind a reference or in an array element was
15
+ // not filled, and a `required` property with one was rejected where ajv
16
+ // accepts it. `anyOf`, `oneOf` and `not` fill nothing, as with ajv;
17
+ // `if`/`then`/`else` fill nothing here, since which branch applies is only
18
+ // known once the value has been checked.
7
19
 
8
- // Extract default values from a schema tree. Returns a function that applies
9
- // defaults to an object in-place (mutates), or null if no defaults exist.
20
+ // Below a value nested this deep the pass stops: data that contains itself,
21
+ // under a recursive schema, would otherwise never end.
22
+ const MAX_DEPTH = 512;
23
+
24
+ // Build the pass for a schema: a function that fills defaults into a value in
25
+ // place (mutates), or null when no default is reachable from the schema.
10
26
  function buildDefaultsApplier(schema) {
11
27
  if (typeof schema !== "object" || schema === null) return null;
12
- const actions = [];
13
- collectDefaults(schema, actions);
14
- if (actions.length === 0) return null;
15
- return (data) => {
16
- for (let i = 0; i < actions.length; i++) actions[i](data);
17
- };
28
+ const path = [];
29
+ const fn = compileNode(schema, schema, new Map(), new Map(), path);
30
+ return fn === null ? null : (data) => { path.length = 0; fn(data, 0, null); };
18
31
  }
19
32
 
20
33
  // Write an own property. Plain assignment of a key named `__proto__` does
@@ -34,55 +47,199 @@ function setOwn(obj, key, val) {
34
47
  }
35
48
  }
36
49
 
37
- function collectDefaults(schema, actions, path) {
38
- if (typeof schema !== "object" || schema === null) return;
39
- const props = schema.properties;
40
- if (!props) return;
41
- for (const [key, prop] of Object.entries(props)) {
42
- if (prop && typeof prop === "object" && prop.default !== undefined) {
43
- const defaultVal = prop.default;
44
- if (!path) {
45
- actions.push((data) => {
46
- if (typeof data === "object" && data !== null && !Object.hasOwn(data, key)) {
47
- setOwn(data,
48
- key,
49
- typeof defaultVal === "object" && defaultVal !== null
50
- ? JSON.parse(JSON.stringify(defaultVal))
51
- : defaultVal);
52
- }
53
- });
54
- } else {
55
- const parentPath = path;
56
- actions.push((data) => {
57
- let target = data;
58
- for (let j = 0; j < parentPath.length; j++) {
59
- if (typeof target !== "object" || target === null) return;
60
- // Own keys only. `target[key]` for an inherited name walks the
61
- // prototype chain: a parent named `__proto__` that the instance
62
- // does not carry resolved to Object.prototype, and the child
63
- // defaults were written onto it, for every object in the realm.
64
- if (!Object.hasOwn(target, parentPath[j])) return;
65
- target = target[parentPath[j]];
66
- }
67
- if (
68
- typeof target === "object" &&
69
- target !== null &&
70
- !Object.hasOwn(target, key)
71
- ) {
72
- setOwn(target,
73
- key,
74
- typeof defaultVal === "object" && defaultVal !== null
75
- ? JSON.parse(JSON.stringify(defaultVal))
76
- : defaultVal);
77
- }
78
- });
79
- }
50
+ // The schema a `$ref` names in the same document: `#`, or a JSON pointer
51
+ // after it. Null for anything else (another document, an anchor), which the
52
+ // pass leaves alone. Normalization renames draft-07 `definitions` to `$defs`
53
+ // and leaves the pointers as written, so either name reads as the other.
54
+ function localRef(root, ref) {
55
+ if (typeof ref !== "string" || ref.charCodeAt(0) !== 35) return null;
56
+ if (ref === "#") return root;
57
+ if (ref.charCodeAt(1) !== 47) return null;
58
+ let t = root;
59
+ for (let seg of ref.slice(2).split("/")) {
60
+ if (seg.indexOf("%") >= 0) { try { seg = decodeURIComponent(seg); } catch { return null; } }
61
+ if (seg.indexOf("~") >= 0) seg = seg.replace(/~1/g, "/").replace(/~0/g, "~");
62
+ if (t === null || typeof t !== "object") return null;
63
+ if (!Object.hasOwn(t, seg)) {
64
+ const alias = seg === "definitions" ? "$defs" : seg === "$defs" ? "definitions" : null;
65
+ if (alias === null || !Object.hasOwn(t, alias)) return null;
66
+ seg = alias;
67
+ }
68
+ t = t[seg];
69
+ }
70
+ return t !== null && typeof t === "object" ? t : null;
71
+ }
72
+
73
+ // Whether a default is reachable from a schema, through the same places the
74
+ // pass goes: 1 yes, 0 no, 2 not known yet because the walk came back to a
75
+ // schema it is still deciding (a recursive reference). Answers are kept per
76
+ // schema object in `memo`, except one that rests on an undecided schema,
77
+ // which is worked out again when asked; the walk used to start over for every
78
+ // schema object it compiled, quadratic in the depth of the schema.
79
+ function reachesDefault(node, root, memo, stack) {
80
+ if (node === null || typeof node !== "object") return 0;
81
+ const known = memo.get(node);
82
+ if (known !== undefined) return known;
83
+ if (stack.has(node)) return 2;
84
+ stack.add(node);
85
+ const props = valuesOf(node.properties);
86
+ let yes = props.some((p) => p && typeof p === "object" && p.default !== undefined);
87
+ let open = false;
88
+ const below = props.concat(localRef(root, node.$ref), listOf(node.allOf), listOf(node.prefixItems), node.items, node.additionalProperties, valuesOf(node.patternProperties), valuesOf(node.dependentSchemas));
89
+ for (let i = 0; !yes && i < below.length; i++) {
90
+ const r = reachesDefault(below[i], root, memo, stack);
91
+ if (r === 1) yes = true;
92
+ else if (r === 2) open = true;
93
+ }
94
+ stack.delete(node);
95
+ if (yes || !open) memo.set(node, yes ? 1 : 0);
96
+ return yes ? 1 : open ? 2 : 0;
97
+ }
98
+ function valuesOf(m) {
99
+ return m && typeof m === "object" && !Array.isArray(m) ? Object.values(m) : [];
100
+ }
101
+ function listOf(a) {
102
+ return Array.isArray(a) ? a : [];
103
+ }
104
+
105
+ // A pattern of `patternProperties` as the validator reads one.
106
+ function keyPattern(src) {
107
+ try { return new RegExp(src, "u"); } catch { try { return new RegExp(src); } catch { return null; } }
108
+ }
109
+
110
+ // A fresh copy of a default for each value it fills. `[]`, the most common
111
+ // object default (2916 of the 4130 SARIF's schema fills into one log), skips
112
+ // the JSON round trip.
113
+ function copyOf(v) {
114
+ return typeof v !== "object" || v === null ? v : Array.isArray(v) && v.length === 0 ? [] : JSON.parse(JSON.stringify(v));
115
+ }
116
+
117
+ // The pass for one schema object, built once: its own defaults, then the
118
+ // schemas below its properties, then its `$ref` and its `allOf` members, each
119
+ // applied to the same value. A schema reached again (a recursive reference)
120
+ // takes the function being built for it, so a cycle in the schema is a loop
121
+ // in the calls, bounded by the depth of the data.
122
+ function compileNode(node, root, memo, reach, path) {
123
+ if (node === null || typeof node !== "object") return null;
124
+ if (memo.has(node)) return memo.get(node);
125
+ if (reachesDefault(node, root, reach, new Set()) !== 1) { memo.set(node, null); return null; }
126
+ let body = null;
127
+ // `path`: the schemas applied to the enclosing values, outermost first.
128
+ // `made`: inside a value the pass itself made from a default, the same as a
129
+ // set, and null elsewhere. There a schema already applied to an enclosing
130
+ // value is not applied again: one that refers back to an enclosing schema,
131
+ // with an object default, would otherwise make a value, fill it, make the
132
+ // next one inside it, and never stop. ajv throws a RangeError on such a
133
+ // schema, and Renovate's took this pass to the heap limit.
134
+ const fn = (data, depth, made) => {
135
+ if (depth > MAX_DEPTH) return;
136
+ if (made !== null) {
137
+ if (made.has(node)) return;
138
+ made.add(node);
139
+ body(data, depth, made);
140
+ made.delete(node);
141
+ return;
80
142
  }
81
- // Recurse into nested object schemas
82
- if (prop && typeof prop === "object" && prop.properties) {
83
- collectDefaults(prop, actions, (path || []).concat(key));
143
+ path.push(node);
144
+ body(data, depth, null);
145
+ path.pop();
146
+ };
147
+ memo.set(node, fn);
148
+
149
+ const own = [];
150
+ const below = [];
151
+ const props = node.properties;
152
+ if (props && typeof props === "object") {
153
+ for (const key of Object.keys(props)) {
154
+ const p = props[key];
155
+ if (!p || typeof p !== "object") continue;
156
+ if (p.default !== undefined) own.push([key, p.default]);
157
+ const child = compileNode(p, root, memo, reach, path);
158
+ if (child !== null) below.push([key, child]);
159
+ }
160
+ }
161
+ // Arrays: every element under `items`, the elements `prefixItems` names by
162
+ // position (then `items` covers only the ones after them).
163
+ const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s, root, memo, reach, path)) : [];
164
+ const each = compileNode(node.items, root, memo, reach, path);
165
+ // Objects: keys a pattern matches, and the keys neither `properties` nor a
166
+ // pattern names; and a schema for each key present.
167
+ const patterns = [];
168
+ if (node.patternProperties && typeof node.patternProperties === "object") {
169
+ for (const src of Object.keys(node.patternProperties)) {
170
+ const re = keyPattern(src);
171
+ const t = compileNode(node.patternProperties[src], root, memo, reach, path);
172
+ if (re === null) continue;
173
+ patterns.push([re, t]);
174
+ }
175
+ }
176
+ const extra = compileNode(node.additionalProperties, root, memo, reach, path);
177
+ const declared = props && typeof props === "object" ? props : null;
178
+ const dependent = [];
179
+ if (node.dependentSchemas && typeof node.dependentSchemas === "object") {
180
+ for (const key of Object.keys(node.dependentSchemas)) {
181
+ const t = compileNode(node.dependentSchemas[key], root, memo, reach, path);
182
+ if (t !== null) dependent.push([key, t]);
183
+ }
184
+ }
185
+ const keyed = extra !== null || patterns.some((p) => p[1] !== null);
186
+
187
+ const also = [];
188
+ if (typeof node.$ref === "string") {
189
+ const t = compileNode(localRef(root, node.$ref), root, memo, reach, path);
190
+ if (t !== null) also.push(t);
191
+ }
192
+ if (Array.isArray(node.allOf)) {
193
+ for (const s of node.allOf) {
194
+ const t = compileNode(s, root, memo, reach, path);
195
+ if (t !== null) also.push(t);
84
196
  }
85
197
  }
198
+
199
+ body = (data, depth, made) => {
200
+ let fresh = null;
201
+ if (typeof data === "object" && data !== null) {
202
+ for (let i = 0; i < own.length; i++) {
203
+ const key = own[i][0];
204
+ if (!Object.hasOwn(data, key)) {
205
+ const v = copyOf(own[i][1]);
206
+ setOwn(data, key, v);
207
+ if (made === null && typeof v === "object" && v !== null) (fresh || (fresh = [])).push(key);
208
+ }
209
+ }
210
+ for (let i = 0; i < below.length; i++) {
211
+ // Own keys only. `data[key]` for an inherited name walks the
212
+ // prototype chain: a property named `__proto__` that the value does
213
+ // not carry resolved to Object.prototype, and the defaults below it
214
+ // were written onto it, for every object in the realm.
215
+ const key = below[i][0];
216
+ if (Object.hasOwn(data, key)) below[i][1](data[key], depth + 1, fresh !== null && fresh.includes(key) ? new Set(path) : made);
217
+ }
218
+ if (Array.isArray(data)) {
219
+ for (let i = 0; i < data.length; i++) {
220
+ const t = i < tuple.length ? tuple[i] : each;
221
+ if (t !== null && t !== undefined) t(data[i], depth + 1, made);
222
+ }
223
+ } else if (keyed) {
224
+ for (const key of Object.keys(data)) {
225
+ let matched = false;
226
+ for (let i = 0; i < patterns.length; i++) {
227
+ if (patterns[i][0].test(key)) { matched = true; if (patterns[i][1] !== null) patterns[i][1](data[key], depth + 1, made); }
228
+ }
229
+ if (extra !== null && !matched && !(declared !== null && Object.hasOwn(declared, key))) extra(data[key], depth + 1, made);
230
+ }
231
+ }
232
+ if (!Array.isArray(data)) {
233
+ for (let i = 0; i < dependent.length; i++) if (Object.hasOwn(data, dependent[i][0])) dependent[i][1](data, depth + 1, made);
234
+ }
235
+ }
236
+ for (let i = 0; i < also.length; i++) also[i](data, depth + 1, made);
237
+ };
238
+ return fn;
86
239
  }
87
240
 
88
- module.exports = { buildDefaultsApplier, setOwn };
241
+ // The functions a module written ahead of time embeds as its applyDefaults
242
+ // where the generated pass declines a schema (lib/aot-impl.js).
243
+ const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
244
+
245
+ module.exports = { buildDefaultsApplier, setOwn, localRef, MAX_DEPTH, EMBED };
@@ -136,6 +136,7 @@ const NATIVE_KEYWORDS = [
136
136
  'not', // 25 not_failed
137
137
  '$ref', // 26 ref_not_found
138
138
  'if', // 27 if_then_else_failed
139
+ '__depth__', // 28 depth_exceeded
139
140
  ]
140
141
 
141
142
  // Keyword and public code for a native ordinal, or null when the ordinal is
@@ -147,6 +148,11 @@ function fromNative (ordinal, format) {
147
148
  // codeFor('format') answers with the first format code, which names a
148
149
  // specific format. Without knowing which format failed, the generic one is
149
150
  // the honest answer.
151
+ // The walker stopped at its depth limit and rejected the document. No
152
+ // public code names that: the APIs that ask the walker route schemas deep or
153
+ // recursive enough to reach it to validate() first (lib/buffer-gate.js), so
154
+ // it is the generic failure.
155
+ if (keyword === '__depth__') return { keyword, code: 'ATA9000' }
150
156
  const code = keyword === 'format'
151
157
  ? (format ? codeFor('format', format) || 'ATA3099' : 'ATA3099')
152
158
  : codeFor(keyword) || 'ATA9001'
@@ -304,4 +304,4 @@ function durationSource (v, isStr) {
304
304
  const inner = `if(!${_formats().DURATION_RE.toString()}.test(${v}))return false`;
305
305
  return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
306
306
  }
307
- module.exports = { uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
307
+ module.exports = { uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
@@ -903,7 +903,10 @@ class Interpreter {
903
903
  _fastRoot() {
904
904
  if (this._fast === undefined) {
905
905
  const compile = planCompiler();
906
- if (compile === null) this._fast = null;
906
+ // Still on its way (the browser build loads it after the page's first
907
+ // checks, lib/interpreter.browser.js): the plain walk answers, and the
908
+ // next call asks again.
909
+ if (compile === null) { if (planPending) return null; this._fast = null; }
907
910
  else {
908
911
  try { this._fast = compile(this); } catch (e) {
909
912
  // The plain walk answers instead, which tests cannot tell apart;
@@ -1530,9 +1533,12 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
1530
1533
  const TIER = 1000;
1531
1534
  let loadPlanCompiler = null;
1532
1535
  let compileInterpreter = null;
1533
- function setPlanCompiler(load) {
1536
+ // True while a plan compiler is being loaded and none is set yet.
1537
+ let planPending = false;
1538
+ function setPlanCompiler(load, pending) {
1534
1539
  loadPlanCompiler = load;
1535
1540
  compileInterpreter = null;
1541
+ planPending = load === null && pending === true;
1536
1542
  }
1537
1543
  function planCompiler() {
1538
1544
  if (compileInterpreter === null && loadPlanCompiler !== null) {
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ // The interpreted engine for the browser build (the package.json browser
4
+ // field maps lib/interpreter.js here). The plan compiler, the closure tree
5
+ // that makes the engine fast on a schema checked many times, is not in the
6
+ // page's first load: it comes through the dynamic import below, which
7
+ // bundlers split into a chunk of its own, once the page is idle after its
8
+ // first validator. Until then the plain walk answers, as it does for every
9
+ // schema's first thousand calls (TIER in lib/interpreter-core.js), with the
10
+ // same answers. Without a bundler that splits, the chunk is inlined and
11
+ // nothing changes.
12
+ const core = require('./interpreter-core');
13
+
14
+ core._setPlanCompiler(null, true);
15
+ let asked = false;
16
+ const idle = (f) => {
17
+ if (typeof requestIdleCallback === 'function') return requestIdleCallback(f, { timeout: 2000 });
18
+ const t = setTimeout(f, 200);
19
+ if (t && typeof t.unref === 'function') t.unref();
20
+ };
21
+ function load() {
22
+ import('./plan-compiler.js').then((m) => {
23
+ const mod = m && m.install ? m : m.default;
24
+ core._setPlanCompiler(() => mod.install);
25
+ }, () => core._setPlanCompiler(null));
26
+ }
27
+
28
+ // The first engine built asks for it, when the page is next idle.
29
+ const create = core.createInterpreter;
30
+ core.createInterpreter = (schema, opts) => {
31
+ if (!asked) { asked = true; idle(load); }
32
+ return create(schema, opts);
33
+ };
34
+ module.exports = core;
@@ -63,24 +63,24 @@ function refsIntoDropped(node, seen) {
63
63
 
64
64
  // keepTitle: the node is a branch of anyOf or oneOf, whose title names the
65
65
  // branch in the collapsed error (params.closestName).
66
- function stripSchema(node, keepTitle) {
66
+ function stripSchema(node, keepTitle, keepDefault) {
67
67
  if (node === null || typeof node !== 'object' || Array.isArray(node)) return node;
68
68
  const out = {};
69
69
  for (const k of Object.keys(node)) {
70
- if (dropped(k) && !(keepTitle === true && k === 'title')) continue;
70
+ if (dropped(k) && !(keepTitle === true && k === 'title') && !(keepDefault === true && k === 'default')) continue;
71
71
  const v = node[k];
72
72
  if (SINGLE.has(k)) {
73
- setOwn(out, k, Array.isArray(v) ? v.map((x) => stripSchema(x, false)) : stripSchema(v, false));
73
+ setOwn(out, k, Array.isArray(v) ? v.map((x) => stripSchema(x, false, keepDefault)) : stripSchema(v, false, keepDefault));
74
74
  } else if (MAPS.has(k) && v !== null && typeof v === 'object' && !Array.isArray(v)) {
75
75
  const m = {};
76
- for (const name of Object.keys(v)) setOwn(m, name, stripSchema(v[name], false));
76
+ for (const name of Object.keys(v)) setOwn(m, name, stripSchema(v[name], false, keepDefault));
77
77
  setOwn(out, k, m);
78
78
  } else if (LISTS.has(k) && Array.isArray(v)) {
79
79
  const branches = k === 'anyOf' || k === 'oneOf';
80
- setOwn(out, k, v.map((x) => stripSchema(x, branches)));
80
+ setOwn(out, k, v.map((x) => stripSchema(x, branches, keepDefault)));
81
81
  } else if (k === 'dependencies' && v !== null && typeof v === 'object' && !Array.isArray(v)) {
82
82
  const m = {};
83
- for (const name of Object.keys(v)) setOwn(m, name, Array.isArray(v[name]) ? v[name] : stripSchema(v[name], false));
83
+ for (const name of Object.keys(v)) setOwn(m, name, Array.isArray(v[name]) ? v[name] : stripSchema(v[name], false, keepDefault));
84
84
  setOwn(out, k, m);
85
85
  } else if (k === 'propertyDependencies' && v !== null && typeof v === 'object' && !Array.isArray(v)) {
86
86
  const m = {};
@@ -88,7 +88,7 @@ function stripSchema(node, keepTitle) {
88
88
  const inner = v[name];
89
89
  if (inner === null || typeof inner !== 'object' || Array.isArray(inner)) { setOwn(m, name, inner); continue; }
90
90
  const mi = {};
91
- for (const val of Object.keys(inner)) setOwn(mi, val, stripSchema(inner[val], false));
91
+ for (const val of Object.keys(inner)) setOwn(mi, val, stripSchema(inner[val], false, keepDefault));
92
92
  setOwn(m, name, mi);
93
93
  }
94
94
  setOwn(out, k, m);
@@ -112,4 +112,11 @@ function stripAnnotations(schema) {
112
112
  return out;
113
113
  }
114
114
 
115
- module.exports = { stripAnnotations };
115
+ // The same copy with every `default` kept, for a module that fills defaults
116
+ // from its embedded schema (lib/aot-impl.js).
117
+ function stripAnnotationsKeepingDefaults(schema) {
118
+ if (schema === null || typeof schema !== 'object') return schema;
119
+ return refsIntoDropped(schema, new Set()) ? schema : stripSchema(schema, false, true);
120
+ }
121
+
122
+ module.exports = { stripAnnotations, stripAnnotationsKeepingDefaults };
@@ -1098,22 +1098,22 @@ class Validator {
1098
1098
  // routes, and generating and compiling the pass again for each was most of
1099
1099
  // what a repeated schema cost to set up.
1100
1100
  let preprocess = null;
1101
+ // A root schema whose text has no `default` has none to fill, and neither
1102
+ // pass looks for one. One reached through a reference into another
1103
+ // document is walked, since its defaults are not in this text.
1104
+ const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
1101
1105
  if (!this._interpretOnly && _codegen !== null) {
1102
1106
  const cacheable = !this._userFormats && !this._usesKeywords;
1103
1107
  const ppKey = cacheable ? mapKey + '\u0000' + String(options.coerceTypes) + '|' + (options.useDefaults !== false) + '|' + String(options.removeAdditional) : null;
1104
1108
  const hit = ppKey !== null ? _preprocessCache.get(ppKey) : undefined;
1105
1109
  if (hit !== undefined) preprocess = hit;
1106
1110
  else {
1107
- preprocess = _codegen.buildPreprocess(preprocessSchema, options) || null;
1111
+ preprocess = _codegen.buildPreprocess(preprocessSchema, mayHaveDefaults || options.useDefaults === false ? options : { ...options, useDefaults: false }) || null;
1108
1112
  if (ppKey !== null && preprocess !== null) _boundedSet(_preprocessCache, ppKey, preprocess);
1109
1113
  }
1110
1114
  }
1111
1115
  if (!preprocess) {
1112
- // A root schema whose text has no `default` has none to fill, and the
1113
- // defaults module then stays unloaded. One reached through a reference
1114
- // into another document is walked, since its defaults are not in this
1115
- // text.
1116
- const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
1116
+ // Without a `default` in the text the defaults module stays unloaded.
1117
1117
  const applyDefaults = options.useDefaults === false || !mayHaveDefaults ? null : buildDefaultsApplier(preprocessSchema);
1118
1118
  const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
1119
1119
  const applyRemove = options.removeAdditional
@@ -1200,11 +1200,11 @@ class Validator {
1200
1200
  // locals and on this object rather than on the instance: every call reads
1201
1201
  // `validate` off the instance, and three more own properties made that
1202
1202
  // read slower across the suite's validators.
1203
- let _rejectBase = null, _rejectPresented = null, _rejectPlain = false, _oneShotRich = null, _richBuilder = null, _oneShotPlain = false, _isCleanShape = null, _onePassOf = null, _isCombined = null;
1203
+ let _rejectBase = null, _rejectPresented = null, _rejectPlain = false, _oneShotRich = null, _richBuilder = null, _oneShotPlain = false, _isCleanShape = null, _onePassOf = null, _isCombined = null, _preVerdict = null, _rawVerdict = null;
1204
1204
  if (jsFn) {
1205
1205
  const _cg = {
1206
1206
  jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr,
1207
- combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null, richBuilder: null, oneShotPlain: false, isCleanShape: null, isCombined: null, onePassOf: null,
1207
+ combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null, richBuilder: null, oneShotPlain: false, isCleanShape: null, isCombined: null, onePassOf: null, preVerdict: null, rawVerdict: null,
1208
1208
  // The combined function with errors built enriched where they happen,
1209
1209
  // for a validator whose errors are read with richErrors on and no
1210
1210
  // schema source to attach frames from (see makeRich).
@@ -1220,6 +1220,8 @@ class Validator {
1220
1220
  _oneShotPlain = _cg.oneShotPlain;
1221
1221
  _isCleanShape = _cg.isCleanShape;
1222
1222
  _onePassOf = _cg.onePassOf;
1223
+ _preVerdict = _cg.preVerdict;
1224
+ _rawVerdict = _cg.rawVerdict;
1223
1225
  _isCombined = _cg.isCombined;
1224
1226
  } else {
1225
1227
  // No JS codegen: the interpreted engine, with or without the addon. The
@@ -1237,6 +1239,7 @@ class Validator {
1237
1239
  // run the keyword twice, the second time on what the first left.
1238
1240
  const once = interp.dataContext === true;
1239
1241
  if (!preprocess && !once) this._fastVerdict = (d) => interp.isValid(d);
1242
+ if (preprocess && !once) _preVerdict = (d) => { preprocess(d); return interp.isValid(d); };
1240
1243
  // abortEarly is a documented contract, not a property of whichever engine
1241
1244
  // answered: it promises the frozen ATA9000 stub instead of a detailed
1242
1245
  // error, so code that branches on it has to behave the same with and
@@ -1251,7 +1254,7 @@ class Validator {
1251
1254
  : (data) => interp.validate(data));
1252
1255
  this.validate = this._verbose ? _verboseWrap(run, schemaObj) : run;
1253
1256
  // Reading errors runs the interpreter once, directly; see _rejectBase.
1254
- if (!preprocess && !options.abortEarly && !this._verbose && !once) _rejectBase = (data) => interp.validate(data);
1257
+ if (!options.abortEarly && !this._verbose && !once) _rejectBase = (data) => interp.validate(data);
1255
1258
  _bindVerdict(this, this._fastVerdict
1256
1259
  ? this._fastVerdict
1257
1260
  : (data) => run(data).valid);
@@ -1442,10 +1445,14 @@ class Validator {
1442
1445
  // engine and materializes `errors` through a getter on first access.
1443
1446
  // A caller that only reads `.valid`, which is every gateway check and
1444
1447
  // every benchmark, skips error construction entirely; a caller that
1445
- // reads `.errors` pays once and the result is cached. Skipped when the
1446
- // schema coerces or defaults (preprocess mutates before the verdict),
1447
- // under abortEarly (already a frozen stub), and for $dynamicRef (the
1448
- // boolean engine is not the authority there).
1448
+ // reads `.errors` pays once and the result is cached. A schema that
1449
+ // coerces or defaults takes it too, with a verdict that runs the
1450
+ // preprocess pass first; the errors are then built from the document as
1451
+ // the pass left it. It built them on every rejection, read or not, which
1452
+ // on a large schema was 25 ms of a first validate() spent ordering
1453
+ // errors nobody asked for. Skipped under abortEarly (already a frozen
1454
+ // stub), for $dynamicRef (the boolean engine is not the authority
1455
+ // there), and with removeAdditional's fused pass.
1449
1456
  // A check registered through _extendValidate joins here when it can: the
1450
1457
  // verdict comes from the generated function with the check compiled in,
1451
1458
  // and the check's own errors are appended when somebody reads them. A
@@ -1453,7 +1460,8 @@ class Validator {
1453
1460
  // is not installed, the end of this method wraps validate() instead.
1454
1461
  const _vx = this._validateTail !== null ? this._validateTail() : null;
1455
1462
  let _vxApplied = false;
1456
- if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
1463
+ const _verdict0 = preprocess ? _preVerdict : this._fastVerdict;
1464
+ if (_verdict0 && !options.abortEarly && this.validate) {
1457
1465
  // The errors of a document the verdict rejected come from the rejection
1458
1466
  // path directly, not from validate() deciding a second time. Not with a
1459
1467
  // check from _extendValidate: that check can reject what the schema
@@ -1461,7 +1469,7 @@ class Validator {
1461
1469
  // Not with removeAdditional's fused pass either, which edits the
1462
1470
  // document before the errors are read.
1463
1471
  const _full = (_vx === null && !fusedRemove && _rejectPresented) ? _rejectPresented : this.validate;
1464
- const _fast = _vx ? _fuseTail(this._fastVerdict, _vx.check) : this._fastVerdict;
1472
+ const _fast = _vx ? _fuseTail(_verdict0, _vx.check) : _verdict0;
1465
1473
  const _extra = _vx ? _vx.errors : null;
1466
1474
  _vxApplied = true;
1467
1475
  const EMPTY_ERRORS = Object.freeze([]);
@@ -1553,13 +1561,16 @@ class Validator {
1553
1561
  // the caller and the checks, and allocation sites of its own. Compiled
1554
1562
  // only once the validator is in use, as the hybrid is.
1555
1563
  const self = this;
1556
- const _factory = (_vx === null && _fast === this._fastVerdict && typeof _fast._resultFactory === 'function') ? _fast._resultFactory : null;
1564
+ // With a preprocess pass the factory's function runs after it, as the
1565
+ // verdict does.
1566
+ const _gv = preprocess ? _rawVerdict : _fast === this._fastVerdict ? _fast : null;
1567
+ const _factory = (_vx === null && _gv !== null && typeof _gv._resultFactory === 'function') ? _gv._resultFactory : null;
1557
1568
  if (_factory !== null) {
1558
1569
  let calls = 0;
1559
1570
  this.validate = (data) => {
1560
1571
  if (++calls === HYBRID_TIER_CALLS && _onePass === null) {
1561
1572
  const gen = _factory(LazyRejection, _errorsOf, _buildRawErrors, EMPTY_ERRORS);
1562
- if (gen) self.validate = gen;
1573
+ if (gen) self.validate = preprocess ? (data) => { preprocess(data); return gen(data); } : gen;
1563
1574
  }
1564
1575
  return _plainValidate(data);
1565
1576
  };
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.57.0';
10
+ module.exports = '1.58.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.57.0",
3
+ "version": "1.58.1",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -87,7 +87,8 @@
87
87
  "browser": {
88
88
  "./lib/native-load.js": "./lib/native-load.browser.js",
89
89
  "./lib/aot.js": "./lib/aot.browser.js",
90
- "./lib/buffer-gate.js": "./lib/buffer-gate.browser.js"
90
+ "./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
91
+ "./lib/interpreter.js": "./lib/interpreter.browser.js"
91
92
  },
92
93
  "scripts": {
93
94
  "prepublishOnly": "node tests/test_pack_purity.js && ATA_ALLOW_DOC_PLACEHOLDERS=1 node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
@@ -185,13 +186,13 @@
185
186
  "codegen.d.ts"
186
187
  ],
187
188
  "optionalDependencies": {
188
- "@ata-validator/native-darwin-arm64": "1.57.0",
189
- "@ata-validator/native-darwin-x64": "1.57.0",
190
- "@ata-validator/native-linux-arm64-gnu": "1.57.0",
191
- "@ata-validator/native-linux-arm64-musl": "1.57.0",
192
- "@ata-validator/native-linux-x64-gnu": "1.57.0",
193
- "@ata-validator/native-linux-x64-musl": "1.57.0",
194
- "@ata-validator/native-win32-x64": "1.57.0"
189
+ "@ata-validator/native-darwin-arm64": "1.58.1",
190
+ "@ata-validator/native-darwin-x64": "1.58.1",
191
+ "@ata-validator/native-linux-arm64-gnu": "1.58.1",
192
+ "@ata-validator/native-linux-arm64-musl": "1.58.1",
193
+ "@ata-validator/native-linux-x64-gnu": "1.58.1",
194
+ "@ata-validator/native-linux-x64-musl": "1.58.1",
195
+ "@ata-validator/native-win32-x64": "1.58.1"
195
196
  },
196
197
  "peerDependencies": {
197
198
  "yaml": "^2.0.0"