ata-validator 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,9 +4,10 @@
4
4
  // schemas the JS codegen cannot represent when the native engine is absent
5
5
  // (browser, edge workers, ATA_NO_NATIVE). It walks the schema directly with
6
6
  // full annotation tracking (unevaluatedProperties/Items), $id/$anchor
7
- // resolution, and $dynamicRef dynamic-scope semantics. It optimizes for
8
- // correctness and clarity, not speed: hot paths belong to codegen and the
9
- // native engine.
7
+ // resolution, and $dynamicRef dynamic-scope semantics. Each schema node is
8
+ // resolved once into a fixed-shape Plan (type bitmask, compiled pattern,
9
+ // looked-up format, a flag per keyword group) so the walk itself reads no
10
+ // schema properties; codegen and the native engine remain the fast paths.
10
11
 
11
12
  const { compileSafe } = require('./safe-regex');
12
13
 
@@ -135,6 +136,10 @@ function walkPointer(root, pointer) {
135
136
  node = node.$defs;
136
137
  } else if (part === '$defs' && node.definitions) {
137
138
  node = node.definitions;
139
+ } else if (part === 'items' && Array.isArray(node.prefixItems)) {
140
+ // draft-07 array-form items is normalized to prefixItems; pointers
141
+ // written against the original document still land.
142
+ node = node.prefixItems;
138
143
  } else {
139
144
  return undefined;
140
145
  }
