ata-validator 1.63.0 → 1.63.2

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/bin/ata.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const { env } = require('../lib/env');
4
+ // Text that comes from a schema file (a $ref, a keyword name in a warning) is
5
+ // written with its control characters as escapes, as `ata validate` writes
6
+ // it: a schema could otherwise clear the screen or forge an `ata:` line.
7
+ const term = (t) => require('../lib/diagnose').term(String(t));
4
8
 
5
9
  const fs = require('fs');
6
10
  const path = require('path');
@@ -299,7 +303,7 @@ function cmdCompile(args) {
299
303
  for (const w of compileWarnings) reportCompileError(input, w);
300
304
  process.exit(1);
301
305
  }
302
- for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
306
+ for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${term(w)}\n`);
303
307
  }
304
308
  if (!src) {
305
309
  reportCompileError(input, 'schema is too complex for standalone compilation');
@@ -397,7 +401,7 @@ function cmdBuild(args) {
397
401
  for (const c of report.compiled) {
398
402
  process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
399
403
  for (const t of (report.bunTargets || []).filter((t) => t.input === c.input)) process.stdout.write(`ata: bun target -> ${t.output} (for bun build --compile --aot)\n`);
400
- for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${d.reason}\n`);
404
+ for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${term(d.reason)}\n`);
401
405
  }
402
406
  if (report.runtime) {
403
407
  process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
@@ -412,7 +416,7 @@ function cmdBuild(args) {
412
416
  noDetail++;
413
417
  process.stderr.write(`ata: warning ${w.input}: no error detail, failures report the single ATA9000 error (the verdict is exact)\n`);
414
418
  } else {
415
- process.stderr.write(`ata: warning ${w.input}: ${w.warning}\n`);
419
+ process.stderr.write(`ata: warning ${w.input}: ${term(w.warning)}\n`);
416
420
  }
417
421
  }
418
422
  if (noDetail > 0 && args.opts.mode !== 'schema') {
@@ -422,7 +426,7 @@ function cmdBuild(args) {
422
426
  process.stdout.write(`ata: cached ${c.input}\n`);
423
427
  }
424
428
  for (const s of report.skipped) {
425
- process.stdout.write(`ata: skipped ${s.input}: ${s.reason}\n`);
429
+ process.stdout.write(`ata: skipped ${s.input}: ${term(s.reason)}\n`);
426
430
  }
427
431
  for (const f of report.failed) {
428
432
  reportCompileError(f.input, f.error);
@@ -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 schema.$ref === 'string' ? schema.$ref : null;
391
- this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
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 (schema.type !== undefined) {
397
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
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 = schema.type !== undefined;
404
- this.enum = schema.enum !== undefined ? schema.enum : null;
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 = schema.const !== undefined;
421
- this.const = schema.const;
422
-
423
- this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
424
- this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
425
- this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
426
- this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
427
- this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
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 = schema.minLength;
431
- this.maxLength = schema.maxLength;
432
- this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
433
- this.patternSource = schema.pattern;
434
- this.format = schema.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 (schema.format !== undefined) {
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 = schema.format;
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 = schema.format !== undefined && nf !== null && typeof nf[schema.format] === 'function' ? nf[schema.format] : null;
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 = schema.minItems;
455
- this.maxItems = schema.maxItems;
456
- this.uniqueItems = schema.uniqueItems === true;
457
- this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
458
- this.items = schema.items !== undefined ? child(schema.items) : undefined;
459
- this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
460
- this.minContains = schema.minContains;
461
- this.maxContains = schema.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(schema.required) ? schema.required : null;
465
- this.minProperties = schema.minProperties;
466
- this.maxProperties = schema.maxProperties;
467
- this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
468
- this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
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(schema.properties)) {
532
+ if (isObjectMap(S.properties)) {
471
533
  this.properties = new Map();
472
- for (const key of Object.keys(schema.properties)) {
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(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
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(schema.patternProperties)) {
495
- this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
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 = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
498
- this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
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(schema.propertyDependencies)) {
501
- this.propertyDependencies = Object.entries(schema.propertyDependencies)
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(schema.allOf) ? schema.allOf.map(child) : null;
508
- this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
509
- this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
510
- this.not = schema.not !== undefined ? child(schema.not) : undefined;
511
- this.if = schema.if !== undefined ? child(schema.if) : undefined;
512
- this.then = schema.then !== undefined ? child(schema.then) : undefined;
513
- this.else = schema.else !== undefined ? child(schema.else) : undefined;
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 = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
531
- this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
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 = schema.unevaluatedProperties === false;
536
- this.unevaluatedItemsFalse = schema.unevaluatedItems === false;
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.keys(data);
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
@@ -16,8 +16,16 @@ const FAST = {
16
16
  pattern: (n, x) => compilePatternInline(n, x),
17
17
  // A string has between length/2 and length code points, so the length
18
18
  // alone settles most cases and the count runs only near the bound.
19
- minLength: (n, x) => `${x}.length>=${2 * n}||${x}.length>=${n}&&cpLen(${x})>=${n}`,
20
- maxLength: (n, x) => `${x}.length<=${n}||${x}.length<=${2 * n}&&cpLen(${x})<=${n}`,
19
+ // The bound is written into the module as a number, so it has to be one: a
20
+ // schema that gave a string here had it written as source, and what the
21
+ // string said ran in whatever imported the module. Refused, as the closure
22
+ // writer refuses it.
23
+ minLength: (n, x) => { const b = bound(n, 'minLength'); return `${x}.length>=${2 * b}||${x}.length>=${b}&&cpLen(${x})>=${b}` },
24
+ maxLength: (n, x) => { const b = bound(n, 'maxLength'); return `${x}.length<=${b}||${x}.length<=${2 * b}&&cpLen(${x})<=${b}` },
25
+ }
26
+ function bound (n, k) {
27
+ if (typeof n !== 'number' || !Number.isFinite(n)) throw new Error(`ir: ${k} is not a number`)
28
+ return n
21
29
  }
22
30
  // enum and const with only primitive values need no deep equality: the
23
31
  // value is one of them exactly when it is === one of them (JSON numbers have
@@ -44,7 +52,14 @@ function inline (fn, vExpr, nExpr) {
44
52
  }
45
53
 
46
54
  // How many names an object must list before a module visits the object's own
47
- // names instead of looking each one up.
55
+ // names instead of looking each one up. Measured on SchemaStore's samples,
56
+ // warmed, against the compiled modules: from 16 names the geometric mean is
57
+ // 0.878 with 23 schemas under half the time, from 32 it is 0.914 with 15,
58
+ // from 64 0.941 with 8, and the count slower by a fifth is 47 at each. A
59
+ // dense document under a short list (sil-kit, csslintrc: most of 16 to 38
60
+ // names present) pays the walk's array and dispatch, 2 to 4x the direct
61
+ // reads; a sparse one under a long list (putout, 475 names) would pay a
62
+ // read per name, 100x the walk.
48
63
  const WALK_MIN = 16
49
64
  // Source characters past which a verdict body is its own function (the
50
65
  // verdict threshold of the current engine's generator, SPLIT_PROP_SCHEMA).
@@ -96,14 +111,90 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
96
111
  use(fn)
97
112
  return fn
98
113
  }
114
+ // An own-property test written in the module with the module's own
115
+ // reference to hasOwnProperty. The kernels' own() is one function for every
116
+ // module of a page, so its property lookup met the objects of every schema
117
+ // and stopped being cached for any (rc3-environment alone 1.0x the compiled
118
+ // module, beside 438 other schemas 2.1x).
119
+ let HOP = null
120
+ const ownSrc = (o, k) => {
121
+ if (HOP === null) HOP = constOf('Object.prototype.hasOwnProperty')
122
+ return `${HOP}.call(${o},${k})`
123
+ }
124
+ // A small verdict written in at its call, as the compiled modules write a
125
+ // referenced schema in at each property: every copy keeps its own record of
126
+ // the shapes it meets, where one function called from forty places met all
127
+ // of them and V8 stopped inlining it (coffeelint, 38 properties naming one
128
+ // definition). Only a verdict with no cycle through it, no guard and no
129
+ // dynamic scope, whose body is INLINE_BODY characters or fewer; a verdict
130
+ // body leaves only by `return false`, so it runs as a block. Its parameters
131
+ // are bound in the block: the value, and the empty paths and list of a
132
+ // verdict call.
133
+ const INLINE_BODY = 500
134
+ // Inlined source one function may take in all: past it a function grows
135
+ // beyond what V8 optimizes (putout's 475 properties, 0.01x the compiled
136
+ // module before, 1.08x with every call written in).
137
+ // 2000 left coffeelint's 38 names calling one 300-character check (2.2x
138
+ // the compiled module); 8000 inlines them and keeps putout's 475-name
139
+ // function optimizable (both measured on SchemaStore, warmed).
140
+ const INLINE_BUDGET = 8000
141
+ let inlineBudget = INLINE_BUDGET
142
+ const inlining = new Set()
143
+ const inlineBodies = new Map()
144
+ const inlineCall = (c) => {
145
+ if (c.proc === null || c.dynamic || c.guard || ir.dynamic || inlining.has(c.proc)) return null
146
+ const p = ir.procs.get(c.proc)
147
+ if (!p || p.mode !== 'verdict') return null
148
+ // Follow a verdict that is only a call of another (procName's alias).
149
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
150
+ 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 })
151
+ // Each procedure is written once, with a full budget for what it
152
+ // inlines itself, and that text is its size: a site inlines it when the
153
+ // text fits the site's remaining budget. Written at every call site
154
+ // under the site's budget, the text depended on the order of the sites,
155
+ // and a definition too large under a deep nesting was rendered again
156
+ // for each site below each site (a suite schema took ten minutes).
157
+ let body = inlineBodies.get(c.proc)
158
+ if (body === undefined) {
159
+ inlining.add(c.proc); const saved = inlineBudget; inlineBudget = INLINE_BUDGET
160
+ try { body = stmt(p.body, true) } finally { inlining.delete(c.proc); inlineBudget = saved }
161
+ if (body.length > INLINE_BODY || /\breturn true\b/.test(body)) body = null
162
+ inlineBodies.set(c.proc, body)
163
+ }
164
+ if (body === null || body.length > inlineBudget) return null
165
+ inlineBudget -= body.length
166
+ const x = expr(c.x); const t = 'i' + (++inlineN)
167
+ return `{const ${t}=${x}\n{const d=${t},V=${t},e=null,P="",SP="",A=null\n${body}}}\n`
168
+ }
169
+ let inlineN = 0
170
+ // The verdict calls already made in the row of statements being written
171
+ // (seq), handed to a row nested directly in it.
172
+ let seqSeen = null
173
+ // The plain-prototype flags a fill has bound, by the object's expression.
174
+ const fillPlain = new Map()
99
175
  const procName = (id) => {
100
176
  if (emitted.has(id)) return emitted.get(id)
101
177
  const p = ir.procs.get(id)
178
+ // A verdict that is one call of another on its own value (a branch that
179
+ // is only a $ref) is that other verdict: no function of its own, and a
180
+ // caller that already ran it can see the repeat (seq).
181
+ if (p.mode === 'verdict' && !ir.dynamic) {
182
+ const b = p.body.t === 'seq' && p.body.body.length === 1 ? p.body.body[0] : p.body
183
+ 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) {
184
+ const to = procName(b.e.proc)
185
+ emitted.set(id, to)
186
+ return to
187
+ }
188
+ }
102
189
  const name = id
103
190
  emitted.set(id, name)
104
191
  const isVerdict = p.mode === 'verdict'
105
- if (p.mode === 'fill') { fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
106
- const body = stmt(p.body, isVerdict)
192
+ if (p.mode === 'fill') { fillPlain.clear(); fns.push(`function ${name}(d){\n${stmt(p.body, true)}}`); return name }
193
+ // Each function written gets its own budget of inlined source (inlineCall).
194
+ const savedBudget = inlineBudget
195
+ inlineBudget = INLINE_BUDGET
196
+ let body
197
+ try { body = stmt(p.body, isVerdict) } finally { inlineBudget = savedBudget }
107
198
  const head = p.mode === 'ann' ? 'if(A===null)A=newAnn()\n' : ''
108
199
  if (p.mode === 'ann') use('newAnn')
109
200
  const wrapped = ir.dynamic
@@ -213,7 +304,23 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
213
304
  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
305
  if (ap) { skip = ap; folded.add(walked.then) }
215
306
  }
216
- return n.body.filter((x) => x !== skip).map((x) => stmt(x, v)).join('')
307
+ // In a verdict the same check of the same value twice in a row of
308
+ // statements is once: the first returned false or passed, and a
309
+ // verdict has no side effects (custom keywords and formats, which
310
+ // could, are refused above). A schema that names one definition
311
+ // under both allOf and anyOf ran it twice per item (devinit).
312
+ // A row nested directly in a row runs unconditionally after what came
313
+ // before it, so it shares the set; anything under a condition, a
314
+ // loop or a let starts its own.
315
+ const seen = v ? (seqSeen || new Set()) : null
316
+ seqSeen = null
317
+ return n.body.filter((x) => x !== skip).map((x) => {
318
+ seqSeen = x.t === 'seq' ? seen : null
319
+ const t = stmt(x, v)
320
+ seqSeen = null
321
+ if (seen !== null && (x.t === 'require' || x.t === 'branches')) { if (seen.has(t)) return ''; seen.add(t) }
322
+ return t
323
+ }).join('')
217
324
  }
218
325
  // An object that lists many names: visit the object's own names, every
219
326
  // one enumerable or not (an entry checks its own presence, so a name it
@@ -235,7 +342,14 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
235
342
  const rows = n.keyed.map(([p, i]) => {
236
343
  const e = n.body[i]
237
344
  const c = v ? verdictCall(e) : null
238
- if (c !== null) return { p, fn: procName(c.proc) }
345
+ if (c !== null) {
346
+ // A small check written in at its name (inlineCall), as the
347
+ // compiled modules write a referenced definition at each
348
+ // property: csslintrc's 31 names each called one enum check.
349
+ const t = inlineCall({ ...c, x: { t: 'var', n: 'y' } })
350
+ if (t !== null) return { p, inl: t }
351
+ return { p, fn: procName(c.proc) }
352
+ }
239
353
  if (v && e.t === 'let' && e.body.t === 'if' && !e.body.else && selfContained(e.body.then, e.name)) {
240
354
  const text = stmt(e.body.then, true)
241
355
  return { p, e, text, src: `{const ${e.name}=${expr(e.value)}\nif(${expr(e.body.c)}){\n${text}}}\n` }
@@ -248,7 +362,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
248
362
  const small = tabled.filter((r) => (sizes.get(r.fn) ?? Infinity) <= INLINE_MAX).length
249
363
  const useTable = tabled.length >= TABLE_MIN || (tabled.length >= WALK_MIN && small * 2 < tabled.length)
250
364
  const left = rows.filter((r) => !useTable || !r.fn)
251
- 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.src
365
+ // In the walk a name comes from the object's own names, so its value
366
+ // is read without a presence test.
367
+ 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
368
  // A key is told from the names by its length first, a switch on an
253
369
  // integer the engine makes a jump table, and then compared with the
254
370
  // few names of that length: a switch on the names compared each key
@@ -265,9 +381,20 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
265
381
  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
382
  // A name the table answers is listed; with the fold every other name
267
383
  // goes through the dispatch, which knows the listed ones.
268
- if (!useTable) return `${always}{const ks=Object.getOwnPropertyNames(${X});for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}}\n`
269
- const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
270
- return `${always}{const ks=Object.getOwnPropertyNames(${X});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')}` : ''}}}\n`
384
+ let walk
385
+ if (!useTable) walk = `for(let j=0;j<ks.length;j++){const k=ks[j];${dispatch('k')}}`
386
+ else {
387
+ const T = constOf(`new Map([${tabled.map((r) => `[${lit(r.p)},${r.fn}]`).join(',')}])`)
388
+ 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')}` : ''}}`
389
+ }
390
+ // A self-measuring switch to a direct read of each listed name on
391
+ // dense documents was tried here and taken out: it won on five
392
+ // config schemas (csslintrc, coffeelint, jshintrc) and lost as much
393
+ // on others (liquibase 0.27 to 1.46 of the compiled module, a
394
+ // 142-name object read in dictionary mode 8x slower than the walk),
395
+ // no rule from the measurements told the two apart, and its direct
396
+ // form once rejected {} through an inherited toString.
397
+ return `${always}{const ks=Object.getOwnPropertyNames(${X});${walk}}\n`
271
398
  }
