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/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
 
package/lib/diagnose.js CHANGED
@@ -111,21 +111,53 @@ function frameFor (err, resolved) {
111
111
  // A caret on the key token when the error names a property, on the value
112
112
  // otherwise. Never wider than the line it is drawn under.
113
113
  const useKey = named != null && hit.keyOffset !== undefined;
114
- const col = useKey ? hit.keyCol : hit.col;
115
- const rawLen = useKey ? hit.keyLength : hit.length;
116
- const lineLen = (hit.text || '').length;
117
- const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - col + 1)));
114
+ return frameOf(hit, useKey ? hit : null, resolved.synthesized);
115
+ }
118
116
 
117
+ // The frame a renderer draws. `col` is the column in the line, for the
118
+ // location; `textCol` is where the caret goes in `text`, which differs when
119
+ // the line was long and `text` is the part of it around the value. Never
120
+ // wider than the text it is drawn under.
121
+ function frameOf (at, key, synthesized) {
122
+ const shift = typeof at.textCol === 'number' && (!key || key.keyLine === at.line) ? at.col - at.textCol : 0;
123
+ const col = key ? key.keyCol : at.col;
124
+ const textCol = Math.max(1, col - shift);
125
+ const rawLen = key ? key.keyLength : at.length;
126
+ const lineLen = termLine(at.text || '').length;
127
+ const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - textCol + 1)));
119
128
  return {
120
- line: useKey ? hit.keyLine : hit.line,
129
+ line: key ? key.keyLine : at.line,
121
130
  col,
131
+ textCol,
122
132
  length,
123
- text: hit.text,
133
+ text: termLine(at.text),
124
134
  spans: rawLen > length,
125
- synthesized: resolved.synthesized,
135
+ synthesized,
126
136
  };
127
137
  }
128
138
 
139
+ // Text from a document or a schema on its way to a terminal: control
140
+ // characters, DEL, the C1 range and the Unicode line separators written as
141
+ // escapes, so a property name cannot start a line, move the cursor or set a
142
+ // colour.
143
+ const TERM_UNSAFE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/g;
144
+ function term (s) {
145
+ if (typeof s !== 'string') return s;
146
+ return s.replace(TERM_UNSAFE, (c) => {
147
+ if (c === '\n') return '\\n';
148
+ if (c === '\r') return '\\r';
149
+ if (c === '\t') return '\\t';
150
+ return '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0');
151
+ });
152
+ }
153
+ // A source line drawn above a caret: one character for one, a space for a
154
+ // tab or a carriage return and U+FFFD for the rest, so the caret stays under
155
+ // the value.
156
+ function termLine (s) {
157
+ if (typeof s !== 'string') return s;
158
+ return s.replace(TERM_UNSAFE, (c) => (c === '\t' || c === '\r' ? ' ' : '\ufffd'));
159
+ }
160
+
129
161
  // No position map in hand, but the error itself may carry one: validateJSON
130
162
  // attaches `dataFrame` and `anchor` at enrichment time, and errors handed
131
163
  // over from another process or a fixture arrive with only those.
@@ -135,18 +167,7 @@ function frameFromError (err) {
135
167
  const a = err.anchor;
136
168
  const named = (err.params && (err.params.additionalProperty || err.params.unevaluatedProperty)) || null;
137
169
  const useKey = named != null && a && a.keyLine !== undefined;
138
- const col = useKey ? a.keyCol : df.col;
139
- const rawLen = useKey ? a.keyLength : df.length;
140
- const lineLen = (df.text || '').length;
141
- const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - col + 1)));
142
- return {
143
- line: useKey ? a.keyLine : df.line,
144
- col,
145
- length,
146
- text: df.text,
147
- spans: rawLen > length,
148
- synthesized: false,
149
- };
170
+ return frameOf(df, useKey ? a : null, false);
150
171
  }
151
172
 
152
173
  function headlineFor (err, src) {
@@ -210,6 +231,7 @@ function decorateBranches (subs, src) {
210
231
  const got = valueAt(src.data, sub.instancePath);
211
232
  if (got !== null) copy.received = got;
212
233
  }
234
+ for (const k of ['keyword', 'message', 'expected', 'received']) if (typeof copy[k] === 'string') copy[k] = term(copy[k]);
213
235
  if (Array.isArray(sub.branchErrors)) copy.branchErrors = decorateBranches(sub.branchErrors, src);
214
236
  return copy;
215
237
  });
