Skip to content

Commit cd3cba0

Browse files
shin19991207datho7561
authored andcommitted
fix schema resolution when $id base URI differs from the loaded schema URI
Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent 93b9e72 commit cd3cba0

4 files changed

Lines changed: 205 additions & 84 deletions

File tree

src/languageservice/jsonSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export enum SchemaDialect {
1717
export interface JSONSchema {
1818
// for internal use
1919
_dialect?: SchemaDialect;
20-
_baseUrl?: string;
20+
_baseUri?: string;
21+
_sourceUri?: string;
2122
_$ref?: string;
2223

2324
id?: string;

src/languageservice/services/yamlSchemaService.ts

Lines changed: 68 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ const REF_SIBLING_NONCONSTRAINT_KEYS = new Set([
6363
'$schema',
6464
'$id',
6565
'id',
66-
'_baseUrl',
66+
'_baseUri',
67+
'_sourceUri',
6768
'_dialect',
6869
'$anchor',
6970
'$dynamicAnchor',
@@ -216,8 +217,13 @@ export class YAMLSchemaService extends JSONSchemaService {
216217
return new ResolvedSchema({}, resolveErrors);
217218
}
218219

220+
const _setSourceUri = (node: JSONSchema, sourceUri: string | undefined): void => {
221+
if (!node || typeof node !== 'object' || !sourceUri) return;
222+
node._sourceUri = sourceUri;
223+
};
224+
219225
const _cloneSchema = (
220-
value: unknown,
226+
value: JSONSchema,
221227
seen: Map<object, unknown>,
222228
stopCondition?: (val: unknown, seenSize: number) => unknown | undefined
223229
): unknown => {
@@ -243,11 +249,12 @@ export class YAMLSchemaService extends JSONSchemaService {
243249
}
244250

245251
// clone objects
246-
const result = {};
252+
const result: JSONSchema = {};
247253
seen.set(value, result);
248254
for (const prop in value) {
249255
result[prop] = _cloneSchema(value[prop], seen, stopCondition);
250256
}
257+
_setSourceUri(result, value._sourceUri);
251258
return result;
252259
};
253260

@@ -331,7 +338,7 @@ export class YAMLSchemaService extends JSONSchemaService {
331338
if (!entryItem.dynamic) continue;
332339

333340
const current = result?.get(name) ?? [];
334-
if (current.some((existing) => existing._baseUrl === resourceUri)) continue;
341+
if (current.some((existing) => existing._baseUri === resourceUri)) continue;
335342

336343
// clone map on first modification
337344
if (result === scope) result = scope ? new Map(scope) : new Map<string, JSONSchema[]>();
@@ -347,28 +354,9 @@ export class YAMLSchemaService extends JSONSchemaService {
347354
return this.normalizeId(ref);
348355
};
349356

350-
const _preferLocalBaseForRemoteId = async (currentBase: string, id: string): Promise<string> => {
351-
try {
352-
const currentBaseUri = URI.parse(currentBase);
353-
if (currentBaseUri.scheme !== 'file') {
354-
return _resolveAgainstBase(currentBase, id);
355-
}
356-
const idUri = URI.parse(id);
357-
const localFileName = path.posix.basename(idUri.path);
358-
const localDir = path.posix.dirname(currentBaseUri.path);
359-
const localPath = path.posix.join(localDir, localFileName);
360-
const localUriStr = currentBaseUri.with({ path: localPath, query: idUri.query, fragment: idUri.fragment }).toString();
361-
if (localUriStr === currentBase) return localUriStr;
362-
const content = await this.requestService(localUriStr);
363-
return content ? localUriStr : _resolveAgainstBase(currentBase, id);
364-
} catch {
365-
return _resolveAgainstBase(currentBase, id);
366-
}
367-
};
368-
369357
const _indexSchemaResources = async (root: JSONSchema, initialBaseUri: string): Promise<void> => {
370-
type WorkItem = { node: JSONSchema; baseUri: string };
371-
const preOrderStack: WorkItem[] = [{ node: root, baseUri: initialBaseUri }];
358+
type WorkItem = { node: JSONSchema; baseUri: string; sourceUri: string };
359+
const preOrderStack: WorkItem[] = [{ node: root, baseUri: initialBaseUri, sourceUri: initialBaseUri }];
372360
const postOrderStack: JSONSchema[] = [];
373361
const childListByNode = new WeakMap<JSONSchema, JSONSchema[]>();
374362

@@ -382,19 +370,20 @@ export class YAMLSchemaService extends JSONSchemaService {
382370
seen.add(node);
383371

384372
let baseUri = current.baseUri;
373+
_setSourceUri(node, current.sourceUri);
385374
const id = node.$id || node.id;
386375
if (id) {
387-
const preferredBaseUri = await _preferLocalBaseForRemoteId(baseUri, id);
388-
node._baseUrl = preferredBaseUri;
389-
const hashIndex = preferredBaseUri.indexOf('#');
390-
if (hashIndex !== -1 && hashIndex < preferredBaseUri.length - 1) {
376+
const resolvedBaseUri = _resolveAgainstBase(baseUri, id);
377+
node._baseUri = resolvedBaseUri;
378+
const hashIndex = resolvedBaseUri.indexOf('#');
379+
if (hashIndex !== -1 && hashIndex < resolvedBaseUri.length - 1) {
391380
// Draft-07 and earlier: $id with fragment defines a plain-name anchor scoped to the resolved base
392-
const frag = preferredBaseUri.slice(hashIndex + 1);
381+
const frag = resolvedBaseUri.slice(hashIndex + 1);
393382
_getResourceIndex(baseUri).fragments.set(frag, { node });
394383
} else {
395384
// $id without fragment creates a new embedded resource scope
396-
baseUri = preferredBaseUri;
397-
const entry = _getResourceIndex(preferredBaseUri);
385+
baseUri = resolvedBaseUri;
386+
const entry = _getResourceIndex(resolvedBaseUri);
398387
if (!entry.root) {
399388
entry.root = node;
400389
}
@@ -406,7 +395,7 @@ export class YAMLSchemaService extends JSONSchemaService {
406395
}
407396
// Draft 2020-12+: $dynamicAnchor keyword
408397
if (node.$dynamicAnchor) {
409-
node._baseUrl = baseUri;
398+
node._baseUri = baseUri;
410399
_getResourceIndex(baseUri).fragments.set(node.$dynamicAnchor, { node, dynamic: true });
411400
}
412401

@@ -417,7 +406,7 @@ export class YAMLSchemaService extends JSONSchemaService {
417406
this.collectSchemaNodes(
418407
(entry) => {
419408
children.push(entry);
420-
preOrderStack.push({ node: entry, baseUri });
409+
preOrderStack.push({ node: entry, baseUri, sourceUri: current.sourceUri });
421410
},
422411
node.not,
423412
node.if,
@@ -497,6 +486,7 @@ export class YAMLSchemaService extends JSONSchemaService {
497486
target[key] = source[key];
498487
}
499488
}
489+
_setSourceUri(target, source._sourceUri);
500490
return;
501491
} else {
502492
resolveErrors.push(l10n.t("$ref '{0}' in '{1}' cannot be resolved.", refPath, sourceURI));
@@ -537,7 +527,7 @@ export class YAMLSchemaService extends JSONSchemaService {
537527
uri: string,
538528
linkPath: string,
539529
parentSchemaURL: string,
540-
fallbackBaseURL: string,
530+
parentSchemaSourceUri: string,
541531
parentSchemaDependencies: SchemaDependencies,
542532
resolutionStack: Set<string>,
543533
recursiveAnchorBase: string,
@@ -558,7 +548,7 @@ export class YAMLSchemaService extends JSONSchemaService {
558548
): Promise<any> => {
559549
parentSchemaDependencies[schemaUri] = true;
560550
_merge(node, schemaRoot, schemaUri, linkPath, !!inheritedDynamicScope || !!recursiveAnchorBase);
561-
if (!recursiveAnchorBase || !node._baseUrl) node._baseUrl = schemaUri;
551+
if (!recursiveAnchorBase || !node._baseUri) node._baseUri = schemaUri;
562552
node.url = schemaUri;
563553

564554
const nextStack = new Set(resolutionStack);
@@ -623,8 +613,8 @@ export class YAMLSchemaService extends JSONSchemaService {
623613
};
624614

625615
const resolvedUri = _resolveRefUri(parentSchemaURL, uri);
626-
const hasEmbeddedTarget = !!resourceIndexByUri.get(resolvedUri)?.root;
627-
const localSiblingUri = hasEmbeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(fallbackBaseURL, resolvedUri);
616+
const embeddedTarget = resourceIndexByUri.get(resolvedUri)?.root;
617+
const localSiblingUri = embeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(parentSchemaSourceUri, resolvedUri);
628618
const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri];
629619
return _resolveByUri(targetUris);
630620
};
@@ -646,44 +636,45 @@ export class YAMLSchemaService extends JSONSchemaService {
646636
// track nodes with their base URL for $id resolution
647637
type WalkItem = {
648638
node: JSONSchema;
649-
baseURL?: string;
650-
fallbackBaseURL?: string;
639+
baseUri?: string;
640+
sourceUri?: string;
651641
dialect?: SchemaDialect;
652642
recursiveAnchorBase?: string;
653643
inheritedDynamicScope?: Map<string, JSONSchema[]>;
654644
siblingRefCycleKeys?: Set<string>;
655645
};
656646
const toWalk: WalkItem[] = [
657-
{ node, baseURL: parentSchemaURL, fallbackBaseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope },
647+
{
648+
node,
649+
baseUri: parentSchemaURL,
650+
sourceUri: node._sourceUri ?? parentSchemaURL,
651+
recursiveAnchorBase,
652+
inheritedDynamicScope,
653+
},
658654
];
659655
const seen = new WeakSet<JSONSchema>(); // prevents re-walking the same schema object graph
660656

661657
// eslint-disable-next-line @typescript-eslint/no-explicit-any
662658
const openPromises: Promise<any>[] = [];
663659

664-
const _getChildFallbackBaseURL = (entry: JSONSchema, currentFallbackBaseURL: string): string => {
665-
const resourceUri = entry?._baseUrl;
666-
return resourceUri && resourceIndexByUri.get(resourceUri)?.root === entry ? resourceUri : currentFallbackBaseURL;
667-
};
668-
669660
// handle $ref with siblings based on dialect
670661
const _handleRef = (
671662
next: JSONSchema,
672-
nodeBaseURL: string,
673-
fallbackBaseURL: string,
663+
nodeBaseUri: string,
664+
nodeSourceUri: string,
674665
nodeDialect: SchemaDialect,
675666
recursiveAnchorBase?: string,
676667
inheritedDynamicScope?: Map<string, JSONSchema[]>,
677668
siblingRefCycleKeys?: Set<string>
678669
): void => {
679-
const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseURL);
670+
const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseUri);
680671

681672
this.collectSchemaNodes(
682673
(entry) =>
683674
toWalk.push({
684675
node: entry,
685-
baseURL: nodeBaseURL,
686-
fallbackBaseURL: _getChildFallbackBaseURL(entry, fallbackBaseURL),
676+
baseUri: nodeBaseUri,
677+
sourceUri: nodeSourceUri,
687678
recursiveAnchorBase,
688679
inheritedDynamicScope: currentDynamicScope,
689680
}),
@@ -757,7 +748,7 @@ export class YAMLSchemaService extends JSONSchemaService {
757748
const segments = ref.split('#', 2);
758749
const baseUri = segments[0];
759750
const frag = segments.length > 1 ? segments[1] : '';
760-
const resolvedRefKey = `${baseUri ? _resolveAgainstBase(nodeBaseURL, baseUri) : nodeBaseURL}#${frag}`;
751+
const resolvedRefKey = `${baseUri ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri}#${frag}`;
761752

762753
if (_hasRefSiblings(next)) {
763754
// Draft-07 and earlier: ignore siblings
@@ -779,8 +770,8 @@ export class YAMLSchemaService extends JSONSchemaService {
779770
}
780771
toWalk.push({
781772
node: entry as JSONSchema,
782-
baseURL: nodeBaseURL,
783-
fallbackBaseURL: _getChildFallbackBaseURL(entry as JSONSchema, fallbackBaseURL),
773+
baseUri: nodeBaseUri,
774+
sourceUri: nodeSourceUri,
784775
recursiveAnchorBase,
785776
inheritedDynamicScope: currentDynamicScope,
786777
siblingRefCycleKeys: nextSiblingRefCycleKeys,
@@ -798,11 +789,11 @@ export class YAMLSchemaService extends JSONSchemaService {
798789

799790
// Draft-2019+: $recursiveRef
800791
if (isRecursiveRef && (ref === '#' || ref === '')) {
801-
const targetRoot = resourceIndexByUri.get(nodeBaseURL)?.root;
802-
const recursiveBase = targetRoot?.$recursiveAnchor && recursiveAnchorBase ? recursiveAnchorBase : nodeBaseURL;
792+
const targetRoot = resourceIndexByUri.get(nodeBaseUri)?.root;
793+
const recursiveBase = targetRoot?.$recursiveAnchor && recursiveAnchorBase ? recursiveAnchorBase : nodeBaseUri;
803794

804795
if (recursiveBase.length > 0) {
805-
if (resolutionStack?.has(recursiveBase) || recursiveBase === nodeBaseURL) {
796+
if (resolutionStack?.has(recursiveBase) || recursiveBase === nodeBaseUri) {
806797
const sourceRoot = resourceIndexByUri.get(recursiveBase)?.root ?? parentSchema;
807798
if (!seenRefs.has(ref)) {
808799
_merge(next, sourceRoot, recursiveBase, '', false);
@@ -815,8 +806,8 @@ export class YAMLSchemaService extends JSONSchemaService {
815806
next,
816807
recursiveBase,
817808
'',
818-
nodeBaseURL,
819-
fallbackBaseURL,
809+
nodeBaseUri,
810+
nodeSourceUri,
820811
parentSchemaDependencies,
821812
resolutionStack,
822813
recursiveAnchorBase,
@@ -830,13 +821,13 @@ export class YAMLSchemaService extends JSONSchemaService {
830821

831822
// Draft-2020+: $dynamicRef
832823
else if (isDynamicRef) {
833-
const targetResource = baseUri.length > 0 ? _resolveAgainstBase(nodeBaseURL, baseUri) : nodeBaseURL;
824+
const targetResource = baseUri.length > 0 ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri;
834825
const targetFragments = resourceIndexByUri.get(targetResource)?.fragments;
835826
const targetHasDynamicAnchor = frag.length > 0 && targetFragments?.get(frag)?.dynamic;
836827
const dynamicTarget = targetHasDynamicAnchor ? currentDynamicScope?.get(frag)?.[0] : undefined;
837-
const resolveResource = dynamicTarget ? dynamicTarget._baseUrl : targetResource;
828+
const resolveResource = dynamicTarget ? dynamicTarget._baseUri : targetResource;
838829

839-
if (dynamicTarget && (resolveResource === nodeBaseURL || resolutionStack.has(resolveResource))) {
830+
if (dynamicTarget && (resolveResource === nodeBaseUri || resolutionStack.has(resolveResource))) {
840831
if (!seenRefs.has(ref)) {
841832
_merge(next, dynamicTarget, resolveResource, '', false);
842833
seenRefs.add(ref);
@@ -851,8 +842,8 @@ export class YAMLSchemaService extends JSONSchemaService {
851842
next,
852843
resolveResource,
853844
frag,
854-
nodeBaseURL,
855-
fallbackBaseURL,
845+
nodeBaseUri,
846+
nodeSourceUri,
856847
parentSchemaDependencies,
857848
resolutionStack,
858849
recursiveAnchorBase,
@@ -864,16 +855,16 @@ export class YAMLSchemaService extends JSONSchemaService {
864855
}
865856
// normal $ref with external baseUri
866857
else if (baseUri.length > 0) {
867-
const resolvedBaseUri = _resolveAgainstBase(nodeBaseURL, baseUri);
858+
const resolvedBaseUri = _resolveAgainstBase(nodeBaseUri, baseUri);
868859
if (_mergeIfResourceAlreadyInResolutionStack(ref, resolvedBaseUri, frag)) continue;
869860
// resolve relative to this node's base URL
870861
openPromises.push(
871862
resolveExternalLink(
872863
next,
873864
baseUri,
874865
frag,
875-
nodeBaseURL,
876-
fallbackBaseURL,
866+
nodeBaseUri,
867+
nodeSourceUri,
877868
parentSchemaDependencies,
878869
resolutionStack,
879870
recursiveAnchorBase,
@@ -885,7 +876,7 @@ export class YAMLSchemaService extends JSONSchemaService {
885876

886877
// local $ref or $dynamicRef
887878
if (!seenRefs.has(ref)) {
888-
_merge(next, parentSchema, nodeBaseURL, frag, isDynamicRef && !!currentDynamicScope);
879+
_merge(next, parentSchema, nodeBaseUri, frag, isDynamicRef && !!currentDynamicScope);
889880
seenRefs.add(ref);
890881
}
891882
}
@@ -895,8 +886,8 @@ export class YAMLSchemaService extends JSONSchemaService {
895886
(entry) =>
896887
toWalk.push({
897888
node: entry,
898-
baseURL: next._baseUrl || nodeBaseURL,
899-
fallbackBaseURL: _getChildFallbackBaseURL(entry, fallbackBaseURL),
889+
baseUri: next._baseUri || nodeBaseUri,
890+
sourceUri: next._sourceUri || nodeSourceUri,
900891
dialect: nodeDialect,
901892
recursiveAnchorBase,
902893
inheritedDynamicScope: currentDynamicScope,
@@ -932,7 +923,7 @@ export class YAMLSchemaService extends JSONSchemaService {
932923
segments[0],
933924
segments[1],
934925
parentSchemaURL,
935-
parentSchemaURL,
926+
schema._sourceUri ?? parentSchemaURL,
936927
parentSchemaDependencies,
937928
resolutionStack,
938929
recursiveAnchorBase,
@@ -953,16 +944,16 @@ export class YAMLSchemaService extends JSONSchemaService {
953944
while (toWalk.length) {
954945
const item = toWalk.pop();
955946
const next = item.node;
956-
const nodeBaseURL = next._baseUrl || item.baseURL;
957-
const fallbackBaseURL = item.fallbackBaseURL || item.baseURL;
947+
const nodeBaseUri = next._baseUri || item.baseUri;
948+
const nodeSourceUri = next._sourceUri || nodeBaseUri;
958949
const nodeDialect = next._dialect || item.dialect;
959-
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseURL : undefined);
950+
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseUri : undefined);
960951
if (seen.has(next)) continue;
961952
seen.add(next);
962953
_handleRef(
963954
next,
964-
nodeBaseURL,
965-
fallbackBaseURL,
955+
nodeBaseUri,
956+
nodeSourceUri,
966957
nodeDialect,
967958
nodeRecursiveAnchorBase,
968959
item.inheritedDynamicScope,
@@ -973,7 +964,7 @@ export class YAMLSchemaService extends JSONSchemaService {
973964
};
974965

975966
const resolutionStack = new Set<string>(); // prevents $ref/$recursiveRef/$dynamicRef loops across schema URIs
976-
const rootResource = schema._baseUrl || schemaURL;
967+
const rootResource = schema._baseUri || schemaURL;
977968
if (rootResource) resolutionStack.add(rootResource);
978969
await resolveRefs(schema, schema, schemaURL, dependencies, resolutionStack);
979970
return new ResolvedSchema(schema, resolveErrors);

0 commit comments

Comments
 (0)