272
399
  case 'if': {
273
400
  // A condition that is a call under annotations merges its collector.
@@ -302,6 +429,13 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
302
429
  // SchemaStore's slower by 6-80% when all of them run in one process.)
303
430
  const last = n.order === 'protoLast' && !v
304
431
  if (last) use('keysOf')
432
+ // A verdict walks the object's own names with for-in, no array, each
433
+ // name tested as the object's own: a name enumerable on
434
+ // Object.prototype is not one of them (tests/test_ir_module_split.mjs).
435
+ if (v) {
436
+ const o = expr(n.obj)
437
+ return `for(const ${n.k} in ${o}){if(!${ownSrc(o, n.k)})continue\n${stmt(n.body, v)}}\n`
438
+ }
305
439
  return `for(const ${n.k} of ${last ? 'keysOf' : 'Object.keys'}(${expr(n.obj)})){\n${stmt(n.body, v)}}\n`
306
440
  }
307
441
  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 +449,48 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
315
449
  use('newAnn', 'mergeAnn')
316
450
  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
451
  }
452
+ if (v && c.e === 'null') { const t = inlineCall(c); if (t !== null) return t }
318
453
  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
454
  return `if(!${callOf(c, 'null', 'null')})${v ? 'return false' : '{if(e===null)return false}'}\n`
