Skip to content

Commit 87eaba1

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 #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 #1069 Signed-off-by: David Thompson <[email protected]>
1 parent 3821411 commit 87eaba1

File tree

8 files changed

+185
-52
lines changed

8 files changed

+185
-52
lines changed

package-lock.json

Lines changed: 9 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"lodash": "4.17.21",
3434
"prettier": "^3.5.0",
3535
"request-light": "^0.5.7",
36-
"vscode-json-languageservice": "4.1.8",
36+
"vscode-json-languageservice": "5.5.0",
3737
"vscode-languageserver": "^9.0.0",
3838
"vscode-languageserver-textdocument": "^1.0.1",
3939
"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';
@@ -30,6 +29,7 @@ import * as Json from 'jsonc-parser';
3029
import Ajv, { DefinedError } from 'ajv';
3130
import Ajv4 from 'ajv-draft-04';
3231
import { getSchemaTitle } from '../utils/schemaUtils';
32+
import { SchemaConfiguration } from 'vscode-json-languageservice';
3333

3434
const ajv = new Ajv();
3535
const ajv4 = new Ajv4();
@@ -160,11 +160,9 @@ export class YAMLSchemaService extends JSONSchemaService {
160160
return result;
161161
}
162162

163-
async resolveSchemaContent(
164-
schemaToResolve: UnresolvedSchema,
165-
schemaURL: string,
166-
dependencies: SchemaDependencies
167-
): Promise<ResolvedSchema> {
163+
async resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaHandle: SchemaHandle): Promise<ResolvedSchema> {
164+
const schemaURL: string = normalizeId(schemaHandle.uri);
165+
const dependencies: SchemaDependencies = schemaHandle.dependencies;
168166
const resolveErrors: string[] = schemaToResolve.errors.slice(0);
169167
let schema: JSONSchema = schemaToResolve.schema;
170168
const contextService = this.contextService;
@@ -381,7 +379,7 @@ export class YAMLSchemaService extends JSONSchemaService {
381379
const schemaHandle = super.createCombinedSchema(resource, schemas);
382380
return schemaHandle.getResolvedSchema().then((schema) => {
383381
if (schema.schema && typeof schema.schema === 'object') {
384-
schema.schema.url = schemaHandle.url;
382+
schema.schema.url = schemaHandle.uri;
385383
}
386384

387385
if (
@@ -438,6 +436,7 @@ export class YAMLSchemaService extends JSONSchemaService {
438436
(schemas) => {
439437
return {
440438
errors: [],
439+
warnings: [],
441440
schema: {
442441
allOf: schemas.map((schemaObj) => {
443442
return schemaObj.schema;
@@ -510,7 +509,7 @@ export class YAMLSchemaService extends JSONSchemaService {
510509

511510
private async resolveCustomSchema(schemaUri, doc): ResolvedSchema {
512511
const unresolvedSchema = await this.loadSchema(schemaUri);
513-
const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []);
512+
const schema = await this.resolveSchemaContent(unresolvedSchema, new SchemaHandle(this, schemaUri));
514513
if (schema.schema && typeof schema.schema === 'object') {
515514
schema.schema.url = schemaUri;
516515
}
@@ -621,8 +620,18 @@ export class YAMLSchemaService extends JSONSchemaService {
621620

622621
normalizeId(id: string): string {
623622
// The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
623+
if (!id.includes(':')) {
624+
return id;
625+
}
624626
try {
625-
return URI.parse(id).toString();
627+
const uri = URI.parse(id);
628+
if (!id.includes('#')) {
629+
return uri.toString();
630+
}
631+
// fragment should be verbatim, but vscode-uri converts `/` to the escaped version (annoyingly, needlessly)
632+
const [first, second] = uri.toString().split('#', 2);
633+
const secondCleaned = second.replace('%2F', '/');
634+
return first + '#' + secondCleaned;
626635
} catch (e) {
627636
return id;
628637
}
@@ -711,25 +720,44 @@ export class YAMLSchemaService extends JSONSchemaService {
711720
}
712721

713722
registerExternalSchema(
714-
uri: string,
715-
filePatterns?: string[],
716-
unresolvedSchema?: JSONSchema,
723+
schemaConfig: SchemaConfiguration,
717724
name?: string,
718725
description?: string,
719726
versions?: SchemaVersions
720727
): SchemaHandle {
721728
if (name || description) {
722-
this.schemaUriToNameAndDescription.set(uri, { name, description, versions });
729+
this.schemaUriToNameAndDescription.set(schemaConfig.uri, { name, description, versions });
723730
}
724-
return super.registerExternalSchema(uri, filePatterns, unresolvedSchema);
731+
this.registeredSchemasIds[schemaConfig.uri] = true;
732+
this.cachedSchemaForResource = undefined;
733+
if (schemaConfig.fileMatch && schemaConfig.fileMatch.length) {
734+
this.addFilePatternAssociation(schemaConfig.fileMatch, schemaConfig.folderUri, [schemaConfig.uri]);
735+
}
736+
return schemaConfig.schema
737+
? this.addSchemaHandle(schemaConfig.uri, schemaConfig.schema)
738+
: this.getOrAddSchemaHandle(schemaConfig.uri);
725739
}
726740

727741
clearExternalSchemas(): void {
728742
super.clearExternalSchemas();
729743
}
730744

731745
setSchemaContributions(schemaContributions: ISchemaContributions): void {
732-
super.setSchemaContributions(schemaContributions);
746+
if (schemaContributions.schemas) {
747+
const schemas = schemaContributions.schemas;
748+
for (const id in schemas) {
749+
const normalizedId = normalizeId(id);
750+
this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]);
751+
}
752+
}
753+
if (Array.isArray(schemaContributions.schemaAssociations)) {
754+
const schemaAssociations = schemaContributions.schemaAssociations;
755+
for (const schemaAssociation of schemaAssociations) {
756+
const uris = schemaAssociation.uris.map(normalizeId);
757+
const association = this.addFilePatternAssociation(schemaAssociation.pattern, schemaAssociation.folderUri, uris);
758+
this.contributionAssociations.push(association);
759+
}
760+
}
733761
}
734762

735763
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -738,14 +766,62 @@ export class YAMLSchemaService extends JSONSchemaService {
738766
}
739767

740768
getResolvedSchema(schemaId: string): Promise<ResolvedSchema> {
741-
return super.getResolvedSchema(schemaId);
769+
const id = normalizeId(schemaId);
770+
const schemaHandle = this.schemasById[id];
771+
if (schemaHandle) {
772+
return schemaHandle.getResolvedSchema();
773+
}
774+
return this.promise.resolve(undefined);
742775
}
743776

744777
onResourceChange(uri: string): boolean {
745-
return super.onResourceChange(uri);
778+
// always clear this local cache when a resource changes
779+
this.cachedSchemaForResource = undefined;
780+
let hasChanges = false;
781+
uri = normalizeId(uri);
782+
const toWalk = [uri];
783+
const all = Object.keys(this.schemasById).map((key) => this.schemasById[key]);
784+
while (toWalk.length) {
785+
const curr = toWalk.pop();
786+
for (let i = 0; i < all.length; i++) {
787+
const handle = all[i];
788+
if (handle && (handle.uri === curr || handle.dependencies.has(curr))) {
789+
if (handle.uri !== curr) {
790+
toWalk.push(handle.uri);
791+
}
792+
if (handle.clearSchema()) {
793+
hasChanges = true;
794+
}
795+
all[i] = undefined;
796+
}
797+
}
798+
}
799+
return hasChanges;
746800
}
747801
}
748802

803+
/**
804+
* Our version of normalize id, which doesn't prepend `file:///` to anything without a scheme.
805+
*
806+
* @param id the id to normalize
807+
* @returns the normalized id.
808+
*/
809+
function normalizeId(id: string): string {
810+
if (id.includes(':')) {
811+
try {
812+
if (id.includes('#')) {
813+
const [mostOfIt, fragment] = id.split('#', 2);
814+
return URI.parse(mostOfIt) + '#' + fragment;
815+
} else {
816+
return URI.parse(id).toString();
817+
}
818+
} catch {
819+
return id;
820+
}
821+
}
822+
return id;
823+
}
824+
749825
function toDisplayString(url: string): string {
750826
try {
751827
const uri = URI.parse(url);
@@ -764,3 +840,47 @@ function getLineAndColumnFromOffset(text: string, offset: number): { line: numbe
764840
const column = lines[lines.length - 1].length + 1; // 1-based column number
765841
return { line, column };
766842
}
843+
844+
class SchemaHandle {
845+
public readonly uri: string;
846+
public readonly dependencies: SchemaDependencies;
847+
public anchors: Map<string, JSONSchema> | undefined;
848+
private resolvedSchema: Promise<ResolvedSchema> | undefined;
849+
private unresolvedSchema: Promise<UnresolvedSchema> | undefined;
850+
private readonly service: JSONSchemaService;
851+
852+
constructor(service: JSONSchemaService, uri: string, unresolvedSchemaContent?: JSONSchema) {
853+
this.service = service;
854+
this.uri = uri;
855+
this.dependencies = new Set();
856+
this.anchors = undefined;
857+
if (unresolvedSchemaContent) {
858+
this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent));
859+
}
860+
}
861+
862+
public getUnresolvedSchema(): Promise<UnresolvedSchema> {
863+
if (!this.unresolvedSchema) {
864+
this.unresolvedSchema = this.service.loadSchema(this.uri);
865+
}
866+
return this.unresolvedSchema;
867+
}
868+
869+
public getResolvedSchema(): Promise<ResolvedSchema> {
870+
if (!this.resolvedSchema) {
871+
this.resolvedSchema = this.getUnresolvedSchema().then((unresolved) => {
872+
return this.service.resolveSchemaContent(unresolved, this);
873+
});
874+
}
875+
return this.resolvedSchema;
876+
}
877+
878+
public clearSchema(): boolean {
879+
const hasChanges = !!this.unresolvedSchema;
880+
this.resolvedSchema = undefined;
881+
this.unresolvedSchema = undefined;
882+
this.dependencies.clear();
883+
this.anchors = undefined;
884+
return hasChanges;
885+
}
886+
}

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,

0 commit comments

Comments
 (0)