Skip to content

Commit 0973ceb

Browse files
committed
fix absolute ref resolution behavior
Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent 9136bd0 commit 0973ceb

File tree

2 files changed

+122
-37
lines changed

2 files changed

+122
-37
lines changed

src/languageservice/services/yamlSchemaService.ts

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,34 @@ export class YAMLSchemaService extends JSONSchemaService {
499499
}
500500
};
501501

502+
const _resolveRefUri = (parentSchemaURL: string, refUri: string): string => {
503+
const resolvedAgainstParent = _resolveAgainstBase(parentSchemaURL, refUri);
504+
if (!refUri.startsWith('/')) return resolvedAgainstParent;
505+
const parentResource = resourceIndexByUri.get(parentSchemaURL)?.root;
506+
const parentResourceId = parentResource?.$id || parentResource?.id;
507+
const resolvedParentId = _resolveAgainstBase(parentSchemaURL, parentResourceId);
508+
if (!resolvedParentId.startsWith('http://') && !resolvedParentId.startsWith('https://')) return resolvedAgainstParent;
509+
510+
return _resolveAgainstBase(resolvedParentId, refUri);
511+
};
512+
513+
const _resolveLocalSiblingFromRemoteUri = (parentSchemaURL: string, resolvedRefUri: string): string | undefined => {
514+
try {
515+
const parentUri = URI.parse(parentSchemaURL);
516+
const targetUri = URI.parse(resolvedRefUri);
517+
if (parentUri.scheme !== 'file') return undefined;
518+
if (targetUri.scheme !== 'http' && targetUri.scheme !== 'https') return undefined;
519+
520+
const localFileName = path.posix.basename(targetUri.path);
521+
if (!localFileName) return undefined;
522+
const localDir = path.posix.dirname(parentUri.path);
523+
const localPath = path.posix.join(localDir, localFileName);
524+
return parentUri.with({ path: localPath, query: targetUri.query, fragment: targetUri.fragment }).toString();
525+
} catch {
526+
return undefined;
527+
}
528+
};
529+
502530
const resolveExternalLink = (
503531
node: JSONSchema,
504532
uri: string,
@@ -541,42 +569,57 @@ export class YAMLSchemaService extends JSONSchemaService {
541569
);
542570
};
543571