320
455
  }
321
456
  case 'fail': return fail(n.err, v) + '\n'
322
457
  case 'branches': return branches(n, v)
323
458
  case 'setDefault': {
324
- use('own')
325
459
  if (n.k === '__proto__') use('setOwn')
326
460
  const val = n.v
327
461
  let src
328
462
  if (typeof val !== 'object' || val === null) src = lit(val)
329
463
  else if (Array.isArray(val) && val.length === 0) src = '[]'
330
- else { use('copyOf'); src = `copyOf(${constOf(lit(val))})` }
464
+ else {
465
+ // A small default written as a literal: a fresh value each time
466
+ // it is read, as the compiled modules write `{}`. copyOf's JSON
467
+ // round trip was 72% of revola's verdict and 39% of changepacks'.
468
+ // Not when a key in it is `__proto__`, which a literal would read
469
+ // as the prototype, nor a large one, which would swell the fill.
470
+ const text = lit(val)
471
+ if (text.length <= 400 && !text.includes('"__proto__"')) src = text
472
+ else { use('copyOf'); src = `copyOf(${constOf(text)})` }
473
+ }
331
474
  const o = expr(n.o)
332
475
  // A store under the name itself: through setOwn's computed key, V8
333
476
  // turned an object given more than a few defaults into a dictionary,
334
477
  // slow from then on for every reader (htmlhint's root, 23 defaults:
335
478
  // getOwnPropertyNames 10 to 999 ns). __proto__ needs defineProperty.
336
- if (n.k === '__proto__') return `if(!own(${o},${lit(n.k)}))setOwn(${o},${lit(n.k)},${src})\n`
479
+ if (n.k === '__proto__') return `if(!${ownSrc(o, lit(n.k))})setOwn(${o},${lit(n.k)},${src})\n`
480
+ // A defined value under the name on an ordinary object is the
481
+ // object's own: the own test runs only for an undefined read, or on
482
+ // an object with another prototype, as the current engine's fill
483
+ // reads it. The flag is bound once per object in the fill.
484
+ if (!(n.k in Object.prototype)) {
485
+ let pl = fillPlain.get(o); let bind = ''
486
+ if (pl === undefined) { pl = 'pl' + fillPlain.size; fillPlain.set(o, pl); bind = `const ${pl}=(PN?${o}.__proto__:Object.getPrototypeOf(${o}))===Object.prototype\n` }
487
+ return `${bind}if((${o}[${lit(n.k)}]===void 0||!${pl})&&!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
488
+ }
337
489
  // A read or `in` before hasOwnProperty, as the current engine's fill
338
490
  // does, was measured both ways: faster where the defaults are mostly
339
491
  // present (jshintrc 1195 to 522 ns), slower where they are mostly
340
492
  // missing (prometheus 3671 to 4601). hasOwnProperty alone is kept.
341
- return `if(!own(${o},${lit(n.k)}))${o}[${lit(n.k)}]=${src}\n`
493
+ return `if(!${ownSrc(o, lit(n.k))})${o}[${lit(n.k)}]=${src}\n`
342
494
  }
343
495
  case 'fcall': return `${procName(n.proc)}(${expr(n.x)})\n`
344
496
  case 'defaultsMask': {
@@ -371,6 +523,9 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
371
523
  const x = expr(n.x); const path = expr(n.path); const ev = expr(n.ev); const sps = n.sps.map(expr)
372
524
  if (v) {
373
525
  const calls = ps.map((p) => `${p}(${x},null,s,"",${ev},"",null)`)
526
+ // One branch passes exactly when it passes, for anyOf and oneOf: the
527
+ // form of a required call, so a repeat of it is seen (seq).
528
+ if (calls.length === 1) return `if(!${calls[0]})return false\n`
374
529
  return `if(${isAny ? `!(${calls.join('||')})` : `(${calls.map((c) => `(${c}?1:0)`).join('+')})!==1`})return false\n`
375
530
  }
376
531
  use('collapse')
@@ -412,7 +567,7 @@ export function emitIR (ir, { from = 'ata-validator/kernels', verdict = false, f
412
567
  case 'var': return n.n
413
568
  case 'get': return `${expr(n.o)}[${expr(n.k)}]`
414
569
  case 'field': return `${expr(n.o)}.${n.k}`
415
- case 'own': use('own'); return `own(${expr(n.o)},${expr(n.k)})`
570
+ case 'own': return ownSrc(expr(n.o), expr(n.k))
416
571
  case 'not': return `!(${expr(n.e)})`
417
572
  case 'and': return `(${expr(n.a)}&&${expr(n.b)})`
418
573
  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
 
@@ -1147,11 +1147,16 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1147
1147
  const expected = types.join(', ')
1148
1148
  // As written: a one-element array stays an array, as the interpreted engine
1149
1149
  // reports it.
1150
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
1150
+ // Escaped like every other schema string written into this source: a
1151
+ // type name is the schema author's text, and a quote in one that is not
1152
+ // a JSON Schema type closed the literal and ran what followed as code,
1153
+ // in the process that compiled the schema and in the module written
1154
+ // from it.
1155
+ const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
1151
1156
  {
1152
1157
  const typeSp = `${schemaPrefix}/type`
1153
1158
  const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
1154
- lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1159
+ lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${esc(expected)}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1155
1160
  }
1156
1161
  }
1157
1162
 
@@ -2038,7 +2043,9 @@ TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
2038
2043
  const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
2039
2044
  function typeMismatchRest(schema, types) {
2040
2045
  const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
2041
- for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
2046
+ // Own names only: a type called `constructor` or `__proto__` read
2047
+ // Object.prototype's member here and threw while compiling.
2048
+ for (const t of types) if (Object.prototype.hasOwnProperty.call(TYPE_KEYWORDS, t)) for (const k of TYPE_KEYWORDS[t]) drop.add(k)
2042
2049
  // A built-in format is written for strings only (`typeof v==='string'&&`
2043
2050
  // off the string path), so for a value that failed `type: string` it can
2044
2051
  // never fire: carried here it was dead code in every module that checks one.
@@ -2435,11 +2442,16 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2435
2442
  const expected = types.join(', ')
2436
2443
  // As written: a one-element array stays an array, as the interpreted engine
2437
2444
  // reports it.
2438
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
2445
+ // Escaped like every other schema string written into this source: a
2446
+ // type name is the schema author's text, and a quote in one that is not
2447
+ // a JSON Schema type closed the literal and ran what followed as code,
2448
+ // in the process that compiled the schema and in the module written
2449
+ // from it.
2450
+ const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
2439
2451
  // Type check: push error but continue — wrap remaining in type-success block
2440
2452
  const typeOk = `_tok${ctx.varCounter++}`
2441
2453
  lines.push(`const ${typeOk}=${conds.join('||')}`)
2442
- lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
2454
+ lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${esc(expected)}'`)}}`)
2443
2455
  // Subsequent optimized code runs inside if(typeOk){...}
2444
2456
  if (types.length === 1) {
2445
2457
  isObj = types[0] === 'object'
@@ -832,9 +832,9 @@ function resolveRef(ref, defs, schemaMap) {
832
832
 
833
833
  function buildTypeCheck(types) {
834
834
  if (types.length === 1) {
835
- return TYPE_CHECKS[types[0]] || (() => true)
835
+ return typeCheckOf(types[0]) || (() => true)
836
836
  }
837
- const fns = types.map(t => TYPE_CHECKS[t]).filter(Boolean)
837
+ const fns = types.map(typeCheckOf).filter(Boolean)
838
838
  return (d) => {
839
839
  for (let i = 0; i < fns.length; i++) {
840
840
  if (fns[i](d)) return true
@@ -902,6 +902,10 @@ function multipleOfBad(v, m) {
902
902
  return `!(Number.isInteger(${v}/${m})||Math.abs(${v}/${m}-Math.round(${v}/${m}))<1e-9)`
903
903
  }
904
904
 
905
+ // Own names only: a type called `__proto__` or `valueOf` read
906
+ // Object.prototype's member and was handed out as the check, and the
907
+ // Validator then threw on the first document (`_fast is not a function`).
908
+ const typeCheckOf = (t) => (Object.prototype.hasOwnProperty.call(TYPE_CHECKS, t) ? TYPE_CHECKS[t] : undefined)
905
909
  const TYPE_CHECKS = {
906
910
  string: (d) => typeof d === 'string',
907
911
  number: (d) => Number.isFinite(d),
@@ -4198,7 +4202,11 @@ function genDynamicRefNode(schema, v, lines, ctx, knownType) {
4198
4202
  function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, types) {
4199
4203
  const si = ctx.varCounter++
4200
4204
  const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4201
- const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4205
+ // `items` speaks for the elements after the tuple only: with `prefixItems`
4206
+ // the first positions may hold objects, and comparing those by identity
4207
+ // took two equal objects for different ones and accepted the array.
4208
+ const noTuple = !Array.isArray(schema.prefixItems) || schema.prefixItems.length === 0
4209
+ const isPrimItems = noTuple && (itemType === 'string' || itemType === 'number' || itemType === 'integer')
4202
4210
  const maxItems = schema.maxItems
4203
4211
  // Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
4204
4212
  // No allocation, no hash computation — just direct === comparison
@@ -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.keys(data);
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
- if (patterns !== null) {
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.keys(data);
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
- if (patterns !== null) {
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
- return seen === reqN;
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
  }
@@ -36,6 +36,15 @@ const { parse } = require('./safe-regex')
36
36
  // holds them: every accepted pattern answers as the linear engine does and
37
37
  // stays fast on inputs built to make a backtracking engine retry.
38
38
  const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
39
+ // How many ways a backtracking engine can split one string over the bounded
40
+ // choices of a pattern. Choices in a row multiply: `(a?){30}` is thirty
41
+ // choices and two to the thirtieth ways, which the engine walks through on a
42
+ // string that does not match. The count used to be a sum capped at 64, so
43
+ // that pattern and `^([a-z]?){64}$` passed as linear and a string of 17
44
+ // characters ran for more than half a minute. Now the product, capped where
45
+ // the worst case stays under a millisecond for a hundred characters.
46
+ const MAX_WAYS = 4096
47
+ // The sum of bounded choices, which both rules below also hold to.
39
48
  const MAX_BOUNDED_CHOICES = 64
40
49
 
41
50
  const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
@@ -52,7 +61,9 @@ function anchored (ast) {
52
61
  function simpleRule (ast) {
53
62
  // Per subtree: how many unbounded quantifiers it holds (`u`) and how many
54
63
  // bounded choices a backtracking engine could try (`b`), or null where the
55
- // shape is not provably linear.
64
+ // shape is not provably linear. The choices are held to a sum
65
+ // (MAX_BOUNDED_CHOICES: a long counted run is slow to compile and to
66
+ // unwind) and to a product (MAX_WAYS, waysOf below).
56
67
  const info = (n) => {
57
68
  switch (n.t) {
58
69
  case 'star': case 'plus':
@@ -78,7 +89,7 @@ function simpleRule (ast) {
78
89
  }
79
90
  }
80
91
  const r = info(ast)
81
- if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
92
+ if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES || !(waysOf(ast) <= MAX_WAYS)) return false
82
93
  return r.u === 0 || anchored(ast)
83
94
  }
84
95
 
@@ -207,9 +218,31 @@ function deterministicRule (ast) {
207
218
  for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
208
219
  if (!ok) return false
209
220
  if (bounded > MAX_BOUNDED_CHOICES) return false
221
+ // A counted repeat of a body that can end in more than one place is not a
222
+ // loop to the walk above (`(a?){30}`, `(a{1,8}){8}`), and it is where the
223
+ // ways multiply: the same product as the simple rule holds here too.
224
+ if (!(waysOf(ast) <= MAX_WAYS)) return false
210
225
  return unbounded === 0 || anchored(ast)
211
226
  }
212
227
 
228
+ // The product the simple rule counts, over any shape: an unbounded
229
+ // quantifier adds no bounded choice of its own.
230
+ function waysOf (n) {
231
+ switch (n.t) {
232
+ case 'star': case 'plus': return waysOf(n.child)
233
+ case 'quest': return atom(n.child) ? 2 : 1 + waysOf(n.child)
234
+ case 'repeat': {
235
+ if (n.max === Infinity) return waysOf(n.child)
236
+ if (atom(n.child)) return n.max - n.min + 1
237
+ return (n.max - n.min + 1) * Math.pow(waysOf(n.child), n.max)
238
+ }
239
+ case 'group': return waysOf(n.child)
240
+ case 'concat': { let w = 1; for (const part of n.parts) w *= waysOf(part); return w }
241
+ case 'alt': { let w = 0; for (const part of n.opts) w += waysOf(part); return w }
242
+ default: return 1
243
+ }
244
+ }
245
+
213
246
  // Two more shapes the rules above decline, both anchored with `^` and `$`
214
247
  // around one top-level sequence.
215
248
  //
@@ -393,6 +426,11 @@ function nativeIsLinear (src) {
393
426
  }
394
427
  let ast
395
428
  try { ast = parse(src) } catch { return false }
429
+ // The product of the bounded choices holds for every rule below, so it is
430
+ // asked here, once: the one-choice and delimited-loop rules did not ask it,
431
+ // and `^(a*b)*` followed by thirty `a?` reached the platform RegExp, where
432
+ // each `a?` more doubles the time on a string that does not match.
433
+ if (!(waysOf(ast) <= MAX_WAYS)) return false
396
434
  return simpleRule(ast) || deterministicRule(ast) || oneChoiceRule(ast) || delimitedLoopRule(ast)
397
435
  }
398
436
 
@@ -981,6 +981,12 @@ function emitObject(schema, ctx, out, depth) {
981
981
  const reqMasks = new Array(words).fill(0);
982
982
  if (required) for (const k of required) { const x = bit.get(k); reqMasks[x.w] |= x.b; }
983
983
 
984
+ // Many names are dispatched on a hash of the key, and two names with one
985
+ // hash cannot be: the code below then took the other branch, which reads
986
+ // variables only the few-names form declares, and the scanner threw a
987
+ // ReferenceError on every document that carried a declared key. Such a
988
+ // schema is left to the parser.
989
+ if (names.length >= HASH_DISPATCH_MIN && new Set(names.map(spanHash)).size !== names.length) decline('properties:hash-collision');
984
990
  out.push('c=s.charCodeAt(++i);');
985
991
  const useHash = names.length >= HASH_DISPATCH_MIN;
986
992
  if (useHash) {
@@ -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)) return false;
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
- function tier0ValidateObject(plan, data) {
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 = plan.constraints;
192
+ const cs = P_constraints;
182
193
  const n = cs.length;
183
- const reqMask = plan.requiredMask;
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 || (_PN ? data.__proto__ : Object.getPrototypeOf(data)) !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
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 (!plan.additionalAllowed) {
236
- const known = plan.knownKeys;
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,
@@ -1669,8 +1669,13 @@ class Validator {
1669
1669
  }
1670
1670
 
1671
1671
  addSchema(schema) {
1672
- if (this._initialized) {
1673
- throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
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, tier0Validate } = require('./tier0');
2364
+ const { buildTier0Plan, tier0Verdict } = require('./tier0');
2360
2365
  const plan = buildTier0Plan(self._schemaObj, self._userFormats);
2361
- if (!_ticks(self)) return (d) => tier0Validate(plan, d);
2366
+ const verdict = tier0Verdict(plan);
2367
+ if (!_ticks(self)) return verdict;
2362
2368
  const tick = _tiering.counter(self);
2363
- return (d) => { tick(); return tier0Validate(plan, d); };
2369
+ return (d) => { tick(); return verdict(d); };
2364
2370
  }
2365
2371
 
2366
2372
  Validator.prototype._coldTwin = function () {
2367
2373
  const cg = _loadCodegen();
2368
- return cg === null || cg.coldTwin === undefined ? null : cg.coldTwin(this, Validator);
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
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.63.0';
10
+ module.exports = '1.63.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.63.0",
3
+ "version": "1.63.2",
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.0",
270
- "@ata-validator/native-darwin-x64": "1.63.0",
271
- "@ata-validator/native-linux-arm64-gnu": "1.63.0",
272
- "@ata-validator/native-linux-arm64-musl": "1.63.0",
273
- "@ata-validator/native-linux-x64-gnu": "1.63.0",
274
- "@ata-validator/native-linux-x64-musl": "1.63.0",
275
- "@ata-validator/native-win32-x64": "1.63.0"
269
+ "@ata-validator/native-darwin-arm64": "1.63.2",
270
+ "@ata-validator/native-darwin-x64": "1.63.2",
271
+ "@ata-validator/native-linux-arm64-gnu": "1.63.2",
272
+ "@ata-validator/native-linux-arm64-musl": "1.63.2",
273
+ "@ata-validator/native-linux-x64-gnu": "1.63.2",
274
+ "@ata-validator/native-linux-x64-musl": "1.63.2",
275
+ "@ata-validator/native-win32-x64": "1.63.2"
276
276
  },
277
277
  "peerDependencies": {
278
278
  "yaml": "^2.0.0"