ata-validator 1.42.0 → 1.43.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.
- package/index.js +2 -2
- package/lib/branch-collapse.js +18 -12
- package/lib/enrich-site.js +2 -2
- package/lib/js-compiler.js +641 -125
- package/lib/scan-compiler.js +210 -25
- package/lib/scan-runtime.js +13 -1
- package/lib/validator-core.js +23 -4
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/scan-compiler.js
CHANGED
|
@@ -90,6 +90,22 @@ const BASE_CHANGING = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef'
|
|
|
90
90
|
// it. Sized for a node per declared property of a large generated schema,
|
|
91
91
|
// not just for hand-written ones.
|
|
92
92
|
const MAX_NODES = 8192;
|
|
93
|
+
// See the size checks in compileScanner. One function past 64 KB is what V8's
|
|
94
|
+
// Maglev tier aborted on: SchemaStore scanners of 0.5 to 1 MB, single
|
|
95
|
+
// functions before references became calls. The total bounds the memory a
|
|
96
|
+
// scanner takes.
|
|
97
|
+
const MAX_FUNCTION_SOURCE = 64 * 1024;
|
|
98
|
+
const MAX_SOURCE = 2 * 1024 * 1024;
|
|
99
|
+
// A reference target is expanded at each reference while its expansions
|
|
100
|
+
// together are at most this much source, and called otherwise.
|
|
101
|
+
const INLINE_REF_MAX = 4096;
|
|
102
|
+
// An object member whose scan is more than this much source is moved into a
|
|
103
|
+
// function of its own (emitMember).
|
|
104
|
+
const SPLIT_MIN = 4 * 1024;
|
|
105
|
+
// The member source an object keeps inline before the rest move out.
|
|
106
|
+
const SPLIT_OBJECT = 16 * 1024;
|
|
107
|
+
// Declared names at least this long on average find a key's end with indexOf.
|
|
108
|
+
const INDEX_KEY_MIN_AVG = 10;
|
|
93
109
|
|
|
94
110
|
// Each position in a tuple emits its own branch inside the element loop, so a
|
|
95
111
|
// long one is both a lot of source and a linear test per element.
|
|
@@ -155,9 +171,19 @@ function resolveLocalRef(root, ref) {
|
|
|
155
171
|
if (ref === '#') return root;
|
|
156
172
|
if (!ref.startsWith('#/')) decline('$ref:not-local');
|
|
157
173
|
let node = root;
|
|
174
|
+
let first = true;
|
|
158
175
|
for (const rawToken of ref.slice(2).split('/')) {
|
|
159
|
-
|
|
176
|
+
let token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
|
|
160
177
|
if (node === null || typeof node !== 'object') decline('$ref:unresolved');
|
|
178
|
+
// Draft 7 schemas reach the scanner with `definitions` renamed to `$defs`
|
|
179
|
+
// (lib/draft7.js) and their references as written; the other engines
|
|
180
|
+
// read either name, and so does this. Without it no draft 7 schema that
|
|
181
|
+
// used a definition got a scanner.
|
|
182
|
+
if (first && node === root && !Array.isArray(node) && !Object.prototype.hasOwnProperty.call(node, token)) {
|
|
183
|
+
if (token === 'definitions' && Object.prototype.hasOwnProperty.call(node, '$defs')) token = '$defs';
|
|
184
|
+
else if (token === '$defs' && Object.prototype.hasOwnProperty.call(node, 'definitions')) token = 'definitions';
|
|
185
|
+
}
|
|
186
|
+
first = false;
|
|
161
187
|
if (Array.isArray(node)) {
|
|
162
188
|
if (!/^\d+$/.test(token)) decline('$ref:unresolved');
|
|
163
189
|
node = node[Number(token)];
|
|
@@ -387,11 +413,45 @@ function spanHash(name) {
|
|
|
387
413
|
// is a jump table.
|
|
388
414
|
const HASH_DISPATCH_MIN = 48;
|
|
389
415
|
|
|
416
|
+
// The scan is first emitted as one function with every reference expanded
|
|
417
|
+
// where it is used, which is the fastest form: a call per member measured
|
|
418
|
+
// about twice the cost of csslintrc's scan, a small enum referenced thirty
|
|
419
|
+
// times. Only when a function would pass MAX_FUNCTION_SOURCE is it emitted
|
|
420
|
+
// again with large reference targets and large object members moved into
|
|
421
|
+
// functions of their own, so the source grows with the schema rather than
|
|
422
|
+
// with the number of references to it.
|
|
390
423
|
function compileScanner(schema, options) {
|
|
391
424
|
const opts = options || {};
|
|
425
|
+
const whole = build(schema, opts, false);
|
|
426
|
+
if (whole !== null || declineReason !== 'size') return finish(whole, opts);
|
|
427
|
+
return finish(build(schema, opts, true), opts);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function finish(built, opts) {
|
|
431
|
+
if (built === null) {
|
|
432
|
+
if (opts.onDecline) opts.onDecline(declineReason);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const { ctx, body } = built;
|
|
436
|
+
let fn;
|
|
437
|
+
try {
|
|
438
|
+
// eslint-disable-next-line no-new-func
|
|
439
|
+
fn = new Function('_h', '_r', ctx.subSource.join('\n') + '\nreturn function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
|
|
440
|
+
} catch {
|
|
441
|
+
return null; // a runtime without code generation has no scanner, by design
|
|
442
|
+
}
|
|
443
|
+
return { scan: fn, source: body, helpers: ctx.helpers.length, functions: [body, ...ctx.subSource] };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function build(schema, opts, split) {
|
|
392
447
|
const ctx = {
|
|
393
448
|
uid: 0, helpers: [], userFormats: opts.userFormats || null,
|
|
394
449
|
root: schema, refStack: [], nodes: 0,
|
|
450
|
+
// With `split`, reference targets map to null when expanded in place,
|
|
451
|
+
// else to the name of the function in subSource that scans one (see the
|
|
452
|
+
// $ref case in emitValue), and large members move out too (emitMember).
|
|
453
|
+
split, subs: new Map(), subSource: [], refCounts: split ? countRefs(schema) : null,
|
|
454
|
+
inlineBytes: new Map(),
|
|
395
455
|
};
|
|
396
456
|
declineReason = null;
|
|
397
457
|
let body;
|
|
@@ -404,21 +464,19 @@ function compileScanner(schema, options) {
|
|
|
404
464
|
out.push('if(i<len)return 0;');
|
|
405
465
|
out.push('return 1;');
|
|
406
466
|
body = out.join('\n');
|
|
467
|
+
// A function this large is a cost of its own, and V8's Maglev tier
|
|
468
|
+
// aborted the process compiling scanners of 500 KB and more (traefik,
|
|
469
|
+
// detekt from SchemaStore), in plain JavaScript, on Node 24 and 25, after
|
|
470
|
+
// about a hundred calls. Unsplit, past the bound the scan is emitted
|
|
471
|
+
// split; split, past either bound the schema has no scanner and the text
|
|
472
|
+
// path parses as it does without one.
|
|
473
|
+
if (body.length > MAX_FUNCTION_SOURCE || ctx.subSource.some((x) => x.length > MAX_FUNCTION_SOURCE)) decline('size');
|
|
474
|
+
if (body.length + ctx.subSource.reduce((n, x) => n + x.length, 0) > MAX_SOURCE) decline('size');
|
|
407
475
|
} catch (e) {
|
|
408
|
-
if (e === DECLINE)
|
|
409
|
-
if (opts.onDecline) opts.onDecline(declineReason);
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
476
|
+
if (e === DECLINE) return null;
|
|
412
477
|
throw e;
|
|
413
478
|
}
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
// eslint-disable-next-line no-new-func
|
|
417
|
-
fn = new Function('_h', '_r', 'return function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
|
|
418
|
-
} catch {
|
|
419
|
-
return null; // a runtime without code generation has no scanner, by design
|
|
420
|
-
}
|
|
421
|
-
return { scan: fn, source: body, helpers: ctx.helpers.length };
|
|
479
|
+
return { ctx, body };
|
|
422
480
|
}
|
|
423
481
|
|
|
424
482
|
function emitValue(schema, ctx, out, depth) {
|
|
@@ -443,9 +501,51 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
443
501
|
const ref = schema.$ref;
|
|
444
502
|
if (ctx.refStack.includes(ref)) decline('$ref:recursive');
|
|
445
503
|
const target = resolveLocalRef(ctx.root, ref);
|
|
504
|
+
if (!ctx.split) {
|
|
505
|
+
ctx.refStack.push(ref);
|
|
506
|
+
emitValue(target, ctx, out, depth);
|
|
507
|
+
ctx.refStack.pop();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Split (see compileScanner): a target whose expansions add up to more
|
|
511
|
+
// than a little source is emitted once, as a function of its own, and
|
|
512
|
+
// called wherever it is referenced. Expanded at every reference, the
|
|
513
|
+
// source grows with the number of references rather than with the
|
|
514
|
+
// schema: SchemaStore's traefik and detekt reach the same definitions
|
|
515
|
+
// from hundreds of places, and their scanners came to 0.5 to 1 MB, which
|
|
516
|
+
// V8's Maglev tier aborted the process compiling.
|
|
517
|
+
const sub = ctx.subs.get(target);
|
|
518
|
+
if (sub !== undefined) {
|
|
519
|
+
if (sub === null) {
|
|
520
|
+
ctx.refStack.push(ref);
|
|
521
|
+
emitValue(target, ctx, out, depth);
|
|
522
|
+
ctx.refStack.pop();
|
|
523
|
+
} else {
|
|
524
|
+
emitSubCall(sub, ctx, out);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const body = [];
|
|
446
529
|
ctx.refStack.push(ref);
|
|
447
|
-
emitValue(target, ctx,
|
|
530
|
+
emitValue(target, ctx, body, depth);
|
|
448
531
|
ctx.refStack.pop();
|
|
532
|
+
const text = body.join('\n');
|
|
533
|
+
// Expanded in place only while all its expansions together stay small:
|
|
534
|
+
// a small definition referenced from hundreds of places is source in
|
|
535
|
+
// hundreds of places.
|
|
536
|
+
if (text.length * (ctx.refCounts.get(ref) || 1) <= INLINE_REF_MAX) {
|
|
537
|
+
ctx.subs.set(target, null);
|
|
538
|
+
for (const line of body) out.push(line);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
|
|
542
|
+
ctx.subs.set(target, name);
|
|
543
|
+
// The emitted scan of a value starts with `i` at its first character and
|
|
544
|
+
// `c` holding it, and ends with `i` past it; it answers 0 (invalid) or -1
|
|
545
|
+
// (bail) by returning. Here it returns the new position instead, which is
|
|
546
|
+
// at least 1, so the three cannot be confused.
|
|
547
|
+
ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + text + '\nreturn i;}');
|
|
548
|
+
emitSubCall(name, ctx, out);
|
|
449
549
|
return;
|
|
450
550
|
}
|
|
451
551
|
|
|
@@ -478,6 +578,12 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
478
578
|
// present, not that it lists anything.
|
|
479
579
|
let compositeAllowed = true;
|
|
480
580
|
let compositeDelegate = null;
|
|
581
|
+
// The kinds the listed values have. A value of any other kind equals none
|
|
582
|
+
// of them, so its branch is left out and the value falls to the final
|
|
583
|
+
// `return 0`: csslintrc's `enum: [2, true, false]` used to scan strings
|
|
584
|
+
// only to reject them, at thirty references, and the dead branches alone
|
|
585
|
+
// pushed its scan past the size bound into calls.
|
|
586
|
+
let listedKinds = null;
|
|
481
587
|
if (has('const') || has('enum')) {
|
|
482
588
|
compositeAllowed = false;
|
|
483
589
|
const listed = [];
|
|
@@ -486,6 +592,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
486
592
|
if (!Array.isArray(schema.enum)) decline('enum:not-array');
|
|
487
593
|
for (const v of schema.enum) listed.push(v);
|
|
488
594
|
}
|
|
595
|
+
listedKinds = new Set(listed.map((v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)));
|
|
489
596
|
// An object or an array can only be compared as a whole. The scan steps
|
|
490
597
|
// over that one subtree, parses its own text, and hands it to the check
|
|
491
598
|
// the validator itself runs, so the document around it is still never
|
|
@@ -508,7 +615,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
508
615
|
+ 'if(!' + compositeDelegate + '(_cv' + k + '))return 0;'
|
|
509
616
|
+ 'i=_ce' + k + ';c=s.charCodeAt(i);'];
|
|
510
617
|
};
|
|
511
|
-
if (allows(types, 'object')) {
|
|
618
|
+
if (allows(types, 'object') && (listedKinds === null || listedKinds.has('object'))) {
|
|
512
619
|
if (compositeDelegate) branches.push(['c===123', compositeBranch()]);
|
|
513
620
|
else if (!compositeAllowed) branches.push(['c===123', ['return 0;']]);
|
|
514
621
|
else {
|
|
@@ -517,7 +624,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
517
624
|
branches.push(['c===123', b]);
|
|
518
625
|
}
|
|
519
626
|
}
|
|
520
|
-
if (allows(types, 'array')) {
|
|
627
|
+
if (allows(types, 'array') && (listedKinds === null || listedKinds.has('array'))) {
|
|
521
628
|
if (compositeDelegate) branches.push(['c===91', compositeBranch()]);
|
|
522
629
|
else if (!compositeAllowed) branches.push(['c===91', ['return 0;']]);
|
|
523
630
|
else {
|
|
@@ -526,22 +633,22 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
526
633
|
branches.push(['c===91', b]);
|
|
527
634
|
}
|
|
528
635
|
}
|
|
529
|
-
if (allows(types, 'string')) {
|
|
636
|
+
if (allows(types, 'string') && (listedKinds === null || listedKinds.has('string'))) {
|
|
530
637
|
const b = [];
|
|
531
638
|
emitString(schema, ctx, b);
|
|
532
639
|
branches.push(['c===34', b]);
|
|
533
640
|
}
|
|
534
|
-
if (allows(types, 'number')) {
|
|
641
|
+
if (allows(types, 'number') && (listedKinds === null || listedKinds.has('number'))) {
|
|
535
642
|
const b = [];
|
|
536
643
|
emitNumber(schema, ctx, b, types);
|
|
537
644
|
branches.push(['c===45||(c>=48&&c<=57)', b]);
|
|
538
645
|
}
|
|
539
|
-
if (allows(types, 'boolean')) {
|
|
646
|
+
if (allows(types, 'boolean') && (listedKinds === null || listedKinds.has('boolean'))) {
|
|
540
647
|
const b = [];
|
|
541
648
|
emitLiteral(schema, ctx, b, 'boolean');
|
|
542
649
|
branches.push(['c===116||c===102', b]);
|
|
543
650
|
}
|
|
544
|
-
if (allows(types, 'null')) {
|
|
651
|
+
if (allows(types, 'null') && (listedKinds === null || listedKinds.has('null'))) {
|
|
545
652
|
const b = [];
|
|
546
653
|
emitLiteral(schema, ctx, b, 'null');
|
|
547
654
|
branches.push(['c===110', b]);
|
|
@@ -558,6 +665,42 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
558
665
|
out.push(parts.join('\n'));
|
|
559
666
|
}
|
|
560
667
|
|
|
668
|
+
// How many times each `$ref` string occurs in the schema, for the expand or
|
|
669
|
+
// call decision. Schema positions and other values alike: a string that is not
|
|
670
|
+
// a reference is never looked up.
|
|
671
|
+
function countRefs(root) {
|
|
672
|
+
const counts = new Map();
|
|
673
|
+
const seen = new Set();
|
|
674
|
+
const walk = (n) => {
|
|
675
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return;
|
|
676
|
+
seen.add(n);
|
|
677
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return; }
|
|
678
|
+
if (typeof n.$ref === 'string') counts.set(n.$ref, (counts.get(n.$ref) || 0) + 1);
|
|
679
|
+
for (const k of Object.keys(n)) walk(n[k]);
|
|
680
|
+
};
|
|
681
|
+
walk(root);
|
|
682
|
+
return counts;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// A name with no character a JSON string would have to escape, so its raw
|
|
686
|
+
// text in a document is the name itself.
|
|
687
|
+
function plainName(name) {
|
|
688
|
+
for (let k = 0; k < name.length; k++) {
|
|
689
|
+
const c = name.charCodeAt(k);
|
|
690
|
+
if (c < 32 || c === 34 || c === 92) return false;
|
|
691
|
+
}
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// A call answers invalid with the literal `return 0;`, as inline code does:
|
|
696
|
+
// an object member rewrites that text into its own failure path, which goes on
|
|
697
|
+
// scanning (a later duplicate key can replace the value), and a different
|
|
698
|
+
// spelling would end the whole scan instead.
|
|
699
|
+
function emitSubCall(name, ctx, out) {
|
|
700
|
+
const r = '_rq' + (ctx.uid++);
|
|
701
|
+
out.push('{var ' + r + '=' + name + '(s,i);if(' + r + '===-1)return -1;if(' + r + '===0)return 0;i=' + r + ';c=s.charCodeAt(i);}');
|
|
702
|
+
}
|
|
703
|
+
|
|
561
704
|
// Any JSON value, validated as JSON and stepped over.
|
|
562
705
|
function emitSkip(out) {
|
|
563
706
|
out.push('{var _j=_r.skipValue(s,i);if(_j===-2)return -1;if(_j<0)return 0;i=_j;c=s.charCodeAt(i);}');
|
|
@@ -749,8 +892,27 @@ function emitArray(schema, ctx, out, depth) {
|
|
|
749
892
|
// the rule reaches nested objects: an inner object's own verdict is a value
|
|
750
893
|
// failure at the level above it.
|
|
751
894
|
function emitMember(sub, ctx, out, depth, objId) {
|
|
752
|
-
|
|
895
|
+
let lines = [];
|
|
753
896
|
emitValue(sub, ctx, lines, depth + 1);
|
|
897
|
+
// A large member value becomes a function of its own, so no one function
|
|
898
|
+
// grows with the whole schema: detekt nests hundreds of rule objects inline,
|
|
899
|
+
// no references involved. The value's code keeps its own labels and
|
|
900
|
+
// variables, so it moves as it is; the call answers invalid with
|
|
901
|
+
// `return 0;`, which the rewrite below turns into this member's failure.
|
|
902
|
+
// An object's members are judged together as well as one by one: thirty
|
|
903
|
+
// members just under SPLIT_MIN each would stay inline and add up to a
|
|
904
|
+
// function past the bound, so once the members kept inline in this object
|
|
905
|
+
// pass SPLIT_OBJECT, the rest move out whatever their size.
|
|
906
|
+
const size = lines.join('\n').length;
|
|
907
|
+
const kept = ctx.split ? (ctx.inlineBytes.get(objId) || 0) : 0;
|
|
908
|
+
if (ctx.split && (size > SPLIT_MIN || kept + size > SPLIT_OBJECT)) {
|
|
909
|
+
const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
|
|
910
|
+
ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + lines.join('\n') + '\nreturn i;}');
|
|
911
|
+
lines = [];
|
|
912
|
+
emitSubCall(name, ctx, lines);
|
|
913
|
+
} else if (ctx.split) {
|
|
914
|
+
ctx.inlineBytes.set(objId, kept + size);
|
|
915
|
+
}
|
|
754
916
|
const body = lines.join('\n');
|
|
755
917
|
if (!body.includes('return 0;')) { out.push(body); return; }
|
|
756
918
|
const label = '_mb' + (ctx.uid++);
|
|
@@ -828,10 +990,32 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
828
990
|
hashById = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
829
991
|
}
|
|
830
992
|
}
|
|
831
|
-
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
|
|
993
|
+
// A key's end is found with indexOf, which is native, where the character
|
|
994
|
+
// loop below costs three compares a character: on csslintrc's 35 keys of
|
|
995
|
+
// twenty-odd characters the loop was most of the scan and made it slower
|
|
996
|
+
// than parsing. Every declared name is plain text, so a key that compares
|
|
997
|
+
// equal to one, character by character, is that name as written, and a
|
|
998
|
+
// key that does not is checked for an escape or a control character before
|
|
999
|
+
// it is read as additional. Either one bails, and so does a span that ends
|
|
1000
|
+
// on a backslash, since that quote may be escaped: the parser decides what
|
|
1001
|
+
// the key really is.
|
|
1002
|
+
// indexOf costs about the same whatever the key's length, the loop about a
|
|
1003
|
+
// nanosecond a character: on short keys the loop wins (problem-object's
|
|
1004
|
+
// `type`, `title`, `status` measured 10 to 15 percent slower by indexOf),
|
|
1005
|
+
// and from ten characters indexOf wins by 40 percent and more, so the
|
|
1006
|
+
// choice follows the declared names' average length.
|
|
1007
|
+
const indexKey = !useHash && names.length > 0 && names.every(plainName)
|
|
1008
|
+
&& names.reduce((n, x) => n + x.length, 0) >= INDEX_KEY_MIN_AVG * names.length;
|
|
1009
|
+
if (indexKey) {
|
|
1010
|
+
// A quote with a backslash before it may be escaped, and then the key
|
|
1011
|
+
// goes on past it: bail before reading what follows as the colon.
|
|
1012
|
+
out.push('var _p' + kn + '=++i,_e' + kn + '=s.indexOf(\'"\',i);if(_e' + kn + '<0)return 0;if(s.charCodeAt(_e' + kn + '-1)===92)return -1;i=_e' + kn + ';c=s.charCodeAt(++i);');
|
|
1013
|
+
} else {
|
|
1014
|
+
emitStringSpan(ctx, out, kn, false, hashById !== null);
|
|
1015
|
+
// A key written with an escape decodes to a name the raw span does not
|
|
1016
|
+
// show, so the dispatch below would read it as a different property.
|
|
1017
|
+
out.push('if(_x' + kn + ')return -1;');
|
|
1018
|
+
}
|
|
835
1019
|
out.push(WS);
|
|
836
1020
|
out.push('if(c!==58)return 0;');
|
|
837
1021
|
out.push('c=s.charCodeAt(++i);');
|
|
@@ -958,6 +1142,7 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
958
1142
|
additional.push(lines.join('\n').split('return 0;').join('return -1;'));
|
|
959
1143
|
}
|
|
960
1144
|
|
|
1145
|
+
if (indexKey) additional.unshift('if(!_r.plainKey(s,_p' + kn + ',_e' + kn + '))return -1;');
|
|
961
1146
|
out.push('switch(_kl' + kn + '){');
|
|
962
1147
|
for (const [len, group] of byLen) {
|
|
963
1148
|
out.push('case ' + len + ':{');
|
package/lib/scan-runtime.js
CHANGED
|
@@ -155,4 +155,16 @@ function span(s, p, e, escaped) {
|
|
|
155
155
|
return JSON.parse(s.slice(p - 1, e + 1));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
// Whether the raw text of a key between `p` and `e` is its name as written:
|
|
159
|
+
// no escape, no control character. A scanner that found the key's end with
|
|
160
|
+
// indexOf calls this for a name it did not recognise; one it recognised was
|
|
161
|
+
// compared character by character against a name that has neither.
|
|
162
|
+
function plainKey(s, p, e) {
|
|
163
|
+
for (let k = p; k < e; k++) {
|
|
164
|
+
const c = s.charCodeAt(k);
|
|
165
|
+
if (c < 32 || c === 92) return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { hex4, skipValue, skipString, skipNumber, span, plainKey, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
|
package/lib/validator-core.js
CHANGED
|
@@ -215,8 +215,27 @@ function codegenAvailable() {
|
|
|
215
215
|
return _codegenAvailable;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
// Schema compilation cache: same schema string -> reuse compiled functions
|
|
218
|
+
// Schema compilation cache: same schema string -> reuse compiled functions.
|
|
219
|
+
// It and the preprocess cache below are bounded, oldest out first. Unbounded,
|
|
220
|
+
// they held every schema's text, compiled functions and their sources for the
|
|
221
|
+
// life of the process: after compiling each of SchemaStore's schemas once and
|
|
222
|
+
// dropping the validators, 1.2 GB stayed on the heap. A schema compiled again
|
|
223
|
+
// after its entry left is compiled again, the cost of a miss and no more.
|
|
224
|
+
const CACHE_ENTRIES = 128;
|
|
225
|
+
const CACHE_KEY_CHARS = 16 * 1024 * 1024;
|
|
219
226
|
const _compileCache = new Map();
|
|
227
|
+
// Adds an entry and drops the oldest past either bound. The key characters
|
|
228
|
+
// are counted on the map itself.
|
|
229
|
+
function _boundedSet(map, key, entry) {
|
|
230
|
+
if (!map.has(key)) map._chars = (map._chars || 0) + key.length;
|
|
231
|
+
map.set(key, entry);
|
|
232
|
+
while (map.size > CACHE_ENTRIES || (map._chars > CACHE_KEY_CHARS && map.size > 1)) {
|
|
233
|
+
const oldest = map.keys().next().value;
|
|
234
|
+
map.delete(oldest);
|
|
235
|
+
map._chars -= oldest.length;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function _compileCacheSet(key, entry) { _boundedSet(_compileCache, key, entry); }
|
|
220
239
|
// Generated preprocess passes, keyed like _compileCache plus the options that
|
|
221
240
|
// shape the pass. Only passes that exist are kept: a schema with nothing to
|
|
222
241
|
// rewrite answers `null` quickly, and keeping that answer held its key, the
|
|
@@ -911,7 +930,7 @@ class Validator {
|
|
|
911
930
|
// A function bound to this validator's keyword functions or formats is
|
|
912
931
|
// not the schema's alone, so it is not shared through the cache.
|
|
913
932
|
if (!uf && !this._keywords) {
|
|
914
|
-
|
|
933
|
+
_compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
|
|
915
934
|
}
|
|
916
935
|
}
|
|
917
936
|
this._jsFn = jsFn;
|
|
@@ -938,7 +957,7 @@ class Validator {
|
|
|
938
957
|
if (hit !== undefined) preprocess = hit;
|
|
939
958
|
else {
|
|
940
959
|
preprocess = _codegen.buildPreprocess(preprocessSchema, options) || null;
|
|
941
|
-
if (ppKey !== null && preprocess !== null) _preprocessCache
|
|
960
|
+
if (ppKey !== null && preprocess !== null) _boundedSet(_preprocessCache, ppKey, preprocess);
|
|
942
961
|
}
|
|
943
962
|
}
|
|
944
963
|
if (!preprocess) {
|
|
@@ -2259,4 +2278,4 @@ module.exports = {
|
|
|
2259
2278
|
// surface of `require('ata-validator')` is what it was.
|
|
2260
2279
|
Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
|
|
2261
2280
|
// What the compiled paths in index.js need from this module.
|
|
2262
|
-
Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
|
|
2281
|
+
Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"release:check": "node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
|
|
65
65
|
"build": "cmake-js build --target ata",
|
|
66
66
|
"rebuild": "cmake-js rebuild --target ata",
|
|
67
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_number_text_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_encoded_refs.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_schema_code_injection.js && node tests/test_aot_html_safe.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_custom_keyword_codegen.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_error_read_paths.js && node tests/test_unique_items_error_pair.js && node tests/test_rich_errors_generated.js && node tests/test_captured_verdict_stub.js && node tests/test_generated_returns_rewrite.js && node tests/test_deep_pointer_refs.js && node tests/test_uneval_props_dynamic.js && node tests/test_one_pass_no_fallback.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_render_without_process.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
|
|
67
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_number_text_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_encoded_refs.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_schema_code_injection.js && node tests/test_aot_html_safe.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_custom_keyword_codegen.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_error_read_paths.js && node tests/test_unique_items_error_pair.js && node tests/test_rich_errors_generated.js && node tests/test_captured_verdict_stub.js && node tests/test_generated_returns_rewrite.js && node tests/test_deep_pointer_refs.js && node tests/test_uneval_props_dynamic.js && node tests/test_one_pass_no_fallback.js && node tests/test_scanner_size_cap.js && node tests/test_scanner_index_keys.js && node tests/test_both_defs_containers.js && node tests/test_uneval_branch_counts.js && node tests/test_branch_verdicts.js && node tests/test_shared_definitions.js && node tests/test_compile_cache_bound.js && node tests/test_uneval_items_dynamic.js && node tests/test_escaped_property_names.js && node tests/test_ref_siblings_combined.js && node tests/test_unicode_property_patterns.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_render_without_process.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
|
|
68
68
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
69
69
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
70
70
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"LICENSE"
|
|
144
144
|
],
|
|
145
145
|
"optionalDependencies": {
|
|
146
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
147
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
148
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
149
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
150
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
151
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
152
|
-
"@ata-validator/native-win32-x64": "1.
|
|
146
|
+
"@ata-validator/native-darwin-arm64": "1.43.0",
|
|
147
|
+
"@ata-validator/native-darwin-x64": "1.43.0",
|
|
148
|
+
"@ata-validator/native-linux-arm64-gnu": "1.43.0",
|
|
149
|
+
"@ata-validator/native-linux-arm64-musl": "1.43.0",
|
|
150
|
+
"@ata-validator/native-linux-x64-gnu": "1.43.0",
|
|
151
|
+
"@ata-validator/native-linux-x64-musl": "1.43.0",
|
|
152
|
+
"@ata-validator/native-win32-x64": "1.43.0"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"yaml": "^2.0.0"
|