@@ -262,13 +284,13 @@ function singleDiagnostic (e, index, resolved, src) {
262
284
 
263
285
  return {
264
286
  code: e.code,
265
- headline: headlineFor(e, src),
287
+ headline: term(headlineFor(e, src)),
266
288
  pointer,
267
- dotted: pathToDotted(anchorErr.instancePath != null ? anchorErr.instancePath : pointer),
289
+ dotted: term(pathToDotted(anchorErr.instancePath != null ? anchorErr.instancePath : pointer)),
268
290
  frame,
269
- found,
270
- help: e.suggestion ? e.suggestion.text : null,
271
- notes,
291
+ found: term(found),
292
+ help: e.suggestion ? term(e.suggestion.text) : null,
293
+ notes: notes.map(term),
272
294
  branchErrors: e.branchErrors ? decorateBranches(e.branchErrors, src) : null,
273
295
  mergedFrom: [e],
274
296
  rank: typeof e.rank === 'number' ? e.rank : 2,
@@ -295,13 +317,13 @@ function mergedDiagnostic (errors, i, j, resolved, src) {
295
317
  const pointer = missing.instancePath != null ? missing.instancePath : (missing.path || '');
296
318
  return {
297
319
  code: missing.code,
298
- headline: `unknown property "${extraName}"`,
320
+ headline: term(`unknown property "${extraName}"`),
299
321
  pointer,
300
- dotted: pathToDotted(pointer),
322
+ dotted: term(pathToDotted(pointer)),
301
323
  frame,
302
324
  found: null,
303
- help: `did you mean "${missingName}"?`,
304
- notes,
325
+ help: term(`did you mean "${missingName}"?`),
326
+ notes: notes.map(term),
305
327
  branchErrors: null,
306
328
  mergedFrom: [missing, extra],
307
329
  rank: 0,
@@ -309,4 +331,4 @@ function mergedDiagnostic (errors, i, j, resolved, src) {
309
331
  };
310
332
  }
311
333
 
312
- module.exports = { toDiagnostics };
334
+ module.exports = { term, termLine, toDiagnostics };
@@ -87,6 +87,15 @@ function reprValue (v) {
87
87
  // fallback prints, so it is taken once either way; the walk and the
88
88
  // serialisation used to run on every container an error named, and a
89
89
  // `required` or `unevaluatedProperties` error names its container.
90
+ // A wide object is counted once per synchronous run: every error that
91
+ // names the same container (additionalProperties: false on an object
92
+ // with N unknown keys gives N of them) counted its keys again, N times N
93
+ // key reads, and 16000 extra keys took 11.8 s to read. The answer is
94
+ // kept until the running synchronous code finishes (a microtask drops
95
+ // it), which covers reading one list. The limit: an object changed and
96
+ // read again within that same run reports the count it had first.
97
+ const known = _wide !== null ? _wide.get(v) : undefined;
98
+ if (known !== undefined) return known;
90
99
  let n;
91
100
  try {
92
101
  n = Object.keys(v).length;
@@ -101,10 +110,21 @@ function reprValue (v) {
101
110
  return '[object, unserializable]';
102
111
  }
103
112
  }
104
- return `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
113
+ const r = `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
114
+ if (n > WIDE) {
115
+ if (_wide === null) {
116
+ _wide = new Map();
117
+ queueMicrotask(() => { _wide = null; });
118
+ }
119
+ _wide.set(v, r);
120
+ }
121
+ return r;
105
122
  }
106
123
  return `[${t}]`;
107
124
  }
125
+ // Past this many keys a count is worth keeping (see reprValue).
126
+ const WIDE = 64;
127
+ let _wide = null;
108
128
 
109
129
  function expectedFor (err) {
110
130
  switch (err.keyword) {
@@ -298,6 +318,7 @@ function enrich (rawErr, opts) {
298
318
  if (positions && positions[path]) {
299
319
  const p = positions[path];
300
320
  out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
321
+ if (p.textCol !== undefined) out.dataFrame.textCol = p.textCol;
301
322
  } else if (!positions && opts && opts.lazyPositions) {
302
323
  defineLazyFrames(out, path, named, opts.lazyPositions);
303
324
  }
@@ -360,6 +381,7 @@ function defineLazyFrames (out, path, named, lazy) {
360
381
  const positions = lazy();
361
382
  const own = positions ? positions[path] : null;
362
383
  const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
384
+ if (dataFrame !== undefined && own.textCol !== undefined) dataFrame.textCol = own.textCol;
363
385
  const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
364
386
  const src = child || own;
365
387
  let anchor;
@@ -46,6 +46,18 @@ function sortErrorsByRank(rootSchema, errors) {
46
46
  prev = r;
47
47
  }
48
48
  if (sorted) return errors;
49
+ // Insertion sort is quadratic: a document with 20000 unknown keys under
50
+ // additionalProperties: false, reported out of order, took seconds here.
51
+ // Past a short list the platform's sort, which is stable, gives the same
52
+ // order, ties kept as they came.
53
+ if (n > 16) {
54
+ const idx = new Array(n);
55
+ for (let i = 0; i < n; i++) idx[i] = i;
56
+ idx.sort((a, b) => compareRanks(keys[a], keys[b]) || a - b);
57
+ const res = new Array(n);
58
+ for (let i = 0; i < n; i++) res[i] = errors[idx[i]];
59
+ return res;
60
+ }
49
61
  const res = errors.slice();
50
62
  for (let i = 1; i < n; i++) {
51
63
  const k = keys[i];
@@ -111,6 +111,17 @@ function hostnameSource (v, isStr) {
111
111
  return guard(v, isStr, hostnameRangeSource(v, '0', `${v}.length`));
112
112
  }
113
113
 
114
+ // The hoisted form: two tables built once per compiled function, a character
115
+ // class (1 hex digit, 2 hyphen) and the class each of the 36 positions wants,
116
+ // and a loop that ORs the mismatches instead of branching on every character.
117
+ // On the 900 ids of the order body: 85 to 35 ns against the per-run loops below.
118
+ const uuidHelperSource = (name) =>
119
+ `const ${name}H=new Uint8Array(128),${name}M=new Uint8Array(36).fill(1);` +
120
+ `for(let _i=48;_i<58;_i++)${name}H[_i]=1;for(let _i=97;_i<103;_i++)${name}H[_i]=1;for(let _i=65;_i<71;_i++)${name}H[_i]=1;` +
121
+ `${name}H[45]=2;${name}M[8]=${name}M[13]=${name}M[18]=${name}M[23]=2;` +
122
+ `function ${name}(_s){if(_s.length!==36)return false;let _b=0;for(let _i=0;_i<36;_i++)_b|=${name}H[_s.charCodeAt(_i)]^${name}M[_i];return _b===0}`
123
+
124
+ // The inline form, for a caller with no function to hoist into.
114
125
  function uuidSource (v, isStr) {
115
126
  const run = (from, to) => `for(let _ui=${from};_ui<${to};_ui++){const _uc=${v}.charCodeAt(_ui);` +
116
127
  'if(!((_uc-48>>>0)<10||((_uc|32)-97>>>0)<6))return false}';
@@ -321,4 +332,4 @@ function durationSource (v, isStr) {
321
332
  const inner = `if(!${_formats().DURATION_RE.toString()}.test(${v}))return false`;
322
333
  return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
323
334
  }
324
- module.exports = { _literalByParsers, uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
335
+ module.exports = { _literalByParsers, uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, uuidHelperSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
package/lib/formats.js CHANGED
@@ -4,9 +4,8 @@
4
4
  // and no allocation. Each has a twin that emits the same check as source for
5
5
  // the code generator; tests/test_formats_single_pass.js holds the two
6
6
  // together and fuzzes both against the regular-expression forms they replaced.
7
- // Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns.
8
- // uuid was tried the same way and measured 57.3 against 59.4 ns: V8's regular
9
- // expression for a fixed-length hex pattern is already that fast, so it stays.
7
+ // Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns,
8
+ // uuid 85 to 35 ns with the class tables below.
10
9
 
11
10
  function isDigit (c) { return c >= 48 && c <= 57; }
12
11
 
@@ -328,25 +327,26 @@ function noReserved (s, from) {
328
327
  return true;
329
328
  }
330
329
 
331
- // 8-4-4-4-12 hexadecimal digits. The four hyphens are read by index, then
332
- // each run of hex digits is read with fixed bounds, so no character pays for a
333
- // test of where it sits. A digit is one unsigned compare and a letter is one
334
- // more after folding case with a single OR, which is what a case-insensitive
335
- // regex spends a state machine on.
336
- function hexRun (s, from, to) {
337
- for (let i = from; i < to; i++) {
338
- const c = s.charCodeAt(i);
339
- if (!((c - 48 >>> 0) < 10 || ((c | 32) - 97 >>> 0) < 6)) return false;
340
- }
341
- return true;
342
- }
330
+ // 8-4-4-4-12 hexadecimal digits. Two tables: the class of a character (1 a
331
+ // hex digit in either case, 2 a hyphen, 0 anything else, including anything
332
+ // past ASCII, which indexes past the table and reads as undefined) and the
333
+ // class each of the 36 positions wants. The loop ORs the mismatches and
334
+ // decides once at the end, so no character costs a branch: 85 to 35 ns on
335
+ // the order body's ids against the earlier per-run loops with two compares a
336
+ // character.
337
+ const UUID_CLASS = new Uint8Array(128);
338
+ for (let i = 48; i < 58; i++) UUID_CLASS[i] = 1;
339
+ for (let i = 97; i < 103; i++) UUID_CLASS[i] = 1;
340
+ for (let i = 65; i < 71; i++) UUID_CLASS[i] = 1;
341
+ UUID_CLASS[45] = 2;
342
+ const UUID_WANT = new Uint8Array(36).fill(1);
343
+ UUID_WANT[8] = UUID_WANT[13] = UUID_WANT[18] = UUID_WANT[23] = 2;
343
344
 
344
345
  function uuid (s) {
345
346
  if (s.length !== 36) return false;
346
- if (s.charCodeAt(8) !== 45 || s.charCodeAt(13) !== 45 ||
347
- s.charCodeAt(18) !== 45 || s.charCodeAt(23) !== 45) return false;
348
- return hexRun(s, 0, 8) && hexRun(s, 9, 13) && hexRun(s, 14, 18) &&
349
- hexRun(s, 19, 23) && hexRun(s, 24, 36);
347
+ let bad = 0;
348
+ for (let i = 0; i < 36; i++) bad |= UUID_CLASS[s.charCodeAt(i)] ^ UUID_WANT[i];
349
+ return bad === 0;
350
350
  }
351
351
 
352
352
  // RFC 3339 full-time: HH:MM:SS, an optional fraction, an optional offset.
@@ -429,9 +429,15 @@ class Plan {
429
429
  this.format = schema.format;
430
430
  this.formatFn = null;
431
431
  if (schema.format !== undefined) {
432
+ // Own properties only, as the generated code reads them: an inherited
433
+ // Object member ('hasOwnProperty', 'constructor') is not a check.
434
+ const own = Object.prototype.hasOwnProperty;
432
435
  const uf = interp.userFormats;
433
- const fc = (uf && typeof uf[schema.format] === 'function') ? uf[schema.format] : require('./formats').formatChecks()[schema.format];
434
- this.formatFn = fc || null;
436
+ const f = schema.format;
437
+ const table = require('./formats').formatChecks();
438
+ const fc = (uf && typeof f === 'string' && own.call(uf, f) && typeof uf[f] === 'function') ? uf[f]
439
+ : (typeof f === 'string' && own.call(table, f) && typeof table[f] === 'function' ? table[f] : null);
440
+ this.formatFn = fc;
435
441
  }
436
442
  this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
437
443
  // A format compat registered for numbers (`type: 'number'`), checked on
@@ -917,14 +923,27 @@ class Interpreter {
917
923
  if (!re) {
918
924
  if (/\\[pP]\{/.test(src)) {
919
925
  // Unicode property escapes need a real RegExp in unicode mode; the
920
- // linear engine does not understand them.
926
+ // linear engine does not understand them (compileSafe refuses them),
927
+ // so the RegExp answers whether or not it runs this pattern in linear
928
+ // time. Asking lib/regex-linear.js would change no answer and would
929
+ // add it to lite. A nested loop around a property escape can
930
+ // backtrack here; SECURITY.md says so.
921
931
  try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
922
932
  } else if (plainLinear(src)) {
923
933
  re = new RegExp(src);
924
934
  } else {
925
935
  try {
926
936
  re = compileSafe(src);
927
- } catch {
937
+ } catch (e) {
938
+ // A parse failure means the engine cannot represent the pattern
939
+ // (a backreference, a lookaround, an escape it does not implement,
940
+ // a repeat past its instruction budget) and the platform RegExp
941
+ // takes it.
942
+ // The stub of a shared error runtime built without the engine
943
+ // (lib/error-runtime.js) throws too, and that one is rethrown: a
944
+ // module whose runtime lacks the engine must fail, not quietly
945
+ // match on a RegExp that can backtrack.
946
+ if (e && typeof e.message === 'string' && e.message.includes('built without the pattern engine')) throw e;
928
947
  try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
929
948
  }
930
949
  }