diff --git a/package-lock.json b/package-lock.json index 90e8ada9..92a82176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", - "vscode-json-languageservice": "4.1.8", + "vscode-json-languageservice": "5.5.0", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", @@ -6454,19 +6454,16 @@ "license": "MIT" }, "node_modules/vscode-json-languageservice": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", - "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.5.0.tgz", + "integrity": "sha512-JchBzp8ArzhCVpRS/LT4wzEEvwHXIUEdZD064cGTI4RVs34rNCZXPUguIYSfGBcHH1GV79ufPcfy3Pd8+ukbKw==", "license": "MIT", "dependencies": { - "jsonc-parser": "^3.0.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-languageserver-types": "^3.16.0", - "vscode-nls": "^5.0.0", - "vscode-uri": "^3.0.2" - }, - "engines": { - "npm": ">=7.0.0" + "@vscode/l10n": "^0.0.18", + "jsonc-parser": "^3.3.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" } }, "node_modules/vscode-jsonrpc": { @@ -6512,12 +6509,6 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT" }, - "node_modules/vscode-nls": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", - "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", - "license": "MIT" - }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", diff --git a/package.json b/package.json index 37af0ebd..b4907d72 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", - "vscode-json-languageservice": "4.1.8", + "vscode-json-languageservice": "5.5.0", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index a9ad2440..450d3400 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -12,7 +12,6 @@ import { JSONSchemaService, SchemaDependencies, ISchemaContributions, - SchemaHandle, } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; import { URI } from 'vscode-uri'; @@ -30,6 +29,7 @@ import * as Json from 'jsonc-parser'; import Ajv, { DefinedError } from 'ajv'; import Ajv4 from 'ajv-draft-04'; import { getSchemaTitle } from '../utils/schemaUtils'; +import { SchemaConfiguration } from 'vscode-json-languageservice'; const ajv = new Ajv(); const ajv4 = new Ajv4(); @@ -160,11 +160,9 @@ export class YAMLSchemaService extends JSONSchemaService { return result; } - async resolveSchemaContent( - schemaToResolve: UnresolvedSchema, - schemaURL: string, - dependencies: SchemaDependencies - ): Promise { + async resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaHandle: SchemaHandle): Promise { + const schemaURL: string = normalizeId(schemaHandle.uri); + const dependencies: SchemaDependencies = schemaHandle.dependencies; const resolveErrors: string[] = schemaToResolve.errors.slice(0); let schema: JSONSchema = schemaToResolve.schema; const contextService = this.contextService; @@ -381,7 +379,7 @@ export class YAMLSchemaService extends JSONSchemaService { const schemaHandle = super.createCombinedSchema(resource, schemas); return schemaHandle.getResolvedSchema().then((schema) => { if (schema.schema && typeof schema.schema === 'object') { - schema.schema.url = schemaHandle.url; + schema.schema.url = schemaHandle.uri; } if ( @@ -438,6 +436,7 @@ export class YAMLSchemaService extends JSONSchemaService { (schemas) => { return { errors: [], + warnings: [], schema: { allOf: schemas.map((schemaObj) => { return schemaObj.schema; @@ -510,7 +509,7 @@ export class YAMLSchemaService extends JSONSchemaService { private async resolveCustomSchema(schemaUri, doc): ResolvedSchema { const unresolvedSchema = await this.loadSchema(schemaUri); - const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []); + const schema = await this.resolveSchemaContent(unresolvedSchema, new SchemaHandle(this, schemaUri)); if (schema.schema && typeof schema.schema === 'object') { schema.schema.url = schemaUri; } @@ -621,8 +620,18 @@ export class YAMLSchemaService extends JSONSchemaService { normalizeId(id: string): string { // The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here + if (!id.includes(':')) { + return id; + } try { - return URI.parse(id).toString(); + const uri = URI.parse(id); + if (!id.includes('#')) { + return uri.toString(); + } + // fragment should be verbatim, but vscode-uri converts `/` to the escaped version (annoyingly, needlessly) + const [first, second] = uri.toString().split('#', 2); + const secondCleaned = second.replace('%2F', '/'); + return first + '#' + secondCleaned; } catch (e) { return id; } @@ -711,17 +720,22 @@ export class YAMLSchemaService extends JSONSchemaService { } registerExternalSchema( - uri: string, - filePatterns?: string[], - unresolvedSchema?: JSONSchema, + schemaConfig: SchemaConfiguration, name?: string, description?: string, versions?: SchemaVersions ): SchemaHandle { if (name || description) { - this.schemaUriToNameAndDescription.set(uri, { name, description, versions }); + this.schemaUriToNameAndDescription.set(schemaConfig.uri, { name, description, versions }); } - return super.registerExternalSchema(uri, filePatterns, unresolvedSchema); + this.registeredSchemasIds[schemaConfig.uri] = true; + this.cachedSchemaForResource = undefined; + if (schemaConfig.fileMatch && schemaConfig.fileMatch.length) { + this.addFilePatternAssociation(schemaConfig.fileMatch, schemaConfig.folderUri, [schemaConfig.uri]); + } + return schemaConfig.schema + ? this.addSchemaHandle(schemaConfig.uri, schemaConfig.schema) + : this.getOrAddSchemaHandle(schemaConfig.uri); } clearExternalSchemas(): void { @@ -729,7 +743,21 @@ export class YAMLSchemaService extends JSONSchemaService { } setSchemaContributions(schemaContributions: ISchemaContributions): void { - super.setSchemaContributions(schemaContributions); + if (schemaContributions.schemas) { + const schemas = schemaContributions.schemas; + for (const id in schemas) { + const normalizedId = normalizeId(id); + this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]); + } + } + if (Array.isArray(schemaContributions.schemaAssociations)) { + const schemaAssociations = schemaContributions.schemaAssociations; + for (const schemaAssociation of schemaAssociations) { + const uris = schemaAssociation.uris.map(normalizeId); + const association = this.addFilePatternAssociation(schemaAssociation.pattern, schemaAssociation.folderUri, uris); + this.contributionAssociations.push(association); + } + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -738,14 +766,62 @@ export class YAMLSchemaService extends JSONSchemaService { } getResolvedSchema(schemaId: string): Promise { - return super.getResolvedSchema(schemaId); + const id = normalizeId(schemaId); + const schemaHandle = this.schemasById[id]; + if (schemaHandle) { + return schemaHandle.getResolvedSchema(); + } + return this.promise.resolve(undefined); } onResourceChange(uri: string): boolean { - return super.onResourceChange(uri); + // always clear this local cache when a resource changes + this.cachedSchemaForResource = undefined; + let hasChanges = false; + uri = normalizeId(uri); + const toWalk = [uri]; + const all = Object.keys(this.schemasById).map((key) => this.schemasById[key]); + while (toWalk.length) { + const curr = toWalk.pop(); + for (let i = 0; i < all.length; i++) { + const handle = all[i]; + if (handle && (handle.uri === curr || handle.dependencies.has(curr))) { + if (handle.uri !== curr) { + toWalk.push(handle.uri); + } + if (handle.clearSchema()) { + hasChanges = true; + } + all[i] = undefined; + } + } + } + return hasChanges; } } +/** + * Our version of normalize id, which doesn't prepend `file:///` to anything without a scheme. + * + * @param id the id to normalize + * @returns the normalized id. + */ +function normalizeId(id: string): string { + if (id.includes(':')) { + try { + if (id.includes('#')) { + const [mostOfIt, fragment] = id.split('#', 2); + return URI.parse(mostOfIt) + '#' + fragment; + } else { + return URI.parse(id).toString(); + } + } catch { + return id; + } + } + return id; +} + function toDisplayString(url: string): string { try { const uri = URI.parse(url); @@ -764,3 +840,47 @@ function getLineAndColumnFromOffset(text: string, offset: number): { line: numbe const column = lines[lines.length - 1].length + 1; // 1-based column number return { line, column }; } + +class SchemaHandle { + public readonly uri: string; + public readonly dependencies: SchemaDependencies; + public anchors: Map | undefined; + private resolvedSchema: Promise | undefined; + private unresolvedSchema: Promise | undefined; + private readonly service: JSONSchemaService; + + constructor(service: JSONSchemaService, uri: string, unresolvedSchemaContent?: JSONSchema) { + this.service = service; + this.uri = uri; + this.dependencies = new Set(); + this.anchors = undefined; + if (unresolvedSchemaContent) { + this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent)); + } + } + + public getUnresolvedSchema(): Promise { + if (!this.unresolvedSchema) { + this.unresolvedSchema = this.service.loadSchema(this.uri); + } + return this.unresolvedSchema; + } + + public getResolvedSchema(): Promise { + if (!this.resolvedSchema) { + this.resolvedSchema = this.getUnresolvedSchema().then((unresolved) => { + return this.service.resolveSchemaContent(unresolved, this); + }); + } + return this.resolvedSchema; + } + + public clearSchema(): boolean { + const hasChanges = !!this.unresolvedSchema; + this.resolvedSchema = undefined; + this.unresolvedSchema = undefined; + this.dependencies.clear(); + this.anchors = undefined; + return hasChanges; + } +} diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index fd414a31..3cbe0b5b 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -212,9 +212,11 @@ export function getLanguageService(params: { const currPriority = settings.priority ? settings.priority : 0; schemaService.addSchemaPriority(settings.uri, currPriority); schemaService.registerExternalSchema( - settings.uri, - settings.fileMatch, - settings.schema, + { + uri: settings.uri, + fileMatch: settings.fileMatch, + schema: settings.schema, + }, settings.name, settings.description, settings.versions diff --git a/test/jsonParser.test.ts b/test/jsonParser.test.ts index 4029a7e4..a46340c7 100644 --- a/test/jsonParser.test.ts +++ b/test/jsonParser.test.ts @@ -1421,6 +1421,7 @@ describe('JSON Parser', () => { it('items as array', function () { const schema: JsonSchema.JSONSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', type: 'array', items: [ { @@ -1456,6 +1457,7 @@ describe('JSON Parser', () => { it('additionalItems', function () { let schema: JsonSchema.JSONSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', type: 'array', items: [ { @@ -1483,6 +1485,7 @@ describe('JSON Parser', () => { assert.strictEqual(semanticErrors.length, 1); } schema = { + $schema: 'http://json-schema.org/draft-07/schema#', type: 'array', items: [ { diff --git a/test/schema.test.ts b/test/schema.test.ts index 2e5aa735..63c0e125 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -337,7 +337,7 @@ describe('JSON Schema', () => { }, }; - service.registerExternalSchema(id, ['*.json'], schema); + service.registerExternalSchema({ uri: id, fileMatch: ['*.json'], schema }); service .getSchemaForResource('test.json', undefined) @@ -373,7 +373,7 @@ describe('JSON Schema', () => { }, }; - service.registerExternalSchema(id, ['*.json'], schema); + service.registerExternalSchema({ uri: id, fileMatch: ['*.json'], schema }); const result = await service.getSchemaForResource('test.json', undefined); @@ -419,11 +419,15 @@ describe('JSON Schema', () => { it('Schema with non uri registers correctly', function (testDone) { const service = new SchemaService.YAMLSchemaService(requestServiceMock, workspaceContext); const non_uri = 'non_uri'; - service.registerExternalSchema(non_uri, ['*.yml', '*.yaml'], { - properties: { - test_node: { - description: 'my test_node description', - enum: ['test 1', 'test 2'], + service.registerExternalSchema({ + uri: non_uri, + fileMatch: ['*.yml', '*.yaml'], + schema: { + properties: { + test_node: { + description: 'my test_node description', + enum: ['test 1', 'test 2'], + }, }, }, }); @@ -529,7 +533,7 @@ describe('JSON Schema', () => { it('Modifying schema works with kubernetes resolution', async () => { const service = new SchemaService.YAMLSchemaService(schemaRequestServiceForURL, workspaceContext); - service.registerExternalSchema(KUBERNETES_SCHEMA_URL); + service.registerExternalSchema({ uri: KUBERNETES_SCHEMA_URL }); await service.addContent({ action: MODIFICATION_ACTIONS.add, @@ -545,7 +549,7 @@ describe('JSON Schema', () => { it('Deleting schema works with Kubernetes resolution', async () => { const service = new SchemaService.YAMLSchemaService(schemaRequestServiceForURL, workspaceContext); - service.registerExternalSchema(KUBERNETES_SCHEMA_URL); + service.registerExternalSchema({ uri: KUBERNETES_SCHEMA_URL }); await service.deleteContent({ action: MODIFICATION_ACTIONS.delete, diff --git a/test/schemaSelectionHandlers.test.ts b/test/schemaSelectionHandlers.test.ts index ede5e658..d259a8dd 100644 --- a/test/schemaSelectionHandlers.test.ts +++ b/test/schemaSelectionHandlers.test.ts @@ -40,7 +40,11 @@ describe('Schema Selection Handlers', () => { }); it('getAllSchemas should return all schemas', async () => { - service.registerExternalSchema('https://some.com/some.json', ['foo.yaml'], undefined, 'Schema name', 'Schema description'); + service.registerExternalSchema( + { uri: 'https://some.com/some.json', fileMatch: ['foo.yaml'] }, + 'Schema name', + 'Schema description' + ); const settings = new SettingsState(); const testTextDocument = setupSchemaIDTextDocument(''); settings.documents = new TextDocumentTestManager(); @@ -61,7 +65,11 @@ describe('Schema Selection Handlers', () => { }); it('getAllSchemas should return all schemas and mark used for current file', async () => { - service.registerExternalSchema('https://some.com/some.json', [SCHEMA_ID], undefined, 'Schema name', 'Schema description'); + service.registerExternalSchema( + { uri: 'https://some.com/some.json', fileMatch: [SCHEMA_ID] }, + 'Schema name', + 'Schema description' + ); const settings = new SettingsState(); const testTextDocument = setupSchemaIDTextDocument(''); settings.documents = new TextDocumentTestManager(); @@ -82,7 +90,11 @@ describe('Schema Selection Handlers', () => { }); it('getSchemas should return all schemas', async () => { - service.registerExternalSchema('https://some.com/some.json', [SCHEMA_ID], undefined, 'Schema name', 'Schema description'); + service.registerExternalSchema( + { uri: 'https://some.com/some.json', fileMatch: [SCHEMA_ID] }, + 'Schema name', + 'Schema description' + ); const settings = new SettingsState(); const testTextDocument = setupSchemaIDTextDocument(''); settings.documents = new TextDocumentTestManager(); diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index a98fb879..72362bff 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -33,7 +33,8 @@ describe('YAML Schema Service', () => { const service = new SchemaService.YAMLSchemaService(requestServiceMock); service.getSchemaForResource('', yamlDock.documents[0]); - expect(requestServiceMock).calledOnceWith('http://json-schema.org/draft-07/schema#'); + // vscode-json-languageserver converts the http to https for all json schema URIs + expect(requestServiceMock).calledOnceWith('https://json-schema.org/draft-07/schema#'); }); it('should handle inline schema https url', () => {