ata-validator 1.61.2 → 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 (52) hide show
  1. package/README.md +159 -37
  2. package/bin/ata.js +35 -2
  3. package/compat.js +15 -2
  4. package/compiled-standard.d.ts +6 -0
  5. package/compiled-standard.mjs +66 -0
  6. package/index.d.ts +7 -0
  7. package/index.js +10 -0
  8. package/lib/aot-build.js +27 -1
  9. package/lib/aot-impl.js +108 -17
  10. package/lib/buffer-gate.js +25 -14
  11. package/lib/bun-target.js +21 -5
  12. package/lib/clone-emit.js +115 -10
  13. package/lib/codegen-engine.js +94 -18
  14. package/lib/compiled-common.js +11 -4
  15. package/lib/compiled-standard.js +33 -0
  16. package/lib/compiled-verdict.js +2 -2
  17. package/lib/compiled.js +1 -1
  18. package/lib/data-positions.js +18 -0
  19. package/lib/defaults.js +101 -23
  20. package/lib/describe-schema.js +38 -5
  21. package/lib/diagnose.js +51 -29
  22. package/lib/enrich-error.js +23 -1
  23. package/lib/error-sort-rank.js +12 -0
  24. package/lib/formats-source.js +12 -1
  25. package/lib/formats.js +19 -19
  26. package/lib/interpreter-core.js +23 -4
  27. package/lib/js-compiler-errors.js +70 -72
  28. package/lib/js-compiler.js +212 -72
  29. package/lib/json-rejections.js +1 -1
  30. package/lib/migrate.js +24 -5
  31. package/lib/native-attach.browser.js +7 -0
  32. package/lib/native-attach.js +76 -0
  33. package/lib/native-positions.js +7 -0
  34. package/lib/plan-compiler.js +7 -1
  35. package/lib/regex-linear.js +88 -0
  36. package/lib/rejections.js +13 -3
  37. package/lib/render-compact.js +5 -5
  38. package/lib/render-pretty.js +23 -10
  39. package/lib/safe-regex.js +29 -4
  40. package/lib/scan-compiler.js +16 -4
  41. package/lib/schema-diff.js +103 -0
  42. package/lib/stream.js +293 -0
  43. package/lib/tier0.js +6 -2
  44. package/lib/ts-gen.js +25 -12
  45. package/lib/unique-items.js +11 -3
  46. package/lib/validator-core.js +61 -66
  47. package/lib/verify.js +25 -6
  48. package/lib/version.js +1 -1
  49. package/package.json +24 -8
  50. package/stream.d.ts +51 -0
  51. package/stream.js +3 -0
  52. package/stream.mjs +3 -0
@@ -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
  };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ // bareStandard: the compiled-module wrapper for a validator that is only ever
4
+ // handed to a Standard Schema consumer (a router's validator middleware, a
5
+ // form library, an RPC layer) and never called by its own code. Such a
6
+ // consumer reads `~standard` and nothing else, and an issue carries a message
7
+ // and a path, so the wrapper is built on a module written with
8
+ // `plainErrors: true`, whose errors are those two things in schema order, and
9
+ // carries no error pipeline, no Standard Schema install and no extension hook.
10
+ // The issues are the runtime's (tests/test_compiled_standard.js). A bundler
11
+ // plugin puts this in place of `new Validator(schema)` where it sees that use.
12
+
13
+ const { standardSchemaFor } = require('./standard-schema');
14
+
15
+ const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
16
+ // When the module's error function answers valid where its verdict said no,
17
+ // the disagreement is reported rather than turned into an acceptance.
18
+ const VERDICT_DISAGREES = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]) });
19
+
20
+ function bareStandard(mod, schema, useDefaults) {
21
+ const fill = useDefaults !== false && typeof mod.applyDefaults === 'function' ? mod.applyDefaults : null;
22
+ const isValid = mod.isValid;
23
+ const errFn = mod.validate;
24
+ const validate = (data) => {
25
+ if (fill !== null) fill(data);
26
+ if (isValid(data)) return VALID;
27
+ const r = errFn(data);
28
+ return r && r.valid === false ? r : VERDICT_DISAGREES;
29
+ };
30
+ return { '~standard': standardSchemaFor(null, () => schema, (_, value) => validate(value)) };
31
+ }
32
+
33
+ module.exports = { bareStandard };
@@ -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];
package/lib/defaults.js CHANGED
@@ -23,11 +23,21 @@ const MAX_DEPTH = 512;
23
23
 
