vscode-json-languageservice 5.7.2 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/esm/jsonContributions.d.ts +1 -1
  3. package/lib/esm/jsonLanguageService.d.ts +2 -2
  4. package/lib/esm/jsonLanguageService.js +13 -13
  5. package/lib/esm/jsonLanguageTypes.d.ts +10 -2
  6. package/lib/esm/jsonSchema.d.ts +71 -2
  7. package/lib/esm/parser/jsonParser.js +224 -93
  8. package/lib/esm/services/configuration.js +2 -2
  9. package/lib/esm/services/jsonCompletion.js +5 -5
  10. package/lib/esm/services/jsonDocumentSymbols.js +4 -4
  11. package/lib/esm/services/jsonFolding.js +1 -1
  12. package/lib/esm/services/jsonHover.js +2 -2
  13. package/lib/esm/services/jsonLinks.js +94 -3
  14. package/lib/esm/services/jsonSchemaService.js +639 -100
  15. package/lib/esm/services/jsonSelectionRanges.js +1 -1
  16. package/lib/esm/services/jsonValidation.js +4 -4
  17. package/lib/esm/services/schemas/draft-2019-09-flat.d.ts +1 -1
  18. package/lib/esm/services/schemas/draft-2020-12-flat.d.ts +1 -1
  19. package/lib/esm/services/vocabularies.js +139 -0
  20. package/lib/esm/utils/format.js +1 -1
  21. package/lib/esm/utils/sort.js +3 -3
  22. package/package.json +23 -20
  23. package/lib/umd/jsonContributions.d.ts +0 -21
  24. package/lib/umd/jsonContributions.js +0 -12
  25. package/lib/umd/jsonLanguageService.d.ts +0 -30
  26. package/lib/umd/jsonLanguageService.js +0 -79
  27. package/lib/umd/jsonLanguageTypes.d.ts +0 -305
  28. package/lib/umd/jsonLanguageTypes.js +0 -109
  29. package/lib/umd/jsonSchema.d.ts +0 -92
  30. package/lib/umd/jsonSchema.js +0 -12
  31. package/lib/umd/parser/jsonParser.js +0 -1365
  32. package/lib/umd/services/configuration.js +0 -536
  33. package/lib/umd/services/jsonCompletion.js +0 -982
  34. package/lib/umd/services/jsonDocumentSymbols.js +0 -285
  35. package/lib/umd/services/jsonFolding.js +0 -133
  36. package/lib/umd/services/jsonHover.js +0 -125
  37. package/lib/umd/services/jsonLinks.js +0 -85
  38. package/lib/umd/services/jsonSchemaService.js +0 -631
  39. package/lib/umd/services/jsonSelectionRanges.js +0 -74
  40. package/lib/umd/services/jsonValidation.js +0 -165
  41. package/lib/umd/services/schemas/draft-2019-09-flat.d.ts +0 -278
  42. package/lib/umd/services/schemas/draft-2019-09-flat.js +0 -314
  43. package/lib/umd/services/schemas/draft-2020-12-flat.d.ts +0 -276
  44. package/lib/umd/services/schemas/draft-2020-12-flat.js +0 -307
  45. package/lib/umd/utils/colors.js +0 -83
  46. package/lib/umd/utils/format.js +0 -33
  47. package/lib/umd/utils/glob.js +0 -137
  48. package/lib/umd/utils/json.js +0 -55
  49. package/lib/umd/utils/objects.js +0 -86
  50. package/lib/umd/utils/propertyTree.js +0 -90
  51. package/lib/umd/utils/sort.js +0 -384
  52. package/lib/umd/utils/strings.js +0 -97
@@ -4,13 +4,15 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as Json from 'jsonc-parser';
6
6
  import { URI } from 'vscode-uri';
7
- import * as Strings from '../utils/strings';
8
- import { asSchema, getSchemaDraftFromId, normalizeId } from '../parser/jsonParser';
9
- import { SchemaDraft, ErrorCode } from '../jsonLanguageTypes';
7
+ import * as Strings from '../utils/strings.js';
8
+ import { asSchema, getSchemaDraftFromId, normalizeId } from '../parser/jsonParser.js';
9
+ import { SchemaDraft, ErrorCode } from '../jsonLanguageTypes.js';
10
10
  import * as l10n from '@vscode/l10n';
11
- import { createRegex } from '../utils/glob';
12
- import { isObject, isString } from '../utils/objects';
11
+ import { createRegex } from '../utils/glob.js';
12
+ import { isString } from '../utils/objects.js';
13
13
  import { Range } from 'vscode-languageserver-types';
14
+ const hasSchemePattern = /^[A-Za-z][A-Za-z0-9+\-.+]*:\/.*/.source;
15
+ const hasSchemeRegex = new RegExp(hasSchemePattern);
14
16
  const BANG = '!';
15
17
  const PATH_SEP = '/';
16
18
  class FilePatternAssociation {
@@ -96,6 +98,11 @@ class SchemaHandle {
96
98
  this.anchors = undefined;
97
99
  return hasChanges;
98
100
  }
101
+ setSchemaContent(schemaContent) {
102
+ this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(schemaContent));
103
+ this.resolvedSchema = undefined;
104
+ this.anchors = undefined;
105
+ }
99
106
  }