544-
const resolvedUri = _resolveAgainstBase(parentSchemaURL, uri);
545-
const embeddedSchema = resourceIndexByUri.get(resolvedUri)?.root;
546-
if (embeddedSchema) {
547-
return _attachResolvedSchema(
548-
node,
549-
embeddedSchema,
550-
resolvedUri,
551-
linkPath,
552-
parentSchemaDependencies,
553-
parentSchemaDependencies,
554-
resolutionStack,
555-
recursiveAnchorBase,
556-
inheritedDynamicScope
557-
);
558-
}
572+
const _resolveByUri = (targetUris: string[], index = 0): Promise<unknown> => {
573+
const targetUri = targetUris[index];
559574

560-
const referencedHandle = this.getOrAddSchemaHandle(resolvedUri);
561-
return referencedHandle.getUnresolvedSchema().then(async (unresolvedSchema) => {
562-
if (unresolvedSchema.errors.length) {
563-
const loc = linkPath ? resolvedUri + '#' + linkPath : resolvedUri;
564-
resolveErrors.push(l10n.t("Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0]));
575+
const embeddedSchema = resourceIndexByUri.get(targetUri)?.root;
576+
if (embeddedSchema) {
577+
return _attachResolvedSchema(
578+
node,
579+
embeddedSchema,
580+
targetUri,
581+
linkPath,
582+
parentSchemaDependencies,
583+
parentSchemaDependencies,
584+
resolutionStack,
585+
recursiveAnchorBase,
586+
inheritedDynamicScope
587+
);
565588
}
566-
// index resources for the newly loaded schema
567-
await _indexSchemaResources(unresolvedSchema.schema, resolvedUri);
568-
return _attachResolvedSchema(
569-
node,
570-
unresolvedSchema.schema,
571-
resolvedUri,
572-
linkPath,
573-
parentSchemaDependencies,
574-
referencedHandle.dependencies,
575-
resolutionStack,
576-
recursiveAnchorBase,
577-
inheritedDynamicScope
578-
);
579-
});
589+
590+
const referencedHandle = this.getOrAddSchemaHandle(targetUri);
591+
return referencedHandle.getUnresolvedSchema().then(async (unresolvedSchema) => {
592+
if (
593+
unresolvedSchema.errors?.some((error) => error.toLowerCase().includes('unable to load schema from')) &&
594+
index + 1 < targetUris.length
595+
) {
596+
return _resolveByUri(targetUris, index + 1);
597+
}
598+
599+
if (unresolvedSchema.errors.length) {
600+
const loc = linkPath ? targetUri + '#' + linkPath : targetUri;
601+
resolveErrors.push(l10n.t("Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0]));
602+
}
603+
// index resources for the newly loaded schema
604+
await _indexSchemaResources(unresolvedSchema.schema, targetUri);
605+
return _attachResolvedSchema(
606+
node,
607+
unresolvedSchema.schema,
608+
targetUri,
609+
linkPath,
610+
parentSchemaDependencies,
611+
referencedHandle.dependencies,
612+
resolutionStack,
613+
recursiveAnchorBase,
614+
inheritedDynamicScope
615+
);
616+
});
617+
};
618+
619+
const resolvedUri = _resolveRefUri(parentSchemaURL, uri);
620+
const localSiblingUri = _resolveLocalSiblingFromRemoteUri(parentSchemaURL, resolvedUri);
621+
const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri];
622+
return _resolveByUri(targetUris);
580623
};
581624

582625
const resolveRefs = async (

test/yamlSchemaService.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe('YAML Schema Service', () => {
164164
expect(requestedUris).to.not.include('https://example.com/schemas/secondary.json');
165165
});
166166

167-
it('should use local schema path for absolute $ref before remote $id ref', async () => {
167+
it('should resolve absolute $ref via remote base and mapped local sibling path', async () => {
168168
const content = `# yaml-language-server: $schema=file:///dir/primary.json\nname: John\nage: -1`;
169169
const yamlDock = parse(content);
170170

@@ -186,7 +186,7 @@ describe('YAML Schema Service', () => {
186186
if (uri === 'file:///dir/primary.json') {
187187
return Promise.resolve(JSON.stringify(primarySchema));
188188
}
189-
if (uri === 'file:///schemas/secondary.json') {
189+
if (uri === 'file:///dir/secondary.json') {
190190
return Promise.resolve(JSON.stringify(secondarySchema));
191191
}
192192
return Promise.reject<string>(`Resource ${uri} not found.`);
@@ -197,10 +197,52 @@ describe('YAML Schema Service', () => {
197197

198198
const requestedUris = requestServiceMock.getCalls().map((call) => call.args[0]);
199199
expect(requestedUris).to.include('file:///dir/primary.json');
200-
expect(requestedUris).to.include('file:///schemas/secondary.json');
200+
expect(requestedUris).to.include('file:///dir/secondary.json');
201+
expect(requestedUris).to.not.include('file:///schemas/secondary.json');
201202
expect(requestedUris).to.not.include('https://example.com/schemas/secondary.json');
202203
});
203204

205+
it('should fallback to remote $id target for absolute $ref when mapped local target is missing', async () => {
206+
const content = `# yaml-language-server: $schema=file:///dir/primary.json\nname: John\nage: -1`;
207+
const yamlDock = parse(content);
208+
209+
const primarySchema = {
210+
$id: 'https://example.com/schemas/primary.json',
211+
$ref: '/schemas/secondary.json',
212+
};
213+
const secondarySchema = {
214+
$id: 'https://example.com/schemas/secondary.json',
215+
type: 'object',
216+
properties: {
217+
name: { type: 'string' },
218+
age: { type: 'integer', minimum: 0 },
219+
},
220+
required: ['name', 'age'],
221+
};
222+
223+
requestServiceMock = sandbox.fake((uri: string) => {
224+
if (uri === 'file:///dir/primary.json') {
225+
return Promise.resolve(JSON.stringify(primarySchema));
226+
}
227+
if (uri === 'https://example.com/schemas/secondary.json') {
228+
return Promise.resolve(JSON.stringify(secondarySchema));
229+
}
230+
return Promise.reject<string>(`Resource ${uri} not found.`);
231+
});
232+
233+
const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext);
234+
await service.getSchemaForResource('', yamlDock.documents[0]);
235+
236+
const requestedUris = requestServiceMock.getCalls().map((call) => call.args[0]);
237+
expect(requestedUris).to.include('file:///dir/primary.json');
238+
expect(requestedUris).to.include('file:///dir/secondary.json');
239+
expect(requestedUris).to.include('https://example.com/schemas/secondary.json');
240+
expect(requestedUris).to.not.include('file:///schemas/secondary.json');
241+
expect(requestedUris.indexOf('file:///dir/secondary.json')).to.be.lessThan(
242+
requestedUris.indexOf('https://example.com/schemas/secondary.json')
243+
);
244+
});
245+
204246
it('should reload local schema after local file change when resolving via local sibling path instead of remote $id', async () => {
205247
const content = `# yaml-language-server: $schema=file:///schemas/primary.json\nmode: stage`;
206248
const yamlDock = parse(content);

0 commit comments

Comments
 (0)