24
24
  // Build the pass for a schema: a function that fills defaults into a value in
25
25
  // place (mutates), or null when no default is reachable from the schema.
26
- function buildDefaultsApplier(schema) {
26
+ // `keyMatcher` turns a patternProperties pattern into something with a
27
+ // `test` method; without one the platform RegExp is used. The runtime hands
28
+ // in the matcher validation uses (see safeKeyMatcher below), so a key that
29
+ // makes a backtracking pattern run for seconds costs no more here than it
30
+ // does to validate. A module written ahead of time embeds this function as
31
+ // text and calls it without one.
32
+ function buildDefaultsApplier(schema, keyMatcher) {
27
33
  if (typeof schema !== "object" || schema === null) return null;
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); };
34
+ // `visits`: per call, the schemas already applied to each object through a
35
+ // `$ref` or an `allOf` member. Applying one twice to the same object fills
36
+ // nothing new, and following every edge of a chain such as
37
+ // n_i = allOf [n_(i-1), n_(i-1)] cost 2^n applications of one schema.
38
+ const ctx = { root: schema, memo: new Map(), reach: new Map(), path: [], res: resourceRoots(schema), match: keyMatcher || keyPattern, visits: null };
39
+ const fn = compileNode(schema, ctx);
40
+ return fn === null ? null : (data) => { ctx.path.length = 0; ctx.visits = null; fn(data, 0, null); };
31
41
  }
32
42
 
33
43
  // Write an own property. Plain assignment of a key named `__proto__` does
@@ -70,13 +80,45 @@ function localRef(root, ref) {
70
80
  return t !== null && typeof t === "object" ? t : null;
71
81
  }
72
82
 
83
+ // For a schema that embeds other resources (a subschema with its own `$id`),
84
+ // the resource each schema object belongs to: a fragment `$ref` inside one
85
+ // is read against that resource, as validation reads it, not against the
86
+ // outer root. Null when nothing is embedded, which is nearly always. An `$id`
87
+ // that is only a fragment (`#foo`, a draft-07 anchor) starts no resource.
88
+ // Values under `default`, `const`, `enum` and `examples` are data, not
89
+ // schemas, and are not walked.
90
+ function resourceRoots(root) {
91
+ if (root === null || typeof root !== "object") return null;
92
+ const map = new Map();
93
+ let embedded = false;
94
+ const walk = (node, res) => {
95
+ if (node === null || typeof node !== "object" || map.has(node)) return;
96
+ if (Array.isArray(node)) { map.set(node, res); for (let i = 0; i < node.length; i++) walk(node[i], res); return; }
97
+ if (node !== root && typeof node.$id === "string" && node.$id.charCodeAt(0) !== 35) { res = node; embedded = true; }
98
+ map.set(node, res);
99
+ for (const k of Object.keys(node)) {
100
+ if (k === "default" || k === "const" || k === "enum" || k === "examples") continue;
101
+ walk(node[k], res);
102
+ }
103
+ };
104
+ walk(root, root);
105
+ return embedded ? map : null;
106
+ }
107
+
108
+ // The schema a `$ref` on `node` names, read against the resource `node`
109
+ // belongs to.
110
+ function refTarget(root, res, node) {
111
+ const base = res !== null ? res.get(node) : undefined;
112
+ return localRef(base !== undefined ? base : root, node.$ref);
113
+ }
114
+
73
115
  // Whether a default is reachable from a schema, through the same places the
74
116
  // pass goes: 1 yes, 0 no, 2 not known yet because the walk came back to a
75
117
  // schema it is still deciding (a recursive reference). Answers are kept per
76
118
  // schema object in `memo`, except one that rests on an undecided schema,
