Skip to content

Commit 0b6b08d

Browse files
committed
Update to latest vscode-json-languageservice
The primary motivation for doing this is to allow warnings to be reported on schemas. I would like to report warnings on schemas as a part of redhat-developer#1065. When updating the json language service, there were many API changes that I needed to adapt to, and some tests that I changed. Notably, I needed to copy a lot of the implementation of `JSONSchemaService` into our subclass of `JSONSchemaService`. `JSONSchemaService` turns all schema identifiers into URIs and escapes all `/` in the fragment, and does this in the private `normalizeId` method that we can't override Combined, these behaviours cause many tests to fail. Closes redhat-developer#1069 Signed-off-by: David Thompson <[email protected]>
1 parent 85b0582 commit 0b6b08d

File tree

8 files changed

+205
-48
lines changed

8 files changed

+205
-48
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"lodash": "4.17.21",
3131
"prettier": "^3.5.0",
3232
"request-light": "^0.5.7",
33-
"vscode-json-languageservice": "4.1.8",
33+
"vscode-json-languageservice": "5.5.0",
3434
"vscode-languageserver": "^9.0.0",
3535
"vscode-languageserver-textdocument": "^1.0.1",
3636
"vscode-languageserver-types": "^3.16.0",

src/languageservice/services/yamlSchemaService.ts

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
JSONSchemaService,
1313
SchemaDependencies,
1414
ISchemaContributions,
15-
SchemaHandle,
1615
} from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
1716

1817
import { URI } from 'vscode-uri';
@@ -29,6 +28,7 @@ import { SchemaVersions } from '../yamlTypes';
2928

3029
import Ajv, { DefinedError } from 'ajv';
3130
import { getSchemaTitle } from '../utils/schemaUtils';
31+
import { SchemaConfiguration } from 'vscode-json-languageservice';
3232

3333
const ajv = new Ajv();
3434

@@ -155,11 +155,9 @@ export class YAMLSchemaService extends JSONSchemaService {
155155
return result;
156156
}
157157

