ata-validator 1.61.3 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +67 -10
  2. package/bin/ata.js +3 -2
  3. package/compat.js +15 -2
  4. package/index.d.ts +7 -0
  5. package/index.js +10 -0
  6. package/lib/aot-build.js +27 -1
  7. package/lib/aot-impl.js +108 -17
  8. package/lib/buffer-gate.js +25 -14
  9. package/lib/bun-target.js +21 -5
  10. package/lib/clone-emit.js +115 -10
  11. package/lib/codegen-engine.js +94 -18
  12. package/lib/compiled-common.js +11 -4
  13. package/lib/compiled-verdict.js +2 -2
  14. package/lib/compiled.js +1 -1
  15. package/lib/data-positions.js +18 -0
  16. package/lib/defaults.js +101 -23
  17. package/lib/describe-schema.js +38 -5
  18. package/lib/diagnose.js +51 -29
  19. package/lib/enrich-error.js +23 -1
  20. package/lib/error-sort-rank.js +12 -0
  21. package/lib/formats-source.js +12 -1
  22. package/lib/formats.js +19 -19
  23. package/lib/interpreter-core.js +23 -4
  24. package/lib/js-compiler-errors.js +70 -72
  25. package/lib/js-compiler.js +212 -72
  26. package/lib/json-rejections.js +1 -1
  27. package/lib/migrate.js +24 -5
  28. package/lib/native-attach.browser.js +7 -0
  29. package/lib/native-attach.js +76 -0
  30. package/lib/native-positions.js +7 -0
  31. package/lib/plan-compiler.js +7 -1
  32. package/lib/regex-linear.js +88 -0
  33. package/lib/rejections.js +13 -3
  34. package/lib/render-compact.js +5 -5
  35. package/lib/render-pretty.js +23 -10
  36. package/lib/safe-regex.js +29 -4
  37. package/lib/scan-compiler.js +16 -4
  38. package/lib/stream.js +293 -0
  39. package/lib/tier0.js +6 -2
  40. package/lib/ts-gen.js +25 -12
  41. package/lib/unique-items.js +11 -3
  42. package/lib/validator-core.js +61 -66
  43. package/lib/verify.js +25 -6
  44. package/lib/version.js +1 -1
  45. package/package.json +17 -8
  46. package/stream.d.ts +51 -0
  47. package/stream.js +3 -0
  48. package/stream.mjs +3 -0
package/lib/clone-emit.js CHANGED
@@ -50,6 +50,12 @@ 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
+ // A schema that embeds another resource (a subschema with its own `$id`)
54
+ // reads a fragment `$ref` inside it against that resource, not against
55
+ // this root, so the pointer resolved here could name a different schema
56
+ // from the one validation applies. No $ref is inlined there; emitClone
57
+ // then declines the $ref as it stands.
58
+ if (embedsResource(root, root, new Set())) return root;
53
59
  // The runtime's useDefaults fills a property's default from the schema
54
60
  // written under the property, the $ref node with any default next to it,
55
61
  // and then the defaults inside the referenced definition, as it fills them
@@ -90,11 +96,83 @@ function inlineRefsForClone(root) {
90
96
  return walk(root, new Set(), false);
91
97
  }
92
98
 
