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
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
  }