Skip to content

Commit 63d60da

Browse files
shin19991207datho7561
authored andcommitted
fix issue with local refs in local schema when $id is unrelated to file name
Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent f7ea162 commit 63d60da

File tree

2 files changed

+151
-4
lines changed

2 files changed

+151
-4
lines changed

src/languageservice/services/yamlSchemaService.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ export class YAMLSchemaService extends JSONSchemaService {
534534
uri: string,
535535
linkPath: string,
536536
parentSchemaURL: string,
537+
fallbackBaseURL: string,
537538
parentSchemaDependencies: SchemaDependencies,
538539
resolutionStack: Set<string>,
539540
recursiveAnchorBase: string,
@@ -619,7 +620,8 @@ export class YAMLSchemaService extends JSONSchemaService {
619620
};
620621

621622
const resolvedUri = _resolveRefUri(parentSchemaURL, uri);
622-
const localSiblingUri = _resolveLocalSiblingFromRemoteUri(parentSchemaURL, resolvedUri);
623+
const hasEmbeddedTarget = !!resourceIndexByUri.get(resolvedUri)?.root;
624+
const localSiblingUri = hasEmbeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(fallbackBaseURL, resolvedUri);
623625
const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri];
624626
return _resolveByUri(targetUris);
625627
};
@@ -642,21 +644,30 @@ export class YAMLSchemaService extends JSONSchemaService {
642644
type WalkItem = {
643645
node: JSONSchema;
644646
baseURL?: string;
647+
fallbackBaseURL?: string;
645648
dialect?: SchemaDialect;
646649
recursiveAnchorBase?: string;
647650
inheritedDynamicScope?: Map<string, JSONSchema[]>;
648651
siblingRefCycleKeys?: Set<string>;
649652
};
650-
const toWalk: WalkItem[] = [{ node, baseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope }];
653+
const toWalk: WalkItem[] = [
654+
{ node, baseURL: parentSchemaURL, fallbackBaseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope },
655+
];
651656
const seen = new WeakSet<JSONSchema>(); // prevents re-walking the same schema object graph
652657

653658
// eslint-disable-next-line @typescript-eslint/no-explicit-any
654659
const openPromises: Promise<any>[] = [];
655660

661+
const _getChildFallbackBaseURL = (entry: JSONSchema, currentFallbackBaseURL: string): string => {
662+
const resourceUri = entry?._baseUrl;
663+
return resourceUri && resourceIndexByUri.get(resourceUri)?.root === entry ? resourceUri : currentFallbackBaseURL;
664+
};
665+
656666
// handle $ref with siblings based on dialect
657667
const _handleRef = (
658668
next: JSONSchema,
659669
nodeBaseURL: string,
670+
fallbackBaseURL: string,
660671
nodeDialect: SchemaDialect,
661672
recursiveAnchorBase?: string,
662673
inheritedDynamicScope?: Map<string, JSONSchema[]>,
@@ -666,7 +677,13 @@ export class YAMLSchemaService extends JSONSchemaService {
666677

667678
this.collectSchemaNodes(
668679
(entry) =>
669-
toWalk.push({ node: entry, baseURL: nodeBaseURL, recursiveAnchorBase, inheritedDynamicScope: currentDynamicScope }),
680+
toWalk.push({
681+
node: entry,
682+
baseURL: nodeBaseURL,
683+
fallbackBaseURL: _getChildFallbackBaseURL(entry, fallbackBaseURL),
684+
recursiveAnchorBase,
685+
inheritedDynamicScope: currentDynamicScope,
686+
}),
670687
this.schemaMapValues(next.definitions || next.$defs)
671688
);
672689

@@ -760,6 +777,7 @@ export class YAMLSchemaService extends JSONSchemaService {
760777
toWalk.push({
761778
node: entry as JSONSchema,
762779
baseURL: nodeBaseURL,
780+
fallbackBaseURL: _getChildFallbackBaseURL(entry as JSONSchema, fallbackBaseURL),
763781
recursiveAnchorBase,
764782
inheritedDynamicScope: currentDynamicScope,
765783
siblingRefCycleKeys: nextSiblingRefCycleKeys,
@@ -795,6 +813,7 @@ export class YAMLSchemaService extends JSONSchemaService {
795813
recursiveBase,
796814
'',
797815
nodeBaseURL,
816+
fallbackBaseURL,
798817
parentSchemaDependencies,
799818
resolutionStack,
800819
recursiveAnchorBase,
@@ -830,6 +849,7 @@ export class YAMLSchemaService extends JSONSchemaService {
830849
resolveResource,
831850
frag,
832851
nodeBaseURL,
852+
fallbackBaseURL,
833853
parentSchemaDependencies,
834854
resolutionStack,
835855
recursiveAnchorBase,
@@ -850,6 +870,7 @@ export class YAMLSchemaService extends JSONSchemaService {
850870
baseUri,
851871
frag,
852872
nodeBaseURL,
873+
fallbackBaseURL,
853874
parentSchemaDependencies,
854875
resolutionStack,
855876
recursiveAnchorBase,
@@ -872,6 +893,7 @@ export class YAMLSchemaService extends JSONSchemaService {
872893
toWalk.push({
873894
node: entry,
874895
baseURL: next._baseUrl || nodeBaseURL,
896+
fallbackBaseURL: _getChildFallbackBaseURL(entry, fallbackBaseURL),
875897
dialect: nodeDialect,
876898
recursiveAnchorBase,
877899
inheritedDynamicScope: currentDynamicScope,
@@ -907,6 +929,7 @@ export class YAMLSchemaService extends JSONSchemaService {
907929
segments[0],
908930
segments[1],
909931
parentSchemaURL,
932+
parentSchemaURL,
910933
parentSchemaDependencies,
911934
resolutionStack,
912935
recursiveAnchorBase,
@@ -928,11 +951,20 @@ export class YAMLSchemaService extends JSONSchemaService {
928951
const item = toWalk.pop();
929952
const next = item.node;
930953
const nodeBaseURL = next._baseUrl || item.baseURL;
954+
const fallbackBaseURL = item.fallbackBaseURL || item.baseURL;
931955
const nodeDialect = next._dialect || item.dialect;
932956
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseURL : undefined);
933957
if (seen.has(next)) continue;
934958
seen.add(next);
935-
_handleRef(next, nodeBaseURL, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope, item.siblingRefCycleKeys);
959+
_handleRef(
960+
next,
961+
nodeBaseURL,
962+
fallbackBaseURL,
963+
nodeDialect,
964+
nodeRecursiveAnchorBase,
965+
item.inheritedDynamicScope,
966+
item.siblingRefCycleKeys
967+
);
936968
}
937969
return Promise.all(openPromises);
938970
};

test/yamlSchemaService.test.ts

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

167+
it('should resolve relative local sibling refs when the root $id basename differs from the local filename', async () => {
168+
const content =
169+
`# yaml-language-server: $schema=file:///schemas/repro_main_schema.json\n` +
170+
`members:\n` +
171+
` - name: Alice\n` +
172+
` age: 30`;
173+
const yamlDock = parse(content);
174+
175+
const primarySchema = {
176+
$id: 'https://example.com/schemas/repro-main-v1',
177+
type: 'object',
178+
properties: {
179+
members: {
180+
type: 'array',
181+
items: {
182+
$ref: './repro_defs.json#/$defs/Person',
183+
},
184+
},
185+
},
186+
required: ['members'],
187+
};
188+
const defsSchema = {
189+
$id: 'https://example.com/schemas/repro-defs-v1',
190+
$defs: {
191+
Person: {
192+
type: 'object',
193+
properties: {
194+
name: { type: 'string' },
195+
age: { type: 'integer' },
196+
},
197+
required: ['name'],
198+
},
199+
},
200+
};
201+
202+
requestServiceMock = sandbox.fake((uri: string) => {
203+
if (uri === 'file:///schemas/repro_main_schema.json') {
204+
return Promise.resolve(JSON.stringify(primarySchema));
205+
}
206+
if (uri === 'file:///schemas/repro_defs.json') {
207+
return Promise.resolve(JSON.stringify(defsSchema));
208+
}
209+
return Promise.reject<string>(`Resource ${uri} not found.`);
210+
});
211+
212+
const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext);
213+
const schema = await service.getSchemaForResource('', yamlDock.documents[0]);
214+
215+
const requestedUris = requestServiceMock.getCalls().map((call) => call.args[0]);
216+
expect(requestedUris).to.include('file:///schemas/repro_main_schema.json');
217+
expect(requestedUris).to.include('file:///schemas/repro_defs.json');
218+
expect(requestedUris).to.not.include('https://example.com/schemas/repro_defs.json');
219+
expect(schema.errors).to.eql([]);
220+
expect(schema.schema.properties.members.items).to.deep.include({
221+
type: 'object',
222+
url: 'file:///schemas/repro_defs.json',
223+
});
224+
});
225+
226+
it('should resolve nested local sibling refs relative to the loaded sibling schema file', async () => {
227+
const content = `# yaml-language-server: $schema=file:///schemas/primary.json\nitem: ok`;
228+
const yamlDock = parse(content);
229+
230+
const primarySchema = {
231+
$schema: 'https://json-schema.org/draft/2020-12/schema',
232+
$id: 'https://example.com/schemas/primary-v1',
233+
$ref: './secondary.json',
234+
};
235+
const secondarySchema = {
236+
$schema: 'https://json-schema.org/draft/2020-12/schema',
237+
$id: 'https://example.com/schemas/secondary-v1',
238+
type: 'object',
239+
properties: {
240+
item: {
241+
$ref: './third.json',
242+
},
243+
},
244+
required: ['item'],
245+
};
246+
const thirdSchema = {
247+
$schema: 'https://json-schema.org/draft/2020-12/schema',
248+
$id: 'https://example.com/schemas/third-v1',
249+
type: 'string',
250+
enum: ['ok'],
251+
};
252+
253+
requestServiceMock = sandbox.fake((uri: string) => {
254+
if (uri === 'file:///schemas/primary.json') {
255+
return Promise.resolve(JSON.stringify(primarySchema));
256+
}
257+
if (uri === 'file:///schemas/secondary.json') {
258+
return Promise.resolve(JSON.stringify(secondarySchema));
259+
}
260+
if (uri === 'file:///schemas/third.json') {
261+
return Promise.resolve(JSON.stringify(thirdSchema));
262+
}
263+
return Promise.reject<string>(`Resource ${uri} not found.`);
264+
});
265+
266+
const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext);
267+
const schema = await service.getSchemaForResource('', yamlDock.documents[0]);
268+
269+
const requestedUris = requestServiceMock.getCalls().map((call) => call.args[0]);
270+
expect(requestedUris).to.include('file:///schemas/primary.json');
271+
expect(requestedUris).to.include('file:///schemas/secondary.json');
272+
expect(requestedUris).to.include('file:///schemas/third.json');
273+
expect(requestedUris).to.not.include('https://example.com/schemas/secondary.json');
274+
expect(requestedUris).to.not.include('https://example.com/schemas/third.json');
275+
expect(schema.errors).to.eql([]);
276+
expect(schema.schema.properties.item).to.deep.include({
277+
type: 'string',
278+
url: 'file:///schemas/third.json',
279+
});
280+
});
281+
167282
it('should resolve absolute $ref via remote base and mapped local sibling path', async () => {
168283
const content = `# yaml-language-server: $schema=file:///dir/primary.json\nname: John\nage: -1`;
169284
const yamlDock = parse(content);

0 commit comments

Comments
 (0)