vscode-json-languageservice 6.0.0-next.1 → 6.0.0-next.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.
@@ -7,12 +7,15 @@ import { isNumber, equals, isBoolean, isString, isDefined, isObject } from '../u
7
7
  import { extendedRegExp, stringLength } from '../utils/strings.js';
8
8
  import { ErrorCode, Diagnostic, DiagnosticSeverity, Range, SchemaDraft } from '../jsonLanguageTypes.js';
9
9
  import { URI } from 'vscode-uri';
10
+ import { isKeywordEnabled, isFormatAssertionEnabled } from '../services/vocabularies.js';
10
11
  import * as l10n from '@vscode/l10n';
11
12
  const formats = {
12
13
  'color-hex': { errorMessage: l10n.t('Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.'), pattern: /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/ },
13
14
  'date-time': { errorMessage: l10n.t('String is not a RFC3339 date-time.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
14
15
  'date': { errorMessage: l10n.t('String is not a RFC3339 date.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/i },
15
16
  'time': { errorMessage: l10n.t('String is not a RFC3339 time.'), pattern: /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
17
+ 'duration': { errorMessage: l10n.t('String is not an ISO 8601 duration.'), pattern: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/ },
18
+ 'uuid': { errorMessage: l10n.t('String is not a valid UUID.'), pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i },
16
19
  'email': { errorMessage: l10n.t('String is not an e-mail address.'), pattern: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}))$/ },
17
20
  'hostname': { errorMessage: l10n.t('String is not a hostname.'), pattern: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i },
18
21
  'ipv4': { errorMessage: l10n.t('String is not an IPv4 address.'), pattern: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ },
@@ -130,8 +133,10 @@ const schemaDraftFromId = {
130
133
  'https://json-schema.org/draft/2020-12/schema': SchemaDraft.v2020_12
131
134
  };
132
135
  class EvaluationContext {
133
- constructor(schemaDraft) {
136
+ constructor(schemaDraft, activeVocabularies, explicitSchemaDraft) {
134
137
  this.schemaDraft = schemaDraft;
138
+ this.activeVocabularies = activeVocabularies;
139
+ this.explicitSchemaDraft = explicitSchemaDraft;
135
140
  }
136
141
  }
137
142
  class SchemaCollector {
@@ -264,10 +269,14 @@ export class JSONDocument {
264
269
  doVisit(this.root);
265
270
  }
266
271
  }
267
- validate(textDocument, schema, severity = DiagnosticSeverity.Warning, schemaDraft) {
272
+ validate(textDocument, schema, severity = DiagnosticSeverity.Warning, schemaDraft, activeVocabularies) {
268
273
  if (this.root && schema) {
269
274
  const validationResult = new ValidationResult();
270
- validate(this.root, schema, validationResult, NoOpSchemaCollector.instance, new EvaluationContext(schemaDraft ?? getSchemaDraft(schema)));
275
+ const { explicitDraft, effectiveDraft } = resolveDrafts(schema, schemaDraft);
276
+ const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
277
+ const schemaStack = [];
278
+ const schemaRoots = [schema];
279
+ validate(this.root, schema, validationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
271
280
  return validationResult.problems.map(p => {
272
281
  const range = Range.create(textDocument.positionAt(p.location.offset), textDocument.positionAt(p.location.offset + p.location.length));
273
282
  return Diagnostic.create(range, p.message, p.severity ?? severity, p.code);
@@ -275,32 +284,123 @@ export class JSONDocument {
275
284
  }
276
285
  return undefined;
277
286
  }
278
- getMatchingSchemas(schema, focusOffset = -1, exclude) {
287
+ getMatchingSchemas(schema, focusOffset = -1, exclude, activeVocabularies) {
279
288
  if (this.root && schema) {
280
289
  const matchingSchemas = new SchemaCollector(focusOffset, exclude);
281
- const schemaDraft = getSchemaDraft(schema);
282
- const context = new EvaluationContext(schemaDraft);
283
- validate(this.root, schema, new ValidationResult(), matchingSchemas, context);
290
+ const { explicitDraft, effectiveDraft } = resolveDrafts(schema);
291
+ const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
292
+ const schemaStack = [];
293
+ const schemaRoots = [schema];
294
+ validate(this.root, schema, new ValidationResult(), matchingSchemas, context, schemaStack, schemaRoots);
284
295
  return matchingSchemas.schemas;
285
296
  }
286
297
  return [];
287
298
  }
288
299
  }
289
- function getSchemaDraft(schema, fallBack = SchemaDraft.v2020_12) {
290
- let schemaId = schema.$schema;
291
- if (schemaId) {
292
- return getSchemaDraftFromId(schemaId) ?? fallBack;
293
- }
294
- return fallBack;
300
+ /*
301
+ * Resolves the schema draft versions for validation.
302
+ * - explicitDraft: the draft explicitly declared via $schema or passed as an override.
303
+ * Undefined when no $schema is present and no override is provided.
304
+ * - effectiveDraft: the draft used for keyword gating, defaulting to 2020-12.
305
+ * These are tracked separately so that behaviors like format assertion can
306
+ * distinguish "explicitly 2020-12" (annotation-only per spec) from
307
+ * "no $schema, defaulting to 2020-12" (asserts for backward compatibility).
308
+ */
309
+ function resolveDrafts(schema, schemaDraftOverride) {
310
+ const explicitDraft = schemaDraftOverride ?? (schema.$schema ? getSchemaDraftFromId(schema.$schema) : undefined);
311
+ const effectiveDraft = explicitDraft ?? SchemaDraft.v2020_12;
312
+ return { explicitDraft, effectiveDraft };
295
313
  }
296
- function validate(n, schema, validationResult, matchingSchemas, context) {
314
+ // Keywords introduced in 2019-09 (not available in draft-07 and earlier)
315
+ const keywords201909 = new Set([
316
+ 'dependentRequired', 'dependentSchemas',
317
+ 'unevaluatedProperties', 'unevaluatedItems',
318
+ 'minContains', 'maxContains'
319
+ ]);
320
+ function validate(n, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots) {
297
321
  if (!n || !matchingSchemas.include(n)) {
298
322
  return;
299
323
  }
300
324
  if (n.type === 'property') {
301
- return validate(n.valueNode, schema, validationResult, matchingSchemas, context);
325
+ return validate(n.valueNode, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
302
326
  }
303
327
  const node = n;
328
+ if (schema.$recursiveRef) {
329
+ const hasRecursiveAnchor = (s) => s.$recursiveAnchor === true;
330
+ const isSchemaRoot = (s) => s.$id || s.id || s.$originalId;
331
+ // Find nearest schema resource root
332
+ const currentResourceRoot = schemaStack.slice().reverse().find(isSchemaRoot);
333
+ // If current resource has $recursiveAnchor, find first $recursiveAnchor in stack
334
+ // Otherwise use current resource root or fall back to document root
335
+ const targetSchema = (currentResourceRoot && hasRecursiveAnchor(currentResourceRoot))
336
+ ? schemaStack.find(hasRecursiveAnchor)
337
+ : currentResourceRoot ?? schemaRoots[0];
338
+ // Validate against the target schema (avoiding infinite loop).
339
+ // Fall through afterwards so sibling keywords (e.g. unevaluatedProperties)
340
+ // are still evaluated against this node, accumulating processedProperties.
341
+ if (targetSchema && targetSchema !== schema) {
342
+ validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
343
+ }
344
+ }
345
+ // Track schema document roots (schemas with $id or $originalId) if not already the root
346
+ const isNewRoot = (schema.$id || schema.id || schema.$originalId) && !schemaRoots.includes(schema);
347
+ if (isNewRoot) {
348
+ schemaRoots.push(schema);
349
+ }
350
+ // Push current schema to stack for $recursiveRef resolution
351
+ schemaStack.push(schema);
352
+ // 2020-12 $dynamicRef. The initial (static) target and the referenced plain-name
353
+ // fragment were recorded during schema resolution in $dynamicRefInfo (target and
354
+ // name). "Bookending": dynamic resolution applies only when that
355
+ // initial target is itself a $dynamicAnchor of the referenced name; then the
356
+ // reference resolves to the outermost matching $dynamicAnchor currently in the
357
+ // dynamic scope (schemaRoots, ordered outermost-first). Otherwise it behaves
358
+ // like a plain $ref to the initial target. This runs after the schemaRoots push
359
+ // so the resource that contains the $dynamicRef participates in its own
360
+ // dynamic-scope resolution.
361
+ const dynamicRefInfo = schema.$dynamicRefInfo;
362
+ const dynamicRefTarget = dynamicRefInfo?.target;
363
+ if (dynamicRefTarget !== undefined) {
364
+ let targetSchema = dynamicRefTarget;
365
+ const dynamicName = dynamicRefInfo?.name;
366
+ if (dynamicName !== undefined && dynamicRefTarget.$dynamicAnchor === dynamicName) {
367
+ for (const root of schemaRoots) {
368
+ const candidate = root.$anchorMaps?.dynamic.get(dynamicName);
369
+ if (candidate) {
370
+ targetSchema = candidate;
371
+ break;
372
+ }
373
+ }
374
+ }
375
+ // Validate the instance against the referenced schema, then fall through so
376
+ // that any sibling keywords on this same node are validated too: per 2020-12
377
+ // $dynamicRef is an applicator that applies alongside its siblings (like $ref),
378
+ // not a redirect that replaces them. The shared cleanup below pops the stack.
379
+ if (targetSchema && targetSchema !== schema) {
380
+ validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
381
+ }
382
+ }
383
+ const enabled = (keyword) => {
384
+ // Draft-based keyword gating: keywords only exist in certain drafts.
385
+ // 'dependencies' was replaced by dependentRequired/dependentSchemas in 2019-09
386
+ if (keyword === 'dependencies') {
387
+ return context.schemaDraft <= SchemaDraft.v7;
388
+ }
389
+ // Keywords introduced in 2019-09 (not available in draft-07 and earlier)
390
+ if (keywords201909.has(keyword)) {
391
+ return context.schemaDraft >= SchemaDraft.v2019_09;
392
+ }
393
+ // 'prefixItems' was introduced in 2020-12
394
+ if (keyword === 'prefixItems') {
395
+ return context.schemaDraft >= SchemaDraft.v2020_12;
396
+ }
397
+ // Vocabulary-based filtering only applies for 2019-09+ (vocabulary is not a concept in older drafts)
398
+ if (context.schemaDraft >= SchemaDraft.v2019_09 && context.activeVocabularies) {
399
+ return isKeywordEnabled(keyword, context.activeVocabularies);
400
+ }
401
+ // No vocabulary info or pre-2019-09: enable all draft-appropriate keywords
402
+ return true;
403
+ };
304
404
  _validateNode();
305
405
  switch (node.type) {
306
406
  case 'object':
@@ -317,11 +417,17 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
317
417
  break;
318
418
  }
319
419
  matchingSchemas.add({ node: node, schema: schema });
420
+ // Pop schema from stack when done
421
+ schemaStack.pop();
422
+ // Pop schema root if we pushed one
423
+ if (isNewRoot) {
424
+ schemaRoots.pop();
425
+ }
320
426
  function _validateNode() {
321
427
  function matchesType(type) {
322
428
  return node.type === type || (type === 'integer' && node.type === 'number' && node.isInteger);
323
429
  }
324
- if (Array.isArray(schema.type)) {
430
+ if (Array.isArray(schema.type) && enabled('type')) {
325
431
  if (!schema.type.some(matchesType)) {
326
432
  validationResult.problems.push({
327
433
  location: { offset: node.offset, length: node.length },
@@ -329,7 +435,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
329
435
  });
330
436
  }
331
437
  }
332
- else if (schema.type) {
438
+ else if (schema.type && !Array.isArray(schema.type) && enabled('type')) {
333
439
  if (!matchesType(schema.type)) {
334
440
  validationResult.problems.push({
335
441
  location: { offset: node.offset, length: node.length },
@@ -337,20 +443,20 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
337
443
  });
338
444
  }
339
445
  }
340
- if (Array.isArray(schema.allOf)) {
446
+ if (Array.isArray(schema.allOf) && enabled('allOf')) {
341
447
  for (const subSchemaRef of schema.allOf) {
342
448
  const subValidationResult = new ValidationResult();
343
449
  const subMatchingSchemas = matchingSchemas.newSub();
344
- validate(node, asSchema(subSchemaRef), subValidationResult, subMatchingSchemas, context);
450
+ validate(node, asSchema(subSchemaRef), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
345
451
  validationResult.merge(subValidationResult);
346
452
  matchingSchemas.merge(subMatchingSchemas);
347
453
  }
348
454
  }
349
455
  const notSchema = asSchema(schema.not);
350
- if (notSchema) {
456
+ if (notSchema && enabled('not')) {
351
457
  const subValidationResult = new ValidationResult();
352
458
  const subMatchingSchemas = matchingSchemas.newSub();
353
- validate(node, notSchema, subValidationResult, subMatchingSchemas, context);
459
+ validate(node, notSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
354
460
  if (!subValidationResult.hasProblems()) {
355
461
  validationResult.problems.push({
356
462
  location: { offset: node.offset, length: node.length },
@@ -371,7 +477,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
371
477
  const subSchema = asSchema(subSchemaRef);
372
478
  const subValidationResult = new ValidationResult();
373
479
  const subMatchingSchemas = matchingSchemas.newSub();
374
- validate(node, subSchema, subValidationResult, subMatchingSchemas, context);
480
+ validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
375
481
  if (!subValidationResult.hasProblems()) {
376
482
  matches.push(subSchema);
377
483
  }
@@ -413,16 +519,16 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
413
519
  }
414
520
  return matches.length;
415
521
  };
416
- if (Array.isArray(schema.anyOf)) {
522
+ if (Array.isArray(schema.anyOf) && enabled('anyOf')) {
417
523
  testAlternatives(schema.anyOf, false);
418
524
  }
419
- if (Array.isArray(schema.oneOf)) {
525
+ if (Array.isArray(schema.oneOf) && enabled('oneOf')) {
420
526
  testAlternatives(schema.oneOf, true);
421
527
  }
422
528
  const testBranch = (schema) => {
423
529
  const subValidationResult = new ValidationResult();
424
530
  const subMatchingSchemas = matchingSchemas.newSub();
425
- validate(node, asSchema(schema), subValidationResult, subMatchingSchemas, context);
531
+ validate(node, asSchema(schema), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
426
532
  validationResult.merge(subValidationResult);
427
533
  matchingSchemas.merge(subMatchingSchemas);
428
534
  };
@@ -430,10 +536,12 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
430
536
  const subSchema = asSchema(ifSchema);
431
537
  const subValidationResult = new ValidationResult();
432
538
  const subMatchingSchemas = matchingSchemas.newSub();
433
- validate(node, subSchema, subValidationResult, subMatchingSchemas, context);
539
+ validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
434
540
  matchingSchemas.merge(subMatchingSchemas);
435
- validationResult.mergeProcessedProperties(subValidationResult);
541
+ // if passed: merge properties from if;
542
+ // otherwise don't merge properties from if, only from else
436
543
  if (!subValidationResult.hasProblems()) {
544
+ validationResult.mergeProcessedProperties(subValidationResult);
437
545
  if (thenSchema) {
438
546
  testBranch(thenSchema);
439
547
  }
@@ -443,10 +551,10 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
443
551
  }
444
552
  };
445
553
  const ifSchema = asSchema(schema.if);
446
- if (ifSchema) {
554
+ if (ifSchema && enabled('if')) {
447
555
  testCondition(ifSchema, asSchema(schema.then), asSchema(schema.else));
448
556
  }
449
- if (Array.isArray(schema.enum)) {
557
+ if (Array.isArray(schema.enum) && enabled('enum')) {
450
558
  const val = getNodeValue(node);
451
559
  let enumValueMatch = false;
452
560
  for (const e of schema.enum) {
@@ -465,7 +573,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
465
573
  });
466
574
  }
467
575
  }
468
- if (isDefined(schema.const)) {
576
+ if (isDefined(schema.const) && enabled('const')) {
469
577
  const val = getNodeValue(node);
470
578
  if (!equals(val, schema.const)) {
471
579
  validationResult.problems.push({
@@ -578,7 +686,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
578
686
  };
579
687
  }
580
688
  ;
581
- if (isNumber(schema.multipleOf)) {
689
+ if (isNumber(schema.multipleOf) && enabled('multipleOf')) {
582
690
  let remainder = -1;
583
691
  if (Number.isInteger(schema.multipleOf)) {
584
692
  remainder = val % schema.multipleOf;
@@ -620,28 +728,28 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
620
728
  return undefined;
621
729
  }
622
730
  const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum);
623
- if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum) {
731
+ if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum && enabled('exclusiveMinimum')) {
624
732
  validationResult.problems.push({
625
733
  location: { offset: node.offset, length: node.length },
626
734
  message: l10n.t('Value is below the exclusive minimum of {0}.', exclusiveMinimum)
627
735
  });
628
736
  }
629
737
  const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum);
630
- if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum) {
738
+ if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum && enabled('exclusiveMaximum')) {
631
739
  validationResult.problems.push({
632
740
  location: { offset: node.offset, length: node.length },
633
741
  message: l10n.t('Value is above the exclusive maximum of {0}.', exclusiveMaximum)
634
742
  });
635
743
  }
636
744
  const minimum = getLimit(schema.minimum, schema.exclusiveMinimum);
637
- if (isNumber(minimum) && val < minimum) {
745
+ if (isNumber(minimum) && val < minimum && enabled('minimum')) {
638
746
  validationResult.problems.push({
639
747
  location: { offset: node.offset, length: node.length },
640
748
  message: l10n.t('Value is below the minimum of {0}.', minimum)
641
749
  });
642
750
  }
643
751
  const maximum = getLimit(schema.maximum, schema.exclusiveMaximum);
644
- if (isNumber(maximum) && val > maximum) {
752
+ if (isNumber(maximum) && val > maximum && enabled('maximum')) {
645
753
  validationResult.problems.push({
646
754
  location: { offset: node.offset, length: node.length },
647
755
  message: l10n.t('Value is above the maximum of {0}.', maximum)
@@ -649,19 +757,19 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
649
757
  }
650
758
  }
651
759
  function _validateStringNode(node) {
652
- if (isNumber(schema.minLength) && stringLength(node.value) < schema.minLength) {
760
+ if (isNumber(schema.minLength) && stringLength(node.value) < schema.minLength && enabled('minLength')) {
653
761
  validationResult.problems.push({
654
762
  location: { offset: node.offset, length: node.length },
655
763
  message: l10n.t('String is shorter than the minimum length of {0}.', schema.minLength)
656
764
  });
657
765
  }
658
- if (isNumber(schema.maxLength) && stringLength(node.value) > schema.maxLength) {
766
+ if (isNumber(schema.maxLength) && stringLength(node.value) > schema.maxLength && enabled('maxLength')) {
659
767
  validationResult.problems.push({
660
768
  location: { offset: node.offset, length: node.length },
661
769
  message: l10n.t('String is longer than the maximum length of {0}.', schema.maxLength)
662
770
  });
663
771
  }
664
- if (isString(schema.pattern)) {
772
+ if (isString(schema.pattern) && enabled('pattern')) {
665
773
  const regex = extendedRegExp(schema.pattern);
666
774
  if (regex && !(regex.test(node.value))) {
667
775
  validationResult.problems.push({
@@ -670,7 +778,8 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
670
778
  });
671
779
  }
672
780
  }
673
- if (schema.format) {
781
+ // Only validate format if format-assertion vocabulary is active (not annotation-only)
782
+ if (schema.format && enabled('format') && isFormatAssertionEnabled(context.activeVocabularies, context.explicitSchemaDraft)) {
674
783
  switch (schema.format) {
675
784
  case 'uri':
676
785
  case 'uri-reference':
@@ -700,6 +809,8 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
700
809
  case 'date-time':
701
810
  case 'date':
702
811
  case 'time':
812
+ case 'duration':
813
+ case 'uuid':
703
814
  case 'email':
704
815
  case 'hostname':
705
816
  case 'ipv4':
@@ -735,7 +846,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
735
846
  const itemValidationResult = new ValidationResult();
736
847
  const item = node.items[index];
737
848
  if (item) {
738
- validate(item, subSchema, itemValidationResult, matchingSchemas, context);
849
+ validate(item, subSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
739
850
  validationResult.mergePropertyMatch(itemValidationResult);
740
851
  }
741
852
  validationResult.processedProperties.add(String(index));
@@ -757,19 +868,19 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
757
868
  else {
758
869
  for (; index < node.items.length; index++) {
759
870
  const itemValidationResult = new ValidationResult();
760
- validate(node.items[index], additionalItemSchema, itemValidationResult, matchingSchemas, context);
871
+ validate(node.items[index], additionalItemSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
761
872
  validationResult.mergePropertyMatch(itemValidationResult);
762
873
  validationResult.processedProperties.add(String(index));
763
874
  }
764
875
  }
765
876
  }
766
877
  const containsSchema = asSchema(schema.contains);
767
- if (containsSchema) {
878
+ if (containsSchema && enabled('contains')) {
768
879
  let containsCount = 0;
769
880
  for (let index = 0; index < node.items.length; index++) {
770
881
  const item = node.items[index];
771
882
  const itemValidationResult = new ValidationResult();
772
- validate(item, containsSchema, itemValidationResult, NoOpSchemaCollector.instance, context);
883
+ validate(item, containsSchema, itemValidationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
773
884
  if (!itemValidationResult.hasProblems()) {
774
885
  containsCount++;
775
886
  if (context.schemaDraft >= SchemaDraft.v2020_12) {
@@ -783,13 +894,13 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
783
894
  message: schema.errorMessage || l10n.t('Array does not contain required item.')
784
895
  });
785
896
  }
786
- if (isNumber(schema.minContains) && containsCount < schema.minContains) {
897
+ if (isNumber(schema.minContains) && containsCount < schema.minContains && enabled('minContains')) {
787
898
  validationResult.problems.push({
788
899
  location: { offset: node.offset, length: node.length },
789
900
  message: schema.errorMessage || l10n.t('Array has too few items that match the contains contraint. Expected {0} or more.', schema.minContains)
790
901
  });
791
902
  }
792
- if (isNumber(schema.maxContains) && containsCount > schema.maxContains) {
903
+ if (isNumber(schema.maxContains) && containsCount > schema.maxContains && enabled('maxContains')) {
793
904
  validationResult.problems.push({
794
905
  location: { offset: node.offset, length: node.length },
795
906
  message: schema.errorMessage || l10n.t('Array has too many items that match the contains contraint. Expected {0} or less.', schema.maxContains)
@@ -797,7 +908,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
797
908
  }
798
909
  }
799
910
  const unevaluatedItems = schema.unevaluatedItems;
800
- if (unevaluatedItems !== undefined) {
911
+ if (unevaluatedItems !== undefined && enabled('unevaluatedItems')) {
801
912
  for (let i = 0; i < node.items.length; i++) {
802
913
  if (!validationResult.processedProperties.has(String(i))) {
803
914
  if (unevaluatedItems === false) {
@@ -808,7 +919,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
808
919
  }
809
920
  else {
810
921
  const itemValidationResult = new ValidationResult();
811
- validate(node.items[i], schema.unevaluatedItems, itemValidationResult, matchingSchemas, context);
922
+ validate(node.items[i], schema.unevaluatedItems, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
812
923
  validationResult.mergePropertyMatch(itemValidationResult);
813
924
  }
814
925
  }
@@ -816,19 +927,19 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
816
927
  validationResult.propertiesValueMatches++;
817
928
  }
818
929
  }
819
- if (isNumber(schema.minItems) && node.items.length < schema.minItems) {
930
+ if (isNumber(schema.minItems) && node.items.length < schema.minItems && enabled('minItems')) {
820
931
  validationResult.problems.push({
821
932
  location: { offset: node.offset, length: node.length },
822
933
  message: l10n.t('Array has too few items. Expected {0} or more.', schema.minItems)
823
934
  });
824
935
  }
825
- if (isNumber(schema.maxItems) && node.items.length > schema.maxItems) {
936
+ if (isNumber(schema.maxItems) && node.items.length > schema.maxItems && enabled('maxItems')) {
826
937
  validationResult.problems.push({
827
938
  location: { offset: node.offset, length: node.length },
828
939
  message: l10n.t('Array has too many items. Expected {0} or fewer.', schema.maxItems)
829
940
  });
830
941
  }
831
- if (schema.uniqueItems === true) {
942
+ if (schema.uniqueItems === true && enabled('uniqueItems')) {
832
943
  const values = getNodeValue(node);
833
944
  function hasDuplicates() {
834
945
  for (let i = 0; i < values.length - 1; i++) {
@@ -857,13 +968,11 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
857
968
  seenKeys[key] = propertyNode.valueNode;
858
969
  unprocessedProperties.add(key);
859
970
  }
860
- if (Array.isArray(schema.required)) {
971
+ if (Array.isArray(schema.required) && enabled('required')) {
861
972
  for (const propertyName of schema.required) {
862
973
  if (!seenKeys[propertyName]) {
863
- const keyNode = node.parent && node.parent.type === 'property' && node.parent.keyNode;
864
- const location = keyNode ? { offset: keyNode.offset, length: keyNode.length } : { offset: node.offset, length: 1 };
865
974
  validationResult.problems.push({
866
- location: location,
975
+ location: { offset: node.offset, length: node.length },
867
976
  message: l10n.t('Missing property "{0}".', propertyName)
868
977
  });
869
978
  }
@@ -873,7 +982,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
873
982
  unprocessedProperties.delete(prop);
874
983
  validationResult.processedProperties.add(prop);
875
984
  };
876
- if (schema.properties) {
985
+ if (schema.properties && enabled('properties')) {
877
986
  for (const propertyName of Object.keys(schema.properties)) {
878
987
  propertyProcessed(propertyName);
879
988
  const propertySchema = schema.properties[propertyName];
@@ -894,20 +1003,28 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
894
1003
  }
895
1004
  else {
896
1005
  const propertyValidationResult = new ValidationResult();
897
- validate(child, propertySchema, propertyValidationResult, matchingSchemas, context);
1006
+ validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
898
1007
  validationResult.mergePropertyMatch(propertyValidationResult);
899
1008
  }
900
1009
  }
901
1010
  }
902
1011
  }
903
- if (schema.patternProperties) {
1012
+ if (schema.patternProperties && enabled('patternProperties')) {
1013
+ // Collect all properties matched by any pattern first
1014
+ // A property must be validated against all patterns it matches
1015
+ // Properties in `properties` can also match `patternProperties`, check all keys
1016
+ const allPropertyNames = Object.keys(seenKeys);
1017
+ const patternPropertyMatches = new Set();
904
1018
  for (const propertyPattern of Object.keys(schema.patternProperties)) {
905
1019
  const regex = extendedRegExp(propertyPattern);
906
1020
  if (regex) {
907
- const processed = [];
908
- for (const propertyName of unprocessedProperties) {
1021
+ for (const propertyName of allPropertyNames) {
909
1022
  if (regex.test(propertyName)) {
910
- processed.push(propertyName);
1023
+ // Only mark as pattern match if not already in `properties`
1024
+ // for additionalProperties tracking
1025
+ if (unprocessedProperties.has(propertyName)) {
1026
+ patternPropertyMatches.add(propertyName);
1027
+ }
911
1028
  const child = seenKeys[propertyName];
912
1029
  if (child) {
913
1030
  const propertySchema = schema.patternProperties[propertyPattern];
@@ -926,18 +1043,19 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
926
1043
  }
927
1044
  else {
928
1045
  const propertyValidationResult = new ValidationResult();
929
- validate(child, propertySchema, propertyValidationResult, matchingSchemas, context);
1046
+ validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
930
1047
  validationResult.mergePropertyMatch(propertyValidationResult);
931
1048
  }
932
1049
  }
933
1050
  }
934
1051
  }
935
- processed.forEach(propertyProcessed);
936
1052
  }
937
1053
  }
1054
+ // Mark all matched properties as processed after all patterns have been checked
1055
+ patternPropertyMatches.forEach(propertyProcessed);
938
1056
  }
939
1057
  const additionalProperties = schema.additionalProperties;
940
- if (additionalProperties !== undefined) {
1058
+ if (additionalProperties !== undefined && enabled('additionalProperties')) {
941
1059
  for (const propertyName of unprocessedProperties) {
942
1060
  propertyProcessed(propertyName);
943
1061
  const child = seenKeys[propertyName];
@@ -951,14 +1069,32 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
951
1069
  }
952
1070
  else if (additionalProperties !== true) {
953
1071
  const propertyValidationResult = new ValidationResult();
954
- validate(child, additionalProperties, propertyValidationResult, matchingSchemas, context);
1072
+ validate(child, additionalProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
955
1073
  validationResult.mergePropertyMatch(propertyValidationResult);
956
1074
  }
957
1075
  }
958
1076
  }
959
1077
  }
1078
+ if (schema.dependentRequired && enabled('dependentRequired')) {
1079
+ for (const key in schema.dependentRequired) {
1080
+ const prop = seenKeys[key];
1081
+ const propertyDeps = schema.dependentRequired[key];
1082
+ if (prop && Array.isArray(propertyDeps)) {
1083
+ _validatePropertyDependencies(key, propertyDeps);
1084
+ }
1085
+ }
1086
+ }
1087
+ if (schema.dependentSchemas && enabled('dependentSchemas')) {
1088
+ for (const key in schema.dependentSchemas) {
1089
+ const prop = seenKeys[key];
1090
+ const propertyDeps = schema.dependentSchemas[key];
1091
+ if (prop) {
1092
+ _validatePropertyDependencies(key, propertyDeps);
1093
+ }
1094
+ }
1095
+ }
960
1096
  const unevaluatedProperties = schema.unevaluatedProperties;
961
- if (unevaluatedProperties !== undefined) {
1097
+ if (unevaluatedProperties !== undefined && enabled('unevaluatedProperties')) {
962
1098
  const processed = [];
963
1099
  for (const propertyName of unprocessedProperties) {
964
1100
  if (!validationResult.processedProperties.has(propertyName)) {
@@ -974,7 +1110,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
974
1110
  }
975
1111
  else if (unevaluatedProperties !== true) {
976
1112
  const propertyValidationResult = new ValidationResult();
977
- validate(child, unevaluatedProperties, propertyValidationResult, matchingSchemas, context);
1113
+ validate(child, unevaluatedProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
978
1114
  validationResult.mergePropertyMatch(propertyValidationResult);
979
1115
  }
980
1116
  }
@@ -982,7 +1118,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
982
1118
  }
983
1119
  processed.forEach(propertyProcessed);
984
1120
  }
985
- if (isNumber(schema.maxProperties)) {
1121
+ if (isNumber(schema.maxProperties) && enabled('maxProperties')) {
986
1122
  if (node.properties.length > schema.maxProperties) {
987
1123
  validationResult.problems.push({
988
1124
  location: { offset: node.offset, length: node.length },
@@ -990,7 +1126,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
990
1126
  });
991
1127
  }
992
1128
  }
993
- if (isNumber(schema.minProperties)) {
1129
+ if (isNumber(schema.minProperties) && enabled('minProperties')) {
994
1130
  if (node.properties.length < schema.minProperties) {
995
1131
  validationResult.problems.push({
996
1132
  location: { offset: node.offset, length: node.length },
@@ -998,25 +1134,7 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
998
1134
  });
999
1135
  }
1000
1136
  }
1001
- if (schema.dependentRequired) {
1002
- for (const key in schema.dependentRequired) {
1003
- const prop = seenKeys[key];
1004
- const propertyDeps = schema.dependentRequired[key];
1005
- if (prop && Array.isArray(propertyDeps)) {
1006
- _validatePropertyDependencies(key, propertyDeps);
1007
- }
1008
- }
1009
- }
1010
- if (schema.dependentSchemas) {
1011
- for (const key in schema.dependentSchemas) {
1012
- const prop = seenKeys[key];
1013
- const propertyDeps = schema.dependentSchemas[key];
1014
- if (prop && isObject(propertyDeps)) {
1015
- _validatePropertyDependencies(key, propertyDeps);
1016
- }
1017
- }
1018
- }
1019
- if (schema.dependencies) {
1137
+ if (schema.dependencies && enabled('dependencies')) {
1020
1138
  for (const key in schema.dependencies) {
1021
1139
  const prop = seenKeys[key];
1022
1140
  if (prop) {
@@ -1025,11 +1143,11 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
1025
1143
  }
1026
1144
  }
1027
1145
  const propertyNames = asSchema(schema.propertyNames);
1028
- if (propertyNames) {
1146
+ if (propertyNames && enabled('propertyNames')) {
1029
1147
  for (const f of node.properties) {
1030
1148
  const key = f.keyNode;
1031
1149
  if (key) {
1032
- validate(key, propertyNames, validationResult, matchingSchemas, context);
1150
+ validate(key, propertyNames, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
1033
1151
  }
1034
1152
  }
1035
1153
  }
@@ -1051,8 +1169,9 @@ function validate(n, schema, validationResult, matchingSchemas, context) {
1051
1169
  const propertySchema = asSchema(propertyDep);
1052
1170
  if (propertySchema) {
1053
1171
  const propertyValidationResult = new ValidationResult();
1054
- validate(node, propertySchema, propertyValidationResult, matchingSchemas, context);
1172
+ validate(node, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
1055
1173
  validationResult.mergePropertyMatch(propertyValidationResult);
1174
+ validationResult.mergeProcessedProperties(propertyValidationResult);
1056
1175
  }
1057
1176
  }
1058
1177
  }