@@ -246,7 +251,10 @@ const FORMAT_CHECKS = {
246
251
 
247
252
  // ---------- evaluation ----------
248
253
 
249
- const NO_ANNOTATIONS = { props: null, items: null };
254
+ // Sink for subschema results whose annotations the caller never reads
255
+ // (properties, items, propertyNames and friends). eval() skips the merge when
256
+ // it sees this object, so it is never mutated.
257
+ const DISCARD = { props: null, items: null };
250
258
 
251
259
  function mergeAnnotations(target, from) {
252
260
  if (from.props && from.props.size) {
@@ -259,6 +267,167 @@ function mergeAnnotations(target, from) {
259
267
  }
260
268
  }
261
269
 
270
+ // ---------- plans ----------
271
+
272
+ // Type bits, one per JSON Schema type. `integer` data also sets the `number`
273
+ // bit so a `number` constraint accepts it.
274
+ const T_STRING = 1;
275
+ const T_NUMBER = 2;
276
+ const T_INTEGER = 4;
277
+ const T_BOOLEAN = 8;
278
+ const T_NULL = 16;
279
+ const T_OBJECT = 32;
280
+ const T_ARRAY = 64;
281
+ const T_ANY = 127;
282
+
283
+ function typeBit(name) {
284
+ switch (name) {
285
+ case 'string': return T_STRING;
286
+ case 'number': return T_NUMBER;
287
+ case 'integer': return T_INTEGER;
288
+ case 'boolean': return T_BOOLEAN;
289
+ case 'null': return T_NULL;
290
+ case 'object': return T_OBJECT;
291
+ case 'array': return T_ARRAY;
292
+ default: return T_ANY; // unknown type names match everything
293
+ }
294
+ }
295
+
296
+ function dataBits(d) {
297
+ switch (typeof d) {
298
+ case 'string': return T_STRING;
299
+ case 'number':
300
+ if (!isFinite(d)) return 0;
301
+ return Number.isInteger(d) ? T_NUMBER | T_INTEGER : T_NUMBER;
302
+ case 'boolean': return T_BOOLEAN;
303
+ case 'object':
304
+ if (d === null) return T_NULL;
305
+ return Array.isArray(d) ? T_ARRAY : T_OBJECT;
306
+ default: return 0;
307
+ }
308
+ }
309
+
310
+ function isObjectMap(v) {
311
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
312
+ }
313
+
314
+ // A plan is the schema node with every keyword resolved once: the type list
315
+ // folded to a bitmask, the format looked up, patterns compiled, and a flag
316
+ // per section so eval() can skip whole blocks without touching the schema
317
+ // object. Plans have a fixed shape, which keeps eval() monomorphic where the
318
+ // raw schema objects are not.
319
+ class Plan {
320
+ // The plan is registered in the cache before `link` runs, so a schema
321
+ // object that contains itself structurally resolves to the same plan
322
+ // instead of recursing without end.
323
+ constructor(schema) {
324
+ this.schema = schema;
325
+ }
326
+
327
+ link(interp) {
328
+ const schema = this.schema;
329
+ // Children are linked as plans (or booleans) up front, so a visit never
330
+ // goes back through the plan cache. Reference targets are resolved at
331
+ // evaluation time and cached per base URI.
332
+ const child = (sub) => interp.node(sub);
333
+ this.refCache = null;
334
+ this.dynCache = null;
335
+ this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
336
+ this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
337
+ this.tracked = this.ref !== null || this.dynamicRef !== null;
338
+
339
+ this.typeMask = 0;
340
+ this.typeNames = null;
341
+ if (schema.type !== undefined) {
342
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
343
+ let mask = 0;
344
+ for (const t of types) mask |= typeBit(t);
345
+ this.typeMask = mask;
346
+ this.typeNames = types;
347
+ }
348
+ this.hasType = schema.type !== undefined;
349
+ this.enum = schema.enum !== undefined ? schema.enum : null;
350
+ this.hasConst = schema.const !== undefined;
351
+ this.const = schema.const;
352
+
353
+ this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
354
+ this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
355
+ this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
356
+ this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
357
+ this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
358
+ this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
359
+
360
+ this.minLength = schema.minLength;
361
+ this.maxLength = schema.maxLength;
362
+ this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
363
+ this.patternSource = schema.pattern;
364
+ this.format = schema.format;
365
+ this.formatFn = null;
366
+ if (schema.format !== undefined) {
367
+ const uf = interp.userFormats;
368
+ const fc = (uf && typeof uf[schema.format] === 'function') ? uf[schema.format] : FORMAT_CHECKS[schema.format];
369
+ this.formatFn = fc || null;
370
+ }
371
+ this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
372
+
373
+ this.minItems = schema.minItems;
374
+ this.maxItems = schema.maxItems;
375
+ this.uniqueItems = schema.uniqueItems === true;
376
+ this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
377
+ this.items = schema.items !== undefined ? child(schema.items) : undefined;
378
+ this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
379
+ this.minContains = schema.minContains;
380
+ this.maxContains = schema.maxContains;
381
+ this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
382
+
383
+ this.required = Array.isArray(schema.required) ? schema.required : null;
384
+ this.minProperties = schema.minProperties;
385
+ this.maxProperties = schema.maxProperties;
386
+ this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
387
+ this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
388
+ this.properties = null;
389
+ if (isObjectMap(schema.properties)) {
390
+ this.properties = new Map();
391
+ for (const key of Object.keys(schema.properties)) {
392
+ const ek = escapePointer(key);
393
+ // Escaped once here; the walk only concatenates.
394
+ this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
395
+ }
396
+ }
397
+ this.patternProperties = null;
398
+ if (isObjectMap(schema.patternProperties)) {
399
+ this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
400
+ }
401
+ this.additionalProperties = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
402
+ this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
403
+ this.propertyDependencies = null;
404
+ if (isObjectMap(schema.propertyDependencies)) {
405
+ this.propertyDependencies = Object.entries(schema.propertyDependencies)
406
+ .filter(([, choices]) => isObjectMap(choices))
407
+ .map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
408
+ }
409
+ 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;
410
+
411
+ this.allOf = Array.isArray(schema.allOf) ? schema.allOf.map(child) : null;
412
+ this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
413
+ this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
414
+ this.not = schema.not !== undefined ? child(schema.not) : undefined;
415
+ this.if = schema.if !== undefined ? child(schema.if) : undefined;
416
+ this.then = schema.then !== undefined ? child(schema.then) : undefined;
417
+ this.else = schema.else !== undefined ? child(schema.else) : undefined;
418
+ this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined;
419
+
420
+ this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
421
+ this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
422
+ this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
423
+
424
+ // Resource bookkeeping, read once instead of two Map lookups per visit.
425
+ const nodeBase = interp.state.nodeBase.get(schema);
426
+ this.nodeBase = nodeBase !== undefined ? nodeBase : null;
427
+ this.isResourceRoot = nodeBase !== undefined && interp.state.resources.get(nodeBase) === schema;
428
+ }
429
+ }
430
+
262
431
  class Interpreter {
263
432
  constructor(rootSchema, options) {
264
433
  const opts = options || {};
@@ -269,6 +438,9 @@ class Interpreter {
269
438
  // requirement. See the `$dynamicRef` branch in eval().
270
439
  this.bookending = !opts.v1;
271
440
  this.patternCache = new Map();
441
+ // Plans are built on first visit; a schema node is never re-read after.
442
+ this.plans = new Map();
443
+ this.rootNode = this.node(rootSchema);
272
444
  }
273
445
 
274
446
  pattern(src) {
@@ -290,57 +462,118 @@ class Interpreter {
290
462
  return re;
291
463
  }
292
464
 
465
+ plan(schema) {
466
+ let p = this.plans.get(schema);
467
+ if (p === undefined) {
468
+ p = new Plan(schema);
469
+ this.plans.set(schema, p);
470
+ p.link(this);
471
+ }
472
+ return p;
473
+ }
474
+
475
+ // $ref resolution depends on the base URI in effect, which can differ
476
+ // between visits of the same node; cache per (plan, base).
477
+ resolveRefCached(P, base) {
478
+ let cache = P.refCache;
479
+ if (cache === null) cache = P.refCache = new Map();
480
+ let r = cache.get(base);
481
+ if (r === undefined) {
482
+ r = resolveRef(P.ref, base, this.state);
483
+ cache.set(base, r);
484
+ }
485
+ return r;
486
+ }
487
+
488
+ // The static half of $dynamicRef (initial target and fragment) per base;
489
+ // the dynamic-scope search still runs on every visit.
490
+ resolveDynamicRefCached(P, base) {
491
+ let cache = P.dynCache;
492
+ if (cache === null) cache = P.dynCache = new Map();
493
+ let r = cache.get(base);
494
+ if (r === undefined) {
495
+ const { node, base: refBase } = resolveRef(P.dynamicRef, base, this.state);
496
+ const [, fragment] = splitFragment(resolveUri(base, P.dynamicRef));
497
+ r = { node, base: refBase, fragment, byAnchor: Boolean(fragment) && !fragment.startsWith('/') };
498
+ cache.set(base, r);
499
+ }
500
+ return r;
501
+ }
502
+
503
+ // Maps any schema value to what eval() accepts: a boolean or a Plan.
504
+ // Non-object, non-boolean values validate everything, as before.
505
+ node(schema) {
506
+ if (schema === true || schema === false) return schema;
507
+ if (typeof schema !== 'object' || schema === null) return true;
508
+ return this.plan(schema);
509
+ }
510
+
293
511
  validate(data) {
294
512
  const errors = [];
295
513
  const dynScope = [this.state.rootBase];
296
- const valid = this.eval(this.root, data, this.state.rootBase, dynScope, errors, '', '#', [], NO_ANNOTATIONS_SINK());
514
+ // Nothing reads the root's annotations, so they are discarded from the start.
515
+ const valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
297
516
  return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
298
517
  }
299
518
 
300
519
  // Evaluates `schema` against `data`. Collected annotations for data are
301
520
  // written into `sink` ({props, items}); errors append to `errors`.
302
521
  // `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
303
- eval(schema, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
304
- if (schema === true) return true;
305
- if (schema === false) {
522
+ eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
523
+ if (P === true) return true;
524
+ if (P === false) {
306
525
  errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
307
526
  return false;
308
527
  }
309
- if (typeof schema !== 'object' || schema === null) return true;
310
-
311
- for (const frame of stack) {
312
- if (frame[0] === schema && frame[1] === data) return true; // fixed point
528
+ const schema = P.schema;
529
+
530
+ // Only a reference can lead evaluation back to a schema it is already
531
+ // inside of, so the cycle guard is maintained on reference nodes alone.
532
+ // The frame is pushed in place and popped on the way out; the array is
533
+ // created per validate() call, so a throw leaves nothing to clean up.
534
+ const tracked = P.tracked;
535
+ if (tracked) {
536
+ for (let i = stack.length - 2; i >= 0; i -= 2) {
537
+ if (stack[i] === schema && stack[i + 1] === data) return true; // fixed point
538
+ }
539
+ stack.push(schema, data);
313
540
  }
314
- stack = stack.concat([[schema, data]]);
315
541
 
316
- const nodeBase = this.state.nodeBase.get(schema);
317
- if (nodeBase !== undefined && nodeBase !== base) base = nodeBase;
318
- // Entering a schema resource extends the dynamic scope.
319
- if (this.state.resources.get(base) === schema && dynScope[dynScope.length - 1] !== base) {
542
+ if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
543
+ // Entering a schema resource extends the dynamic scope, whether evaluation
544
+ // lands on the resource's root or on a subschema inside it: a reference
545
+ // to first#/$defs/stuff still passes through resource `first`.
546
+ if (dynScope[dynScope.length - 1] !== base) {
320
547
  dynScope = dynScope.concat(base);
321
548
  }
322
549
 
323
550
  let valid = true;
324
- const local = { props: null, items: null };
551
+ // Annotations only matter if this node or an ancestor reads them. When
552
+ // nobody does, in-place children get the discard sink too and `local`
553
+ // stays unallocated.
554
+ const collect = sink !== DISCARD || P.hasUnevaluated;
555
+ const local = collect ? { props: null, items: null } : DISCARD;
325
556
 
326
557
  // $ref / $dynamicRef: in-place applicators, annotations flow through
327
- if (typeof schema.$ref === 'string') {
328
- const { node, base: refBase } = resolveRef(schema.$ref, base, this.state);
558
+ if (P.ref !== null) {
559
+ const { node, base: refBase } = this.resolveRefCached(P, base);
329
560
  if (node === undefined) {
330
- errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: schema.$ref }, `cannot resolve $ref ${schema.$ref}`));
561
+ errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
331
562
  valid = false;
332
563
  } else {
333
- const sub = { props: null, items: null };
334
- if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
335
- else mergeAnnotations(local, sub);
564
+ const sub = collect ? { props: null, items: null } : DISCARD;
565
+ if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
566
+ else if (collect) mergeAnnotations(local, sub);
336
567
  }
337
568
  }
338
569
 
339
- if (typeof schema.$dynamicRef === 'string') {
340
- const ref = schema.$dynamicRef;
341
- let { node, base: refBase } = resolveRef(ref, base, this.state);
342
- const [, fragment] = splitFragment(resolveUri(base, ref));
343
- if (fragment && !fragment.startsWith('/')) {
570
+ if (P.dynamicRef !== null) {
571
+ const ref = P.dynamicRef;
572
+ const initial = this.resolveDynamicRefCached(P, base);
573
+ let node = initial.node;
574
+ let refBase = initial.base;
575
+ const fragment = initial.fragment;
576
+ if (initial.byAnchor) {
344
577
  // Under 2020-12 the initial target must itself carry the matching
345
578
  // $dynamicAnchor before the dynamic scope is consulted (bookending);
346
579
  // otherwise the keyword behaves like $ref. v1 removes that
@@ -367,58 +600,54 @@ class Interpreter {
367
600
  errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
368
601
  valid = false;
369
602
  } else {
370
- const sub = { props: null, items: null };
371
- if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
372
- else mergeAnnotations(local, sub);
603
+ const sub = collect ? { props: null, items: null } : DISCARD;
604
+ if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
605
+ else if (collect) mergeAnnotations(local, sub);
373
606
  }
374
607
  }
375
608
 
609
+ const bits = dataBits(data);
610
+
376
611
  // type / enum / const
377
- if (schema.type !== undefined) {
378
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
379
- if (!types.some((t) => typeMatches(t, data))) {
380
- errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${types.join(' or ')}`));
381
- valid = false;
382
- }
612
+ if (P.hasType && (bits & P.typeMask) === 0) {
613
+ errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
614
+ valid = false;
383
615
  }
384
- if (schema.enum !== undefined) {
385
- if (!schema.enum.some((v) => deepEqual(v, data))) {
386
- errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: schema.enum }, 'must be equal to one of the allowed values'));
616
+ if (P.enum !== null) {
617
+ let found = false;
618
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
619
+ if (!found) {
620
+ errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
387
621
  valid = false;
388
622
  }
389
623
  }
390
- if (schema.const !== undefined) {
391
- if (!deepEqual(schema.const, data)) {
392
- errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: schema.const }, 'must be equal to constant'));
393
- valid = false;
394
- }
624
+ if (P.hasConst && !deepEqual(P.const, data)) {
625
+ errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
626
+ valid = false;
395
627
  }
396
628
 
397
629
  // numbers
398
- if (typeof data === 'number') {
399
- if (typeof schema.minimum === 'number' && !(data >= schema.minimum)) { errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: schema.minimum }, `must be >= ${schema.minimum}`)); valid = false; }
400
- if (typeof schema.maximum === 'number' && !(data <= schema.maximum)) { errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: schema.maximum }, `must be <= ${schema.maximum}`)); valid = false; }
401
- if (typeof schema.exclusiveMinimum === 'number' && !(data > schema.exclusiveMinimum)) { errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: schema.exclusiveMinimum }, `must be > ${schema.exclusiveMinimum}`)); valid = false; }
402
- if (typeof schema.exclusiveMaximum === 'number' && !(data < schema.exclusiveMaximum)) { errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: schema.exclusiveMaximum }, `must be < ${schema.exclusiveMaximum}`)); valid = false; }
403
- if (typeof schema.multipleOf === 'number' && !multipleOfOk(data, schema.multipleOf)) { errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: schema.multipleOf }, `must be multiple of ${schema.multipleOf}`)); valid = false; }
630
+ if (P.hasNumber && typeof data === 'number') {
631
+ if (P.minimum !== undefined && !(data >= P.minimum)) { errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
632
+ if (P.maximum !== undefined && !(data <= P.maximum)) { errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
633
+ if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
634
+ if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
635
+ if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
404
636
  }
405
637
 
406
638
  // strings
407
- if (typeof data === 'string') {
408
- if (schema.minLength !== undefined && codePointLength(data) < schema.minLength) { errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: schema.minLength }, `must NOT have fewer than ${schema.minLength} characters`)); valid = false; }
409
- if (schema.maxLength !== undefined && codePointLength(data) > schema.maxLength) { errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: schema.maxLength }, `must NOT have more than ${schema.maxLength} characters`)); valid = false; }
410
- if (schema.pattern !== undefined && !this.pattern(schema.pattern).test(data)) { errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: schema.pattern }, `must match pattern "${schema.pattern}"`)); valid = false; }
411
- if (schema.format !== undefined) {
412
- const fc = (this.userFormats && typeof this.userFormats[schema.format] === 'function') ? this.userFormats[schema.format] : FORMAT_CHECKS[schema.format];
413
- if (fc && !fc(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: schema.format }, `must match format "${schema.format}"`)); valid = false; }
414
- }
639
+ if (P.hasString && bits === T_STRING) {
640
+ if (P.minLength !== undefined && codePointLength(data) < P.minLength) { errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
641
+ if (P.maxLength !== undefined && codePointLength(data) > P.maxLength) { errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
642
+ if (P.pattern !== null && !P.pattern.test(data)) { errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
643
+ if (P.formatFn !== null && !P.formatFn(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
415
644
  }
416
645
 
417
646
  // arrays
418
- if (Array.isArray(data)) {
419
- if (schema.minItems !== undefined && data.length < schema.minItems) { errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: schema.minItems }, `must NOT have fewer than ${schema.minItems} items`)); valid = false; }
420
- if (schema.maxItems !== undefined && data.length > schema.maxItems) { errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: schema.maxItems }, `must NOT have more than ${schema.maxItems} items`)); valid = false; }
421
- if (schema.uniqueItems === true) {
647
+ if (P.hasArray && bits === T_ARRAY) {
648
+ if (P.minItems !== undefined && data.length < P.minItems) { errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
649
+ if (P.maxItems !== undefined && data.length > P.maxItems) { errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
650
+ if (P.uniqueItems) {
422
651
  outer: for (let i = 0; i < data.length; i++) {
423
652
  for (let j = i + 1; j < data.length; j++) {
424
653
  if (deepEqual(data[i], data[j])) {
@@ -429,35 +658,31 @@ class Interpreter {
429
658
  }
430
659
  }
431
660
  }
432
- const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : null;
433
- if (prefix) {
434
- for (let i = 0; i < Math.min(prefix.length, data.length); i++) {
435
- const sub = { props: null, items: null };
436
- if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, sub)) valid = false;
437
- if (!local.items) local.items = new Set();
438
- local.items.add(i);
661
+ const prefix = P.prefixItems;
662
+ if (prefix !== null) {
663
+ const n = Math.min(prefix.length, data.length);
664
+ for (let i = 0; i < n; i++) {
665
+ if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, DISCARD)) valid = false;
666
+ if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
439
667
  }
440
668
  }
441
- if (schema.items !== undefined) {
442
- const start = prefix ? prefix.length : 0;
669
+ if (P.items !== undefined) {
670
+ const start = prefix !== null ? prefix.length : 0;
443
671
  for (let i = start; i < data.length; i++) {
444
- const sub = { props: null, items: null };
445
- if (!this.eval(schema.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, sub)) valid = false;
446
- if (!local.items) local.items = new Set();
447
- local.items.add(i);
672
+ if (!this.eval(P.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, DISCARD)) valid = false;
673
+ if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
448
674
  }
449
675
  }
450
- if (schema.contains !== undefined) {
676
+ if (P.contains !== undefined) {
451
677
  const matched = [];
452
678
  for (let i = 0; i < data.length; i++) {
453
679
  const scratch = [];
454
- const sub = { props: null, items: null };
455
- if (this.eval(schema.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, sub)) matched.push(i);
680
+ if (this.eval(P.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, DISCARD)) matched.push(i);
456
681
  }
457
- const minC = schema.minContains !== undefined ? schema.minContains : 1;
682
+ const minC = P.minContains !== undefined ? P.minContains : 1;
458
683
  if (matched.length < minC) { errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
459
- if (schema.maxContains !== undefined && matched.length > schema.maxContains) { errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: schema.maxContains }, `must NOT contain more than ${schema.maxContains} valid item(s)`)); valid = false; }
460
- if (matched.length) {
684
+ if (P.maxContains !== undefined && matched.length > P.maxContains) { errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
685
+ if (collect && matched.length) {
461
686
  if (!local.items) local.items = new Set();
462
687
  for (const i of matched) local.items.add(i);
463
688
  }
@@ -465,63 +690,65 @@ class Interpreter {
465
690
  }
466
691
 
467
692
  // objects
468
- if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
693
+ if (P.hasObject && bits === T_OBJECT) {
469
694
  const keys = Object.keys(data);
470
- if (Array.isArray(schema.required)) {
471
- for (const key of schema.required) {
472
- if (!Object.prototype.hasOwnProperty.call(data, key)) { errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
695
+ if (P.required !== null) {
696
+ const req = P.required;
697
+ for (let i = 0; i < req.length; i++) {
698
+ const key = req[i];
699
+ if (!Object.hasOwn(data, key)) { errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
473
700
  }
474
701
  }
475
- if (schema.minProperties !== undefined && keys.length < schema.minProperties) { errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: schema.minProperties }, `must NOT have fewer than ${schema.minProperties} properties`)); valid = false; }
476
- if (schema.maxProperties !== undefined && keys.length > schema.maxProperties) { errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: schema.maxProperties }, `must NOT have more than ${schema.maxProperties} properties`)); valid = false; }
477
- if (schema.dependentRequired && typeof schema.dependentRequired === 'object') {
478
- for (const [key, deps] of Object.entries(schema.dependentRequired)) {
479
- if (Object.prototype.hasOwnProperty.call(data, key)) {
702
+ if (P.minProperties !== undefined && keys.length < P.minProperties) { errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
703
+ if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
704
+ if (P.dependentRequired !== null) {
705
+ for (const [key, deps] of P.dependentRequired) {
706
+ if (Object.hasOwn(data, key)) {
480
707
  for (const dep of deps) {
481
- if (!Object.prototype.hasOwnProperty.call(data, dep)) { errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
708
+ if (!Object.hasOwn(data, dep)) { errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
482
709
  }
483
710
  }
484
711
  }
485
712
  }
486
- if (schema.propertyNames !== undefined) {
713
+ if (P.propertyNames !== undefined) {
487
714
  for (const key of keys) {
488
- if (!this.eval(schema.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, { props: null, items: null })) valid = false;
715
+ if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
489
716
  }
490
717
  }
491
- const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : null;
492
- const patterns = schema.patternProperties && typeof schema.patternProperties === 'object' ? Object.keys(schema.patternProperties) : null;
493
- for (const key of keys) {
718
+ const props = P.properties;
719
+ const patterns = P.patternProperties;
720
+ for (let k = 0; k < keys.length; k++) {
721
+ const key = keys[k];
494
722
  let evaluated = false;
495
- if (props && Object.prototype.hasOwnProperty.call(props, key)) {
496
- const sub = { props: null, items: null };
497
- if (!this.eval(props[key], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/properties/' + escapePointer(key), stack, sub)) valid = false;
723
+ const prop = props !== null ? props.get(key) : undefined;
724
+ if (prop !== undefined) {
725
+ if (!this.eval(prop.node, data[key], base, dynScope, errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack, DISCARD)) valid = false;
498
726
  evaluated = true;
499
727
  }
500
- if (patterns) {
501
- for (const src of patterns) {
502
- if (this.pattern(src).test(key)) {
503
- const sub = { props: null, items: null };
504
- if (!this.eval(schema.patternProperties[src], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(src), stack, sub)) valid = false;
728
+ if (patterns !== null) {
729
+ for (let pi = 0; pi < patterns.length; pi++) {
730
+ const pp = patterns[pi];
731
+ if (pp.re.test(key)) {
732
+ if (!this.eval(pp.node, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack, DISCARD)) valid = false;
505
733
  evaluated = true;
506
734
  }
507
735
  }
508
736
  }
509
- if (!evaluated && schema.additionalProperties !== undefined) {
510
- const sub = { props: null, items: null };
511
- if (!this.eval(schema.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, sub)) valid = false;
737
+ if (!evaluated && P.additionalProperties !== undefined) {
738
+ if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
512
739
  evaluated = true;
513
740
  }
514
- if (evaluated) {
741
+ if (evaluated && collect) {
515
742
  if (!local.props) local.props = new Set();
516
743
  local.props.add(key);
517
744
  }
518
745
  }
519
- if (schema.dependentSchemas && typeof schema.dependentSchemas === 'object') {
520
- for (const [key, dep] of Object.entries(schema.dependentSchemas)) {
521
- if (Object.prototype.hasOwnProperty.call(data, key)) {
522
- const sub = { props: null, items: null };
746
+ if (P.dependentSchemas !== null) {
747
+ for (const [key, dep] of P.dependentSchemas) {
748
+ if (Object.hasOwn(data, key)) {
749
+ const sub = collect ? { props: null, items: null } : DISCARD;
523
750
  if (!this.eval(dep, data, base, dynScope, errors, instancePath, schemaPath + '/dependentSchemas/' + escapePointer(key), stack, sub)) valid = false;
524
- else mergeAnnotations(local, sub);
751
+ else if (collect) mergeAnnotations(local, sub);
525
752
  }
526
753
  }
527
754
  }
@@ -529,119 +756,124 @@ class Interpreter {
529
756
  // value rather than its presence. Only string values can match, since the
530
757
  // inner keys are object keys. Equivalent to
531
758
  // `if: { properties: { p: { const: v } }, required: [p] }, then: <schema>`.
532
- if (schema.propertyDependencies && typeof schema.propertyDependencies === 'object') {
533
- for (const [key, choices] of Object.entries(schema.propertyDependencies)) {
534
- if (!choices || typeof choices !== 'object' || Array.isArray(choices)) continue;
535
- if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
759
+ if (P.propertyDependencies !== null) {
760
+ for (const [key, choices] of P.propertyDependencies) {
761
+ if (!Object.hasOwn(data, key)) continue;
536
762
  const value = data[key];
537
763
  if (typeof value !== 'string') continue;
538
- if (!Object.prototype.hasOwnProperty.call(choices, value)) continue;
539
- const sub = { props: null, items: null };
764
+ const choice = choices.get(value);
765
+ if (choice === undefined) continue;
766
+ const sub = collect ? { props: null, items: null } : DISCARD;
540
767
  const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(key) + '/' + escapePointer(value);
541
- if (!this.eval(choices[value], data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
542
- else mergeAnnotations(local, sub);
768
+ if (!this.eval(choice, data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
769
+ else if (collect) mergeAnnotations(local, sub);
543
770
  }
544
771
  }
545
772
  }
546
773
 
547
774
  // in-place applicators
548
- if (Array.isArray(schema.allOf)) {
549
- for (let i = 0; i < schema.allOf.length; i++) {
550
- const sub = { props: null, items: null };
551
- if (!this.eval(schema.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
552
- else mergeAnnotations(local, sub);
553
- }
554
- }
555
- if (Array.isArray(schema.anyOf)) {
556
- let any = false;
557
- const scratch = [];
558
- for (let i = 0; i < schema.anyOf.length; i++) {
559
- const sub = { props: null, items: null };
560
- if (this.eval(schema.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
561
- any = true;
562
- mergeAnnotations(local, sub);
775
+ if (P.hasApplicators) {
776
+ if (P.allOf !== null) {
777
+ for (let i = 0; i < P.allOf.length; i++) {
778
+ const sub = collect ? { props: null, items: null } : DISCARD;
779
+ if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
780
+ else if (collect) mergeAnnotations(local, sub);
563
781
  }
564
782
  }
565
- if (!any) {
566
- for (const e of scratch) errors.push(e);
567
- errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
568
- valid = false;
569
- }
570
- }
571
- if (Array.isArray(schema.oneOf)) {
572
- let count = 0;
573
- let winner = null;
574
- const scratch = [];
575
- for (let i = 0; i < schema.oneOf.length; i++) {
576
- const sub = { props: null, items: null };
577
- if (this.eval(schema.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
578
- count++;
579
- winner = sub;
783
+ if (P.anyOf !== null) {
784
+ let any = false;
785
+ const scratch = [];
786
+ for (let i = 0; i < P.anyOf.length; i++) {
787
+ const sub = collect ? { props: null, items: null } : DISCARD;
788
+ if (this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
789
+ any = true;
790
+ if (collect) mergeAnnotations(local, sub);
791
+ }
792
+ }
793
+ if (!any) {
794
+ for (const e of scratch) errors.push(e);
795
+ errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
796
+ valid = false;
580
797
  }
581
798
  }
582
- if (count === 1) mergeAnnotations(local, winner);
583
- else {
584
- if (count === 0) for (const e of scratch) errors.push(e);
585
- errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
586
- valid = false;
799
+ if (P.oneOf !== null) {
800
+ let count = 0;
801
+ let winner = null;
802
+ const scratch = [];
803
+ for (let i = 0; i < P.oneOf.length; i++) {
804
+ const sub = collect ? { props: null, items: null } : DISCARD;
805
+ if (this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
806
+ count++;
807
+ winner = sub;
808
+ }
809
+ }
810
+ if (count === 1) { if (collect) mergeAnnotations(local, winner); }
811
+ else {
812
+ if (count === 0) for (const e of scratch) errors.push(e);
813
+ errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
814
+ valid = false;
815
+ }
587
816
  }
588
- }
589
- if (schema.not !== undefined) {
590
- const scratch = [];
591
- if (this.eval(schema.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, { props: null, items: null })) {
592
- errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
593
- valid = false;
817
+ if (P.not !== undefined) {
818
+ const scratch = [];
819
+ if (this.eval(P.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, DISCARD)) {
820
+ errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
821
+ valid = false;
822
+ }
594
823
  }
595
- }
596
- if (schema.if !== undefined) {
597
- const ifSub = { props: null, items: null };
598
- const ifScratch = [];
599
- const ifOk = this.eval(schema.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
600
- if (ifOk) {
601
- mergeAnnotations(local, ifSub);
602
- if (schema.then !== undefined) {
603
- const sub = { props: null, items: null };
604
- if (!this.eval(schema.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
605
- else mergeAnnotations(local, sub);
824
+ if (P.if !== undefined) {
825
+ const ifSub = collect ? { props: null, items: null } : DISCARD;
826
+ const ifScratch = [];
827
+ const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
828
+ if (ifOk) {
829
+ if (collect) mergeAnnotations(local, ifSub);
830
+ if (P.then !== undefined) {
831
+ const sub = collect ? { props: null, items: null } : DISCARD;
832
+ if (!this.eval(P.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
833
+ else if (collect) mergeAnnotations(local, sub);
834
+ }
835
+ } else if (P.else !== undefined) {
836
+ const sub = collect ? { props: null, items: null } : DISCARD;
837
+ if (!this.eval(P.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
838
+ else if (collect) mergeAnnotations(local, sub);
606
839
  }
607
- } else if (schema.else !== undefined) {
608
- const sub = { props: null, items: null };
609
- if (!this.eval(schema.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
610
- else mergeAnnotations(local, sub);
611
840
  }
612
841
  }
613
842
 
614
843
  // unevaluated*: run last, against annotations from everything above
615
- if (schema.unevaluatedProperties !== undefined && typeof data === 'object' && data !== null && !Array.isArray(data)) {
616
- for (const key of Object.keys(data)) {
617
- if (local.props && local.props.has(key)) continue;
618
- const sub = { props: null, items: null };
619
- if (!this.eval(schema.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, sub)) valid = false;
620
- if (!local.props) local.props = new Set();
621
- local.props.add(key);
844
+ if (P.hasUnevaluated) {
845
+ if (P.unevaluatedProperties !== undefined && bits === T_OBJECT) {
846
+ for (const key of Object.keys(data)) {
847
+ if (local.props && local.props.has(key)) continue;
848
+ if (!this.eval(P.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, DISCARD)) valid = false;
849
+ if (!local.props) local.props = new Set();
850
+ local.props.add(key);
851
+ }
622
852
  }
623
- }
624
- if (schema.unevaluatedItems !== undefined && Array.isArray(data)) {
625
- for (let i = 0; i < data.length; i++) {
626
- if (local.items && local.items.has(i)) continue;
627
- const sub = { props: null, items: null };
628
- if (!this.eval(schema.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, sub)) valid = false;
629
- if (!local.items) local.items = new Set();
630
- local.items.add(i);
853
+ if (P.unevaluatedItems !== undefined && bits === T_ARRAY) {
854
+ for (let i = 0; i < data.length; i++) {
855
+ if (local.items && local.items.has(i)) continue;
856
+ if (!this.eval(P.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, DISCARD)) valid = false;
857
+ if (!local.items) local.items = new Set();
858
+ local.items.add(i);
859
+ }
631
860
  }
632
861
  }
633
862
 
634
- if (valid) mergeAnnotations(sink, local);
863
+ if (tracked) stack.length -= 2;
864
+ if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
635
865
  return valid;
636
866
  }
637
867
  }
638
868
 
639
- function NO_ANNOTATIONS_SINK() {
640
- return { props: null, items: null };
641
- }
642
-
643
869
  function escapePointer(s) {
644
- return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
870
+ if (typeof s !== 'string') s = String(s);
871
+ // Keys almost never contain `~` or `/`; skip the two regex passes when so.
872
+ for (let i = 0; i < s.length; i++) {
873
+ const c = s.charCodeAt(i);
874
+ if (c === 0x7e || c === 0x2f) return s.replace(/~/g, '~0').replace(/\//g, '~1');
875
+ }
876
+ return s;
645
877
  }
646
878
 
647
879
  function err(code, keyword, instancePath, schemaPath, params, message) {