Skip to content

Commit 04f05f2

Browse files
committed
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 0ae5603 commit 04f05f2

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
@@ -533,6 +533,7 @@ export class YAMLSchemaService extends JSONSchemaService {
533533
uri: string,
534534
linkPath: string,
535535
parentSchemaURL: string,
536+
fallbackBaseURL: string,
536537
parentSchemaDependencies: SchemaDependencies,
537538
resolutionStack: Set<string>,
538539
recursiveAnchorBase: string,
@@ -618,7 +619,8 @@ export class YAMLSchemaService extends JSONSchemaService {
618619
};
619620

620621
const resolvedUri = _resolveRefUri(parentSchemaURL, uri);
621-
const localSiblingUri = _resolveLocalSiblingFromRemoteUri(parentSchemaURL, resolvedUri);
622+
const hasEmbeddedTarget = !!resourceIndexByUri.get(resolvedUri)?.root;
623+
const localSiblingUri = hasEmbeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(fallbackBaseURL, resolvedUri);
622624
const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri];
623625
return _resolveByUri(targetUris);
624626
};
@@ -641,21 +643,30 @@ export class YAMLSchemaService extends JSONSchemaService {
641643
type WalkItem = {
642644
node: JSONSchema;
643645
baseURL?: string;
646+
fallbackBaseURL?: string;
644647
dialect?: SchemaDialect;
645648
recursiveAnchorBase?: string;
646649
inheritedDynamicScope?: Map<string, JSONSchema[]>;
647650
siblingRefCycleKeys?: Set<string>;
648651
};
649-
const toWalk: WalkItem[] = [{ node, baseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope }];
652+
const toWalk: WalkItem[] = [
653+
{ node, baseURL: parentSchemaURL, fallbackBaseURL: parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope },
654+
];
650655
const seen = new WeakSet<JSONSchema>(); // prevents re-walking the same schema object graph
651656

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

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

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

@@ -759,6 +776,7 @@ export class YAMLSchemaService extends JSONSchemaService {
759776
toWalk.push({
760777
node: entry as JSONSchema,
761778
baseURL: nodeBaseURL,
779+
fallbackBaseURL: _getChildFallbackBaseURL(entry as JSONSchema, fallbackBaseURL),
762780
recursiveAnchorBase,
763781
inheritedDynamicScope: currentDynamicScope,
764782
siblingRefCycleKeys: nextSiblingRefCycleKeys,
@@ -794,6 +812,7 @@ export class YAMLSchemaService extends JSONSchemaService {
794812
recursiveBase,
795813
'',
796814
nodeBaseURL,
815+
fallbackBaseURL,
797816
parentSchemaDependencies,
798817
resolutionStack,
799818
recursiveAnchorBase,
@@ -829,6 +848,7 @@ export class YAMLSchemaService extends JSONSchemaService {
829848
resolveResource,
830849
frag,
831850
nodeBaseURL,
851+
fallbackBaseURL,
832852
parentSchemaDependencies,
833853
resolutionStack,
834854
recursiveAnchorBase,
@@ -849,6 +869,7 @@ export class YAMLSchemaService extends JSONSchemaService {
849869
baseUri,
850870
frag,
851871
nodeBaseURL,
872+
fallbackBaseURL,
852873
parentSchemaDependencies,
853874
resolutionStack,
854875
recursiveAnchorBase,
@@ -871,6 +892,7 @@ export class YAMLSchemaService extends JSONSchemaService {
871892
toWalk.push({
872893
node: entry,
873894
baseURL: next._baseUrl || nodeBaseURL,
895+
fallbackBaseURL: _getChildFallbackBaseURL(entry, fallbackBaseURL),
874896
dialect: nodeDialect,
875897
recursiveAnchorBase,
876898
inheritedDynamicScope: currentDynamicScope,
@@ -906,6 +928,7 @@ export class YAMLSchemaService extends JSONSchemaService {
906928
segments[0],
907929
segments[1],
908930
parentSchemaURL,
931+
parentSchemaURL,
909932
parentSchemaDependencies,
910933
resolutionStack,
911934
recursiveAnchorBase,
@@ -927,11 +950,20 @@ export class YAMLSchemaService extends JSONSchemaService {
927950
const item = toWalk.pop();
928951
const next = item.node;
929952
const nodeBaseURL = next._baseUrl || item.baseURL;
953+
const fallbackBaseURL = item.fallbackBaseURL || item.baseURL;
930954
const nodeDialect = next._dialect || item.dialect;
931955
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseURL : undefined);
932956
if (seen.has(next)) continue;
933957
seen.add(next);
934-
_handleRef(next, nodeBaseURL, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope, item.siblingRefCycleKeys);
958+
_handleRef(
959+
next,
960+
nodeBaseURL,
961+
fallbackBaseURL,
962+
nodeDialect,
963+
nodeRecursiveAnchorBase,
964+
item.inheritedDynamicScope,
965+
item.siblingRefCycleKeys
966+
);
935967
}
936968
return Promise.all(openPromises);
937969
};

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)