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.
- package/CHANGELOG.md +13 -0
- package/lib/esm/jsonContributions.d.ts +1 -1
- package/lib/esm/jsonLanguageService.d.ts +2 -2
- package/lib/esm/jsonLanguageService.js +13 -13
- package/lib/esm/jsonLanguageTypes.d.ts +10 -2
- package/lib/esm/jsonSchema.d.ts +71 -2
- package/lib/esm/parser/jsonParser.js +224 -93
- package/lib/esm/services/configuration.js +2 -2
- package/lib/esm/services/jsonCompletion.js +5 -5
- package/lib/esm/services/jsonDocumentSymbols.js +4 -4
- package/lib/esm/services/jsonFolding.js +1 -1
- package/lib/esm/services/jsonHover.js +2 -2
- package/lib/esm/services/jsonLinks.js +94 -3
- package/lib/esm/services/jsonSchemaService.js +639 -100
- package/lib/esm/services/jsonSelectionRanges.js +1 -1
- package/lib/esm/services/jsonValidation.js +4 -4
- package/lib/esm/services/schemas/draft-2019-09-flat.d.ts +1 -1
- package/lib/esm/services/schemas/draft-2020-12-flat.d.ts +1 -1
- package/lib/esm/services/vocabularies.js +139 -0
- package/lib/esm/utils/format.js +1 -1
- package/lib/esm/utils/sort.js +3 -3
- package/package.json +23 -20
- package/lib/umd/jsonContributions.d.ts +0 -21
- package/lib/umd/jsonContributions.js +0 -12
- package/lib/umd/jsonLanguageService.d.ts +0 -30
- package/lib/umd/jsonLanguageService.js +0 -79
- package/lib/umd/jsonLanguageTypes.d.ts +0 -305
- package/lib/umd/jsonLanguageTypes.js +0 -109
- package/lib/umd/jsonSchema.d.ts +0 -92
- package/lib/umd/jsonSchema.js +0 -12
- package/lib/umd/parser/jsonParser.js +0 -1365
- package/lib/umd/services/configuration.js +0 -536
- package/lib/umd/services/jsonCompletion.js +0 -982
- package/lib/umd/services/jsonDocumentSymbols.js +0 -285
- package/lib/umd/services/jsonFolding.js +0 -133
- package/lib/umd/services/jsonHover.js +0 -125
- package/lib/umd/services/jsonLinks.js +0 -85
- package/lib/umd/services/jsonSchemaService.js +0 -631
- package/lib/umd/services/jsonSelectionRanges.js +0 -74
- package/lib/umd/services/jsonValidation.js +0 -165
- package/lib/umd/services/schemas/draft-2019-09-flat.d.ts +0 -278
- package/lib/umd/services/schemas/draft-2019-09-flat.js +0 -314
- package/lib/umd/services/schemas/draft-2020-12-flat.d.ts +0 -276
- package/lib/umd/services/schemas/draft-2020-12-flat.js +0 -307
- package/lib/umd/utils/colors.js +0 -83
- package/lib/umd/utils/format.js +0 -33
- package/lib/umd/utils/glob.js +0 -137
- package/lib/umd/utils/json.js +0 -55
- package/lib/umd/utils/objects.js +0 -86
- package/lib/umd/utils/propertyTree.js +0 -90
- package/lib/umd/utils/sort.js +0 -384
- 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 {
|
|
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
|
|
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
|
-
|
|
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)
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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 (
|
|
393
|
-
const ref =
|
|
826
|
+
while (schema.$ref) {
|
|
827
|
+
const ref = schema.$ref;
|
|
394
828
|
const segments = ref.split('#', 2);
|
|
395
|
-
delete
|
|
829
|
+
delete schema.$ref;
|
|
396
830
|
if (segments[0].length > 0) {
|
|
397
|
-
// This is a reference to an external schema
|
|
398
|
-
|
|
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
|
|
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
|
|
405
|
-
mergeRef(
|
|
846
|
+
const refId = segments[1];
|
|
847
|
+
mergeRef(schema, newBase, newBaseHandle, refId);
|
|
406
848
|
seenRefs.add(ref);
|
|
407
849
|
}
|
|
408
850
|
}
|
|
409
851
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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,
|
|
902
|
+
result.set(anchor, node);
|
|
430
903
|
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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) {
|