99
+ function embedsResource(node, root, seen) {
100
+ if (!node || typeof node !== 'object' || seen.has(node)) return false;
101
+ seen.add(node);
102
+ if (Array.isArray(node)) return node.some((n) => embedsResource(n, root, seen));
103
+ if (node !== root && typeof node.$id === 'string' && !node.$id.startsWith('#')) return true;
104
+ for (const k of Object.keys(node)) {
105
+ if (_INLINE_DATA_KEYS.has(k)) continue;
106
+ if (embedsResource(node[k], root, seen)) return true;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ // The names a node requires through itself and the in-place applicators
112
+ // below it (allOf, anyOf, oneOf, if, then, else, dependentRequired and the
113
+ // array form of dependencies). Every one has to be a declared property, or
114
+ // the copy, which keeps only declared keys, drops a key the schema demands
115
+ // and hands back a value validate() refuses. `not` is left out: a name
116
+ // required under it is one the value must not carry. Returns false where
117
+ // the walk cannot tell: dependentSchemas, the schema form of dependencies,
118
+ // or a $ref inside an applicator.
119
+ function requiredNames(node, out, depth) {
120
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return true;
121
+ if (depth > 16) return false;
122
+ if (depth > 0 && node.$ref !== undefined) return false;
123
+ if (node.dependentSchemas !== undefined) return false;
124
+ if (Array.isArray(node.required)) for (const n of node.required) out.add(n);
125
+ if (node.dependentRequired && typeof node.dependentRequired === 'object') {
126
+ for (const k of Object.keys(node.dependentRequired)) {
127
+ out.add(k);
128
+ const list = node.dependentRequired[k];
129
+ if (Array.isArray(list)) for (const n of list) out.add(n);
130
+ }
131
+ }
132
+ if (node.dependencies !== undefined) {
133
+ if (!node.dependencies || typeof node.dependencies !== 'object') return false;
134
+ for (const k of Object.keys(node.dependencies)) {
135
+ const v = node.dependencies[k];
136
+ if (!Array.isArray(v)) return false;
137
+ out.add(k);
138
+ for (const n of v) out.add(n);
139
+ }
140
+ }
141
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
142
+ if (node[k] === undefined) continue;
143
+ if (!Array.isArray(node[k])) return false;
144
+ for (const b of node[k]) if (!requiredNames(b, out, depth + 1)) return false;
145
+ }
146
+ for (const k of ['if', 'then', 'else']) {
147
+ if (node[k] !== undefined && !requiredNames(node[k], out, depth + 1)) return false;
148
+ }
149
+ return true;
150
+ }
151
+
152
+ // The source budget for default values written into one copy. A shared
153
+ // definition is inlined at every $ref to it, and its default with it: 200
154
+ // references to a 50 KB default made 10 MB of source. Past the budget the
155
+ // copy declines, as it does for anything else it cannot do well.
156
+ const DEFAULT_SOURCE_BUDGET = 256 * 1024;
157
+
158
+ // A default as source that builds it fresh on every parse. An object literal
159
+ // reads a "__proto__" key as the prototype slot, so a default holding one (an
160
+ // own key, as JSON.parse writes it) is rebuilt by JSON.parse from its text:
161
+ // as a literal, the filled value had no such key and inherited from the
162
+ // default's inner object. Null past the source budget.
163
+ function defaultSource(v, budget) {
164
+ const text = JSON.stringify(v);
165
+ const src = v !== null && typeof v === 'object' && text.includes('"__proto__"') ? `JSON.parse(${JSON.stringify(text)})` : text;
166
+ budget.left -= src.length;
167
+ return budget.left < 0 ? null : src;
168
+ }
169
+
93
170
  const PROTO_KEY_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
94
171
 
95
- function emitClone(node, access, depth) {
172
+ function emitClone(node, access, depth, budget) {
96
173
  if (!node || typeof node !== 'object') return null;
97
174
  if (depth > 12) return null;
175
+ if (budget === undefined) budget = { left: DEFAULT_SOURCE_BUDGET };
98
176
  // Only plain object nodes are cloneable, and only when the schema names
99
177
  // every property it keeps. A $ref means the set of allowed keys is not
100
178
  // this node's to decide. In-place applicators (allOf, anyOf, oneOf,
@@ -151,6 +229,13 @@ function emitClone(node, access, depth) {
151
229
  for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
152
230
  }
153
231
  const required = new Set(Array.isArray(node.required) ? node.required : []);
232
+ // Outside the record shape, where every key is kept, every name the
233
+ // schema requires must be one the copy keeps.
234
+ if (!apSchema) {
235
+ const demanded = new Set();
236
+ if (!requiredNames(node, demanded, 0)) return null;
237
+ for (const n of demanded) if (!node.properties || !Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
238
+ }
154
239
 
155
240
  const fixed = [];
156
241
  const conditional = [];
@@ -174,7 +259,7 @@ function emitClone(node, access, depth) {
174
259
  if (!probe || !probe(prop.default)) return null;
175
260
  }
176
261
  const read = `${access}[${JSON.stringify(key)}]`;
177
- const value = emitValueExpr(prop, read, depth);
262
+ const value = emitValueExpr(prop, read, depth, budget);
178
263
  if (value === null) return null;
179
264
  // A plain `"__proto__":` in an object literal is the prototype slot, not
180
265
  // a property; `["__proto__"]:` is a property. Only that key is written as
@@ -197,7 +282,7 @@ function emitClone(node, access, depth) {
197
282
  if (apSchema) {
198
283
  const kVar = `_k${depth}`;
199
284
  const vVar = `_v${depth}`;
200
- const valueExpr = emitValueExpr(apSchema, vVar, depth);
285
+ const valueExpr = emitValueExpr(apSchema, vVar, depth, budget);
201
286
  if (valueExpr === null) return null;
202
287
  let skip = '';
203
288
  let declSet = '';
@@ -217,8 +302,15 @@ function emitClone(node, access, depth) {
217
302
  if (conditional.length === 0 && !recordLoop) return literal;
218
303
  // Optional properties are added only when present, so the result never
219
304
  // gains a key the input did not have.
305
+ const dfltSrc = [];
306
+ for (const c of conditional) {
307
+ if (c.dflt === undefined) { dfltSrc.push(null); continue; }
308
+ const src = defaultSource(c.dflt, budget);
309
+ if (src === null) return null;
310
+ dfltSrc.push(src);
311
+ }
220
312
  const adds = conditional
221
- .map(({ key, value, dflt }) => {
313
+ .map(({ key, value, dflt }, i) => {
222
314
  // Object.hasOwn, not `in`: `in` also answers for what the instance
223
315
  // inherits, so a property named `constructor` would copy the inherited
224
316
  // function into the output that validate().data does not carry. And a
@@ -235,12 +327,12 @@ function emitClone(node, access, depth) {
235
327
  ? `Object.prototype.hasOwnProperty.call(${access}, ${k})`
236
328
  : `(${k} in ${access} && (${access}.__proto__ === Object.prototype || Object.prototype.hasOwnProperty.call(${access}, ${k})))`;
237
329
  const set = `if (${own}) ${assign}`;
238
- // The default is emitted as a literal inside the call, so an object or
330
+ // The default is emitted as source inside the call, so an object or
239
331
  // array default is a fresh value on every parse, never shared state.
240
332
  if (dflt === undefined) return set;
241
333
  const setDflt = key === '__proto__'
242
- ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
243
- : `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
334
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${dfltSrc[i]}, writable: true, enumerable: true, configurable: true });`
335
+ : `${tmp}[${JSON.stringify(key)}] = ${dfltSrc[i]};`;
244
336
  return `${set} else ${setDflt}`;
245
337
  })
246
338
  .join(' ');
@@ -278,9 +370,22 @@ function isPrimitiveOnly(s) {
278
370
  return false;
279
371
  }
280
372
 
281
- function emitValueExpr(prop, read, depth) {
373
+ function emitValueExpr(prop, read, depth, budget) {
282
374
  if (!prop || typeof prop !== 'object') return null;
283
375
  if (prop.$ref) return null;
376
+ // Positions named by prefixItems (or the array form of items, which
377
+ // normalization turns into prefixItems) each have their own schema, and
378
+ // copying them all with the strategy for `items` handed back an object in
379
+ // a prefix slot as the input's own reference, its undeclared keys kept.
380
+ // A tuple of primitives, with nothing or primitives after it, has nowhere
381
+ // to hide a key, and is copied as a fresh array like any other.
382
+ if (prop.prefixItems !== undefined || Array.isArray(prop.items)) {
383
+ const tuple = Array.isArray(prop.prefixItems) ? prop.prefixItems : prop.items;
384
+ const rest = Array.isArray(prop.items) ? prop.additionalItems : prop.items;
385
+ if (prop.type === 'array' && Array.isArray(tuple) && tuple.every(isPrimitiveOnly) &&
386
+ (rest === false || (rest !== undefined && isPrimitiveOnly(rest)))) return `${read}.slice()`;
387
+ return null;
388
+ }
284
389
  if (isPrimitiveOnly(prop)) return read;
285
390
  // A value checked with `instanceof` (the keyword @ata-project/keywords adds)
286
391
  // is an instance of a class, such as a Date, not a JSON object whose keys
@@ -290,7 +395,7 @@ function emitValueExpr(prop, read, depth) {
290
395
  const isRecordable = (s) => s.properties ||
291
396
  (s.additionalProperties && typeof s.additionalProperties === 'object');
292
397
  if (prop.type === 'object' && isRecordable(prop)) {
293
- return emitClone(prop, read, depth + 1);
398
+ return emitClone(prop, read, depth + 1, budget);
294
399
  }
295
400
  if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
296
401
  !Array.isArray(prop.items)) {
@@ -300,7 +405,7 @@ function emitValueExpr(prop, read, depth) {
300
405
  if (isPrimitiveOnly(it)) return `${read}.slice()`;
301
406
  if (it.type === 'object' && isRecordable(it)) {
302
407
  const el = '_e' + depth;
303
- const inner = emitClone(it, el, depth + 1);
408
+ const inner = emitClone(it, el, depth + 1, budget);
304
409
  if (!inner) return null;
305
410
  return `${read}.map((${el}) => (${inner}))`;
306
411
  }
@@ -440,7 +440,7 @@ function installCodegenPaths (ctx) {
440
440
  let nativeText; // undefined until the first large document
441
441
  const nativeVerdict = (jsonStr) => {
442
442
  if (nativeText === undefined) {
443
- nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
443
+ nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords, this._userFormats);
444
444
  if (nativeText) {
445
445
  this._ensureNative();
446
446
  if (!(this._fastSlot >= 0)) nativeText = false;
@@ -715,7 +715,7 @@ installCodegenPaths.installScanner = function installScanner (schemaObj, options
715
715
  // access expression read through a parent that could be absent or null:
716
716
  // `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
717
717
  // TypeError from validate({}) whenever the optional `c` was missing.
718
- function emitRemovals(node, access, lines, depth, seen) {
718
+ function emitRemovals(node, access, lines, depth, seen, own) {
719
719
  if (!node || typeof node !== 'object' || !node.properties) return;
720
720
  if (seen.has(node)) return;
721
721
  seen.add(node);
@@ -733,13 +733,15 @@ function emitRemovals(node, access, lines, depth, seen) {
733
733
  for (const key of keys) {
734
734
  const prop = node.properties[key];
735
735
  if (prop && typeof prop === 'object' && prop.properties) {
736
- emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen);
736
+ // Into own keys only: a nested object the document inherits is not
737
+ // part of it, and deleting from it would strip a shared object.
738
+ emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen, `Object.hasOwn(${access},${JSON.stringify(key)})&&`);
737
739
  }
738
740
  }
739
741
  seen.delete(node);
740
742
  if (body.length === 0) return;
741
743
  if (depth === 0) lines.push(...body);
742
- else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
744
+ else lines.push(`if(${own}${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
743
745
  }
744
746
 
745
747
  // The coercions the closure pass applies, emitted. Values: `number` and
@@ -779,6 +781,15 @@ function emitCoercions(node, ov, lines, st, depth) {
779
781
  if (st.seen.has(node)) { st.cycle = true; return; }
780
782
  st.seen.add(node);
781
783
  if (node.properties) {
784
+ // Own properties only, as the closure pass has it: a value the object
785
+ // inherits belongs to some other object, and `k in o` once wrapped
786
+ // Object.prototype's own `constructor` into an own array key. On a plain
787
+ // object (prototype Object.prototype) a read that gives a primitive or
788
+ // an object is own for every key but the ones Object.prototype has, so
789
+ // the flag settles the common case and hasOwn is consulted otherwise.
790
+ const pk = '_pk' + st.n++;
791
+ let flagged = false;
792
+ const flag = () => { if (!flagged) { flagged = true; lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`); } };
782
793
  for (const [key, prop] of Object.entries(node.properties)) {
783
794
  // Coercion writes with plain assignment, which for a key named
784
795
  // __proto__ rewrites the prototype instead. The raw value still goes
@@ -786,13 +797,15 @@ function emitCoercions(node, ov, lines, st, depth) {
786
797
  if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
787
798
  const k = JSON.stringify(key);
788
799
  const a = `${ov}[${k}]`;
789
- if (isScalarCoercible(prop)) lines.push(...scalarCoercion(a, prop.type));
800
+ const own = key in Object.prototype ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
801
+ if (isScalarCoercible(prop)) { flag(); lines.push(`if(${own}){`, ...scalarCoercion(a, prop.type), '}'); }
790
802
  // Wrapping a lone value in an array is a top-level rule only, as it was.
791
- else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${k} in ${ov}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`);
803
+ else if (depth === 0 && prop.type === 'array' && st.arrayMode) { flag(); lines.push(`if(${own}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`); }
792
804
  // A leaf, the common case, has nothing below it to coerce.
793
805
  if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
806
+ flag();
794
807
  const n = '_c' + st.n++;
795
- lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
808
+ lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
796
809
  emitCoercions(prop, n, lines, st, depth + 1);
797
810
  lines.push('}}');
798
811
  }
@@ -815,14 +828,23 @@ function emitCoercions(node, ov, lines, st, depth) {
815
828
  }
816
829
  // From the closure pass, on the first reference a defaults walk meets: loaded
817
830
  // with this module it joined every process's cold start.
818
- let _localRef = null;
819
- const localRef = (root, ref) => (_localRef || (_localRef = require('./defaults').localRef))(root, ref);
831
+ // A fragment `$ref` is read against the resource its node belongs to (an
832
+ // embedded `$id`), as validation and the closure pass read it.
833
+ let _defaultsMod = null;
834
+ const defaultsMod = () => _defaultsMod || (_defaultsMod = require('./defaults'));
835
+ const refOf = (rs, node) => {
836
+ if (typeof node.$ref !== 'string') return null;
837
+ if (rs.res === undefined) rs.res = defaultsMod().resourceRoots(rs.root);
838
+ return defaultsMod().refTarget(rs.root, rs.res, node);
839
+ };
820
840
  // Whether a default is reachable from a schema through the places the pass
821
841
  // goes (lib/defaults.js reachesDefault, which this pass is held to): the
822
842
  // same walk, with answers kept per schema object for the whole pass, except
823
843
  // one that rests on a schema still being decided (a recursive reference). It
824
844
  // started over for every property, quadratic in the depth of the schema.
825
- function reachState(root) { return { root, memo: new Map() }; }
845
+ // `res`, the resources map, is built on the first `$ref` the walk follows:
846
+ // most schemas have none, and the walk over the schema is not worth making.
847
+ function reachState(root) { return { root, memo: new Map(), res: undefined }; }
826
848
  function hasDefaultsInside(node, rs) { return reachDefault(node, rs, new Set()) === 1; }
827
849
  function reachDefault(node, rs, stack) {
828
850
  if (!node || typeof node !== 'object') return 0;
@@ -838,7 +860,7 @@ function reachDefault(node, rs, stack) {
838
860
  if (prop && typeof prop === 'object' && (prop.default !== undefined || check(prop))) { yes = true; break; }
839
861
  }
840
862
  }
841
- if (!yes && typeof node.$ref === 'string') yes = check(localRef(rs.root, node.$ref));
863
+ if (!yes && typeof node.$ref === 'string') yes = check(refOf(rs, node));
842
864
  if (!yes) for (const list of [node.allOf, node.prefixItems]) { if (Array.isArray(list) && list.some(check)) { yes = true; break; } }
843
865
  if (!yes) for (const one of [node.items, node.additionalProperties]) { if (check(one)) { yes = true; break; } }
844
866
  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; } }
@@ -868,6 +890,8 @@ function emitDefaultsAt(schema, expr, lines, st) {
868
890
  // A pass that would expand to more than this many lines (references written
869
891
  // out again at every use) is left to the closure pass.
870
892
  const DEFAULTS_MAX_LINES = 4000;
893
+ // The most default-value source the generated pass writes, in characters.
894
+ const DEFAULTS_MAX_BYTES = 256 * 1024;
871
895
  // How many schema objects emitDefaults would write out, references expanded
872
896
  // at every use, or Infinity where it would decline (a cycle, or defaults
873
897
  // under patternProperties). Each writes at least one line, so a count over
@@ -886,13 +910,22 @@ function defaultsVisits(node, st, stack) {
886
910
  add(node.items);
887
911
  add(node.additionalProperties);
888
912
  if (node.dependentSchemas && typeof node.dependentSchemas === 'object') Object.values(node.dependentSchemas).forEach(add);
889
- if (typeof node.$ref === 'string') add(localRef(st.root, node.$ref));
913
+ if (typeof node.$ref === 'string') add(refOf(st.reach, node));
890
914
  if (Array.isArray(node.allOf)) node.allOf.forEach(add);
891
915
  stack.delete(node);
892
916
  if (n > DEFAULTS_MAX_LINES) n = Infinity;
893
917
  st.visits.set(node, n);
894
918
  return n;
895
919
  }
920
+ // A default as source that rebuilds it fresh on every fill. An object
921
+ // literal reads a "__proto__" key as the prototype slot, so a default that
922
+ // holds one (own, as JSON.parse writes it) is rebuilt by JSON.parse from its
923
+ // text instead: as a literal the filled value had no such key and inherited
924
+ // from the default's inner object.
925
+ function defaultLiteral(v) {
926
+ const text = JSON.stringify(v);
927
+ return v !== null && typeof v === 'object' && text.includes('"__proto__"') ? `JSON.parse(${JSON.stringify(text)})` : text;
928
+ }
896
929
  function emitDefaults(node, ov, lines, st) {
897
930
  if (st.seen.has(node) || lines.length > DEFAULTS_MAX_LINES) { st.cycle = true; return; }
898
931
  st.seen.add(node);
@@ -915,7 +948,13 @@ function emitDefaults(node, ov, lines, st) {
915
948
  const inherited = key in Object.prototype;
916
949
  const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
917
950
  if (prop.default !== undefined) {
918
- const def = JSON.stringify(prop.default);
951
+ const def = defaultLiteral(prop.default);
952
+ // A shared definition is written out at every reference to it, its
953
+ // defaults with it: 200 references to a 50 KB default made 10 MB of
954
+ // source. Past the budget the closure pass, which holds each default
955
+ // once, takes the schema.
956
+ st.defaultBytes = (st.defaultBytes || 0) + def.length;
957
+ if (st.defaultBytes > DEFAULTS_MAX_BYTES) { st.cycle = true; st.seen.delete(node); return; }
919
958
  // Assignment to a key named __proto__ hits the prototype setter
920
959
  // instead of creating a property; defineProperty writes an own key.
921
960
  lines.push(key === '__proto__'
@@ -978,7 +1017,7 @@ function emitDefaults(node, ov, lines, st) {
978
1017
  // written out here (one back into what is being written is a cycle, left
979
1018
  // to the closure pass), then each `allOf` member, in the closure pass's order.
980
1019
  if (typeof node.$ref === 'string') {
981
- const target = localRef(st.root, node.$ref);
1020
+ const target = refOf(st.reach, node);
982
1021
  if (target && hasDefaultsInside(target, st.reach)) emitDefaults(target, ov, lines, st);
983
1022
  }
984
1023
  if (Array.isArray(node.allOf)) {
@@ -1075,8 +1114,15 @@ function buildParse (self, decline, extended) {
1075
1114
  self._ensureCompiled();
1076
1115
  const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
1077
1116
  if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
1078
- return (data) => {
1079
- if (!verdict(data)) {
1117
+ // With a preprocess pass (defaults, removeAdditional), validate() checks
1118
+ // the document after the pass has run, and a check of the raw input can
1119
+ // answer the other way: a default can make a conditional apply. The
1120
+ // verdict is then taken on a filled clone of the input, the caller's
1121
+ // object left alone, and the copy handed back is checked as well, so
1122
+ // parse() never returns a value validate() would refuse.
1123
+ const fill = self._preprocess;
1124
+ const filled = typeof fill === 'function';
1125
+ const reject = (data) => {
1080
1126
  const e = new Error('validation failed');
1081
1127
  e.name = 'AtaValidationError';
1082
1128
  let target = data;
@@ -1099,8 +1145,28 @@ function buildParse (self, decline, extended) {
1099
1145
  set (v) { Object.defineProperty(e, 'errors', { value: v, writable: true, configurable: true, enumerable: true }); },
1100
1146
  });
1101
1147
  throw e;
1148
+ };
1149
+ if (!filled) {
1150
+ return (data) => {
1151
+ if (!verdict(data)) reject(data);
1152
+ return copy(data);
1153
+ };
1154
+ }
1155
+ return (data) => {
1156
+ let doc;
1157
+ try { doc = structuredClone(data); } catch { doc = undefined; }
1158
+ if (doc === undefined && data !== undefined) {
1159
+ throw new TypeError('parse() is not available for this value: it cannot be copied to fill its defaults before the check. Use validate() instead.');
1160
+ }
1161
+ if (doc !== null && typeof doc === 'object') fill(doc);
1162
+ if (!verdict(doc)) reject(data);
1163
+ // The copy is built from the input, filling its defaults as written, as
1164
+ // it is without a preprocess pass.
1165
+ const out = copy(data);
1166
+ if (!verdict(out)) {
1167
+ throw new TypeError('parse() is not available for this value: the copy that keeps the declared properties is not one the schema accepts. Use validate() instead.');
1102
1168
  }
1103
- return copy(data);
1169
+ return out;
1104
1170
  };
1105
1171
  }
1106
1172
 
@@ -1147,7 +1213,17 @@ function coldTwin (self, Validator) {
1147
1213
  if (t === undefined) {
1148
1214
  t = null;
1149
1215
  if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
1150
- try { t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' })); } catch { t = null; }
1216
+ try {
1217
+ t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' }));
1218
+ // The registry as it is now, not as the constructor options had it:
1219
+ // a schema registered with addSchema() after construction is in the
1220
+ // live map only, and a twin without it answered against the schema
1221
+ // the registry had replaced.
1222
+ if (self._schemaMap && self._schemaMap.size > 0) {
1223
+ t._schemaMap = new Map(self._schemaMap);
1224
+ t._schemaMapShared = false;
1225
+ }
1226
+ } catch { t = null; }
1151
1227
  }
1152
1228
  self._twin = t;
1153
1229
  }
@@ -109,13 +109,20 @@ function combine(a, b) {
109
109
  };
110
110
  }
111
111
 
112
- // The JSON entry points under a check, as the runtime has them.
113
- function extendJSON(base, { check, errors }) {
112
+ // The JSON entry points under a check, as the runtime has them. `fill` is
113
+ // the wrapper's defaults pass, or null: the check sees the document the
114
+ // schema checked, defaults filled, not the raw text.
115
+ function extendJSON(base, { check, errors }, fill) {
116
+ const prepared = (text) => {
117
+ const p = parse(text);
118
+ if (!p.error && fill) fill(p.value);
119
+ return p;
120
+ };
114
121
  return {
115
122
  validateJSON: base.validateJSON && ((text) => {
116
123
  const res = base.validateJSON(text);
117
124
  if (!res.valid) return res;
118
- const p = parse(text);
125
+ const p = prepared(text);
119
126
  if (p.error) return res;
120
127
  if (check(p.value)) return res;
121
128
  const e = errors(p.value);
@@ -123,7 +130,7 @@ function extendJSON(base, { check, errors }) {
123
130
  }),
124
131
  isValidJSON: (text) => {
125
132
  if (!base.isValidJSON(text)) return false;
126
- const p = parse(text);
133
+ const p = prepared(text);
127
134
  return p.error ? true : check(p.value);
128
135
  },
129
136
  };
@@ -10,7 +10,7 @@
10
10
  const { prepare, parse, installExtension, extendJSON } = require('./compiled-common');
11
11
 
12
12
  function fromCompiledVerdict(mod, schema, options) {
13
- const { isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
13
+ const { fill, isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
14
14
  const api = {
15
15
  isValidObject(data) {
16
16
  return isValid(data);
@@ -22,7 +22,7 @@ function fromCompiledVerdict(mod, schema, options) {
22
22
  };
23
23
  installExtension(api, schema, (base, ext) => ({
24
24
  isValidObject: (d) => base.isValidObject(d) && ext.check(d),
25
- isValidJSON: extendJSON(base, ext).isValidJSON,
25
+ isValidJSON: extendJSON(base, ext, fill).isValidJSON,
26
26
  }));
27
27
  return api;
28
28
  }
package/lib/compiled.js CHANGED
@@ -87,7 +87,7 @@ function fromCompiled(mod, schema, options) {
87
87
  },
88
88
  };
89
89
  installExtension(api, schema, (base, ext) => {
90
- const json = extendJSON(base, ext);
90
+ const json = extendJSON(base, ext, fill);
91
91
  return {
92
92
  // The verdict is the schema's and the check's. A failure lists the
93
93
  // schema's errors, then the check's, read when somebody asks.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+
3
4
  /**
4
5
  * Build pointer → { byteOffset, length, line, col, text } from a JSON
5
6
  * input buffer. Called only when validation fails AND richErrors is on
@@ -156,6 +157,16 @@ function buildDataPositionMap (input) {
156
157
  col: off - lineStart[line] + 1,
157
158
  text: lines[line] || '',
158
159
  };
160
+ // A minified document is one line, and each error carried all of it:
161
+ // 4000 errors on a 100 KB line serialized to 800 MB. A long line is cut
162
+ // to 200 characters from 60 before the value, and textCol says where the
163
+ // value starts in the cut text; col stays the column in the line.
164
+ if (entry.text.length > 200) {
165
+ let s = entry.col > 61 ? entry.col - 61 : 0;
166
+ if (s + 200 > entry.text.length) s = entry.text.length - 200;
167
+ entry.text = entry.text.slice(s, s + 200);
168
+ entry.textCol = entry.col - s;
169
+ }
159
170
  if (keyStarts[k] >= 0) {
160
171
  const keyLine = lineOf(keyStarts[k]);
161
172
  entry.keyOffset = keyStarts[k];
@@ -355,6 +366,13 @@ function buildTargetedPositionMap (input, wanted) {
355
366
  col: off - lineStart[line] + 1,
356
367
  text: lines[line] || '',
357
368
  };
369
+ // A long line is cut around the value, as in buildDataPositionMap.
370
+ if (entry.text.length > 200) {
371
+ let s = entry.col > 61 ? entry.col - 61 : 0;
372
+ if (s + 200 > entry.text.length) s = entry.text.length - 200;
373
+ entry.text = entry.text.slice(s, s + 200);
374
+ entry.textCol = entry.col - s;
375
+ }
358
376
  if (keyStarts[k] >= 0) {
359
377
  const keyLine = lineOf(keyStarts[k]);
360
378
  entry.keyOffset = keyStarts[k];