100
107
  export class UnresolvedSchema {
101
108
  constructor(schema, errors = []) {
@@ -111,11 +118,12 @@ function toDiagnostic(message, code, relatedURL) {
111
118
  return { message, code, relatedInformation };
112
119
  }
113
120
  export class ResolvedSchema {
114
- constructor(schema, errors = [], warnings = [], schemaDraft) {
121
+ constructor(schema, errors = [], warnings = [], schemaDraft, activeVocabularies) {
115
122
  this.schema = schema;
116
123
  this.errors = errors;
117
124
  this.warnings = warnings;
118
125
  this.schemaDraft = schemaDraft;
126
+ this.activeVocabularies = activeVocabularies;
119
127
  }
120
128
  getSection(path) {
121
129
  const schemaRef = this.getSectionRecursive(path, this.schema);
@@ -158,6 +166,47 @@ export class ResolvedSchema {
158
166
  }
159
167
  }
160
168
  export class JSONSchemaService {
169
+ static traverseSchemaProperties(node, callback) {
170
+ // `$defs`/`definitions` are visited first so that a reusable resource they
171
+ // contain (which may itself carry a `$ref` chain) begins resolving before an
172
+ // `if`/`then`/`else`/`items`/… sibling references it. During asynchronous
173
+ // `$ref` resolution the referenced resource must be fully assembled before a
174
+ // referrer merges it, otherwise the referrer captures a half-resolved snapshot
175
+ // (e.g. a `$dynamicRef` chain reached through `then: { $ref: "numberList" }`).
176
+ const schemaMapProps = ['definitions', '$defs', 'properties', 'patternProperties',
177
+ 'dependencies', 'dependentSchemas'];
178
+ const singleSchemaProps = ['additionalItems', 'additionalProperties', 'not', 'contains',
179
+ 'propertyNames', 'if', 'then', 'else', 'unevaluatedItems', 'unevaluatedProperties', 'items'];
180
+ const schemaArrayProps = ['anyOf', 'allOf', 'oneOf', 'prefixItems'];
181
+ const visitValue = (value) => {
182
+ if (value) {
183
+ if (Array.isArray(value)) {
184
+ value.forEach(item => visitValue(item));
185
+ }
186
+ else if (typeof value === 'object') {
187
+ callback(value);
188
+ }
189
+ }
190
+ };
191
+ for (const prop of schemaMapProps) {
192
+ const map = node[prop];
193
+ if (map && typeof map === 'object') {
194
+ Object.values(map).forEach(visitValue);
195
+ }
196
+ }
197
+ for (const prop of singleSchemaProps) {
198
+ const propValue = node[prop];
199
+ if (propValue) {
200
+ visitValue(propValue);
201
+ }
202
+ }
203
+ for (const prop of schemaArrayProps) {
204
+ const propValue = node[prop];
205
+ if (propValue) {
206
+ visitValue(propValue);
207
+ }
208
+ }
209
+ }
161
210
  constructor(requestService, contextService, promiseConstructor) {
162
211
  this.contextService = contextService;
163
212
  this.requestService = requestService;
@@ -314,12 +363,50 @@ export class JSONSchemaService {
314
363
  resolveSchemaContent(schemaToResolve, handle) {
315
364
  const resolveErrors = schemaToResolve.errors.slice(0);
316
365
  const schema = schemaToResolve.schema;
366
+ // External documents whose embedded $id resources have already been registered
367
+ // as resolvable handles, so we only do it once per referenced document.
368
+ const embeddedRegisteredFor = new Set();
317
369
  const schemaDraft = schema.$schema ? getSchemaDraftFromId(schema.$schema) : undefined;
318
370
  if (schemaDraft === SchemaDraft.v3) {
319
- return this.promise.resolve(new ResolvedSchema({}, [toDiagnostic(l10n.t("Draft-03 schemas are not supported."), ErrorCode.SchemaUnsupportedFeature)], [], schemaDraft));
371
+ return this.promise.resolve(new ResolvedSchema({}, [toDiagnostic(l10n.t("Draft-03 schemas are not supported."), ErrorCode.SchemaUnsupportedFeature)], [], schemaDraft, undefined));
320
372
  }
321
- let usesUnsupportedFeatures = new Set();
373
+ let activeVocabularies = undefined;
374
+ const extractVocabularies = (metaschema) => {
375
+ if (!metaschema.$vocabulary || typeof metaschema.$vocabulary !== 'object') {
376
+ return undefined;
377
+ }
378
+ // Both true and false values indicate the vocabulary is active.
379
+ // The boolean indicates whether the vocabulary is required (true) or optional (false),
380
+ // not whether it's in use. All listed vocabularies should be included.
381
+ const vocabs = new Map();
382
+ for (const [uri, required] of Object.entries(metaschema.$vocabulary)) {
383
+ vocabs.set(uri, required);
384
+ }
385
+ return vocabs.size > 0 ? vocabs : undefined;
386
+ };
322
387
  const contextService = this.contextService;
388
+ // Attach internal, non-enumerable metadata to a schema node. Hidden so it is
389
+ // invisible to schema traversal, merging and consumers, but available to the
390
+ // validator (e.g. for $recursiveRef/$dynamicRef resolution).
391
+ const setHidden = (obj, key, value) => {
392
+ Object.defineProperty(obj, key, {
393
+ value,
394
+ enumerable: false,
395
+ writable: true,
396
+ configurable: true
397
+ });
398
+ };
399
+ // Get (creating on first use) the hidden $dynamicRefInfo record for a $dynamicRef
400
+ // node. Its fields are populated across two passes — `scope` while collecting
401
+ // anchors, `target`/`name` while resolving the reference — over the same object.
402
+ const dynamicRefInfoOf = (node) => {
403
+ let info = node.$dynamicRefInfo;
404
+ if (!info) {
405
+ info = {};
406
+ setHidden(node, '$dynamicRefInfo', info);
407
+ }
408
+ return info;
409
+ };
323
410
  const findSectionByJSONPointer = (schema, path) => {
324
411
  path = decodeURIComponent(path);
325
412
  let current = schema;
@@ -329,38 +416,284 @@ export class JSONSchemaService {
329
416
  path.split('/').some((part) => {
330
417
  part = part.replace(/~1/g, '/').replace(/~0/g, '~');
331
418
  current = current[part];
332
- return !current;
419
+ // A boolean `false` is a valid schema, not a "missing" section, so only
420
+ // stop on genuinely absent values (undefined/null).
421
+ return current === undefined || current === null;
333
422
  });
334
423
  return current;
335
424
  };
425
+ // Like findSectionByJSONPointer, but also tracks the effective base URI
426
+ // through any $id encountered along the path. This is needed so that
427
+ // $ref values merged from the found section can be resolved against the
428
+ // correct base, not the document root.
429
+ const findSectionAndBase = (schema, path, baseHandle) => {
430
+ path = decodeURIComponent(path);
431
+ let current = schema;
432
+ let currentBaseHandle = baseHandle;
433
+ if (path[0] === '/') {
434
+ path = path.substring(1);
435
+ }
436
+ path.split('/').some((part) => {
437
+ part = part.replace(/~1/g, '/').replace(/~0/g, '~');
438
+ current = current[part];
439
+ // A boolean `false` is a valid schema, not a "missing" section, so only
440
+ // stop on genuinely absent values (undefined/null).
441
+ if (current === undefined || current === null) {
442
+ return true;
443
+ }
444
+ const id = getSchemaId(current);
445
+ if (isString(id) && id.charAt(0) !== '#') {
446
+ let resolvedUri = id;
447
+ if (contextService && !hasSchemeRegex.test(id)) {
448
+ resolvedUri = contextService.resolveRelativePath(id, currentBaseHandle.uri);
449
+ }
450
+ resolvedUri = normalizeId(resolvedUri);
451
+ currentBaseHandle = this.getOrAddSchemaHandle(resolvedUri);
452
+ }
453
+ return false;
454
+ });
455
+ return { section: current, baseHandle: currentBaseHandle };
456
+ };
336
457
  const findSchemaById = (schema, handle, id) => {
337
458
  if (!handle.anchors) {
338
459
  handle.anchors = collectAnchors(schema);
339
460
  }
340
461
  return handle.anchors.get(id);
341
462
  };
463
+ const getSchemaId = (schema) => schema.$id || schema.id;
464
+ // Fold a source resource's $dynamicAnchor names into a target node's dynamic
465
+ // map, creating it on demand. Existing entries win, so the outermost resource
466
+ // in a dynamic scope retains precedence — this is what lets a $dynamicAnchor in
467
+ // an outer resource override an inner (referenced) one during the validation
468
+ // walk. Only the `dynamic` map is folded: the `local` map must stay per-resource
469
+ // (it resolves an internal $dynamicRef's initial target within its own lexical
470
+ // resource), so a sibling/referenced resource's identically-named anchor must
471
+ // not leak into it.
472
+ const unionAnchorMaps = (target, srcMaps) => {
473
+ if (srcMaps === undefined) {
474
+ return;
475
+ }
476
+ let maps = target.$anchorMaps;
477
+ if (maps === undefined) {
478
+ maps = { dynamic: new Map(), local: new Map() };
479
+ setHidden(target, '$anchorMaps', maps);
480
+ }
481
+ for (const [name, node] of srcMaps.dynamic) {
482
+ if (!maps.dynamic.has(name)) {
483
+ maps.dynamic.set(name, node);
484
+ }
485
+ }
486
+ };
342
487
  const merge = (target, section) => {
343
488
  for (const key in section) {
344
- if (section.hasOwnProperty(key) && key !== 'id' && key !== '$id') {
345
- target[key] = section[key];
489
+ if (!section.hasOwnProperty(key) || key === 'id' || key === '$id') {
490
+ continue;
346
491
  }
492
+ // Deep merge for properties and patternProperties to combine them
493
+ const shouldDeepMerge = (key === 'properties' || key === 'patternProperties') &&
494
+ typeof section[key] === 'object' && section[key] !== null &&
495
+ typeof target[key] === 'object' && target[key] !== null;
496
+ target[key] = shouldDeepMerge
497
+ ? { ...target[key], ...section[key] }
498
+ : section[key];
499
+ }
500
+ // Preserve $id as a non-enumerable hidden property for $recursiveRef resolution.
501
+ // This allows $recursiveRef to correctly resolve references within the schema without exposing $id publicly.
502
+ const id = section.$id || section.id;
503
+ if (id) {
504
+ Object.defineProperty(target, '$originalId', {
505
+ value: id,
506
+ enumerable: false,
507
+ writable: true,
508
+ configurable: true
509
+ });
510
+ }
511
+ // Propagate hidden $dynamicRef metadata. When a $dynamicRef lives directly on
512
+ // a $ref'd schema resource, that resource is a distinct object from the
513
+ // referencing schema and is often resolved standalone first — deleting its
514
+ // $dynamicRef and recording the (non-enumerable, so not copied above)
515
+ // $dynamicRefInfo. Carry it over so the merged schema still resolves
516
+ // dynamically. (When the $dynamicRef lives on a child of the merged resource,
517
+ // that child is shared by reference and already carries its own metadata.)
518
+ const src = section;
519
+ const dst = target;
520
+ if (src.$dynamicRefInfo !== undefined && dst.$dynamicRefInfo === undefined) {
521
+ setHidden(target, '$dynamicRefInfo', src.$dynamicRefInfo);
522
+ }
523
+ // When the merged section is itself a schema resource (it carries anchor
524
+ // maps), fold its $dynamicAnchor / $anchor maps into the target's. The
525
+ // merged node stands in for that resource during validation (it becomes a
526
+ // dynamic-scope root via $originalId), so those anchors must participate in
527
+ // dynamic-scope resolution — this is what lets a $dynamicAnchor defined in
528
+ // an external (or $ref'd sub-) resource override a base default. Existing
529
+ // entries are kept so the outermost resource retains precedence.
530
+ unionAnchorMaps(dst, src.$anchorMaps);
531
+ };
532
+ const hasKeyword = (schema, keyword) => schema[keyword] !== undefined;
533
+ const hasAnyKeyword = (schema, keywords) => keywords.some(keyword => hasKeyword(schema, keyword));
534
+ const objectPropertyKeywords = ['properties', 'patternProperties'];
535
+ const objectEvaluatorKeywords = [
536
+ ...objectPropertyKeywords,
537
+ 'additionalProperties',
538
+ 'dependentSchemas',
539
+ 'allOf',
540
+ 'anyOf',
541
+ 'oneOf',
542
+ 'if',
543
+ 'then',
544
+ 'else'
545
+ ];
546
+ const arrayEvaluatorKeywords = [
547
+ 'items',
548
+ 'additionalItems',
549
+ 'prefixItems',
550
+ 'allOf',
551
+ 'anyOf',
552
+ 'oneOf',
553
+ 'if',
554
+ 'then',
555
+ 'else'
556
+ ];
557
+ const containsBoundKeywords = ['minContains', 'maxContains'];
558
+ const conditionalBranchKeywords = ['then', 'else'];
559
+ const arrayApplicatorKeywords = ['items', 'prefixItems', 'additionalItems'];
560
+ const uses2020_12ArrayAnnotations = schemaDraft === undefined || schemaDraft >= SchemaDraft.v2020_12;
561
+ // Some keywords only make sense relative to adjacent keywords in the same schema object.
562
+ // If they live behind $ref, sibling keywords on the referencing schema must not be
563
+ // flattened into the same scope.
564
+ const needsScopeIsolation = (referencedSchema, referencingSchema) => {
565
+ const hasSiblingObjectProperties = hasAnyKeyword(referencingSchema, objectPropertyKeywords);
566
+ if (hasKeyword(referencedSchema, 'additionalProperties') && hasSiblingObjectProperties) {
567
+ return true;
568
+ }
569
+ if (hasKeyword(referencedSchema, 'additionalItems') && Array.isArray(referencingSchema.items)) {
570
+ return true;
571
+ }
572
+ const hasSiblingObjectEvaluators = hasAnyKeyword(referencingSchema, objectEvaluatorKeywords);
573
+ if (hasKeyword(referencedSchema, 'unevaluatedProperties') && hasSiblingObjectEvaluators) {
574
+ return true;
575
+ }
576
+ const hasSiblingArrayEvaluators = hasAnyKeyword(referencingSchema, arrayEvaluatorKeywords) ||
577
+ (uses2020_12ArrayAnnotations && hasKeyword(referencingSchema, 'contains'));
578
+ if (hasKeyword(referencedSchema, 'unevaluatedItems') && hasSiblingArrayEvaluators) {
579
+ return true;
580
+ }
581
+ // Reverse of the above: the *referencing* schema declares unevaluatedItems
582
+ // while the referenced schema constrains items positionally and may contain
583
+ // internal self-references (`$ref: "#"`). Flattening would pull the referenced
584
+ // items into the referencing scope, so those self-references would inherit the
585
+ // referencing unevaluatedItems. Isolate so the referenced resource keeps its
586
+ // own annotation scope.
587
+ if (hasKeyword(referencingSchema, 'unevaluatedItems') && hasAnyKeyword(referencedSchema, arrayApplicatorKeywords)) {
588
+ return true;
589
+ }
590
+ if (hasKeyword(referencedSchema, 'contains') && hasAnyKeyword(referencingSchema, containsBoundKeywords)) {
591
+ return true;
592
+ }
593
+ // Both the referenced and referencing schemas constrain array items
594
+ // (tuple `items`, or 2020-12 `items`/`prefixItems`). A flatten-merge can
595
+ // only keep one `items`, silently dropping the other; isolate into an
596
+ // allOf so both position constraints apply and item-evaluation
597
+ // annotations (for unevaluatedItems) accumulate across both.
598
+ if (Array.isArray(referencedSchema.items) && Array.isArray(referencingSchema.items)) {
599
+ return true;
347
600
  }
601
+ if (hasAnyKeyword(referencedSchema, containsBoundKeywords) && hasKeyword(referencingSchema, 'contains')) {
602
+ return true;
603
+ }
604
+ if (hasKeyword(referencedSchema, 'if') && hasAnyKeyword(referencingSchema, conditionalBranchKeywords)) {
605
+ return true;
606
+ }
607
+ if (hasAnyKeyword(referencedSchema, conditionalBranchKeywords) && hasKeyword(referencingSchema, 'if')) {
608
+ return true;
609
+ }
610
+ return uses2020_12ArrayAnnotations &&
611
+ hasKeyword(referencedSchema, 'items') &&
612
+ hasKeyword(referencingSchema, 'prefixItems');
348
613
  };
349
614
  const mergeRef = (target, sourceRoot, sourceHandle, refSegment) => {
350
615
  let section;
616
+ let sectionBaseHandle = sourceHandle;
351
617
  if (refSegment === undefined || refSegment.length === 0) {
352
618
  section = sourceRoot;
353
619
  }
354
620
  else if (refSegment.charAt(0) === '/') {
355
621
  // A $ref to a JSON Pointer (i.e #/definitions/foo)
356
- section = findSectionByJSONPointer(sourceRoot, refSegment);
622
+ // Track $id base changes along the path so inner $refs resolve correctly.
623
+ ({ section, baseHandle: sectionBaseHandle } = findSectionAndBase(sourceRoot, refSegment, sourceHandle));
357
624
  }
358
625
  else {
359
626
  // A $ref to a sub-schema with an $id (i.e #hello)
360
627
  section = findSchemaById(sourceRoot, sourceHandle, refSegment);
361
628
  }
629
+ // A boolean is a valid JSON Schema: `true` accepts everything ({}) and
630
+ // `false` rejects everything ({ not: {} }). Normalize so it merges like any
631
+ // other schema (and isn't mistaken for an unresolved section below).
632
+ if (typeof section === 'boolean') {
633
+ section = section ? {} : { not: {} };
634
+ }
362
635
  if (section) {
363
- merge(target, section);
636
+ // If the found section contains a $ref that needs to be resolved
637
+ // relative to a different base (e.g. it's inside a schema with $id),
638
+ // pre-resolve it now so the merged target carries the correct base.
639
+ // Clone before rewriting to avoid mutating the cached source schema.
640
+ if (section.$ref && sectionBaseHandle !== sourceHandle) {
641
+ const innerRef = section.$ref;
642
+ const innerSegments = innerRef.split('#', 2);
643
+ if (innerSegments[0].length > 0 && contextService && !hasSchemeRegex.test(innerSegments[0])) {
644
+ section = {
645
+ ...section,
646
+ $ref: contextService.resolveRelativePath(innerSegments[0], sectionBaseHandle.uri) +
647
+ (innerSegments[1] !== undefined ? '#' + innerSegments[1] : '')
648
+ };
649
+ }
650
+ }
651
+ const reservedKeys = new Set(['$ref', '$defs', 'definitions', '$schema', '$id', 'id']);
652
+ // Keywords that must stay on the wrapper rather than move into the allOf
653
+ // sibling. Scope-anchor keywords ($recursiveAnchor/$dynamicAnchor) keep this
654
+ // resource discoverable as a $recursiveRef/$dynamicRef bookending target.
655
+ // The unevaluated* annotations must observe the results of *all* in-place
656
+ // applicators (including the $ref'd schema in allOf[0]); leaving them on the
657
+ // wrapper lets them see the aggregated annotations rather than only the
658
+ // sibling's own evaluations.
659
+ const keepOnWrapperKeys = new Set([
660
+ '$recursiveAnchor', '$dynamicAnchor', 'unevaluatedItems', 'unevaluatedProperties'
661
+ ]);
662
+ // In JSON Schema draft-04 through draft-07, $ref completely overrides any sibling keywords.
663
+ // Starting in 2019-09, sibling keywords are processed alongside $ref.
664
+ // Only strip siblings when schema explicitly declares a pre-2019-09 draft via $schema.
665
+ const isPreDraft201909 = schemaDraft !== undefined && schemaDraft < SchemaDraft.v2019_09;
666
+ if (isPreDraft201909) {
667
+ // Clear all sibling keywords from target - $ref takes precedence
668
+ for (const key in target) {
669
+ if (target.hasOwnProperty(key) && !reservedKeys.has(key)) {
670
+ delete target[key];
671
+ }
672
+ }
673
+ merge(target, section);
674
+ }
675
+ else if (needsScopeIsolation(section, target)) {
676
+ // In JSON Schema 2019-09 or greater, $ref creates a new scope when sibling
677
+ // keywords would otherwise change the meaning of same-object-dependent keywords
678
+ // from the referenced schema.
679
+ // To achieve this, we wrap the $ref in an allOf when needed.
680
+ const siblingSchema = {};
681
+ const refSchema = { ...section };
682
+ // Move all existing properties from target to siblingSchema, except
683
+ // keywords that must remain on the wrapper resource.
684
+ for (const key in target) {
685
+ if (target.hasOwnProperty(key) && !reservedKeys.has(key) && !keepOnWrapperKeys.has(key)) {
686
+ const k = key;
687
+ siblingSchema[k] = target[k];
688
+ delete target[k];
689
+ }
690
+ }
691
+ // Create allOf with the $ref'd schema and sibling schema
692
+ target.allOf = [refSchema, siblingSchema];
693
+ }
694
+ else {
695
+ merge(target, section);
696
+ }
364
697
  }
365
698
  else {
366
699
  const message = l10n.t('$ref \'{0}\' in \'{1}\' can not be resolved.', refSegment || '', sourceHandle.uri);
@@ -368,7 +701,7 @@ export class JSONSchemaService {
368
701
  }
369
702
  };
370
703
  const resolveExternalLink = (node, uri, refSegment, parentHandle) => {
371
- if (contextService && !/^[A-Za-z][A-Za-z0-9+\-.+]*:\/.*/.test(uri)) {
704
+ if (contextService && !hasSchemeRegex.test(uri)) {
372
705
  uri = contextService.resolveRelativePath(uri, parentHandle.uri);
373
706
  }
374
707
  uri = normalizeId(uri);
@@ -381,132 +714,338 @@ export class JSONSchemaService {
381
714
  const errorMessage = refSegment ? l10n.t('Problems loading reference \'{0}\': {1}', refSegment, error.message) : error.message;
382
715
  resolveErrors.push(toDiagnostic(errorMessage, error.code, uri));
383
716
  }
717
+ // A referenced document may itself embed subschemas with their own
718
+ // absolute $id. Register those as resolvable handles (once per document)
719
+ // so a nested $ref by that $id resolves to the embedded resource instead
720
+ // of being (mis)loaded as a standalone document. registerEmbeddedSchemas
721
+ // only runs for the root document otherwise.
722
+ if (!embeddedRegisteredFor.has(uri)) {
723
+ embeddedRegisteredFor.add(uri);
724
+ registerEmbeddedSchemas(unresolvedSchema.schema, uri);
725
+ }
726
+ // 2020-12: collect the referenced document's own resource anchor maps
727
+ // (once) before merging, so its $dynamicAnchor declarations can join the
728
+ // dynamic scope and its $dynamicRef nodes get their lexical resource
729
+ // recorded — mirroring what is done eagerly for the root document, before
730
+ // $ref merging flattens resource boundaries.
731
+ if (unresolvedSchema.schema.$anchorMaps === undefined) {
732
+ collectDynamicAnchors(unresolvedSchema.schema);
733
+ }
384
734
  mergeRef(node, unresolvedSchema.schema, referencedHandle, refSegment);
735
+ // Referencing a fragment of another resource (e.g. "other#/$defs/x")
736
+ // still *enters* that resource's dynamic scope, so its $dynamicAnchor
737
+ // declarations must join `node`'s scope even though only the sub-schema
738
+ // was merged. (For a whole-document ref the merge above already did this,
739
+ // since the merged section is the resource root; this is a no-op then.)
740
+ unionAnchorMaps(node, unresolvedSchema.schema.$anchorMaps);
385
741
  return resolveRefs(node, unresolvedSchema.schema, referencedHandle);
386
742
  });
387
743
  };
744
+ const resolveDynamicRef = (schema, newBase, newBaseHandle, currentBaseHandle) => {
745
+ // 2020-12 $dynamicRef. Its *initial* target is resolved statically, exactly
746
+ // like a plain $ref, and kept as hidden metadata (info.target) instead of
747
+ // being merged into `schema`, so `schema`'s own keywords are preserved. The
748
+ // referenced plain-name fragment is recorded as info.name. The "bookending"
749
+ // decision (does the initial target name a $dynamicAnchor?) and the
750
+ // dynamic-scope walk are both deferred to validation time, where the (possibly
751
+ // asynchronously resolved) target is fully populated. Returns the external-
752
+ // resolution promise (if any) for the caller to add to openPromises.
753
+ const info = dynamicRefInfoOf(schema);
754
+ const ref = schema.$dynamicRef;
755
+ const segments = ref.split('#', 2);
756
+ delete schema.$dynamicRef;
757
+ // A JSON-pointer fragment ("#/…") or a missing fragment can never name a
758
+ // $dynamicAnchor, so such a $dynamicRef always behaves like a plain $ref.
759
+ const fragment = segments[1];
760
+ const dynamicName = (isString(fragment) && fragment.length > 0 && fragment.charAt(0) !== '/') ? fragment : undefined;
761
+ if (dynamicName !== undefined) {
762
+ info.name = dynamicName;
763
+ }
764
+ if (segments[0].length > 0) {
765
+ // External / relative reference: resolve the target document into a
766
+ // throwaway container so `schema`'s own keywords are preserved.
767
+ const target = {};
768
+ info.target = target;
769
+ const refBase = (newBase === schema) ? currentBaseHandle : newBaseHandle;
770
+ return resolveExternalLink(target, segments[0], segments[1], refBase);
771
+ }
772
+ // Internal reference. Resolve #name within the *lexical* schema resource
773
+ // (recorded as info.scope before $ref merging flattened resource boundaries)
774
+ // so a sibling resource's identically-named anchor cannot leak in.
775
+ let target;
776
+ if (dynamicName !== undefined && info.scope?.$anchorMaps) {
777
+ target = info.scope.$anchorMaps.local.get(dynamicName);
778
+ }
779
+ if (target === undefined) {
780
+ // JSON-pointer fragment, or no lexical anchor found: fall back to a plain
781
+ // static $ref resolution against the current base.
782
+ const container = {};
783
+ mergeRef(container, newBase, newBaseHandle, segments[1]);
784
+ target = container;
785
+ }
786
+ info.target = target;
787
+ return undefined;
788
+ };
388
789
  const resolveRefs = (node, parentSchema, parentHandle) => {
389
790
  const openPromises = [];
390
- this.traverseNodes(node, next => {
791
+ // Traversal that tracks the current base schema for internal refs.
792
+ // When we encounter a schema with its own $id, that becomes the new base
793
+ // for resolving fragment refs (#...) in its descendants
794
+ const traverseWithBaseTracking = (schema, currentBase, currentBaseHandle, isRoot, seen) => {
795
+ if (!schema || typeof schema !== 'object' || seen.has(schema)) {
796
+ return;
797
+ }
798
+ seen.add(schema);
799
+ // A schema with its own $id defines a new base URI scope for everything
800
+ // inside it. Resolve the id (relative to the current base) and use that
801
+ // handle as the base going forward.
802
+ //
803
+ // We do this even when `isRoot` is true so that a recursive call from
804
+ // `resolveExternalLink` — which passes the external schema's handle as
805
+ // `currentBaseHandle` — still uses *this* node's own $id as its base
806
+ // when re-traversing it. Without this, nested $id values inside the
807
+ // original schema would be resolved against the external schema's URI.
808
+ const id = getSchemaId(schema);
809
+ let newBase = currentBase;
810
+ let newBaseHandle = currentBaseHandle;
811
+ if (isString(id) && id.charAt(0) !== '#') {
812
+ let resolvedUri = id;
813
+ if (contextService && !hasSchemeRegex.test(id)) {
814
+ resolvedUri = contextService.resolveRelativePath(id, currentBaseHandle.uri);
815
+ }
816
+ resolvedUri = normalizeId(resolvedUri);
817
+ newBase = schema;
818
+ newBaseHandle = this.getOrAddSchemaHandle(resolvedUri);
819
+ // Register the schema under its id if we haven't already.
820
+ if (!this.schemasById[resolvedUri]) {
821
+ this.addSchemaHandle(resolvedUri, schema);
822
+ }
823
+ }
824
+ // Process refs in this schema
391
825
  const seenRefs = new Set();
392
- while (next.$ref) {
393
- const ref = next.$ref;
826
+ while (schema.$ref) {
827
+ const ref = schema.$ref;
394
828
  const segments = ref.split('#', 2);
395
- delete next.$ref;
829
+ delete schema.$ref;
396
830
  if (segments[0].length > 0) {
397
- // This is a reference to an external schema
398
- openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentHandle));
831
+ // This is a reference to an external schema (like "foo.json" or "foo.json#/bar")
832
+ // Per JSON Schema spec, $ref is resolved against the current base URI.
833
+ // If this schema has its own $id (sibling case), the $ref should resolve
834
+ // against the parent's base, not the sibling $id. Otherwise, use the
835
+ // nearest ancestor's base (newBaseHandle).
836
+ const refBase = (newBase === schema) ? currentBaseHandle : newBaseHandle;
837
+ openPromises.push(resolveExternalLink(schema, segments[0], segments[1], refBase));
399
838
  return;
400
839
  }
401
840
  else {
402
- // This is a reference inside the current schema
841
+ // This is an internal reference (like "#/definitions/foo")
842
+ // Internal refs are resolved within the current document
843
+ // If this schema has its own $id, it is a new document, so use newBase
844
+ // Otherwise, use the parent's base (currentBase)
403
845
  if (!seenRefs.has(ref)) {
404
- const id = segments[1];
405
- mergeRef(next, parentSchema, parentHandle, id);
846
+ const refId = segments[1];
847
+ mergeRef(schema, newBase, newBaseHandle, refId);
406
848
  seenRefs.add(ref);
407
849
  }
408
850
  }
409
851
  }
410
- if (next.$recursiveRef) {
411
- usesUnsupportedFeatures.add('$recursiveRef');
412
- }
413
- if (next.$dynamicRef) {
414
- usesUnsupportedFeatures.add('$dynamicRef');
852
+ // $dynamicRef is a 2020-12 core keyword. In earlier drafts it is an
853
+ // unknown keyword, so leave it untouched and unresolved (ignored) there.
854
+ if (schema.$dynamicRef && (schemaDraft === undefined || schemaDraft >= SchemaDraft.v2020_12)) {
855
+ const dynamicRefPromise = resolveDynamicRef(schema, newBase, newBaseHandle, currentBaseHandle);
856
+ if (dynamicRefPromise) {
857
+ openPromises.push(dynamicRefPromise);
858
+ }
415
859
  }
416
- });
860
+ // Continue traversing child schemas with the potentially updated base
861
+ JSONSchemaService.traverseSchemaProperties(schema, (childSchema) => {
862
+ traverseWithBaseTracking(childSchema, newBase, newBaseHandle, false, seen);
863
+ });
864
+ };
865
+ traverseWithBaseTracking(node, parentSchema, parentHandle, true, new Set());
417
866
  return this.promise.all(openPromises);
418
867
  };
419
868
  const collectAnchors = (root) => {
420
869
  const result = new Map();
421
- this.traverseNodes(root, next => {
422
- const id = next.$id || next.id;
423
- const anchor = isString(id) && id.charAt(0) === '#' ? id.substring(1) : next.$anchor;
424
- if (anchor) {
425
- if (result.has(anchor)) {
870
+ const seen = new Set();
871
+ // Use the schema's own $schema to determine draft, so that an external
872
+ // schema referenced from a doesn't inherit the parent's anchor rules.
873
+ const draft = root.$schema ? getSchemaDraftFromId(root.$schema) : schemaDraft;
874
+ // Traversal that stops at sub-schemas with their own $id
875
+ // because those create a new URI scope for anchors
876
+ const traverseForAnchors = (node, isRoot) => {
877
+ if (!node || typeof node !== 'object' || seen.has(node)) {
878
+ return;
879
+ }
880
+ seen.add(node);
881
+ // If this node has its own $id, and it's not the root, it's a new URI scope
882
+ const id = getSchemaId(node);
883
+ if (!isRoot && isString(id) && id.charAt(0) !== '#') {
884
+ return;
885
+ }
886
+ // Collect anchor from this node
887
+ // In draft-04/06/07, anchors are defined via $id/#fragment (e.g., "$id": "#myanchor")
888
+ // In 2019-09+, $id fragments are no longer anchors; $anchor is used instead
889
+ // In 2020-12, $dynamicAnchor also defines a plain-name fragment that a
890
+ // (static) $ref can resolve to, just like $anchor.
891
+ const fragmentAnchor = (draft === undefined || draft < SchemaDraft.v2019_09) && isString(id) && id.charAt(0) === '#' ? id.substring(1) : undefined;
892
+ const dollarAnchor = (draft === undefined || draft >= SchemaDraft.v2019_09) ? node.$anchor : undefined;
893
+ const dynamicAnchor = (draft === undefined || draft >= SchemaDraft.v2020_12) ? node.$dynamicAnchor : undefined;
894
+ const registerAnchor = (anchor) => {
895
+ if (!anchor) {
896
+ return;
897
+ }
898
+ if (result.has(anchor) && result.get(anchor) !== node) {
426
899
  resolveErrors.push(toDiagnostic(l10n.t('Duplicate anchor declaration: \'{0}\'', anchor), ErrorCode.SchemaResolveError));
427
900
  }
428
901
  else {
429
- result.set(anchor, next);
902
+ result.set(anchor, node);
430
903
  }
431
- }
432
- if (next.$recursiveAnchor) {
433
- usesUnsupportedFeatures.add('$recursiveAnchor');
434
- }
435
- if (next.$dynamicAnchor) {
436
- usesUnsupportedFeatures.add('$dynamicAnchor');
437
- }
438
- });
904
+ };
905
+ registerAnchor(fragmentAnchor ?? dollarAnchor);
906
+ registerAnchor(dynamicAnchor);
907
+ // Continue traversing child schemas
908
+ JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
909
+ traverseForAnchors(childSchema, false);
910
+ });
911
+ };
912
+ traverseForAnchors(root, true);
439
913
  return result;
440
914
  };
441
- return resolveRefs(schema, schema, handle).then(_ => {
442
- let resolveWarnings = [];
443
- if (usesUnsupportedFeatures.size) {
444
- resolveWarnings.push(toDiagnostic(l10n.t('The schema uses meta-schema features ({0}) that are not yet supported by the validator.', Array.from(usesUnsupportedFeatures.keys()).join(', ')), ErrorCode.SchemaUnsupportedFeature));
915
+ // 2020-12: group $anchor / $dynamicAnchor declarations by the schema resource
916
+ // ($id scope) that contains them, and attach each resource's name→node maps to
917
+ // its resource-root node as hidden metadata ($anchorMaps):
918
+ // - $anchorMaps.dynamic: only $dynamicAnchor names, used by the validator to
919
+ // walk the dynamic scope (outermost resource first).
920
+ // - $anchorMaps.local: both $anchor and $dynamicAnchor names, used to resolve
921
+ // an internal $dynamicRef's initial target within its own resource.
922
+ // Each $dynamicRef node also gets its lexical resource recorded as
923
+ // $dynamicRefInfo.scope. This must run on the original schema, before $ref
924
+ // resolution merges (and thereby flattens) resource boundaries; node identities
925
+ // are preserved through in-place resolution, so the attached metadata stays valid.
926
+ const collectDynamicAnchors = (root) => {
927
+ // Respect the resource's own $schema (like collectAnchors) so a referenced
928
+ // document with a different, pre-2020-12 dialect is not given $dynamicAnchor
929
+ // semantics just because the root document is 2020-12.
930
+ const draft = root && typeof root === 'object' && root.$schema ? getSchemaDraftFromId(root.$schema) : schemaDraft;
931
+ if (!(draft === undefined || draft >= SchemaDraft.v2020_12)) {
932
+ return;
445
933
  }
446
- return new ResolvedSchema(schema, resolveErrors, resolveWarnings, schemaDraft);
447
- });
448
- }
449
- traverseNodes(root, handle) {
450
- if (!root || typeof root !== 'object') {
451
- return Promise.resolve(null);
452
- }
453
- const seen = new Set();
454
- const collectEntries = (...entries) => {
455
- for (const entry of entries) {
456
- if (isObject(entry)) {
457
- toWalk.push(entry);
934
+ const seen = new Set();
935
+ const visit = (node, resourceRoot) => {
936
+ if (!node || typeof node !== 'object' || seen.has(node)) {
937
+ return;
458
938
  }
459
- }
460
- };
461
- const collectMapEntries = (...maps) => {
462
- for (const map of maps) {
463
- if (isObject(map)) {
464
- for (const k in map) {
465
- const key = k;
466
- const entry = map[key];
467
- if (isObject(entry)) {
468
- toWalk.push(entry);
469
- }
939
+ seen.add(node);
940
+ // A node with its own $id (or the document root) starts a new resource.
941
+ let currentRoot = resourceRoot;
942
+ const id = getSchemaId(node);
943
+ if (node === root || (isString(id) && id.charAt(0) !== '#')) {
944
+ currentRoot = node;
945
+ if (!currentRoot.$anchorMaps) {
946
+ const maps = { dynamic: new Map(), local: new Map() };
947
+ setHidden(currentRoot, '$anchorMaps', maps);
470
948
  }
471
949
  }
472
- }
473
- };
474
- const collectArrayEntries = (...arrays) => {
475
- for (const array of arrays) {
476
- if (Array.isArray(array)) {
477
- for (const entry of array) {
478
- if (isObject(entry)) {
479
- toWalk.push(entry);
480
- }
950
+ const maps = currentRoot.$anchorMaps;
951
+ if (isString(node.$dynamicAnchor)) {
952
+ if (!maps.dynamic.has(node.$dynamicAnchor)) {
953
+ maps.dynamic.set(node.$dynamicAnchor, node);
954
+ }
955
+ if (!maps.local.has(node.$dynamicAnchor)) {
956
+ maps.local.set(node.$dynamicAnchor, node);
481
957
  }
482
958
  }
483
- }
959
+ if (isString(node.$anchor) && !maps.local.has(node.$anchor)) {
960
+ maps.local.set(node.$anchor, node);
961
+ }
962
+ if (isString(node.$dynamicRef)) {
963
+ dynamicRefInfoOf(node).scope = currentRoot;
964
+ }
965
+ JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
966
+ visit(childSchema, currentRoot);
967
+ });
968
+ };
969
+ visit(root, root);
484
970
  };
485
- const collectEntryOrArrayEntries = (items) => {
486
- if (Array.isArray(items)) {
487
- for (const entry of items) {
488
- if (isObject(entry)) {
489
- toWalk.push(entry);
971
+ // Collect and register embedded schemas with $id so they can be resolved as external refs
972
+ // This traversal tracks the current base URI so nested $id values are resolved correctly
973
+ const registerEmbeddedSchemas = (root, baseUri) => {
974
+ const seen = new Set();
975
+ const resolveId = (id, currentBaseUri) => {
976
+ if (contextService && !hasSchemeRegex.test(id)) {
977
+ return normalizeId(contextService.resolveRelativePath(id, currentBaseUri));
978
+ }
979
+ return normalizeId(id);
980
+ };
981
+ const visit = (node, currentBaseUri) => {
982
+ if (!node || typeof node !== 'object' || seen.has(node)) {
983
+ return;
984
+ }
985
+ seen.add(node);
986
+ // Check if this node has its own $id that changes the base URI
987
+ const id = getSchemaId(node);
988
+ let newBaseUri = currentBaseUri;
989
+ if (isString(id) && id.charAt(0) !== '#') {
990
+ const resolvedUri = resolveId(id, currentBaseUri);
991
+ const existingHandle = this.schemasById[resolvedUri];
992
+ if (!existingHandle) {
993
+ this.addSchemaHandle(resolvedUri, node);
994
+ }
995
+ else {
996
+ // Update existing handle with embedded schema content
997
+ // This ensures embedded schemas take precedence over external schemas
998
+ existingHandle.setSchemaContent(node);
490
999
  }
1000
+ newBaseUri = resolvedUri;
491
1001
  }
1002
+ // Visit child schemas
1003
+ JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
1004
+ visit(childSchema, newBaseUri);
1005
+ });
1006
+ };
1007
+ visit(root, baseUri);
1008
+ };
1009
+ // Register embedded schemas before resolving refs
1010
+ registerEmbeddedSchemas(schema, handle.uri);
1011
+ // Collect anchors eagerly before $ref resolution mutates the schema.
1012
+ // resolveRefs merges referenced nodes (including $anchor) into $ref targets,
1013
+ // so a lazy collectAnchors call could see duplicates from merged copies.
1014
+ handle.anchors = collectAnchors(schema);
1015
+ // Collect $dynamicAnchor maps eagerly too, for the same reason: resource
1016
+ // boundaries must be read before $ref resolution flattens them.
1017
+ collectDynamicAnchors(schema);
1018
+ // Resolve meta-schema to extract vocabularies if present
1019
+ const resolveMetaschemaVocabularies = () => {
1020
+ if (!schema.$schema || typeof schema.$schema !== 'string') {
1021
+ return this.promise.resolve(undefined);
492
1022
  }
493
- else if (isObject(items)) {
494
- toWalk.push(items);
1023
+ let metaschemaUri = schema.$schema;
1024
+ if (contextService && !hasSchemeRegex.test(metaschemaUri)) {
1025
+ metaschemaUri = contextService.resolveRelativePath(metaschemaUri, handle.uri);
495
1026
  }
1027
+ const normalizedMetaschemaUri = normalizeId(metaschemaUri);
1028
+ const metaschemaHandle = this.getOrAddSchemaHandle(normalizedMetaschemaUri);
1029
+ return metaschemaHandle.getUnresolvedSchema().then(unresolvedMetaschema => {
1030
+ // Only extract vocabularies if the meta-schema has a $vocabulary property
1031
+ // or if it's draft 2019-09 or later which support vocabularies.
1032
+ const metaschemaDraft = unresolvedMetaschema.schema.$schema ? getSchemaDraftFromId(unresolvedMetaschema.schema.$schema) : undefined;
1033
+ const isDraft2019OrLater = metaschemaDraft && metaschemaDraft >= SchemaDraft.v2019_09;
1034
+ const hasVocabulary = unresolvedMetaschema.schema.$vocabulary && typeof unresolvedMetaschema.schema.$vocabulary === 'object';
1035
+ if (hasVocabulary || isDraft2019OrLater) {
1036
+ activeVocabularies = extractVocabularies(unresolvedMetaschema.schema);
1037
+ }
1038
+ return undefined;
1039
+ }, () => {
1040
+ // If we can't load the meta-schema, proceed without vocabulary info
1041
+ return undefined;
1042
+ });
496
1043
  };
497
- const toWalk = [root];
498
- let next = toWalk.pop();
499
- while (next) {
500
- if (!seen.has(next)) {
501
- seen.add(next);
502
- handle(next);
503
- collectEntries(next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else, next.unevaluatedItems, next.unevaluatedProperties);
504
- collectMapEntries(next.definitions, next.$defs, next.properties, next.patternProperties, next.dependencies, next.dependentSchemas);
505
- collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems);
506
- collectEntryOrArrayEntries(next.items);
507
- }
508
- next = toWalk.pop();
509
- }
1044
+ return resolveMetaschemaVocabularies().then(() => {
1045
+ return resolveRefs(schema, schema, handle).then(_ => {
1046
+ return new ResolvedSchema(schema, resolveErrors, [], schemaDraft, activeVocabularies);
1047
+ });
1048
+ });
510
1049
  }
511
1050
  ;
512
1051
  getSchemaFromProperty(resource, document) {