ata-validator 1.53.0 → 1.55.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/README.md +35 -26
- package/compat.js +13 -2
- package/error-runtime.d.ts +7 -0
- package/error-runtime.js +10 -1
- package/error-runtime.mjs +1 -1
- package/lib/aot-impl.js +26 -3
- package/lib/codegen-engine.js +2 -1
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +17 -28
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +9 -4
- package/lib/formats-source.js +50 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +55 -144
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +537 -65
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +113 -33
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +19 -28
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +20 -11
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/interpreter-core.js
CHANGED
|
@@ -460,6 +460,21 @@ class Plan {
|
|
|
460
460
|
this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
+
// A required key holding undefined is missing. Where the key is also a
|
|
464
|
+
// declared property, the property walk, which reads the value anyway,
|
|
465
|
+
// finds that (requiredInProps); the required check reads the value only
|
|
466
|
+
// for the other keys (requiredUndef), so a valid object pays no second
|
|
467
|
+
// read of each required value.
|
|
468
|
+
this.requiredInProps = null;
|
|
469
|
+
this.requiredUndef = null;
|
|
470
|
+
if (this.required !== null) {
|
|
471
|
+
const undef = this.required.map((k) => this.properties === null || !this.properties.has(k));
|
|
472
|
+
if (undef.includes(true)) this.requiredUndef = undef;
|
|
473
|
+
if (this.properties !== null) {
|
|
474
|
+
const inProps = this.required.filter((k) => this.properties.has(k));
|
|
475
|
+
if (inProps.length > 0) this.requiredInProps = new Set(inProps);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
463
478
|
this.patternProperties = null;
|
|
464
479
|
if (isObjectMap(schema.patternProperties)) {
|
|
465
480
|
this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
|
|
@@ -601,7 +616,8 @@ function evalLeafV(P, data) {
|
|
|
601
616
|
if (P.required !== null) {
|
|
602
617
|
const req = P.required;
|
|
603
618
|
for (let i = 0; i < req.length; i++) {
|
|
604
|
-
|
|
619
|
+
// A key holding undefined is missing, as in the default validator.
|
|
620
|
+
if (!Object.hasOwn(data, req[i]) || (P.requiredUndef !== null && P.requiredUndef[i] && data[req[i]] === undefined)) return false;
|
|
605
621
|
}
|
|
606
622
|
}
|
|
607
623
|
if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
|
|
@@ -659,71 +675,6 @@ function dataCtx(instancePath, schemaPath, keyword) {
|
|
|
659
675
|
return { instancePath, parentData: parent, parentDataProperty: key, rootData: root, schemaPath: schemaPath + '/' + keyword };
|
|
660
676
|
}
|
|
661
677
|
|
|
662
|
-
function isDiscriminated(schema) {
|
|
663
|
-
const d = schema.discriminator;
|
|
664
|
-
return d !== null && typeof d === 'object' && typeof d.propertyName === 'string';
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// The reference's coercion: the first type in the list the value converts to,
|
|
668
|
-
// after unwrapping a one-element array in 'array' mode, which then converts
|
|
669
|
-
// the element.
|
|
670
|
-
const NO_COERCION = Symbol('no coercion');
|
|
671
|
-
function fitsType(d, t) {
|
|
672
|
-
switch (t) {
|
|
673
|
-
case 'string': return typeof d === 'string';
|
|
674
|
-
case 'number': return typeof d === 'number' && isFinite(d);
|
|
675
|
-
case 'integer': return typeof d === 'number' && Number.isInteger(d);
|
|
676
|
-
case 'boolean': return typeof d === 'boolean';
|
|
677
|
-
case 'null': return d === null;
|
|
678
|
-
case 'array': return Array.isArray(d);
|
|
679
|
-
case 'object': return d !== null && typeof d === 'object' && !Array.isArray(d);
|
|
680
|
-
default: return false;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
// Whether an unwrapped element is kept as it is. The reference tests a list
|
|
684
|
-
// of types here with every type at once, so a list of two or more keeps
|
|
685
|
-
// nothing (`number` stands in for `integer` beside it), and the element goes
|
|
686
|
-
// on to be converted, as one of a single type does when it does not fit.
|
|
687
|
-
function unwrappedFits(d, types) {
|
|
688
|
-
let list = types;
|
|
689
|
-
if (list.length > 1 && list.includes('number')) list = list.filter((t) => t !== 'integer');
|
|
690
|
-
if (list.length > 1 && list.includes('array') && list.includes('object')) return false;
|
|
691
|
-
for (const t of list) if (!fitsType(d, t)) return false;
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
function coerceValue(data, types, arrayMode) {
|
|
695
|
-
if (arrayMode && Array.isArray(data) && data.length === 1) {
|
|
696
|
-
data = data[0];
|
|
697
|
-
if (unwrappedFits(data, types)) return data;
|
|
698
|
-
}
|
|
699
|
-
const kind = data === null ? 'null' : typeof data;
|
|
700
|
-
for (const t of types) {
|
|
701
|
-
switch (t) {
|
|
702
|
-
case 'number':
|
|
703
|
-
case 'integer':
|
|
704
|
-
if (kind === 'boolean' || kind === 'null') return +data;
|
|
705
|
-
// eslint-disable-next-line eqeqeq
|
|
706
|
-
if (kind === 'string' && data !== '' && data == +data && (t === 'number' || !(data % 1))) return +data;
|
|
707
|
-
break;
|
|
708
|
-
case 'string':
|
|
709
|
-
if (kind === 'number' || kind === 'boolean') return '' + data;
|
|
710
|
-
if (kind === 'null') return '';
|
|
711
|
-
break;
|
|
712
|
-
case 'boolean':
|
|
713
|
-
if (data === 'false' || data === 0 || data === null) return false;
|
|
714
|
-
if (data === 'true' || data === 1) return true;
|
|
715
|
-
break;
|
|
716
|
-
case 'null':
|
|
717
|
-
if (data === '' || data === 0 || data === false) return null;
|
|
718
|
-
break;
|
|
719
|
-
case 'array':
|
|
720
|
-
if (arrayMode && (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'null')) return [data];
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return NO_COERCION;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
678
|
// After a context-reading keyword, the value is read again from where it sits:
|
|
728
679
|
// the keyword may have replaced it (a date deserialized, a value serialized),
|
|
729
680
|
// and the keywords after it check what it left, as the default validator's
|
|
@@ -855,7 +806,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
|
855
806
|
const req = P.required;
|
|
856
807
|
for (let i = 0; i < req.length; i++) {
|
|
857
808
|
const key = req[i];
|
|
858
|
-
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
809
|
+
if (!Object.hasOwn(data, key) || (P.requiredUndef !== null && P.requiredUndef[i] && data[key] === undefined)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
859
810
|
}
|
|
860
811
|
}
|
|
861
812
|
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
@@ -909,6 +860,9 @@ class Interpreter {
|
|
|
909
860
|
// `discriminator` option: the tag picks the one oneOf branch to apply.
|
|
910
861
|
this.discriminator = opts.discriminator === true;
|
|
911
862
|
this._discr = null;
|
|
863
|
+
// Both come from compat, which installs the code for them (see
|
|
864
|
+
// lib/interpreter-compat.js), so a bundle without compat carries none of it.
|
|
865
|
+
this.ext = this.coerce !== false || this.discriminator ? compatExt() : null;
|
|
912
866
|
// The compiled closures know none of these.
|
|
913
867
|
if (this.dataContext || this.discriminator || this.numberFormats !== null) this._fast = null;
|
|
914
868
|
this._ctxRoot = undefined;
|
|
@@ -922,7 +876,14 @@ class Interpreter {
|
|
|
922
876
|
if (this._fast === undefined) {
|
|
923
877
|
const compile = planCompiler();
|
|
924
878
|
if (compile === null) this._fast = null;
|
|
925
|
-
else
|
|
879
|
+
else {
|
|
880
|
+
try { this._fast = compile(this); } catch (e) {
|
|
881
|
+
// The plain walk answers instead, which tests cannot tell apart;
|
|
882
|
+
// tests/test_plan_compiles.js holds the compiler to the suite.
|
|
883
|
+
if (typeof process === 'object' && process !== null && process.env && process.env.ATA_DEBUG) console.error('ata: plan compiler failed, plain walk used:', e && e.message);
|
|
884
|
+
this._fast = null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
926
887
|
}
|
|
927
888
|
return this._fast;
|
|
928
889
|
}
|
|
@@ -1028,75 +989,6 @@ class Interpreter {
|
|
|
1028
989
|
}
|
|
1029
990
|
}
|
|
1030
991
|
|
|
1031
|
-
// The discriminator in place of oneOf: the branch the tag names is applied,
|
|
1032
|
-
// and a tag that is not a string, or names no branch, is the error.
|
|
1033
|
-
discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, local) {
|
|
1034
|
-
const d = this.discrMap(P, base);
|
|
1035
|
-
const tag = data[d.tagName];
|
|
1036
|
-
if (typeof tag !== 'string') {
|
|
1037
|
-
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'tag', tag: d.tagName, tagValue: tag }, `tag "${d.tagName}" must be string`));
|
|
1038
|
-
return false;
|
|
1039
|
-
}
|
|
1040
|
-
const i = d.map.get(tag);
|
|
1041
|
-
if (i === undefined) {
|
|
1042
|
-
if (errors !== NOERRORS) errors.push(err('discriminator', 'discriminator', instancePath, schemaPath + '/discriminator', { error: 'mapping', tag: d.tagName, tagValue: tag }, `value of tag "${d.tagName}" must be in oneOf`));
|
|
1043
|
-
return false;
|
|
1044
|
-
}
|
|
1045
|
-
const sub = local !== DISCARD ? { props: null, items: null } : DISCARD;
|
|
1046
|
-
const ok = this.eval(P.oneOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/oneOf/' + i, stack, sub);
|
|
1047
|
-
if (ok && local !== DISCARD) mergeAnnotations(local, sub);
|
|
1048
|
-
return ok;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Tag value to branch index, read from each branch's `properties`, through
|
|
1052
|
-
// a `$ref`, with the reference's rules: a `const` or `enum` of unique
|
|
1053
|
-
// strings, and the tag required by the node or by every branch.
|
|
1054
|
-
discrMap(P, base) {
|
|
1055
|
-
if (this._discr === null) this._discr = new Map();
|
|
1056
|
-
let d = this._discr.get(P);
|
|
1057
|
-
if (d !== undefined) return d;
|
|
1058
|
-
const tagName = P.schema.discriminator.propertyName;
|
|
1059
|
-
if (P.schema.discriminator.mapping !== undefined) throw new Error('discriminator: mapping is not supported');
|
|
1060
|
-
const hasRequired = (s) => s !== null && typeof s === 'object' && Array.isArray(s.required) && s.required.includes(tagName);
|
|
1061
|
-
let topRequired = hasRequired(P.schema);
|
|
1062
|
-
const map = new Map();
|
|
1063
|
-
for (let i = 0; i < P.oneOf.length; i++) {
|
|
1064
|
-
let node = P.oneOf[i];
|
|
1065
|
-
if (node !== null && typeof node === 'object' && node.ref !== null && node.ref !== undefined && (node.schema.properties === undefined)) {
|
|
1066
|
-
const r = this.resolveRefCached(node, base);
|
|
1067
|
-
if (r.child === undefined) throw new Error(`discriminator: cannot resolve $ref ${node.ref}`);
|
|
1068
|
-
node = r.child;
|
|
1069
|
-
}
|
|
1070
|
-
const sch = node !== null && typeof node === 'object' ? node.schema : undefined;
|
|
1071
|
-
const propSch = sch && sch.properties ? sch.properties[tagName] : undefined;
|
|
1072
|
-
if (propSch === null || typeof propSch !== 'object') throw new Error(`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`);
|
|
1073
|
-
topRequired = topRequired || hasRequired(sch);
|
|
1074
|
-
const values = propSch.const !== undefined ? [propSch.const] : Array.isArray(propSch.enum) ? propSch.enum : null;
|
|
1075
|
-
if (values === null) throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`);
|
|
1076
|
-
for (const v of values) {
|
|
1077
|
-
if (typeof v !== 'string' || map.has(v)) throw new Error(`discriminator: "${tagName}" values must be unique strings`);
|
|
1078
|
-
map.set(v, i);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
if (!topRequired) throw new Error(`discriminator: "${tagName}" must be required`);
|
|
1082
|
-
d = { tagName, map };
|
|
1083
|
-
this._discr.set(P, d);
|
|
1084
|
-
return d;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// Coerces the value at `instancePath` to the node's type and writes it where
|
|
1088
|
-
// it sits; at the root, where it sits nowhere, the walk carries it on.
|
|
1089
|
-
coerceAt(P, data, instancePath) {
|
|
1090
|
-
const to = coerceValue(data, P.typeNames, this.coerce === 'array');
|
|
1091
|
-
if (to === NO_COERCION) return data;
|
|
1092
|
-
if (instancePath === '') this._ctxRoot = to;
|
|
1093
|
-
else {
|
|
1094
|
-
const ctx = dataCtx(instancePath, '', '');
|
|
1095
|
-
if (ctx.parentData !== null && typeof ctx.parentData === 'object') ctx.parentData[ctx.parentDataProperty] = to;
|
|
1096
|
-
}
|
|
1097
|
-
return to;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
992
|
// The value at `instancePath` as it is now, after a subschema may have
|
|
1101
993
|
// coerced it.
|
|
1102
994
|
here(instancePath, data) {
|
|
@@ -1183,7 +1075,7 @@ class Interpreter {
|
|
|
1183
1075
|
const local = collect ? { props: null, items: null } : DISCARD;
|
|
1184
1076
|
|
|
1185
1077
|
// Coercion first, before anything reads the value, as the reference does it.
|
|
1186
|
-
if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.coerceAt(P, data, instancePath);
|
|
1078
|
+
if (this.coerce !== false && P.hasType && (dataBits(data) & P.typeMask) === 0) data = this.ext.coerceAt(this, P, data, instancePath);
|
|
1187
1079
|
|
|
1188
1080
|
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
1189
1081
|
if (P.ref !== null) {
|
|
@@ -1331,7 +1223,7 @@ class Interpreter {
|
|
|
1331
1223
|
const req = P.required;
|
|
1332
1224
|
for (let i = 0; i < req.length; i++) {
|
|
1333
1225
|
const key = req[i];
|
|
1334
|
-
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1226
|
+
if (!Object.hasOwn(data, key) || (P.requiredUndef !== null && P.requiredUndef[i] && data[key] === undefined)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1335
1227
|
}
|
|
1336
1228
|
}
|
|
1337
1229
|
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
@@ -1359,8 +1251,12 @@ class Interpreter {
|
|
|
1359
1251
|
if (prop !== undefined) {
|
|
1360
1252
|
// A declared property whose value is undefined is not checked, as the
|
|
1361
1253
|
// generated code does not check it: no JSON document carries one, and
|
|
1362
|
-
// serialized the key is not there.
|
|
1363
|
-
|
|
1254
|
+
// serialized the key is not there. Required, it is missing (see
|
|
1255
|
+
// requiredInProps).
|
|
1256
|
+
const value = data[key];
|
|
1257
|
+
if (value !== undefined) {
|
|
1258
|
+
if (!this.eval(prop.node, value, base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
|
|
1259
|
+
} else if (P.requiredInProps !== null && P.requiredInProps.has(key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1364
1260
|
evaluated = true;
|
|
1365
1261
|
}
|
|
1366
1262
|
if (patterns !== null) {
|
|
@@ -1467,8 +1363,8 @@ class Interpreter {
|
|
|
1467
1363
|
valid = false;
|
|
1468
1364
|
}
|
|
1469
1365
|
}
|
|
1470
|
-
if (P.oneOf !== null && this.discriminator && isDiscriminated(schema)) {
|
|
1471
|
-
if (bits === T_OBJECT && !this.discriminate(P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
|
|
1366
|
+
if (P.oneOf !== null && this.discriminator && this.ext.isDiscriminated(schema)) {
|
|
1367
|
+
if (bits === T_OBJECT && !this.ext.discriminate(this, P, data, base, dynScope, errors, instancePath, schemaPath, stack, collect ? local : DISCARD)) valid = false;
|
|
1472
1368
|
} else if (P.oneOf !== null) {
|
|
1473
1369
|
let count = 0;
|
|
1474
1370
|
let winner = null;
|
|
@@ -1613,18 +1509,33 @@ function planCompiler() {
|
|
|
1613
1509
|
compileInterpreter = loadPlanCompiler()({
|
|
1614
1510
|
Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
1615
1511
|
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
1616
|
-
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1512
|
+
T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1617
1513
|
}).compileInterpreter;
|
|
1618
1514
|
}
|
|
1619
1515
|
return compileInterpreter;
|
|
1620
1516
|
}
|
|
1621
1517
|
|
|
1518
|
+
let installCompat = null;
|
|
1519
|
+
let compatInstalled = null;
|
|
1520
|
+
function setCompat(install) {
|
|
1521
|
+
installCompat = install;
|
|
1522
|
+
compatInstalled = null;
|
|
1523
|
+
}
|
|
1524
|
+
function compatExt() {
|
|
1525
|
+
if (compatInstalled === null) {
|
|
1526
|
+
if (installCompat === null) throw new Error('coercion where each type is checked and the discriminator are installed by ata-validator/compat');
|
|
1527
|
+
compatInstalled = installCompat({ NOERRORS, DISCARD, err, mergeAnnotations, dataCtx });
|
|
1528
|
+
}
|
|
1529
|
+
return compatInstalled;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1622
1532
|
function createInterpreter(schema, options) {
|
|
1623
1533
|
return new Interpreter(schema, options);
|
|
1624
1534
|
}
|
|
1625
1535
|
|
|
1626
1536
|
module.exports = { createInterpreter };
|
|
1627
1537
|
Object.defineProperty(module.exports, '_setPlanCompiler', { value: setPlanCompiler, enumerable: false });
|
|
1538
|
+
Object.defineProperty(module.exports, '_setCompat', { value: setCompat, enumerable: false });
|
|
1628
1539
|
// The resolver, for the code generator: it resolves references at compile
|
|
1629
1540
|
// time the way this engine does at run time (normalizeRefs in js-compiler).
|
|
1630
1541
|
Object.defineProperty(module.exports, '_refInternals', {
|
|
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
|
|
|
9
9
|
// the first call that compiles errors. Everything here was written as part
|
|
10
10
|
// of js-compiler.js and reads its helpers from that module's _internal.
|
|
11
11
|
const core = require('./js-compiler')
|
|
12
|
-
const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
|
|
12
|
+
const { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag } = core._internal
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
function emitPtrEsc(ctx) {
|
|
@@ -590,9 +590,19 @@ function subtreeGuard (sub, ctx, gv) {
|
|
|
590
590
|
// 25-definition API schema came to 55 KB of source in a 133 KB function,
|
|
591
591
|
// and a function that size reaches the optimizing compiler late.
|
|
592
592
|
ctx.shareDefs = true
|
|
593
|
+
// A record of many optional properties takes the verdict's key-loop form
|
|
594
|
+
// (keyLoopEligible): its optional properties are checked from the record's
|
|
595
|
+
// own keys. Written one read per property, the guard of a 150-property
|
|
596
|
+
// record was 16.9 KB, over GUARD_MAX, so there was no guard at all and
|
|
597
|
+
// every record of the list was collected in full; on records of mixed
|
|
598
|
+
// shapes those reads are megamorphic, and 2000 of them cost 18 ms where
|
|
599
|
+
// the verdict took 0.3. The key-loop form is 10 KB and reads only the keys
|
|
600
|
+
// the record has.
|
|
601
|
+
const outerKeyLoop = ctx.keyLoopNode
|
|
602
|
+
if (keyLoopEligible(sub)) ctx.keyLoopNode = sub
|
|
593
603
|
try {
|
|
594
604
|
const bl = []
|
|
595
|
-
try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare }
|
|
605
|
+
try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare; ctx.keyLoopNode = outerKeyLoop }
|
|
596
606
|
if (bl.length === 0) { restoreCtx(ctx, saved); return null }
|
|
597
607
|
const body = bl.join(';')
|
|
598
608
|
// The verdict generator's recursion names (`_validate`, the cycle guard
|
|
@@ -612,17 +622,20 @@ function subtreeGuard (sub, ctx, gv) {
|
|
|
612
622
|
}
|
|
613
623
|
}
|
|
614
624
|
// The collecting code for `sub` at `v`, behind its guard when there is one.
|
|
615
|
-
|
|
625
|
+
// `when`, a boolean variable, makes the guard conditional: when it is false
|
|
626
|
+
// the value is collected without a verdict first (see the items case).
|
|
627
|
+
function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix, when) {
|
|
616
628
|
const gi = ctx.varCounter++
|
|
617
629
|
const gv = `_gv${gi}`
|
|
618
630
|
const g = subtreeGuard(sub, ctx, gv)
|
|
619
631
|
if (g === null) { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix); return }
|
|
620
|
-
|
|
632
|
+
const cond = when ? `!${when}||` : ''
|
|
633
|
+
if (g.name !== undefined) lines.push(`if(${cond}!${g.name}(${v})){`)
|
|
621
634
|
else {
|
|
622
635
|
// The verdict's exits become jumps out of the block: a failure leaves
|
|
623
636
|
// the flag set, success clears it. replaceTopLevel keeps nested
|
|
624
637
|
// closures' returns as they are (see the hybrid rewrite).
|
|
625
|
-
lines.push(`{const ${gv}=${v};let _g${gi}=1
|
|
638
|
+
lines.push(`{const ${gv}=${v};let _g${gi}=1;${when ? `if(${when})` : ''}g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
|
|
626
639
|
}
|
|
627
640
|
ctx.guardDepth = (ctx.guardDepth || 0) + 1
|
|
628
641
|
try { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix) } finally { ctx.guardDepth-- }
|
|
@@ -1164,7 +1177,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1164
1177
|
const reqSp = `${schemaPrefix}/required`
|
|
1165
1178
|
const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
|
|
1166
1179
|
for (const key of schema.required) {
|
|
1167
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
1180
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
1168
1181
|
}
|
|
1169
1182
|
}
|
|
1170
1183
|
|
|
@@ -1354,7 +1367,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1354
1367
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
1355
1368
|
const sp = schemaPrefix+'/properties/'+ptrSeg(key)
|
|
1356
1369
|
const from = lines.length
|
|
1357
|
-
|
|
1370
|
+
// A value that is undefined is not checked (see the verdict generator).
|
|
1371
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
|
|
1358
1372
|
// A large subschema as a function of its own (emitPropFnE); false means inline.
|
|
1359
1373
|
if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
|
|
1360
1374
|
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
|
|
@@ -1769,7 +1783,10 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
|
|
|
1769
1783
|
// the definition and the reference's own comes in as `_sp`.
|
|
1770
1784
|
const rel = sp.startsWith(SP_PLACEHOLDER)
|
|
1771
1785
|
ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
|
|
1772
|
-
|
|
1786
|
+
// The site's ordinal key, as the plain form passes it: a list that comes out
|
|
1787
|
+
// in order then needs no sorting, and one that does not is put in order from
|
|
1788
|
+
// the keys collected beside it (sr) instead of looking up each error's path.
|
|
1789
|
+
return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),${apOrd(ctx, sp)}))`
|
|
1773
1790
|
}
|
|
1774
1791
|
|
|
1775
1792
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
@@ -1935,7 +1952,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
1935
1952
|
// The checked sort (_srX) compares with the reader, which a module
|
|
1936
1953
|
// does not carry: there `_SRT` keeps the list as it is.
|
|
1937
1954
|
`\n return _e?new _ER(_A.mo?_e:${env('ATA_CHECK_SORT') && !(opts && opts.emit) ? '_srX(_A,_e,_SRT)' : '_sr(_A,_e,_SRT)'}):${opts && opts.moduleShape ? 'R' : '{valid:true,data:d,errors:_EE}'}}catch(_x){return _FB(d)}`
|
|
1938
|
-
|
|
1955
|
+
// The rich form also says whether its list came out in the reader's
|
|
1956
|
+
// order (`mo`, kept by _ap from each site's ordinal key), so the reader
|
|
1957
|
+
// (richBuilder) sorts only a list that did not.
|
|
1958
|
+
: hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e${ctx.rich ? ',mo:_A.mo' : ''}}:R`
|
|
1939
1959
|
// The verdict generator's hoisted functions (subtreeGuard writes guards
|
|
1940
1960
|
// through it) come first: a oneOf branch it could not hoist was a closure
|
|
1941
1961
|
// built at every call of the guard, six per element of a json-patch list.
|
|
@@ -1952,7 +1972,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
1952
1972
|
console.log('=== COMBINED PARTS ===', JSON.stringify(Object.entries(acc).sort((a, b) => b[1] - a[1])))
|
|
1953
1973
|
}
|
|
1954
1974
|
const programSrc = assignOrdKeys(ctx, `${helpers}return function _vC(d){${inner}}`)
|
|
1955
|
-
|
|
1975
|
+
// The run-time program is tagged (seqTag); the emitted source is not.
|
|
1976
|
+
const factory = compileFunction('R' + (closureParams ? ',' + closureParams : ''), (opts && opts.emit ? '' : seqTag()) + programSrc)
|
|
1956
1977
|
const made = factory(VALID_RESULT, ...closureArgs)
|
|
1957
1978
|
// For a standalone module (lib/aot-impl.js): the program's source and the
|
|
1958
1979
|
// closure values it takes, after the function was built and tried on
|
|
@@ -2405,7 +2426,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2405
2426
|
}
|
|
2406
2427
|
}
|
|
2407
2428
|
for (const key of schema.required) {
|
|
2408
|
-
const check = hoisted[key] ? `${hoisted[key]}===undefined` :
|
|
2429
|
+
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})`
|
|
2409
2430
|
if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
|
|
2410
2431
|
const ei = ctx.varCounter++
|
|
2411
2432
|
const errVar = `_E${ei}`
|
|
@@ -2428,7 +2449,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2428
2449
|
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
2429
2450
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
|
|
2430
2451
|
} else {
|
|
2431
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
2452
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)})){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
2432
2453
|
}
|
|
2433
2454
|
}
|
|
2434
2455
|
}
|
|
@@ -2612,7 +2633,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2612
2633
|
sub(`_o${oi}`)
|
|
2613
2634
|
lines.push(`}}`)
|
|
2614
2635
|
} else {
|
|
2615
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2636
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${v}[${JSON.stringify(key)}]!==undefined&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2616
2637
|
sub(`${v}[${JSON.stringify(key)}]`)
|
|
2617
2638
|
lines.push(`}`)
|
|
2618
2639
|
}
|
|
@@ -2643,9 +2664,24 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2643
2664
|
const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
|
|
2644
2665
|
ctx.varCounter++
|
|
2645
2666
|
const childPath = childPathDynExpr(pathExpr, idx)
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2667
|
+
if (ctx.inCombined && keyLoopEligible(schema.items)) {
|
|
2668
|
+
// Records of many optional properties (see subtreeGuard): the guard,
|
|
2669
|
+
// a key loop per record, is run only for a list of mixed shapes,
|
|
2670
|
+
// decided as the verdict decides (the first, middle and last record's
|
|
2671
|
+
// keys). On records of one shape the collecting code's reads are
|
|
2672
|
+
// monomorphic and cheaper than any guard: 2000 uniform records read
|
|
2673
|
+
// for errors in 16 us, with a guard on each 69.
|
|
2674
|
+
hoistOnce(ctx, 'sameKeysHelper', SAME_KEYS_HELPER)
|
|
2675
|
+
const n = `_n${ctx.varCounter++}`
|
|
2676
|
+
const mx = `_mx${ctx.varCounter++}`
|
|
2677
|
+
lines.push(`if(Array.isArray(${v})){const ${n}=${v}.length;const ${mx}=${n}-${startIdx}>2&&(!_sameKeys(${v}[${startIdx}],${v}[(${n}+${startIdx})>>1])||!_sameKeys(${v}[${startIdx}],${v}[${n}-1]));for(let ${idx}=${startIdx};${idx}<${n};${idx}++){const ${elem}=${v}[${idx}]`)
|
|
2678
|
+
genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items', mx)
|
|
2679
|
+
lines.push(`}}`)
|
|
2680
|
+
} else {
|
|
2681
|
+
lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
2682
|
+
genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
|
|
2683
|
+
lines.push(`}}`)
|
|
2684
|
+
}
|
|
2649
2685
|
}
|
|
2650
2686
|
|
|
2651
2687
|
// prefixItems
|