158-
async resolveSchemaContent(
159-
schemaToResolve: UnresolvedSchema,
160-
schemaURL: string,
161-
dependencies: SchemaDependencies
162-
): Promise<ResolvedSchema> {
158+
async resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaHandle: SchemaHandle): Promise<ResolvedSchema> {
159+
const schemaURL: string = normalizeId(schemaHandle.uri);
160+
const dependencies: SchemaDependencies = schemaHandle.dependencies;
163161
const resolveErrors: string[] = schemaToResolve.errors.slice(0);
164162
let schema: JSONSchema = schemaToResolve.schema;
165163
const contextService = this.contextService;
@@ -374,7 +372,7 @@ export class YAMLSchemaService extends JSONSchemaService {
374372
const schemaHandle = super.createCombinedSchema(resource, schemas);
375373
return schemaHandle.getResolvedSchema().then((schema) => {
376374
if (schema.schema && typeof schema.schema === 'object') {
377-
schema.schema.url = schemaHandle.url;
375+
schema.schema.url = schemaHandle.uri;
378376
}
379377

380378
if (
@@ -431,6 +429,7 @@ export class YAMLSchemaService extends JSONSchemaService {
431429
(schemas) => {
432430
return {
433431
errors: [],
432+
warnings: [],
434433
schema: {
435434
allOf: schemas.map((schemaObj) => {
436435
return schemaObj.schema;
@@ -503,7 +502,7 @@ export class YAMLSchemaService extends JSONSchemaService {
503502

504503
private async resolveCustomSchema(schemaUri, doc): ResolvedSchema {
505504
const unresolvedSchema = await this.loadSchema(schemaUri);
506-
const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []);
505+
const schema = await this.resolveSchemaContent(unresolvedSchema, new SchemaHandle(this, schemaUri));
507506
if (schema.schema && typeof schema.schema === 'object') {
508507
schema.schema.url = schemaUri;
509508
}
@@ -614,8 +613,18 @@ export class YAMLSchemaService extends JSONSchemaService {
614613

615614
normalizeId(id: string): string {
616615
// The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
616+
if (!id.includes(':')) {
617+
return id;
618+
}
617619
try {
618-
return URI.parse(id).toString();
620+
const uri = URI.parse(id);
621+
if (!id.includes('#')) {
622+
return uri.toString();
623+
}
624+
// fragment should be verbatim, but vscode-uri converts `/` to the escaped version (annoyingly, needlessly)
625+
const [first, second] = uri.toString().split('#', 2);
626+
const secondCleaned = second.replace('%2F', '/');
627+
return first + '#' + secondCleaned;
619628
} catch (e) {
620629
return id;
621630
}
@@ -684,25 +693,44 @@ export class YAMLSchemaService extends JSONSchemaService {
684693
}
685694

686695
registerExternalSchema(
687-
uri: string,
688-
filePatterns?: string[],
689-
unresolvedSchema?: JSONSchema,
696+
schemaConfig: SchemaConfiguration,
690697
name?: string,
691698
description?: string,
692699
versions?: SchemaVersions
693700
): SchemaHandle {
694701
if (name || description) {
695-
this.schemaUriToNameAndDescription.set(uri, { name, description, versions });
702+
this.schemaUriToNameAndDescription.set(schemaConfig.uri, { name, description, versions });
696703
}
697-
return super.registerExternalSchema(uri, filePatterns, unresolvedSchema);
704+
this.registeredSchemasIds[schemaConfig.uri] = true;
705+
this.cachedSchemaForResource = undefined;
706+
if (schemaConfig.fileMatch && schemaConfig.fileMatch.length) {
707+
this.addFilePatternAssociation(schemaConfig.fileMatch, schemaConfig.folderUri, [schemaConfig.uri]);
708+
}
709+
return schemaConfig.schema
710+
? this.addSchemaHandle(schemaConfig.uri, schemaConfig.schema)
711+
: this.getOrAddSchemaHandle(schemaConfig.uri);
698712
}
699713

700714
clearExternalSchemas(): void {
701715
super.clearExternalSchemas();
702716
}
703717

704718
setSchemaContributions(schemaContributions: ISchemaContributions): void {
705-
super.setSchemaContributions(schemaContributions);
719+
if (schemaContributions.schemas) {
720+
const schemas = schemaContributions.schemas;
721+
for (const id in schemas) {
722+
const normalizedId = normalizeId(id);
723+
this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]);
724+
}
725+
}
726+
if (Array.isArray(schemaContributions.schemaAssociations)) {
727+
const schemaAssociations = schemaContributions.schemaAssociations;
728+
for (const schemaAssociation of schemaAssociations) {
729+
const uris = schemaAssociation.uris.map(normalizeId);
730+
const association = this.addFilePatternAssociation(schemaAssociation.pattern, schemaAssociation.folderUri, uris);
731+
this.contributionAssociations.push(association);
732+
}
733+
}
706734
}
707735

708736
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -711,14 +739,62 @@ export class YAMLSchemaService extends JSONSchemaService {
711739
}
712740

713741
getResolvedSchema(schemaId: string): Promise<ResolvedSchema> {
714-
return super.getResolvedSchema(schemaId);
742+
const id = normalizeId(schemaId);
743+
const schemaHandle = this.schemasById[id];
744+
if (schemaHandle) {
745+
return schemaHandle.getResolvedSchema();
746+
}
747+
return this.promise.resolve(undefined);
715748
}
716749

717750
onResourceChange(uri: string): boolean {
718-
return super.onResourceChange(uri);
751+
// always clear this local cache when a resource changes
752+
this.cachedSchemaForResource = undefined;
753+
let hasChanges = false;
754+
uri = normalizeId(uri);
755+
const toWalk = [uri];
756+
const all = Object.keys(this.schemasById).map((key) => this.schemasById[key]);
757+
while (toWalk.length) {
758+
const curr = toWalk.pop();
759+
for (let i = 0; i < all.length; i++) {
760+
const handle = all[i];
761+
if (handle && (handle.uri === curr || handle.dependencies.has(curr))) {
762+
if (handle.uri !== curr) {
763+
toWalk.push(handle.uri);
764+
}
765+
if (handle.clearSchema()) {
766+
hasChanges = true;
767+
}
768+
all[i] = undefined;
769+
}
770+
}
771+
}
772+
return hasChanges;
719773
}
720774
}
721775

776+
/**
777+
* Our version of normalize id, which doesn't prepend `file:///` to anything without a scheme.
778+
*
779+
* @param id the id to normalize
780+
* @returns the normalized id.
781+
*/
782+
function normalizeId(id: string): string {
783+
if (id.includes(':')) {
784+
try {
785+
if (id.includes('#')) {
786+
const [mostOfIt, fragment] = id.split('#', 2);
787+
return URI.parse(mostOfIt) + '#' + fragment;
788+
} else {
789+
return URI.parse(id).toString();
790+
}
791+
} catch {
792+
return id;
793+
}
794+
}
795+
return id;
796+
}
797+
722798
function toDisplayString(url: string): string {
723799
try {
724800
const uri = URI.parse(url);
@@ -730,3 +806,47 @@ function toDisplayString(url: string): string {
730806
}
731807
return url;
732808
}
809+
810+
class SchemaHandle {
811+
public readonly uri: string;
812+
public readonly dependencies: SchemaDependencies;
813+
public anchors: Map<string, JSONSchema> | undefined;
814+
private resolvedSchema: Promise<ResolvedSchema> | undefined;
815+
private unresolvedSchema: Promise<UnresolvedSchema> | undefined;
816+
private readonly service: JSONSchemaService;
817+
818+
constructor(service: JSONSchemaService, uri: string, unresolvedSchemaContent?: JSONSchema) {
819+
this.service = service;
820+
this.uri = uri;
821+
this.dependencies = new Set();
822+
this.anchors = undefined;
823+
if (unresolvedSchemaContent) {
824+
this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent));
825+
}
826+
}
827+
828+
public getUnresolvedSchema(): Promise<UnresolvedSchema> {
829+
if (!this.unresolvedSchema) {
830+
this.unresolvedSchema = this.service.loadSchema(this.uri);
831+
}
832+
return this.unresolvedSchema;
833+
}
834+
835+
public getResolvedSchema(): Promise<ResolvedSchema> {
836+
if (!this.resolvedSchema) {
837+
this.resolvedSchema = this.getUnresolvedSchema().then((unresolved) => {
838+
return this.service.resolveSchemaContent(unresolved, this);
839+
});
840+
}
841+
return this.resolvedSchema;
842+
}
843+
844+
public clearSchema(): boolean {
845+
const hasChanges = !!this.unresolvedSchema;
846+
this.resolvedSchema = undefined;
847+
this.unresolvedSchema = undefined;
848+
this.dependencies.clear();
849+
this.anchors = undefined;
850+
return hasChanges;
851+
}
852+
}

src/languageservice/yamlLanguageService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,11 @@ export function getLanguageService(params: {
212212
const currPriority = settings.priority ? settings.priority : 0;
213213
schemaService.addSchemaPriority(settings.uri, currPriority);
214214
schemaService.registerExternalSchema(
215-
settings.uri,
216-
settings.fileMatch,
217-
settings.schema,
215+
{
216+
uri: settings.uri,
217+
fileMatch: settings.fileMatch,
218+
schema: settings.schema,
219+
},
218220
settings.name,
219221
settings.description,
220222
settings.versions

test/jsonParser.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,7 @@ describe('JSON Parser', () => {
14211421

14221422
it('items as array', function () {
14231423
const schema: JsonSchema.JSONSchema = {
1424+
$schema: 'http://json-schema.org/draft-07/schema#',
14241425
type: 'array',
14251426
items: [
14261427
{
@@ -1456,6 +1457,7 @@ describe('JSON Parser', () => {
14561457

14571458
it('additionalItems', function () {
14581459
let schema: JsonSchema.JSONSchema = {
1460+
$schema: 'http://json-schema.org/draft-07/schema#',
14591461
type: 'array',
14601462
items: [
14611463
{
@@ -1483,6 +1485,7 @@ describe('JSON Parser', () => {
14831485
assert.strictEqual(semanticErrors.length, 1);
14841486
}
14851487
schema = {
1488+
$schema: 'http://json-schema.org/draft-07/schema#',
14861489
type: 'array',
14871490
items: [
14881491
{

test/schema.test.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ describe('JSON Schema', () => {
337337
},
338338
};
339339

340-
service.registerExternalSchema(id, ['*.json'], schema);
340+
service.registerExternalSchema({ uri: id, fileMatch: ['*.json'], schema });
341341

342342
service
343343
.getSchemaForResource('test.json', undefined)
@@ -373,7 +373,7 @@ describe('JSON Schema', () => {
373373
},
374374
};
375375

376-
service.registerExternalSchema(id, ['*.json'], schema);
376+
service.registerExternalSchema({ uri: id, fileMatch: ['*.json'], schema });
377377

378378
const result = await service.getSchemaForResource('test.json', undefined);
379379

@@ -419,11 +419,15 @@ describe('JSON Schema', () => {
419419
it('Schema with non uri registers correctly', function (testDone) {
420420
const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext);
421421
const non_uri = 'non_uri';
422-
service.registerExternalSchema(non_uri, ['*.yml', '*.yaml'], {
423-
properties: {
424-
test_node: {
425-
description: 'my test_node description',
426-
enum: ['test 1', 'test 2'],
422+
service.registerExternalSchema({
423+
uri: non_uri,
424+
fileMatch: ['*.yml', '*.yaml'],
425+
schema: {
426+
properties: {
427+
test_node: {
428+
description: 'my test_node description',
429+
enum: ['test 1', 'test 2'],
430+
},
427431
},
428432
},
429433
});
@@ -529,7 +533,7 @@ describe('JSON Schema', () => {
529533

530534
it('Modifying schema works with kubernetes resolution', async () => {
531535
const service = new SchemaService.YAMLSchemaService(schemaRequestServiceForURL, workspaceContext);
532-
service.registerExternalSchema(KUBERNETES_SCHEMA_URL);
536+
service.registerExternalSchema({ uri: KUBERNETES_SCHEMA_URL });
533537

534538
await service.addContent({
535539
action: MODIFICATION_ACTIONS.add,
@@ -545,7 +549,7 @@ describe('JSON Schema', () => {
545549

546550
it('Deleting schema works with Kubernetes resolution', async () => {
547551
const service = new SchemaService.YAMLSchemaService(schemaRequestServiceForURL, workspaceContext);
548-
service.registerExternalSchema(KUBERNETES_SCHEMA_URL);
552+
service.registerExternalSchema({ uri: KUBERNETES_SCHEMA_URL });
549553

550554
await service.deleteContent({
551555
action: MODIFICATION_ACTIONS.delete,

test/schemaSelectionHandlers.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ describe('Schema Selection Handlers', () => {
4040
});
4141

4242
it('getAllSchemas should return all schemas', async () => {
43-
service.registerExternalSchema('https://some.com/some.json', ['foo.yaml'], undefined, 'Schema name', 'Schema description');
43+
service.registerExternalSchema(
44+
{ uri: 'https://some.com/some.json', fileMatch: ['foo.yaml'] },
45+
'Schema name',
46+
'Schema description'
47+
);
4448
const settings = new SettingsState();
4549
const testTextDocument = setupSchemaIDTextDocument('');
4650
settings.documents = new TextDocumentTestManager();
@@ -61,7 +65,11 @@ describe('Schema Selection Handlers', () => {
6165
});
6266

6367
it('getAllSchemas should return all schemas and mark used for current file', async () => {
64-
service.registerExternalSchema('https://some.com/some.json', [SCHEMA_ID], undefined, 'Schema name', 'Schema description');
68+
service.registerExternalSchema(
69+
{ uri: 'https://some.com/some.json', fileMatch: [SCHEMA_ID] },
70+
'Schema name',
71+
'Schema description'
72+
);
6573
const settings = new SettingsState();
6674
const testTextDocument = setupSchemaIDTextDocument('');
6775
settings.documents = new TextDocumentTestManager();
@@ -82,7 +90,11 @@ describe('Schema Selection Handlers', () => {
8290
});
8391

8492
it('getSchemas should return all schemas', async () => {
85-
service.registerExternalSchema('https://some.com/some.json', [SCHEMA_ID], undefined, 'Schema name', 'Schema description');
93+
service.registerExternalSchema(
94+
{ uri: 'https://some.com/some.json', fileMatch: [SCHEMA_ID] },
95+
'Schema name',
96+
'Schema description'
97+
);
8698
const settings = new SettingsState();
8799
const testTextDocument = setupSchemaIDTextDocument('');
88100
settings.documents = new TextDocumentTestManager();

test/yamlSchemaService.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ describe('YAML Schema Service', () => {
3333
const service = new SchemaService.YAMLSchemaService(requestServiceMock);
3434
service.getSchemaForResource('', yamlDock.documents[0]);
3535

36-
expect(requestServiceMock).calledOnceWith('http://json-schema.org/draft-07/schema#');
36+
// vscode-json-languageserver converts the http to https for all json schema URIs
37+
expect(requestServiceMock).calledOnceWith('https://json-schema.org/draft-07/schema#');
3738
});
3839

3940
it('should handle inline schema https url', () => {

0 commit comments

Comments
 (0)