77
119
  // which is worked out again when asked; the walk used to start over for every
78
120
  // schema object it compiled, quadratic in the depth of the schema.
79
- function reachesDefault(node, root, memo, stack) {
121
+ function reachesDefault(node, root, memo, stack, res) {
80
122
  if (node === null || typeof node !== "object") return 0;
81
123
  const known = memo.get(node);
82
124
  if (known !== undefined) return known;
@@ -85,9 +127,9 @@ function reachesDefault(node, root, memo, stack) {
85
127
  const props = valuesOf(node.properties);
86
128
  let yes = props.some((p) => p && typeof p === "object" && p.default !== undefined);
87
129
  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));
130
+ const below = props.concat(refTarget(root, res, node), listOf(node.allOf), listOf(node.prefixItems), node.items, node.additionalProperties, valuesOf(node.patternProperties), valuesOf(node.dependentSchemas));
89
131
  for (let i = 0; !yes && i < below.length; i++) {
90
- const r = reachesDefault(below[i], root, memo, stack);
132
+ const r = reachesDefault(below[i], root, memo, stack, res);
91
133
  if (r === 1) yes = true;
92
134
  else if (r === 2) open = true;
93
135
  }
@@ -119,10 +161,11 @@ function copyOf(v) {
119
161
  // applied to the same value. A schema reached again (a recursive reference)
120
162
  // takes the function being built for it, so a cycle in the schema is a loop
121
163
  // in the calls, bounded by the depth of the data.
122
- function compileNode(node, root, memo, reach, path) {
164
+ function compileNode(node, ctx) {
123
165
  if (node === null || typeof node !== "object") return null;
166
+ const memo = ctx.memo, path = ctx.path;
124
167
  if (memo.has(node)) return memo.get(node);
125
- if (reachesDefault(node, root, reach, new Set()) !== 1) { memo.set(node, null); return null; }
168
+ if (reachesDefault(node, ctx.root, ctx.reach, new Set(), ctx.res) !== 1) { memo.set(node, null); return null; }
126
169
  let body = null;
127
170
  // `path`: the schemas applied to the enclosing values, outermost first.
128
171
  // `made`: inside a value the pass itself made from a default, the same as a
@@ -133,6 +176,16 @@ function compileNode(node, root, memo, reach, path) {
133
176
  // schema, and Renovate's took this pass to the heap limit.
134
177
  const fn = (data, depth, made) => {
135
178
  if (depth > MAX_DEPTH) return;
179
+ // Only what is applied through a `$ref` or `allOf` is counted (fn.shared,
180
+ // set below), and only outside a made value, where every application
181
+ // does the same work.
182
+ if (fn.shared === true && made === null && typeof data === "object" && data !== null) {
183
+ const visits = ctx.visits || (ctx.visits = new Map());
184
+ let done = visits.get(data);
185
+ if (done === undefined) visits.set(data, (done = new Set()));
186
+ if (done.has(node)) return;
187
+ done.add(node);
188
+ }
136
189
  if (made !== null) {
137
190
  if (made.has(node)) return;
138
191
  made.add(node);
@@ -154,31 +207,31 @@ function compileNode(node, root, memo, reach, path) {
154
207
  const p = props[key];
155
208
  if (!p || typeof p !== "object") continue;
156
209
  if (p.default !== undefined) own.push([key, p.default]);
157
- const child = compileNode(p, root, memo, reach, path);
210
+ const child = compileNode(p, ctx);
158
211
  if (child !== null) below.push([key, child]);
159
212
  }
160
213
  }
161
214
  // Arrays: every element under `items`, the elements `prefixItems` names by
162
215
  // 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);
216
+ const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s, ctx)) : [];
217
+ const each = compileNode(node.items, ctx);
165
218
  // Objects: keys a pattern matches, and the keys neither `properties` nor a
166
219
  // pattern names; and a schema for each key present.
167
220
  const patterns = [];
168
221
  if (node.patternProperties && typeof node.patternProperties === "object") {
169
222
  for (const src of Object.keys(node.patternProperties)) {
170
- const re = keyPattern(src);
171
- const t = compileNode(node.patternProperties[src], root, memo, reach, path);
223
+ const re = ctx.match(src);
224
+ const t = compileNode(node.patternProperties[src], ctx);
172
225
  if (re === null) continue;
173
226
  patterns.push([re, t]);
174
227
  }
175
228
  }
176
- const extra = compileNode(node.additionalProperties, root, memo, reach, path);
229
+ const extra = compileNode(node.additionalProperties, ctx);
177
230
  const declared = props && typeof props === "object" ? props : null;
178
231
  const dependent = [];
179
232
  if (node.dependentSchemas && typeof node.dependentSchemas === "object") {
180
233
  for (const key of Object.keys(node.dependentSchemas)) {
181
- const t = compileNode(node.dependentSchemas[key], root, memo, reach, path);
234
+ const t = compileNode(node.dependentSchemas[key], ctx);
182
235
  if (t !== null) dependent.push([key, t]);
183
236
  }
184
237
  }
@@ -186,19 +239,22 @@ function compileNode(node, root, memo, reach, path) {
186
239
 
187
240
  const also = [];
188
241
  if (typeof node.$ref === "string") {
189
- const t = compileNode(localRef(root, node.$ref), root, memo, reach, path);
190
- if (t !== null) also.push(t);
242
+ const t = compileNode(refTarget(ctx.root, ctx.res, node), ctx);
243
+ if (t !== null) { t.shared = true; also.push(t); }
191
244
  }
192
245
  if (Array.isArray(node.allOf)) {
193
246
  for (const s of node.allOf) {
194
- const t = compileNode(s, root, memo, reach, path);
195
- if (t !== null) also.push(t);
247
+ const t = compileNode(s, ctx);
248
+ if (t !== null) { t.shared = true; also.push(t); }
196
249
  }
197
250
  }
198
251
 
199
252
  body = (data, depth, made) => {
253
+ // A value that is not an object has nothing to fill, here or in any
254
+ // schema applied to it.
255
+ if (typeof data !== "object" || data === null) return;
200
256
  let fresh = null;
201
- if (typeof data === "object" && data !== null) {
257
+ {
202
258
  for (let i = 0; i < own.length; i++) {
203
259
  const key = own[i][0];
204
260
  if (!Object.hasOwn(data, key)) {
@@ -240,6 +296,28 @@ function compileNode(node, root, memo, reach, path) {
240
296
 
241
297
  // The functions a module written ahead of time embeds as its applyDefaults
242
298
  // where the generated pass declines a schema (lib/aot-impl.js).
243
- const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
299
+ const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'resourceRoots', 'refTarget', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
300
+
301
+ // patternProperties keys tested in linear time: the linear-time engine
302
+ // (lib/safe-regex.js, which validation uses for these patterns too) for a
303
+ // pattern in its subset, the platform RegExp otherwise, as validation does.
304
+ // A key that makes a backtracking pattern run for seconds then costs no more
305
+ // here than it does to validate. lib/js-compiler.js also hands patterns the
306
+ // RegExp runs in linear time to the RegExp, for speed; that check is left
307
+ // out here so the closure pass, which lite carries, does not pull it in.
308
+ // Loaded on the first pattern; most schemas have none.
309
+ const PROP_ESCAPE = /\\[pP]\{/;
310
+ function safeKeyMatcher(src) {
311
+ if (!PROP_ESCAPE.test(src)) {
312
+ try {
313
+ const safe = require("./safe-regex");
314
+ if (safe.patternIsSafe(src)) return safe.compileSafe(src);
315
+ } catch {}
316
+ }
317
+ return keyPattern(src);
318
+ }
244
319
 
245
- module.exports = { buildDefaultsApplier, setOwn, localRef, MAX_DEPTH, EMBED };
320
+ module.exports = {
321
+ buildDefaultsApplier: (schema) => buildDefaultsApplier(schema, safeKeyMatcher),
322
+ setOwn, localRef, resourceRoots, refTarget, safeKeyMatcher, MAX_DEPTH, EMBED,
323
+ };
@@ -26,6 +26,10 @@
26
26
  // $defs. Anything else is described as `any` rather than guessed at.
27
27
 
28
28
  const MAX_DEPTH = 12;
29
+ // anyOf branches that each lead to the same next definition describe
30
+ // width^layers lines, 16 million for 4 wide and 12 deep. A prompt has no use
31
+ // for more than this; past it the description stops and says so.
32
+ const MAX_LINES = 2000;
29
33
 
30
34
  function lit (v) {
31
35
  try { return JSON.stringify(v); } catch (_) { return String(v); }
@@ -95,6 +99,7 @@ function describeObjectBody (schema, depth, defs, lines) {
95
99
  }
96
100
 
97
101
  function describeNode (schema, label, depth, defs, lines) {
102
+ if (lines.length >= MAX_LINES) return;
98
103
  const pad = ' '.repeat(depth);
99
104
  if (schema === true || schema === undefined) { lines.push(`${pad}${label}: any`); return; }
100
105
  if (schema === false) { lines.push(`${pad}${label}: nothing is allowed here`); return; }
@@ -103,19 +108,43 @@ function describeNode (schema, label, depth, defs, lines) {
103
108
  return;
104
109
  }
105
110
 
106
- const target = schema.$ref ? resolveRef(schema, defs) : null;
107
- if (target) { describeNode(target, label, depth, defs, lines); return; }
111
+ // A chain of references is followed here, each definition once: one that
112
+ // leads back to itself without a property in between describes no value.
113
+ let target = schema.$ref ? resolveRef(schema, defs) : null;
114
+ if (target) {
115
+ const chain = new Set([schema]);
116
+ while (target && typeof target === 'object' && target.$ref && !chain.has(target)) {
117
+ chain.add(target);
118
+ const next = resolveRef(target, defs);
119
+ if (!next) break;
120
+ target = next;
121
+ }
122
+ if (chain.has(target)) { lines.push(`${pad}${label}: any`); return; }
123
+ describeNode(target, label, depth, defs, lines);
124
+ return;
125
+ }
108
126
 
109
- // allOf is the intersection, so its parts describe the same value.
127
+ // allOf is the intersection, so its parts describe the same value. The
128
+ // parts' own allOf are merged in the same pass, each schema once: a part
129
+ // that led back to this schema copied its allOf back in and the merge
130
+ // recursed until the stack ran out.
110
131
  if (Array.isArray(schema.allOf) && schema.allOf.length) {
111
132
  const merged = Object.assign({}, schema);
112
133
  delete merged.allOf;
113
- for (const part of schema.allOf) {
134
+ const seen = new Set([schema]);
135
+ const queue = schema.allOf.slice();
136
+ while (queue.length) {
137
+ const part = queue.shift();
114
138
  const resolved = part && part.$ref ? resolveRef(part, defs) || part : part;
115
- if (resolved && typeof resolved === 'object') Object.assign(merged, resolved, {
139
+ if (!resolved || typeof resolved !== 'object' || seen.has(resolved)) continue;
140
+ seen.add(resolved);
141
+ if (Array.isArray(resolved.allOf)) queue.push(...resolved.allOf);
142
+ Object.assign(merged, resolved, {
116
143
  properties: Object.assign({}, merged.properties, resolved.properties),
117
144
  required: [].concat(merged.required || [], resolved.required || []),
118
145
  });
146
+ delete merged.allOf;
147
+ if (resolved.$ref !== undefined && resolved !== part) delete merged.$ref;
119
148
  }
120
149
  if (!merged.properties) delete merged.properties;
121
150
  if (!merged.required || !merged.required.length) delete merged.required;
@@ -170,6 +199,10 @@ function describeSchema (schema, opts) {
170
199
  const defs = (schema && (schema.$defs || schema.definitions)) || null;
171
200
  const lines = [];
172
201
  describeNode(schema, name, 0, defs, lines);
202
+ if (lines.length >= MAX_LINES) {
203
+ lines.length = MAX_LINES;
204
+ lines.push(`(description cut at ${MAX_LINES} lines)`);
205
+ }
173
206
  return lines.join('\n');
174
207
  }
175
208