ata-validator 1.63.0 → 1.63.1
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/lib/interpreter-core.js +131 -61
- package/lib/ir/emit.mjs +161 -14
- package/lib/ir/front.mjs +32 -1
- package/lib/plan-compiler.js +31 -9
- package/lib/shape-classifier.js +13 -1
- package/lib/tier0.js +38 -8
- package/lib/validator-core.js +24 -6
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/interpreter-core.js
CHANGED
|
@@ -247,6 +247,7 @@ function enumMember(P, data) {
|
|
|
247
247
|
// JSON equality: the same function the code generator emits as `_deq`, so the
|
|
248
248
|
// engines agree on what equal means (an object's own keys, in any order).
|
|
249
249
|
const deepEqual = _deq;
|
|
250
|
+
const propEnum = Object.prototype.propertyIsEnumerable;
|
|
250
251
|
|
|
251
252
|
function codePointLength(s) {
|
|
252
253
|
let n = 0;
|
|
@@ -369,6 +370,60 @@ function isObjectMap(v) {
|
|
|
369
370
|
// per section so eval() can skip whole blocks without touching the schema
|
|
370
371
|
// object. Plans have a fixed shape, which keeps eval() monomorphic where the
|
|
371
372
|
// raw schema objects are not.
|
|
373
|
+
// The keyword values of a schema node, from one pass over its enumerable
|
|
374
|
+
// names (its own, as JSON gives them), in a record of one shape for link().
|
|
375
|
+
const KEYWORD_RECORD = () => ({ '$ref': undefined, '$dynamicRef': undefined, type: undefined, enum: undefined, const: undefined, minimum: undefined, maximum: undefined, exclusiveMinimum: undefined, exclusiveMaximum: undefined, multipleOf: undefined, minLength: undefined, maxLength: undefined, pattern: undefined, format: undefined, minItems: undefined, maxItems: undefined, uniqueItems: undefined, prefixItems: undefined, items: undefined, contains: undefined, minContains: undefined, maxContains: undefined, required: undefined, minProperties: undefined, maxProperties: undefined, dependentRequired: undefined, propertyNames: undefined, properties: undefined, patternProperties: undefined, additionalProperties: undefined, dependentSchemas: undefined, propertyDependencies: undefined, allOf: undefined, anyOf: undefined, oneOf: undefined, not: undefined, if: undefined, then: undefined, else: undefined, unevaluatedProperties: undefined, unevaluatedItems: undefined });
|
|
376
|
+
function readKeywords (schema) {
|
|
377
|
+
const S = KEYWORD_RECORD();
|
|
378
|
+
for (const k in schema) {
|
|
379
|
+
const v = schema[k];
|
|
380
|
+
switch (k) {
|
|
381
|
+
case '$ref': S['$ref'] = v; break;
|
|
382
|
+
case '$dynamicRef': S['$dynamicRef'] = v; break;
|
|
383
|
+
case 'type': S.type = v; break;
|
|
384
|
+
case 'enum': S.enum = v; break;
|
|
385
|
+
case 'const': S.const = v; break;
|
|
386
|
+
case 'minimum': S.minimum = v; break;
|
|
387
|
+
case 'maximum': S.maximum = v; break;
|
|
388
|
+
case 'exclusiveMinimum': S.exclusiveMinimum = v; break;
|
|
389
|
+
case 'exclusiveMaximum': S.exclusiveMaximum = v; break;
|
|
390
|
+
case 'multipleOf': S.multipleOf = v; break;
|
|
391
|
+
case 'minLength': S.minLength = v; break;
|
|
392
|
+
case 'maxLength': S.maxLength = v; break;
|
|
393
|
+
case 'pattern': S.pattern = v; break;
|
|
394
|
+
case 'format': S.format = v; break;
|
|
395
|
+
case 'minItems': S.minItems = v; break;
|
|
396
|
+
case 'maxItems': S.maxItems = v; break;
|
|
397
|
+
case 'uniqueItems': S.uniqueItems = v; break;
|
|
398
|
+
case 'prefixItems': S.prefixItems = v; break;
|
|
399
|
+
case 'items': S.items = v; break;
|
|
400
|
+
case 'contains': S.contains = v; break;
|
|
401
|
+
case 'minContains': S.minContains = v; break;
|
|
402
|
+
case 'maxContains': S.maxContains = v; break;
|
|
403
|
+
case 'required': S.required = v; break;
|
|
404
|
+
case 'minProperties': S.minProperties = v; break;
|
|
405
|
+
case 'maxProperties': S.maxProperties = v; break;
|
|
406
|
+
case 'dependentRequired': S.dependentRequired = v; break;
|
|
407
|
+
case 'propertyNames': S.propertyNames = v; break;
|
|
408
|
+
case 'properties': S.properties = v; break;
|
|
409
|
+
case 'patternProperties': S.patternProperties = v; break;
|
|
410
|
+
case 'additionalProperties': S.additionalProperties = v; break;
|
|
411
|
+
case 'dependentSchemas': S.dependentSchemas = v; break;
|
|
412
|
+
case 'propertyDependencies': S.propertyDependencies = v; break;
|
|
413
|
+
case 'allOf': S.allOf = v; break;
|
|
414
|
+
case 'anyOf': S.anyOf = v; break;
|
|
415
|
+
case 'oneOf': S.oneOf = v; break;
|
|
416
|
+
case 'not': S.not = v; break;
|
|
417
|
+
case 'if': S.if = v; break;
|
|
418
|
+
case 'then': S.then = v; break;
|
|
419
|
+
case 'else': S.else = v; break;
|
|
420
|
+
case 'unevaluatedProperties': S.unevaluatedProperties = v; break;
|
|
421
|
+
case 'unevaluatedItems': S.unevaluatedItems = v; break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return S;
|
|
425
|
+
}
|
|
426
|
+
|
|
372
427
|
class Plan {
|
|
373
428
|
// The plan is registered in the cache before `link` runs, so a schema
|
|
374
429
|
// object that contains itself structurally resolves to the same plan
|
|
@@ -379,6 +434,13 @@ class Plan {
|
|
|
379
434
|
|
|
380
435
|
link(interp) {
|
|
381
436
|
const schema = this.schema;
|
|
437
|
+
// The keywords read once, by one pass over the schema's own names, into
|
|
438
|
+
// a record of one shape: read by name on the schema, each absent keyword
|
|
439
|
+
// was a lookup that missed on an object of yet another shape, and with
|
|
440
|
+
// many schemas in a process the 41 reads cost 3.7 us a node against 8 ns
|
|
441
|
+
// for the pass (measured on 64 shapes). Reads on the record hit one
|
|
442
|
+
// shape every time.
|
|
443
|
+
const S = readKeywords(schema);
|
|
382
444
|
// Children are linked as plans (or booleans) up front, so a visit never
|
|
383
445
|
// goes back through the plan cache. Reference targets are resolved at
|
|
384
446
|
// evaluation time and cached per base URI.
|
|
@@ -387,21 +449,21 @@ class Plan {
|
|
|
387
449
|
this.refBase0 = undefined;
|
|
388
450
|
this.refRes0 = null;
|
|
389
451
|
this.dynCache = null;
|
|
390
|
-
this.ref = typeof
|
|
391
|
-
this.dynamicRef = typeof
|
|
452
|
+
this.ref = typeof S.$ref === 'string' ? S.$ref : null;
|
|
453
|
+
this.dynamicRef = typeof S.$dynamicRef === 'string' ? S.$dynamicRef : null;
|
|
392
454
|
this.tracked = this.ref !== null || this.dynamicRef !== null;
|
|
393
455
|
|
|
394
456
|
this.typeMask = 0;
|
|
395
457
|
this.typeNames = null;
|
|
396
|
-
if (
|
|
397
|
-
const types = Array.isArray(
|
|
458
|
+
if (S.type !== undefined) {
|
|
459
|
+
const types = Array.isArray(S.type) ? S.type : [S.type];
|
|
398
460
|
let mask = 0;
|
|
399
461
|
for (const t of types) mask |= typeBit(t);
|
|
400
462
|
this.typeMask = mask;
|
|
401
463
|
this.typeNames = types;
|
|
402
464
|
}
|
|
403
|
-
this.hasType =
|
|
404
|
-
this.enum =
|
|
465
|
+
this.hasType = S.type !== undefined;
|
|
466
|
+
this.enum = S.enum !== undefined ? S.enum : null;
|
|
405
467
|
// Membership for enumMember(): the primitive values in a Set, so a long
|
|
406
468
|
// enum answers in one lookup, and the object and array values, which only
|
|
407
469
|
// deepEqual can compare, in a list. Scanning every value with deepEqual
|
|
@@ -417,28 +479,28 @@ class Plan {
|
|
|
417
479
|
else if (!(typeof v === 'number' && v !== v)) this.enumPrims.add(v);
|
|
418
480
|
}
|
|
419
481
|
}
|
|
420
|
-
this.hasConst =
|
|
421
|
-
this.const =
|
|
422
|
-
|
|
423
|
-
this.minimum = typeof
|
|
424
|
-
this.maximum = typeof
|
|
425
|
-
this.exclusiveMinimum = typeof
|
|
426
|
-
this.exclusiveMaximum = typeof
|
|
427
|
-
this.multipleOf = typeof
|
|
482
|
+
this.hasConst = S.const !== undefined;
|
|
483
|
+
this.const = S.const;
|
|
484
|
+
|
|
485
|
+
this.minimum = typeof S.minimum === 'number' ? S.minimum : undefined;
|
|
486
|
+
this.maximum = typeof S.maximum === 'number' ? S.maximum : undefined;
|
|
487
|
+
this.exclusiveMinimum = typeof S.exclusiveMinimum === 'number' ? S.exclusiveMinimum : undefined;
|
|
488
|
+
this.exclusiveMaximum = typeof S.exclusiveMaximum === 'number' ? S.exclusiveMaximum : undefined;
|
|
489
|
+
this.multipleOf = typeof S.multipleOf === 'number' ? S.multipleOf : undefined;
|
|
428
490
|
this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
|
|
429
491
|
|
|
430
|
-
this.minLength =
|
|
431
|
-
this.maxLength =
|
|
432
|
-
this.pattern =
|
|
433
|
-
this.patternSource =
|
|
434
|
-
this.format =
|
|
492
|
+
this.minLength = S.minLength;
|
|
493
|
+
this.maxLength = S.maxLength;
|
|
494
|
+
this.pattern = S.pattern !== undefined ? interp.pattern(S.pattern) : null;
|
|
495
|
+
this.patternSource = S.pattern;
|
|
496
|
+
this.format = S.format;
|
|
435
497
|
this.formatFn = null;
|
|
436
|
-
if (
|
|
498
|
+
if (S.format !== undefined) {
|
|
437
499
|
// Own properties only, as the generated code reads them: an inherited
|
|
438
500
|
// Object member ('hasOwnProperty', 'constructor') is not a check.
|
|
439
501
|
const own = Object.prototype.hasOwnProperty;
|
|
440
502
|
const uf = interp.userFormats;
|
|
441
|
-
const f =
|
|
503
|
+
const f = S.format;
|
|
442
504
|
const table = require('./formats').formatChecks();
|
|
443
505
|
const fc = (uf && typeof f === 'string' && own.call(uf, f) && typeof uf[f] === 'function') ? uf[f]
|
|
444
506
|
: (typeof f === 'string' && own.call(table, f) && typeof table[f] === 'function' ? table[f] : null);
|
|
@@ -448,31 +510,31 @@ class Plan {
|
|
|
448
510
|
// A format compat registered for numbers (`type: 'number'`), checked on
|
|
449
511
|
// numbers as the reference checks it.
|
|
450
512
|
const nf = interp.numberFormats;
|
|
451
|
-
this.numFormatFn =
|
|
513
|
+
this.numFormatFn = S.format !== undefined && nf !== null && typeof nf[S.format] === 'function' ? nf[S.format] : null;
|
|
452
514
|
if (this.numFormatFn !== null) this.hasNumber = true;
|
|
453
515
|
|
|
454
|
-
this.minItems =
|
|
455
|
-
this.maxItems =
|
|
456
|
-
this.uniqueItems =
|
|
457
|
-
this.prefixItems = Array.isArray(
|
|
458
|
-
this.items =
|
|
459
|
-
this.contains =
|
|
460
|
-
this.minContains =
|
|
461
|
-
this.maxContains =
|
|
516
|
+
this.minItems = S.minItems;
|
|
517
|
+
this.maxItems = S.maxItems;
|
|
518
|
+
this.uniqueItems = S.uniqueItems === true;
|
|
519
|
+
this.prefixItems = Array.isArray(S.prefixItems) ? S.prefixItems.map(child) : null;
|
|
520
|
+
this.items = S.items !== undefined ? child(S.items) : undefined;
|
|
521
|
+
this.contains = S.contains !== undefined ? child(S.contains) : undefined;
|
|
522
|
+
this.minContains = S.minContains;
|
|
523
|
+
this.maxContains = S.maxContains;
|
|
462
524
|
this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
|
|
463
525
|
|
|
464
|
-
this.required = Array.isArray(
|
|
465
|
-
this.minProperties =
|
|
466
|
-
this.maxProperties =
|
|
467
|
-
this.dependentRequired = isObjectMap(
|
|
468
|
-
this.propertyNames =
|
|
526
|
+
this.required = Array.isArray(S.required) ? S.required : null;
|
|
527
|
+
this.minProperties = S.minProperties;
|
|
528
|
+
this.maxProperties = S.maxProperties;
|
|
529
|
+
this.dependentRequired = isObjectMap(S.dependentRequired) ? Object.entries(S.dependentRequired) : null;
|
|
530
|
+
this.propertyNames = S.propertyNames !== undefined ? child(S.propertyNames) : undefined;
|
|
469
531
|
this.properties = null;
|
|
470
|
-
if (isObjectMap(
|
|
532
|
+
if (isObjectMap(S.properties)) {
|
|
471
533
|
this.properties = new Map();
|
|
472
|
-
for (const key of Object.keys(
|
|
534
|
+
for (const key of Object.keys(S.properties)) {
|
|
473
535
|
const ek = escapePointer(key);
|
|
474
536
|
// Escaped once here; the walk only concatenates.
|
|
475
|
-
this.properties.set(key, { node: child(
|
|
537
|
+
this.properties.set(key, { node: child(S.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
|
|
476
538
|
}
|
|
477
539
|
}
|
|
478
540
|
// A required key holding undefined is missing. Where the key is also a
|
|
@@ -491,26 +553,26 @@ class Plan {
|
|
|
491
553
|
}
|
|
492
554
|
}
|
|
493
555
|
this.patternProperties = null;
|
|
494
|
-
if (isObjectMap(
|
|
495
|
-
this.patternProperties = Object.keys(
|
|
556
|
+
if (isObjectMap(S.patternProperties)) {
|
|
557
|
+
this.patternProperties = Object.keys(S.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(S.patternProperties[src]) }));
|
|
496
558
|
}
|
|
497
|
-
this.additionalProperties =
|
|
498
|
-
this.dependentSchemas = isObjectMap(
|
|
559
|
+
this.additionalProperties = S.additionalProperties !== undefined ? child(S.additionalProperties) : undefined;
|
|
560
|
+
this.dependentSchemas = isObjectMap(S.dependentSchemas) ? Object.entries(S.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
|
|
499
561
|
this.propertyDependencies = null;
|
|
500
|
-
if (isObjectMap(
|
|
501
|
-
this.propertyDependencies = Object.entries(
|
|
562
|
+
if (isObjectMap(S.propertyDependencies)) {
|
|
563
|
+
this.propertyDependencies = Object.entries(S.propertyDependencies)
|
|
502
564
|
.filter(([, choices]) => isObjectMap(choices))
|
|
503
565
|
.map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
|
|
504
566
|
}
|
|
505
567
|
this.hasObject = this.required !== null || this.minProperties !== undefined || this.maxProperties !== undefined || this.dependentRequired !== null || this.propertyNames !== undefined || this.properties !== null || this.patternProperties !== null || this.additionalProperties !== undefined || this.dependentSchemas !== null || this.propertyDependencies !== null;
|
|
506
568
|
|
|
507
|
-
this.allOf = Array.isArray(
|
|
508
|
-
this.anyOf = Array.isArray(
|
|
509
|
-
this.oneOf = Array.isArray(
|
|
510
|
-
this.not =
|
|
511
|
-
this.if =
|
|
512
|
-
this.then =
|
|
513
|
-
this.else =
|
|
569
|
+
this.allOf = Array.isArray(S.allOf) ? S.allOf.map(child) : null;
|
|
570
|
+
this.anyOf = Array.isArray(S.anyOf) ? S.anyOf.map(child) : null;
|
|
571
|
+
this.oneOf = Array.isArray(S.oneOf) ? S.oneOf.map(child) : null;
|
|
572
|
+
this.not = S.not !== undefined ? child(S.not) : undefined;
|
|
573
|
+
this.if = S.if !== undefined ? child(S.if) : undefined;
|
|
574
|
+
this.then = S.then !== undefined ? child(S.then) : undefined;
|
|
575
|
+
this.else = S.else !== undefined ? child(S.else) : undefined;
|
|
514
576
|
// Macro keywords: the schema the macro returns is applied in place, the
|
|
515
577
|
// way the reference validator does it, so its errors carry the keyword's
|
|
516
578
|
// path and the keyword adds an error of its own when that schema fails.
|
|
@@ -527,13 +589,13 @@ class Plan {
|
|
|
527
589
|
}
|
|
528
590
|
this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined || this.macros !== null;
|
|
529
591
|
|
|
530
|
-
this.unevaluatedProperties =
|
|
531
|
-
this.unevaluatedItems =
|
|
592
|
+
this.unevaluatedProperties = S.unevaluatedProperties !== undefined ? child(S.unevaluatedProperties) : undefined;
|
|
593
|
+
this.unevaluatedItems = S.unevaluatedItems !== undefined ? child(S.unevaluatedItems) : undefined;
|
|
532
594
|
// A `false` here rejects the member outright, and the error belongs to this
|
|
533
595
|
// keyword on this object, not to a boolean schema that happens to sit
|
|
534
596
|
// under it. Recorded at compile time so the check below is one test.
|
|
535
|
-
this.unevaluatedPropertiesFalse =
|
|
536
|
-
this.unevaluatedItemsFalse =
|
|
597
|
+
this.unevaluatedPropertiesFalse = S.unevaluatedProperties === false;
|
|
598
|
+
this.unevaluatedItemsFalse = S.unevaluatedItems === false;
|
|
537
599
|
this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
|
|
538
600
|
|
|
539
601
|
// Custom keywords present on this node, in schema order. Each op holds
|
|
@@ -1251,9 +1313,15 @@ class Interpreter {
|
|
|
1251
1313
|
}
|
|
1252
1314
|
}
|
|
1253
1315
|
|
|
1254
|
-
// objects
|
|
1316
|
+
// objects. The object's own names, enumerable or not: a declared
|
|
1317
|
+
// property is read by name, as the generated code reads it, so one
|
|
1318
|
+
// hidden under its name is checked; patternProperties, propertyNames
|
|
1319
|
+
// and additionalProperties see the names for-in sees, as there too, and
|
|
1320
|
+
// minProperties and maxProperties count those.
|
|
1255
1321
|
if (P.hasObject && bits === T_OBJECT) {
|
|
1256
|
-
const keys = Object.
|
|
1322
|
+
const keys = Object.getOwnPropertyNames(data);
|
|
1323
|
+
let hidden = 0;
|
|
1324
|
+
if (P.minProperties !== undefined || P.maxProperties !== undefined) for (let k = 0; k < keys.length; k++) if (!propEnum.call(data, keys[k])) hidden++;
|
|
1257
1325
|
if (P.required !== null) {
|
|
1258
1326
|
const req = P.required;
|
|
1259
1327
|
for (let i = 0; i < req.length; i++) {
|
|
@@ -1261,8 +1329,8 @@ class Interpreter {
|
|
|
1261
1329
|
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; }
|
|
1262
1330
|
}
|
|
1263
1331
|
}
|
|
1264
|
-
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; }
|
|
1265
|
-
if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
1332
|
+
if (P.minProperties !== undefined && keys.length - hidden < 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; }
|
|
1333
|
+
if (P.maxProperties !== undefined && keys.length - hidden > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
1266
1334
|
if (P.dependentRequired !== null) {
|
|
1267
1335
|
for (const [key, deps] of P.dependentRequired) {
|
|
1268
1336
|
if (Object.hasOwn(data, key)) {
|
|
@@ -1274,6 +1342,7 @@ class Interpreter {
|
|
|
1274
1342
|
}
|
|
1275
1343
|
if (P.propertyNames !== undefined) {
|
|
1276
1344
|
for (const key of keys) {
|
|
1345
|
+
if (!propEnum.call(data, key)) continue;
|
|
1277
1346
|
if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath, E ? schemaPath + '/propertyNames' : '', stack, DISCARD)) valid = false;
|
|
1278
1347
|
}
|
|
1279
1348
|
}
|
|
@@ -1283,6 +1352,7 @@ class Interpreter {
|
|
|
1283
1352
|
const key = keys[k];
|
|
1284
1353
|
let evaluated = false;
|
|
1285
1354
|
const prop = props !== null ? props.get(key) : undefined;
|
|
1355
|
+
const seen = prop !== undefined && patterns === null && P.additionalProperties === undefined || propEnum.call(data, key);
|
|
1286
1356
|
if (prop !== undefined) {
|
|
1287
1357
|
// A declared property whose value is undefined is not checked, as the
|
|
1288
1358
|
// generated code does not check it: no JSON document carries one, and
|
|
@@ -1294,7 +1364,7 @@ class Interpreter {
|
|
|
1294
1364
|
} 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; }
|
|
1295
1365
|
evaluated = true;
|
|
1296
1366
|
}
|
|
1297
|
-
if (patterns !== null) {
|
|
1367
|
+
if (patterns !== null && seen) {
|
|
1298
1368
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
1299
1369
|
const pp = patterns[pi];
|
|
1300
1370
|
if (pp.re.test(key)) {
|
|
@@ -1303,7 +1373,7 @@ class Interpreter {
|
|
|
1303
1373
|
}
|
|
1304
1374
|
}
|
|
1305
1375
|
}
|
|
1306
|
-
if (!evaluated && P.additionalProperties !== undefined) {
|
|
1376
|
+
if (!evaluated && seen && P.additionalProperties !== undefined) {
|
|
1307
1377
|
// `additionalProperties: false` keeps its own keyword and names the
|
|
1308
1378
|
// property it turned away. Running it as a plain false subschema
|
|
1309
1379
|
// reported `not` at the key's path, which drops both.
|
package/lib/ir/emit.mjs
CHANGED
|
@@ -44,7 +44,14 @@ function inline (fn, vExpr, nExpr) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// How many names an object must list before a module visits the object's own
|
|
47
|
-
// names instead of looking each one up.
|
|
47
|
+
// names instead of looking each one up. Measured on SchemaStore's samples,
|
|
48
|
+
// warmed, against the compiled modules: from 16 names the geometric mean is
|
|
49
|
+
// 0.878 with 23 schemas under half the time, from 32 it is 0.914 with 15,
|
|
50
|
+
// from 64 0.941 with 8, and the count slower by a fifth is 47 at each. A
|
|
51
|
+
// dense document under a short list (sil-kit, csslintrc: most of 16 to 38
|
|
52
|
+
// names present) pays the walk's array and dispatch, 2 to 4x the direct
|
|
53
|
+
// reads; a sparse one under a long list (putout, 475 names) would pay a
|
|
54
|
+
// read per name, 100x the walk.
|
|
48
55
|
const WALK_MIN = 16
|
|
49
56
|
// Source characters past which a verdict body is its own function (the
|
|
50
57
|
// verdict threshold of the current engine's generator, SPLIT_PROP_SCHEMA).
|
|
@@ -96,14 +103,90 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
96
103
|
use(fn)
|
|
97
104
|
return fn
|
|
98
105
|
}
|
|
106
|
+
// An own-property test written in the module with the module's own
|
|
107
|
+
// reference to hasOwnProperty. The kernels' own() is one function for every
|
|
108
|
+
// module of a page, so its property lookup met the objects of every schema
|
|
109
|
+
// and stopped being cached for any (rc3-environment alone 1.0x the compiled
|
|
110
|
+
// module, beside 438 other schemas 2.1x).
|
|
111
|
+
let HOP = null
|
|
112
|
+
const ownSrc = (o, k) => {
|
|
113
|
+
if (HOP === null) HOP = constOf('Object.prototype.hasOwnProperty')
|
|
114
|
+
return `${HOP}.call(${o},${k})`
|
|
115
|
+
}
|
|
116
|
+
// A small verdict written in at its call, as the compiled modules write a
|
|
117
|
+
// referenced schema in at each property: every copy keeps its own record of
|
|
118
|
+
// the shapes it meets, where one function called from forty places met all
|
|
119
|
+
// of them and V8 stopped inlining it (coffeelint, 38 properties naming one
|
|
120
|
+
// definition). Only a verdict with no cycle through it, no guard and no
|
|
121
|
+
// dynamic scope, whose body is INLINE_BODY characters or fewer; a verdict
|
|
122
|
+
// body leaves only by `return false`, so it runs as a block. Its parameters
|
|
123
|
+
// are bound in the block: the value, and the empty paths and list of a
|
|
124
|
+
// verdict call.
|
|
125
|
+
const INLINE_BODY = 500
|
|
126
|
+
// Inlined source one function may take in all: past it a function grows
|
|
127
|
+
// beyond what V8 optimizes (putout's 475 properties, 0.01x the compiled
|
|
128
|
+
// module before, 1.08x with every call written in).
|
|
129
|
+
// 2000 left coffeelint's 38 names calling one 300-character check (2.2x
|
|
130
|
+
// the compiled module); 8000 inlines them and keeps putout's 475-name
|
|
131
|
+
// function optimizable (both measured on SchemaStore, warmed).
|
|
132
|
+
const INLINE_BUDGET = 8000
|
|
133
|
+
let inlineBudget = INLINE_BUDGET
|
|
134
|
+
const inlining = new Set()
|
|
135
|
+
const inlineBodies = new Map()
|
|
136
|
+
const inlineCall = (c) => {
|
|
137
|
+
if (c.proc === null || c.dynamic || c.guard || ir.dynamic || inlining.has(c.proc)) return null
|
|
138
|
+
const p = ir.procs.get(c.proc)
|
|
139
|
+
if (!p || p.mode !== 'verdict') return null
|
|
140
|
+
// Follow a verdict that is only a call of another (procName's alias).
|
|
141
|
+
const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
|
|
142
|
+
if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== c.proc) return inlineCall({ ...b.e, x: c.x })
|
|
143
|
+
// Each procedure is written once, with a full budget for what it
|
|
144
|
+
// inlines itself, and that text is its size: a site inlines it when the
|
|
145
|
+
// text fits the site's remaining budget. Written at every call site
|
|
146
|
+
// under the site's budget, the text depended on the order of the sites,
|
|
147
|
+
// and a definition too large under a deep nesting was rendered again
|
|
148
|
+
// for each site below each site (a suite schema took ten minutes).
|
|
149
|
+
let body = inlineBodies.get(c.proc)
|
|
150
|
+
if (body === undefined) {
|
|
151
|
+
inlining.add(c.proc); const saved = inlineBudget; inlineBudget = INLINE_BUDGET
|
|
152
|
+
try { body = stmt(p.body, true) } finally { inlining.delete(c.proc); inlineBudget = saved }
|
|
153
|
+
if (body.length > INLINE_BODY || /\breturn true\b/.test(body)) body = null
|
|
154
|
+
inlineBodies.set(c.proc, body)
|
|
155
|
+
}
|
|
156
|
+
if (body === null || body.length > inlineBudget) return null
|
|
157
|
+
inlineBudget -= body.length
|
|
158
|
+
const x = expr(c.x); const t = 'i' + (++inlineN)
|
|
159
|
+
return `{const ${t}=${x}\n{const d=${t},V=${t},e=null,P="",SP="",A=null\n${body}}}\n`
|
|
160
|
+
}
|
|
161
|
+
let inlineN = 0
|
|
162
|
+
// The verdict calls already made in the row of statements being written
|
|
163
|
+
// (seq), handed to a row nested directly in it.
|
|
164
|
+
let seqSeen = null
|
|
165
|
+
// The plain-prototype flags a fill has bound, by the object's expression.
|
|
166
|
+
const fillPlain = new Map()
|
|
99
167
|
const procName = (id) => {
|
|
100
168
|
if (emitted.has(id)) return emitted.get(id)
|
|
101
169
|
const p = ir.procs.get(id)
|
|
170
|
+
// A verdict that is one call of another on its own value (a branch that
|
|
171
|
+
// is only a $ref) is that other verdict: no function of its own, and a
|
|
172
|
+
// caller that already ran it can see the repeat (seq).
|
|
173
|
+
if (p.mode === 'verdict' && !ir.dynamic) {
|
|
174
|
+
const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
|
|
175
|
+
if (b.t === 'require' && b.e.t === 'pcall' && b.e.e === 'null' && !b.e.dynamic && !b.e.guard && b.e.x.t === 'var' && b.e.x.n === 'd' && b.e.proc !== id) {
|
|
176
|
+
const to = procName(b.e.proc)
|
|
177
|
+
emitted.set(id, to)
|
|
178
|
+
return to
|
|
179
|
+
}
|
|
180
|
+
}
|
|
102
181
|
const name = id
|
|
103
182
|
emitted.set(id, name)
|
|
104
183
|
const isVerdict = p.mode === 'verdict'
|
|
105
|
-
if (p.mode === 'fill') { fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
|
|
106
|
-
|
|
184
|
+
if (p.mode === 'fill') { fillPlain.clear(); fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
|
|
185
|
+
// Each function written gets its own budget of inlined source (inlineCall).
|
|
186
|
+
const savedBudget = inlineBudget
|
|
187
|
+
inlineBudget = INLINE_BUDGET
|
|
188
|
+
let body
|
|
189
|
+
try { body = stmt(p.body, isVerdict) } finally { inlineBudget = savedBudget }
|
|
107
190
|
const head = p.mode === 'ann' ? 'if(A===null)A=newAnn()\n' : ''
|
|
108
191
|
if (p.mode === 'ann') use('newAnn')
|
|
109
192
|
const wrapped = ir.dynamic
|
|
@@ -213,7 +296,23 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
213
296
|
const ap = walked && n.body.find((c) => c.t === 'if' && c.then.t === 'keys' && c.then.apClosed && c.then.obj.t === 'var' && c.then.obj.n === walked.then.x.n)
|
|
214
297
|
if (ap) { skip = ap; folded.add(walked.then) }
|
|
215
298
|
}
|
|
216
|
-
|
|
299
|
+
// In a verdict the same check of the same value twice in a row of
|
|
300
|
+
// statements is once: the first returned false or passed, and a
|
|
301
|
+
// verdict has no side effects (custom keywords and formats, which
|
|
302
|
+
// could, are refused above). A schema that names one definition
|
|
303
|
+
// under both allOf and anyOf ran it twice per item (devinit).
|
|
304
|
+
// A row nested directly in a row runs unconditionally after what came
|
|
305
|
+
// before it, so it shares the set; anything under a condition, a
|
|
306
|
+
// loop or a let starts its own.
|
|
307
|
+
const seen = v ? (seqSeen || new Set()) : null
|
|
308
|
+
seqSeen = null
|
|
309
|
+
return n.body.filter((x) => x !== skip).map((x) => {
|
|
310
|
+
seqSeen = x.t === 'seq' ? seen : null
|
|
311
|
+
const t = stmt(x, v)
|
|
312
|
+
seqSeen = null
|
|
313
|
+
if (seen !== null && (x.t === 'require' || x.t === 'branches')) { if (seen.has(t)) return ''; seen.add(t) }
|
|
314
|
+
return t
|
|
315
|
+
}).join('')
|
|
217
316
|
}
|
|
218
317
|
// An object that lists many names: visit the object's own names, every
|
|
219
318
|
// one enumerable or not (an entry checks its own presence, so a name it
|
|
@@ -235,7 +334,14 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
235
334
|
const rows = n.keyed.map(([p, i]) => {
|
|
236
335
|
const e = n.body[i]
|
|
237
336
|
const c = v ? verdictCall(e) : null
|
|
238
|
-
if (c !== null)
|
|
337
|
+
if (c !== null) {
|
|
338
|
+
// A small check written in at its name (inlineCall), as the
|
|
339
|
+
// compiled modules write a referenced definition at each
|
|
340
|
+
// property: csslintrc's 31 names each called one enum check.
|
|
341
|
+
const t = inlineCall({ ...c, x: { t: 'var', n: 'y' } })
|
|
342
|
+
if (t !== null) return { p, inl: t }
|
|
343
|
+
return { p, fn: procName(c.proc) }
|
|
344
|
+
}
|
|
239
345
|
if (v && e.t === 'let' && e.body.t === 'if' && !e.body.else && selfContained(e.body.then, e.name)) {
|
|
240
346
|
const text = stmt(e.body.then, true)
|
|
241
347
|
return { p, e, text, src: `{const ${e.name}=${expr(e.value)}\nif(${expr(e.body.c)}){\n${text}}}\n` }
|
|
@@ -248,7 +354,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
248
354
|
const small = tabled.filter((r) => (sizes.get(r.fn) ?? Infinity) <= INLINE_MAX).length
|
|
249
355
|
const useTable = tabled.length >= TABLE_MIN || (tabled.length >= WALK_MIN && small * 2 < tabled.length)
|
|
250
356
|
const left = rows.filter((r) => !useTable || !r.fn)
|
|
251
|
-
|
|
357
|
+
// In the walk a name comes from the object's own names, so its value
|
|
358
|
+
// is read without a presence test.
|
|
359
|
+
const body = (r) => r.fn ? `{const y=${X}[${lit(r.p)}];if(y!==void 0&&!${r.fn}(y,null,s,"",y,"",null))return false}\n` : r.inl ? `{const y=${X}[${lit(r.p)}];if(y!==void 0){${r.inl}}}\n` : r.src
|
|
252
360
|
// A key is told from the names by its length first, a switch on an
|
|
253
361
|
// integer the engine makes a jump table, and then compared with the
|
|
254
362
|
// few names of that length: a switch on the names compared each key
|
|
@@ -265,9 +373,20 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
265
373
|
const dispatch = (k) => `switch(${k}.length){\n${[...byLen].map(([l, rs]) => `case ${l}:${rs.map((r) => `if(${k}===${lit(r.p)}){\n${r.empty ? '' : body(r)}}`).join('else ')}${fold ? `else{${unknown}}` : ''}break\n`).join('')}${fold ? `default:${unknown}` : ''}}`
|
|
266
374
|
// A name the table answers is listed; with the fold every other name
|
|
267
375
|
// goes through the dispatch, which knows the listed ones.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
376
|
+
let walk
|
|
377
|
+
if (!useTable) walk = `for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}`
|
|
378
|
+
else {
|
|
379
|
+
const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
|
|
380
|
+
walk = `for(let j=0;j<ks.length;j++){const k=ks[j],f=${T}.get(k);if(f!==void 0){const y=${X}[k];if(y!==void 0&&!f(y,null,s,"",y,"",null))return false}${left.length || fold ? `else ${dispatch('k')}` : ''}}`
|
|
381
|
+
}
|
|
382
|
+
// A self-measuring switch to a direct read of each listed name on
|
|
383
|
+
// dense documents was tried here and taken out: it won on five
|
|
384
|
+
// config schemas (csslintrc, coffeelint, jshintrc) and lost as much
|
|
385
|
+
// on others (liquibase 0.27 to 1.46 of the compiled module, a
|
|
386
|
+
// 142-name object read in dictionary mode 8x slower than the walk),
|
|
387
|
+
// no rule from the measurements told the two apart, and its direct
|
|
388
|
+
// form once rejected {} through an inherited toString.
|
|
389
|
+
return `${always}{const ks=Object.getOwnPropertyNames(${X});${walk}}\n`
|
|
271
390
|
}
|
|
272
391
|
case 'if': {
|
|
273
392
|
// A condition that is a call under annotations merges its collector.
|
|
@@ -302,6 +421,13 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
302
421
|
// SchemaStore's slower by 6-80% when all of them run in one process.)
|
|
303
422
|
const last = n.order === 'protoLast' && !v
|
|
304
423
|
if (last) use('keysOf')
|
|
424
|
+
// A verdict walks the object's own names with for-in, no array, each
|
|
425
|
+
// name tested as the object's own: a name enumerable on
|
|
426
|
+
// Object.prototype is not one of them (tests/test_ir_module_split.mjs).
|
|
427
|
+
if (v) {
|
|
428
|
+
const o = expr(n.obj)
|
|
429
|
+
return `for(const ${n.k} in ${o}){if(!${ownSrc(o, n.k)})continue\n${stmt(n.body, v)}}\n`
|
|
430
|
+
}
|
|
305
431
|
return `for(const ${n.k} of ${last ? 'keysOf' : 'Object.keys'}(${expr(n.obj)})){\n${stmt(n.body, v)}}\n`
|
|
306
432
|
}
|
|
307
433
|
case 'range': return `for(let ${n.i}=${typeof n.from === 'number' ? n.from : expr(n.from)};${n.i}<${expr(n.arr)}.length;${n.i}++){\n${stmt(n.body, v)}}\n`
|
|
@@ -315,30 +441,48 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
315
441
|
use('newAnn', 'mergeAnn')
|
|
316
442
|
return `{const a=newAnn(),c=e===null?0:e.length;if(${callOf(c, 'e', 'a')}&&(e===null||e.length===c))mergeAnn(A,a);else if(e===null)return false}\n`
|
|
317
443
|
}
|
|
444
|
+
if (v && c.e === 'null') { const t = inlineCall(c); if (t !== null) return t }
|
|
318
445
|
if (c.e === 'inherit') return v ? `if(!${callOf(c, 'null', 'null')})return false\n` : `if(!${callOf(c, 'e', 'null')}&&e===null)return false\n`
|
|
319
446
|
return `if(!${callOf(c, 'null', 'null')})${v ? 'return false' : '{if(e===null)return false}'}\n`
|
|
320
447
|
}
|
|
321
448
|
case 'fail': return fail(n.err, v) + '\n'
|
|
322
449
|
case 'branches': return branches(n, v)
|
|
323
450
|
case 'setDefault': {
|
|
324
|
-
use('own')
|
|
325
451
|
if (n.k === '__proto__') use('setOwn')
|
|
326
452
|
const val = n.v
|
|
327
453
|
let src
|
|
328
454
|
if (typeof val !== 'object' || val === null) src = lit(val)
|
|
329
455
|
else if (Array.isArray(val) && val.length === 0) src = '[]'
|
|
330
|
-
else {
|
|
456
|
+
else {
|
|
457
|
+
// A small default written as a literal: a fresh value each time
|
|
458
|
+
// it is read, as the compiled modules write `{}`. copyOf's JSON
|
|
459
|
+
// round trip was 72% of revola's verdict and 39% of changepacks'.
|
|
460
|
+
// Not when a key in it is `__proto__`, which a literal would read
|
|
461
|
+
// as the prototype, nor a large one, which would swell the fill.
|
|
462
|
+
const text = lit(val)
|
|
463
|
+
if (text.length <= 400 && !text.includes('"__proto__"')) src = text
|
|
464
|
+
else { use('copyOf'); src = `copyOf(${constOf(text)})` }
|
|
465
|
+
}
|
|
331
466
|
const o = expr(n.o)
|
|
332
467
|
// A store under the name itself: through setOwn's computed key, V8
|
|
333
468
|
// turned an object given more than a few defaults into a dictionary,
|
|
334
469
|
// slow from then on for every reader (htmlhint's root, 23 defaults:
|
|
335
470
|
// getOwnPropertyNames 10 to 999 ns). __proto__ needs defineProperty.
|
|
336
|
-
if (n.k === '__proto__') return `if(
|
|
471
|
+
if (n.k === '__proto__') return `if(!${ownSrc(o, lit(n.k))})setOwn(${o},${lit(n.k)},${src})\n`
|
|
472
|
+
// A defined value under the name on an ordinary object is the
|
|
473
|
+
// object's own: the own test runs only for an undefined read, or on
|
|
474
|
+
// an object with another prototype, as the current engine's fill
|
|
475
|
+
// reads it. The flag is bound once per object in the fill.
|
|
476
|
+
if (!(n.k in Object.prototype)) {
|
|
477
|
+
let pl = fillPlain.get(o); let bind = ''
|
|
478
|
+
if (pl === undefined) { pl = 'pl' + fillPlain.size; fillPlain.set(o, pl); bind = `const ${pl}=(PN?${o}.__proto__:Object.getPrototypeOf(${o}))===Object.prototype\n` }
|
|
479
|
+
return `${bind}if((${o}[${lit(n.k)}]===void 0||!${pl})&&!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
|
|
480
|
+
}
|
|
337
481
|
// A read or `in` before hasOwnProperty, as the current engine's fill
|
|
338
482
|
// does, was measured both ways: faster where the defaults are mostly
|
|
339
483
|
// present (jshintrc 1195 to 522 ns), slower where they are mostly
|
|
340
484
|
// missing (prometheus 3671 to 4601). hasOwnProperty alone is kept.
|
|
341
|
-
return `if(
|
|
485
|
+
return `if(!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
|
|
342
486
|
}
|
|
343
487
|
case 'fcall': return `${procName(n.proc)}(${expr(n.x)})\n`
|
|
344
488
|
case 'defaultsMask': {
|
|
@@ -371,6 +515,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
371
515
|
const x = expr(n.x); const path = expr(n.path); const ev = expr(n.ev); const sps = n.sps.map(expr)
|
|
372
516
|
if (v) {
|
|
373
517
|
const calls = ps.map((p) => `${p}(${x},null,s,"",${ev},"",null)`)
|
|
518
|
+
// One branch passes exactly when it passes, for anyOf and oneOf: the
|
|
519
|
+
// form of a required call, so a repeat of it is seen (seq).
|
|
520
|
+
if (calls.length === 1) return `if(!${calls[0]})return false\n`
|
|
374
521
|
return `if(${isAny ? `!(${calls.join('||')})` : `(${calls.map((c) => `(${c}?1:0)`).join('+')})!==1`})return false\n`
|
|
375
522
|
}
|
|
376
523
|
use('collapse')
|
|
@@ -412,7 +559,7 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
|
|
|
412
559
|
case 'var': return n.n
|
|
413
560
|
case 'get': return `${expr(n.o)}[${expr(n.k)}]`
|
|
414
561
|
case 'field': return `${expr(n.o)}.${n.k}`
|
|
415
|
-
case 'own':
|
|
562
|
+
case 'own': return ownSrc(expr(n.o), expr(n.k))
|
|
416
563
|
case 'not': return `!(${expr(n.e)})`
|
|
417
564
|
case 'and': return `(${expr(n.a)}&&${expr(n.b)})`
|
|
418
565
|
case 'or': return `(${expr(n.a)}||${expr(n.b)})`
|
package/lib/ir/front.mjs
CHANGED
|
@@ -332,6 +332,27 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
|
|
|
332
332
|
if (objKw && !knownObj) lets.push([o.n, { t: 'isObj', x }], [plain.n, and(o, { t: 'plainProto', x })])
|
|
333
333
|
const keys = known !== null ? ['type', ...Object.keys(s).filter((k) => k !== 'type')] : Object.keys(s)
|
|
334
334
|
const customOps = []
|
|
335
|
+
// A verdict with patternProperties beside additionalProperties: false
|
|
336
|
+
// walks the object's names once: each pattern tested once per name, a
|
|
337
|
+
// name no pattern matches and properties does not list fails. Two loops
|
|
338
|
+
// tested every pattern twice per name (minecraft-custom-main-menu-mod,
|
|
339
|
+
// 2.7x the compiled module). Errors keep the two loops: their order is
|
|
340
|
+
// the current engine's.
|
|
341
|
+
let patsFused = false
|
|
342
|
+
const fusedPats = () => {
|
|
343
|
+
const pats = s.patternProperties; const kv = vr(fresh('k')); const m = fresh('m')
|
|
344
|
+
const inner = []
|
|
345
|
+
for (const p in pats) {
|
|
346
|
+
const y = vr(fresh('x'))
|
|
347
|
+
const body = child(pats[p], y, catE(path, catE(lit('/'), { t: 'seg', k: kv })), cat(sp, '/patternProperties/' + seg(p)))
|
|
348
|
+
inner.push(iff({ t: 're', src: p, k: kv }, seq({ t: 'incr', name: m }, nop(body) ? null : { t: 'let', name: y.n, value: get(x, kv), body })))
|
|
349
|
+
}
|
|
350
|
+
const names = Object.keys(props)
|
|
351
|
+
inner.push(iff(and(cmp('===', vr(m), lit(0)), ...(names.length ? [not({ t: 'known', k: kv, names, pats: [] })] : []), has(null, kv)), { t: 'ret' }))
|
|
352
|
+
patsFused = true
|
|
353
|
+
return iff(o, { t: 'keys', obj: x, k: kv.n, order: 'keys', body: { t: 'count', name: m, body: seq(inner) } })
|
|
354
|
+
}
|
|
355
|
+
const canFuse = verdict && !ann && kw === null && s.additionalProperties === false && isObj(s.patternProperties)
|
|
335
356
|
// Under ctx each keyword's statements go to its place in the current
|
|
336
357
|
// engine's walk (lib/interpreter-core.js eval), not schema order.
|
|
337
358
|
const buckets = ctx ? new Map() : null
|
|
@@ -399,6 +420,8 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
|
|
|
399
420
|
const closed = s.additionalProperties === false && !isObj(s.patternProperties) && !ann && kw === null ? Object.keys(props) : null
|
|
400
421
|
if (inner.length) out.push(iff(o, { t: 'props', x, body: inner, keyed: ann || kw !== null ? null : keyed, closed }))
|
|
401
422
|
} else if (k === 'patternProperties') {
|
|
423
|
+
if (patsFused) continue
|
|
424
|
+
if (canFuse) { out.push(fusedPats()); continue }
|
|
402
425
|
const kv = vr(fresh('k'))
|
|
403
426
|
const inner = []
|
|
404
427
|
for (const p in n) {
|
|
@@ -417,6 +440,8 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
|
|
|
417
440
|
for (const a in n) inner.push(iff(own(x, lit(a)), seq(n[a].map((r) => iff(or(cmp('===', get(x, lit(r)), lit(undefined)), not(has(r, lit(r)))), fail(err('ATA7005', 'required', path, cat(sp, '/dependentRequired'), paramsOf(required.params(r)), lit(required.msg(r)), required.exp(r), required.rank, ev, required.detail, required.suggest)))))))
|
|
418
441
|
out.push(iff(o, seq(inner)))
|
|
419
442
|
} else if (k === 'additionalProperties') {
|
|
443
|
+
if (patsFused) continue
|
|
444
|
+
if (canFuse) { out.push(fusedPats()); continue }
|
|
420
445
|
const known2 = Object.keys(s.properties || {})
|
|
421
446
|
const pats = Object.keys(s.patternProperties || {})
|
|
422
447
|
const kv = vr(fresh('k'))
|
|
@@ -611,8 +636,14 @@ function build (schema, { assertFormat = true, schemas = null, metaschemas = nul
|
|
|
611
636
|
}
|
|
612
637
|
}
|
|
613
638
|
if (post !== null) out.push(post)
|
|
639
|
+
// A value nothing below reads is not computed: an item schema of a type
|
|
640
|
+
// alone read each array element's prototype for a flag no check used
|
|
641
|
+
// (bitrise-step, half its verdict).
|
|
642
|
+
const text = JSON.stringify(out)
|
|
643
|
+
for (const n of out) if (n.t === 'bind') n.names = n.names.filter(([name]) => text.includes('"n":' + JSON.stringify(name)))
|
|
644
|
+
out = out.filter((n) => n.t !== 'bind' || n.names.length)
|
|
614
645
|
let body = seq(out)
|
|
615
|
-
for (let i = lets.length - 1; i >= 0; i--) body = { t: 'let', name: lets[i][0], value: lets[i][1], body }
|
|
646
|
+
for (let i = lets.length - 1; i >= 0; i--) if (text.includes('"n":' + JSON.stringify(lets[i][0]))) body = { t: 'let', name: lets[i][0], value: lets[i][1], body }
|
|
616
647
|
return body
|
|
617
648
|
}
|
|
618
649
|
|
package/lib/plan-compiler.js
CHANGED
|
@@ -107,6 +107,7 @@ function install(deps) {
|
|
|
107
107
|
function fresh() { return { props: null, n: 0, x: null, keys: null }; }
|
|
108
108
|
// Object.keys once per record: every node that shares a record sees the
|
|
109
109
|
// same data object.
|
|
110
|
+
const propEnum = Object.prototype.propertyIsEnumerable;
|
|
110
111
|
function keysOf(rec, data) {
|
|
111
112
|
if (rec.keys === null) rec.keys = Object.keys(data);
|
|
112
113
|
return rec.keys;
|
|
@@ -609,9 +610,10 @@ function install(deps) {
|
|
|
609
610
|
steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
|
|
610
611
|
if (dataBits(data) !== T_OBJECT) return true;
|
|
611
612
|
let ok = true;
|
|
612
|
-
const keys = collect ? keysOf(rec, data) : Object.
|
|
613
|
+
const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
|
|
613
614
|
if (pnFn !== null) {
|
|
614
615
|
for (const k of keys) {
|
|
616
|
+
if (!collect && !propEnum.call(data, k)) continue;
|
|
615
617
|
// propertyNames applies its subschema to the property NAME, not to
|
|
616
618
|
// the value sitting at that key, so the failure is reported against
|
|
617
619
|
// the object that carries the name rather than against `/<key>`.
|
|
@@ -643,7 +645,11 @@ function install(deps) {
|
|
|
643
645
|
}
|
|
644
646
|
evaluated = true;
|
|
645
647
|
}
|
|
646
|
-
|
|
648
|
+
// A name only properties lists is read whether enumerable or not,
|
|
649
|
+
// as the short walk above and the generated code read it by name;
|
|
650
|
+
// patterns and additionalProperties see the names for-in sees.
|
|
651
|
+
const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
|
|
652
|
+
if (patterns !== null && seen) {
|
|
647
653
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
648
654
|
const pp = patterns[pi];
|
|
649
655
|
if (pp.re.test(key)) {
|
|
@@ -655,7 +661,7 @@ function install(deps) {
|
|
|
655
661
|
}
|
|
656
662
|
}
|
|
657
663
|
}
|
|
658
|
-
if (!evaluated && apFn !== null) {
|
|
664
|
+
if (!evaluated && apFn !== null && seen) {
|
|
659
665
|
if (apFalse) {
|
|
660
666
|
ok = false;
|
|
661
667
|
if (errors === NOERRORS) return false;
|
|
@@ -715,9 +721,9 @@ function install(deps) {
|
|
|
715
721
|
vsteps.push((data, stack, rec) => {
|
|
716
722
|
if (fusedLeaf !== null && !fusedLeaf(data)) return false;
|
|
717
723
|
if (dataBits(data) !== T_OBJECT) return true;
|
|
718
|
-
const keys = collect ? keysOf(rec, data) : Object.
|
|
724
|
+
const keys = collect ? keysOf(rec, data) : Object.getOwnPropertyNames(data);
|
|
719
725
|
if (pnFn !== null) {
|
|
720
|
-
for (const k of keys) if (!pnFn.v(k, stack)) return false;
|
|
726
|
+
for (const k of keys) if ((collect || propEnum.call(data, k)) && !pnFn.v(k, stack)) return false;
|
|
721
727
|
}
|
|
722
728
|
let seenReq = 0;
|
|
723
729
|
for (let k = 0; k < keys.length; k++) {
|
|
@@ -731,7 +737,8 @@ function install(deps) {
|
|
|
731
737
|
if (prop.req) seenReq++;
|
|
732
738
|
evaluated = true;
|
|
733
739
|
}
|
|
734
|
-
|
|
740
|
+
const seen = collect || prop !== undefined && patterns === null && apFn === null || propEnum.call(data, key);
|
|
741
|
+
if (patterns !== null && seen) {
|
|
735
742
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
736
743
|
const pp = patterns[pi];
|
|
737
744
|
if (pp.re.test(key)) {
|
|
@@ -740,7 +747,7 @@ function install(deps) {
|
|
|
740
747
|
}
|
|
741
748
|
}
|
|
742
749
|
}
|
|
743
|
-
if (!evaluated && apFn !== null) {
|
|
750
|
+
if (!evaluated && apFn !== null && seen) {
|
|
744
751
|
if (!apFn.v(data[key], stack)) return false;
|
|
745
752
|
evaluated = true;
|
|
746
753
|
}
|
|
@@ -762,6 +769,13 @@ function install(deps) {
|
|
|
762
769
|
const n = propKeys.length;
|
|
763
770
|
const fns = propList.map((e) => e.fn);
|
|
764
771
|
const reqs = propList.map((e) => e.req);
|
|
772
|
+
// Names met, as bits: the required ones must all be set, and a
|
|
773
|
+
// listed name not met may still be the object's own, hidden from
|
|
774
|
+
// for-in (defineProperty, enumerable: false), which the generated
|
|
775
|
+
// code and this engine's error path read by name: those are read
|
|
776
|
+
// after the walk, which on an ordinary document is the absent ones.
|
|
777
|
+
const reqMask = reqs.reduce((m, r, i) => r ? m | (1 << i) : m, 0);
|
|
778
|
+
const allMask = n === 32 ? -1 : (1 << n) - 1;
|
|
765
779
|
// A property whose check is a coded leaf is checked in place.
|
|
766
780
|
const codes = propList.map((e) => (e.fn.leaf && e.fn.v.lc !== undefined ? e.fn.v.lc : null));
|
|
767
781
|
vsteps.push((data, stack, rec) => {
|
|
@@ -772,13 +786,21 @@ function install(deps) {
|
|
|
772
786
|
let i = 0;
|
|
773
787
|
while (i < n && propKeys[i] !== key) i++;
|
|
774
788
|
if (i === n) { if (Object.hasOwn(data, key)) return false; continue; }
|
|
789
|
+
seen |= 1 << i;
|
|
775
790
|
const value = data[key];
|
|
776
791
|
if (value === undefined) { if (reqs[i]) return false; continue; }
|
|
777
792
|
const lc = codes[i];
|
|
778
793
|
if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
|
|
779
|
-
if (reqs[i]) seen++;
|
|
780
794
|
}
|
|
781
|
-
|
|
795
|
+
if ((seen & reqMask) !== reqMask) return false;
|
|
796
|
+
for (let miss = seen ^ allMask; miss !== 0; miss &= miss - 1) {
|
|
797
|
+
const i = 31 - Math.clz32(miss & -miss);
|
|
798
|
+
const value = data[propKeys[i]];
|
|
799
|
+
if (value === undefined || !Object.hasOwn(data, propKeys[i])) continue;
|
|
800
|
+
const lc = codes[i];
|
|
801
|
+
if (lc !== null ? !leafOk(lc, value) : !fns[i].v(value, stack)) return false;
|
|
802
|
+
}
|
|
803
|
+
return true;
|
|
782
804
|
});
|
|
783
805
|
}
|
|
784
806
|
}
|
package/lib/shape-classifier.js
CHANGED
|
@@ -40,9 +40,21 @@ function isPrimitiveEnumValue(v) {
|
|
|
40
40
|
return v === null || t === 'string' || t === 'number' || t === 'boolean';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// A node of `enum` or `const` alone, no type: the values decide, as the full
|
|
44
|
+
// engines decide it (enumMember, deepEqual), and every value is a primitive,
|
|
45
|
+
// so a Set lookup or a === is the whole check. Form fields are written this
|
|
46
|
+
// way often (`{ enum: ['TR', 'DE'] }`, `{ const: true }`), and one of them
|
|
47
|
+
// sent the whole object to the slower engines.
|
|
48
|
+
const TIER0_UNTYPED_ALLOWED = new Set(['enum', 'const', ...META_KEYS]);
|
|
49
|
+
|
|
43
50
|
function isTier0Primitive(schema) {
|
|
44
51
|
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
|
|
45
|
-
if (!isPrimitiveType(schema.type))
|
|
52
|
+
if (!isPrimitiveType(schema.type)) {
|
|
53
|
+
if (schema.type !== undefined || (schema.enum === undefined && schema.const === undefined)) return false;
|
|
54
|
+
for (const k of Object.keys(schema)) {
|
|
55
|
+
if (!TIER0_UNTYPED_ALLOWED.has(k)) return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
46
58
|
for (const k of Object.keys(schema)) {
|
|
47
59
|
if (!TIER0_PRIMITIVE_ALLOWED.has(k)) return false;
|
|
48
60
|
}
|
package/lib/tier0.js
CHANGED
|
@@ -166,9 +166,10 @@ function checkPrimitive(c, v) {
|
|
|
166
166
|
}
|
|
167
167
|
} else if (m === T_BOOLEAN) {
|
|
168
168
|
if (typeof v !== 'boolean') return false;
|
|
169
|
-
} else {
|
|
169
|
+
} else if (m !== 0) {
|
|
170
170
|
return false;
|
|
171
171
|
}
|
|
172
|
+
// m === 0: a node of enum or const alone; the checks below decide.
|
|
172
173
|
if (c.hasEnum && !c.enumSet.has(v)) return false;
|
|
173
174
|
if (c.hasConst && v !== c.constVal) return false;
|
|
174
175
|
return true;
|
|
@@ -176,18 +177,32 @@ function checkPrimitive(c, v) {
|
|
|
176
177
|
|
|
177
178
|
// Inlined object validator. Separating the primitive path removes a dead
|
|
178
179
|
// branch from the hot object path.
|
|
179
|
-
|
|
180
|
+
// The object verdict as one closure over the plan's fields, read once: the
|
|
181
|
+
// bound isValidObject() is then one call into a function whose loop reads
|
|
182
|
+
// captured constants, where a plan passed each call cost two calls and a
|
|
183
|
+
// field load per use (an empty object 17 ns against 7 for a validator built
|
|
184
|
+
// the same way by hand; a number 9 against 4).
|
|
185
|
+
function makeObjectVerdict(plan) {
|
|
186
|
+
const P_additionalAllowed = plan.additionalAllowed;
|
|
187
|
+
const P_constraints = plan.constraints;
|
|
188
|
+
const P_knownKeys = plan.knownKeys;
|
|
189
|
+
const P_requiredMask = plan.requiredMask;
|
|
190
|
+
return function tier0ObjectVerdict(data) {
|
|
180
191
|
if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
|
|
181
|
-
const cs =
|
|
192
|
+
const cs = P_constraints;
|
|
182
193
|
const n = cs.length;
|
|
183
|
-
const reqMask =
|
|
194
|
+
const reqMask = P_requiredMask;
|
|
195
|
+
// Whether the object is an ordinary one, read once: then a defined value
|
|
196
|
+
// under a name Object.prototype lacks is the object's own. Read for each
|
|
197
|
+
// present field, it was one test per field on a valid document.
|
|
198
|
+
const plain = (_PN ? data.__proto__ : Object.getPrototypeOf(data)) === Object.prototype;
|
|
184
199
|
let seenMask = 0;
|
|
185
200
|
for (let i = 0; i < n; i++) {
|
|
186
201
|
const c = cs[i];
|
|
187
202
|
let v = data[c.key];
|
|
188
203
|
// Presence is an own-property question: a value the object inherits is
|
|
189
204
|
// treated as absent, as the interpreted engine treats it.
|
|
190
|
-
if (v !== undefined && (c.protoName ||
|
|
205
|
+
if (v !== undefined && (c.protoName || !plain) && !_hop.call(data, c.key)) v = undefined;
|
|
191
206
|
if (v === undefined) {
|
|
192
207
|
// A required key holding undefined is missing, as in every engine.
|
|
193
208
|
if (reqMask & (1 << i)) return false;
|
|
@@ -225,21 +240,35 @@ function tier0ValidateObject(plan, data) {
|
|
|
225
240
|
}
|
|
226
241
|
} else if (m === T_BOOLEAN) {
|
|
227
242
|
if (typeof v !== 'boolean') return false;
|
|
228
|
-
} else {
|
|
243
|
+
} else if (m !== 0) {
|
|
229
244
|
return false;
|
|
230
245
|
}
|
|
246
|
+
// m === 0: a node of enum or const alone; the checks below decide.
|
|
231
247
|
if (c.hasEnum && !c.enumSet.has(v)) return false;
|
|
232
248
|
if (c.hasConst && v !== c.constVal) return false;
|
|
233
249
|
}
|
|
234
250
|
if ((seenMask & reqMask) !== reqMask) return false;
|
|
235
|
-
if (!
|
|
236
|
-
const known =
|
|
251
|
+
if (!P_additionalAllowed) {
|
|
252
|
+
const known = P_knownKeys;
|
|
237
253
|
for (const k in data) {
|
|
238
254
|
if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
|
|
239
255
|
if (!known.has(k)) return false;
|
|
240
256
|
}
|
|
241
257
|
}
|
|
242
258
|
return true;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function tier0ValidateObject(plan, data) {
|
|
262
|
+
let f = plan._objectVerdict;
|
|
263
|
+
if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
|
|
264
|
+
return f(data);
|
|
265
|
+
}
|
|
266
|
+
// The verdict for a plan as one closure, for a validator to bind directly.
|
|
267
|
+
function tier0Verdict(plan) {
|
|
268
|
+
if (plan.isPrimitive) { const c = plan.constraints[0]; return (data) => checkPrimitive(c, data); }
|
|
269
|
+
let f = plan._objectVerdict;
|
|
270
|
+
if (f === undefined) f = plan._objectVerdict = makeObjectVerdict(plan);
|
|
271
|
+
return f;
|
|
243
272
|
}
|
|
244
273
|
|
|
245
274
|
function tier0Validate(plan, data) {
|
|
@@ -250,6 +279,7 @@ function tier0Validate(plan, data) {
|
|
|
250
279
|
module.exports = {
|
|
251
280
|
buildTier0Plan,
|
|
252
281
|
tier0Validate,
|
|
282
|
+
tier0Verdict,
|
|
253
283
|
tier0ValidateObject,
|
|
254
284
|
checkPrimitive,
|
|
255
285
|
TYPE_MASK,
|
package/lib/validator-core.js
CHANGED
|
@@ -1669,8 +1669,13 @@ class Validator {
|
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
1671
|
addSchema(schema) {
|
|
1672
|
-
|
|
1673
|
-
|
|
1672
|
+
// After a compile of either kind: the verdict-only one isValidObject()
|
|
1673
|
+
// makes (_jsFn) binds a function over the registry as it was, and a
|
|
1674
|
+
// schema added after it reached validate() but not isValidObject(),
|
|
1675
|
+
// which kept answering against the old definition. A call the
|
|
1676
|
+
// interpreted twin answered compiled nothing, and the twin is rebuilt.
|
|
1677
|
+
if (this._initialized || this._jsFn) {
|
|
1678
|
+
throw new Error('Cannot add schema after compilation: call addSchema() before validate(), isValidObject(), validateJSON() or isValidJSON()')
|
|
1674
1679
|
}
|
|
1675
1680
|
if (!schema || !schema.$id) {
|
|
1676
1681
|
throw new Error('Schema must have $id')
|
|
@@ -2356,16 +2361,22 @@ function _ticks(self) {
|
|
|
2356
2361
|
// _ticks says so.
|
|
2357
2362
|
function _tier0Verdict(self) {
|
|
2358
2363
|
if (require('./shape-classifier').classify(self._schemaObj).tier !== 0) return null;
|
|
2359
|
-
const { buildTier0Plan,
|
|
2364
|
+
const { buildTier0Plan, tier0Verdict } = require('./tier0');
|
|
2360
2365
|
const plan = buildTier0Plan(self._schemaObj, self._userFormats);
|
|
2361
|
-
|
|
2366
|
+
const verdict = tier0Verdict(plan);
|
|
2367
|
+
if (!_ticks(self)) return verdict;
|
|
2362
2368
|
const tick = _tiering.counter(self);
|
|
2363
|
-
return (d) => { tick(); return
|
|
2369
|
+
return (d) => { tick(); return verdict(d); };
|
|
2364
2370
|
}
|
|
2365
2371
|
|
|
2366
2372
|
Validator.prototype._coldTwin = function () {
|
|
2367
2373
|
const cg = _loadCodegen();
|
|
2368
|
-
|
|
2374
|
+
// Where no code can be generated (a strict CSP), the interpreted engine is
|
|
2375
|
+
// the compile itself: a twin would only delay it, and its size test read
|
|
2376
|
+
// the schema's text for nothing (6.5% of a schema's first check there).
|
|
2377
|
+
// The probe of the realm (codegenAvailable) is not run for a validator
|
|
2378
|
+
// asked for the interpreted engine, which never generates code.
|
|
2379
|
+
return cg === null || cg.coldTwin === undefined || this._interpretOnly || !codegenAvailable() ? null : cg.coldTwin(this, Validator);
|
|
2369
2380
|
};
|
|
2370
2381
|
|
|
2371
2382
|
_defineLazyMethod('validate', (self) => (data) => {
|
|
@@ -2402,7 +2413,14 @@ _defineLazyMethod('isValidObject', (self) => {
|
|
|
2402
2413
|
_bindVerdict(self, _ticks(self) ? _t0 : (d) => {
|
|
2403
2414
|
const r = _t0(d);
|
|
2404
2415
|
if (++_n === 2) {
|
|
2416
|
+
const before = self.isValidObject;
|
|
2405
2417
|
try { self._ensureCodegen(); } catch {}
|
|
2418
|
+
// Where the generated code did not take the method over (no code
|
|
2419
|
+
// generation in this realm, or the generator declined), the plan's
|
|
2420
|
+
// closure answers directly from now on: this wrapper cost every
|
|
2421
|
+
// later call a count and a call, 13.5 against 4.7 ns on an empty
|
|
2422
|
+
// object, on exactly the pages and workers that have no generator.
|
|
2423
|
+
if (self.isValidObject === before) _bindVerdict(self, _t0);
|
|
2406
2424
|
}
|
|
2407
2425
|
return r;
|
|
2408
2426
|
});
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.63.
|
|
3
|
+
"version": "1.63.1",
|
|
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
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -266,13 +266,13 @@
|
|
|
266
266
|
"codegen.d.ts"
|
|
267
267
|
],
|
|
268
268
|
"optionalDependencies": {
|
|
269
|
-
"@ata-validator/native-darwin-arm64": "1.63.
|
|
270
|
-
"@ata-validator/native-darwin-x64": "1.63.
|
|
271
|
-
"@ata-validator/native-linux-arm64-gnu": "1.63.
|
|
272
|
-
"@ata-validator/native-linux-arm64-musl": "1.63.
|
|
273
|
-
"@ata-validator/native-linux-x64-gnu": "1.63.
|
|
274
|
-
"@ata-validator/native-linux-x64-musl": "1.63.
|
|
275
|
-
"@ata-validator/native-win32-x64": "1.63.
|
|
269
|
+
"@ata-validator/native-darwin-arm64": "1.63.1",
|
|
270
|
+
"@ata-validator/native-darwin-x64": "1.63.1",
|
|
271
|
+
"@ata-validator/native-linux-arm64-gnu": "1.63.1",
|
|
272
|
+
"@ata-validator/native-linux-arm64-musl": "1.63.1",
|
|
273
|
+
"@ata-validator/native-linux-x64-gnu": "1.63.1",
|
|
274
|
+
"@ata-validator/native-linux-x64-musl": "1.63.1",
|
|
275
|
+
"@ata-validator/native-win32-x64": "1.63.1"
|
|
276
276
|
},
|
|
277
277
|
"peerDependencies": {
|
|
278
278
|
"yaml": "^2.0.0"
|