From d508781423acb7555015a8f5477b75d7b4e565a8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 15 Jul 2026 10:36:12 -0400 Subject: [PATCH 1/4] feat: adds schema store support to automatically load the relevant schema for the file Signed-off-by: Vincent Biret --- src/jsonLanguageService.ts | 1 + src/jsonLanguageTypes.ts | 25 +++++ src/services/jsonSchemaService.ts | 176 +++++++++++++++++++++++++++++- src/services/jsonValidation.ts | 3 +- src/test/schema.test.ts | 120 ++++++++++++++++++++ 5 files changed, 319 insertions(+), 6 deletions(-) diff --git a/src/jsonLanguageService.ts b/src/jsonLanguageService.ts index 4a0c90df..e2873511 100644 --- a/src/jsonLanguageService.ts +++ b/src/jsonLanguageService.ts @@ -73,6 +73,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi configure: (settings: LanguageSettings) => { jsonSchemaService.clearExternalSchemas(); settings.schemas?.forEach(jsonSchemaService.registerExternalSchema.bind(jsonSchemaService)); + jsonSchemaService.configureSchemaStore(settings.schemaStore); jsonValidation.configure(settings); }, resetSchema: (uri: string) => jsonSchemaService.onResourceChange(uri), diff --git a/src/jsonLanguageTypes.ts b/src/jsonLanguageTypes.ts index af765ac5..d61873a9 100644 --- a/src/jsonLanguageTypes.ts +++ b/src/jsonLanguageTypes.ts @@ -130,6 +130,14 @@ export interface MatchingSchema { export interface JSONLanguageStatus { schemas: string[]; + schemaDiagnostics?: JSONLanguageStatusDiagnostic[]; +} + +export interface JSONLanguageStatusDiagnostic { + message: string; + code: ErrorCode; + severity: SeverityLevel; + uri?: string; } export interface LanguageSettings { @@ -147,6 +155,23 @@ export interface LanguageSettings { * A list of known schemas and/or associations of schemas to file names. */ schemas?: SchemaConfiguration[]; + /** + * Optional support for loading schema associations from the SchemaStore catalog. + * Enabled by default. Requires a schemaRequestService to load the catalog and schemas. + */ + schemaStore?: SchemaStoreSettings; +} + +export interface SchemaStoreSettings { + /** + * If set to false, the language service will not load schema associations from the SchemaStore catalog. + */ + enable?: boolean; + /** + * Optional URL for a SchemaStore-compatible catalog. + * If not set, https://www.schemastore.org/api/json/catalog.json is used. + */ + url?: string; } export type SeverityLevel = 'error' | 'warning' | 'ignore'; diff --git a/src/services/jsonSchemaService.ts b/src/services/jsonSchemaService.ts index d8683c6a..bb4fe4d0 100644 --- a/src/services/jsonSchemaService.ts +++ b/src/services/jsonSchemaService.ts @@ -8,7 +8,7 @@ import { JSONSchema, JSONSchemaRef, MergedJSONSchema, DynamicRefInfo, AnchorMaps import { URI } from 'vscode-uri'; import * as Strings from '../utils/strings.js'; import { asSchema, getSchemaDraftFromId, JSONDocument, normalizeId } from '../parser/jsonParser.js'; -import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, MatchingSchema, TextDocument, SchemaConfiguration, SchemaDraft, ErrorCode, Vocabularies } from '../jsonLanguageTypes.js'; +import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, MatchingSchema, TextDocument, SchemaConfiguration, SchemaDraft, ErrorCode, Vocabularies, SchemaStoreSettings, JSONLanguageStatusDiagnostic } from '../jsonLanguageTypes.js'; import * as l10n from '@vscode/l10n'; import { createRegex } from '../utils/glob.js'; @@ -74,9 +74,25 @@ export interface ISchemaHandle { getResolvedSchema(): PromiseLike; } +export const defaultSchemaStoreCatalogUrl = 'https://www.schemastore.org/api/json/catalog.json'; + const BANG = '!'; const PATH_SEP = '/'; +interface SchemaStoreCatalogEntry { + name: string; + description: string; + fileMatch?: string[]; + url: string; + versions?: { [version: string]: string }; +} + +interface SchemaStoreCatalog { + $schema?: string; + version: number; + schemas: SchemaStoreCatalogEntry[]; +} + interface IGlobWrapper { regexp: RegExp; include: boolean; @@ -277,6 +293,13 @@ export class JSONSchemaService implements IJSONSchemaService { private callOnDispose: Function[]; private requestService: SchemaRequestService | undefined; private promiseConstructor: PromiseConstructor; + private schemaStoreEnabled: boolean; + private schemaStoreCatalogUrl: string; + private schemaStoreCatalogPromise: PromiseLike | undefined; + private schemaStoreCatalogLoaded: boolean; + private schemaStoreSchemaIds: { [id: string]: boolean }; + private schemaStoreCatalogDiagnostics: JSONLanguageStatusDiagnostic[]; + private schemaDiagnosticsByResource: { [resource: string]: JSONLanguageStatusDiagnostic[] }; private static traverseSchemaProperties(node: JSONSchema, callback: (schema: JSONSchema) => void): void { // `$defs`/`definitions` are visited first so that a reusable resource they @@ -336,6 +359,12 @@ export class JSONSchemaService implements IJSONSchemaService { this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; + this.schemaStoreEnabled = true; + this.schemaStoreCatalogUrl = defaultSchemaStoreCatalogUrl; + this.schemaStoreCatalogLoaded = false; + this.schemaStoreSchemaIds = {}; + this.schemaStoreCatalogDiagnostics = []; + this.schemaDiagnosticsByResource = {}; } public getRegisteredSchemaIds(filter?: (scheme: string) => boolean): string[] { @@ -433,6 +462,11 @@ export class JSONSchemaService implements IJSONSchemaService { this.filePatternAssociations = []; this.registeredSchemasIds = {}; this.cachedSchemaForResource = undefined; + this.schemaStoreCatalogPromise = undefined; + this.schemaStoreCatalogLoaded = false; + this.schemaStoreSchemaIds = {}; + this.schemaStoreCatalogDiagnostics = []; + this.schemaDiagnosticsByResource = {}; for (const id in this.contributionSchemas) { this.schemasById[id] = this.contributionSchemas[id]; @@ -443,6 +477,117 @@ export class JSONSchemaService implements IJSONSchemaService { } } + public configureSchemaStore(settings: SchemaStoreSettings | undefined): void { + this.schemaStoreEnabled = settings?.enable !== false; + this.schemaStoreCatalogUrl = normalizeId(settings?.url || defaultSchemaStoreCatalogUrl); + this.schemaStoreCatalogPromise = undefined; + this.schemaStoreCatalogLoaded = false; + this.schemaStoreSchemaIds = {}; + this.schemaStoreCatalogDiagnostics = []; + this.schemaDiagnosticsByResource = {}; + this.cachedSchemaForResource = undefined; + } + + public getSchemaDiagnosticsForResource(resource: string): JSONLanguageStatusDiagnostic[] | undefined { + const diagnostics: JSONLanguageStatusDiagnostic[] = []; + if (this.schemaStoreEnabled) { + diagnostics.push(...this.schemaStoreCatalogDiagnostics); + } + const resourceDiagnostics = this.schemaDiagnosticsByResource[resource]; + if (resourceDiagnostics) { + diagnostics.push(...resourceDiagnostics); + } + return diagnostics.length ? diagnostics : undefined; + } + + private setSchemaStoreCatalogDiagnostic(message: string, code: ErrorCode = ErrorCode.SchemaResolveError): void { + this.schemaStoreCatalogDiagnostics = [{ + message, + code, + severity: 'warning', + uri: this.schemaStoreCatalogUrl + }]; + } + + private getErrorMessage(error: any): string { + if (error && typeof error.message === 'string') { + return error.message; + } + let errorMessage = error?.toString ? error.toString() as string : String(error); + const errorSplit = errorMessage.split('Error: '); + if (errorSplit.length > 1) { + errorMessage = errorSplit[1]; + } + if (Strings.endsWith(errorMessage, '.')) { + errorMessage = errorMessage.substr(0, errorMessage.length - 1); + } + return errorMessage; + } + + private toStatusDiagnostics(diagnostics: readonly SchemaDiagnostic[], uri: string | undefined): JSONLanguageStatusDiagnostic[] { + return diagnostics.map(diagnostic => ({ + message: diagnostic.message, + code: diagnostic.code, + severity: 'warning', + uri + })); + } + + private loadSchemaStoreCatalog(): PromiseLike { + if (!this.schemaStoreEnabled || this.schemaStoreCatalogLoaded) { + return this.promise.resolve(undefined); + } + if (!this.requestService) { + this.schemaStoreCatalogLoaded = true; + return this.promise.resolve(undefined); + } + if (!this.schemaStoreCatalogPromise) { + this.schemaStoreCatalogDiagnostics = []; + this.schemaStoreCatalogPromise = this.requestService(this.schemaStoreCatalogUrl).then(content => { + if (!content) { + this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to load SchemaStore catalog from \'{0}\': No content.', toDisplayString(this.schemaStoreCatalogUrl))); + this.schemaStoreCatalogLoaded = true; + return; + } + + const jsonErrors: Json.ParseError[] = []; + const catalog = Json.parse(content, jsonErrors) as SchemaStoreCatalog; + if (jsonErrors.length) { + this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to parse SchemaStore catalog from \'{0}\': Parse error at offset {1}.', toDisplayString(this.schemaStoreCatalogUrl), jsonErrors[0].offset)); + this.schemaStoreCatalogLoaded = true; + return; + } + if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.schemas)) { + this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to parse SchemaStore catalog from \'{0}\': Expected a catalog object with a schemas array.', toDisplayString(this.schemaStoreCatalogUrl))); + this.schemaStoreCatalogLoaded = true; + return; + } + + for (const schema of catalog.schemas) { + if (!schema || typeof schema !== 'object' || typeof schema.url !== 'string' || !Array.isArray(schema.fileMatch)) { + continue; + } + + const fileMatch = schema.fileMatch.filter((pattern): pattern is string => typeof pattern === 'string'); + if (fileMatch.length) { + this.registerExternalSchema({ uri: schema.url, fileMatch }); + this.schemaStoreSchemaIds[normalizeId(schema.url)] = true; + } + } + + this.schemaStoreCatalogLoaded = true; + }, error => { + let errorCode = ErrorCode.SchemaResolveError; + if (typeof error?.code === 'number' && error.code < 0x10000) { + errorCode += error.code; + } + this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to load SchemaStore catalog from \'{0}\': {1}.', toDisplayString(this.schemaStoreCatalogUrl), this.getErrorMessage(error)), errorCode); + this.schemaStoreCatalogLoaded = true; + }); + } + return this.schemaStoreCatalogPromise; + } + public getResolvedSchema(schemaId: string): PromiseLike { const id = normalizeId(schemaId); const schemaHandle = this.schemasById[id]; @@ -1308,10 +1453,31 @@ export class JSONSchemaService implements IJSONSchemaService { if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) { return this.cachedSchemaForResource.resolvedSchema; } - const schemas = this.getAssociatedSchemas(resource); - const resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined); - this.cachedSchemaForResource = { resource, resolvedSchema }; - return resolvedSchema; + const resolveSchema = () => { + const schemas = this.getAssociatedSchemas(resource); + const resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined); + const usesSchemaStore = schemas.some(schemaId => this.schemaStoreSchemaIds[schemaId]); + if (usesSchemaStore) { + const resolvedSchemaWithDiagnostics = resolvedSchema.then(schema => { + const diagnostics = schema ? this.toStatusDiagnostics([...schema.errors, ...schema.warnings], schemas.length === 1 ? schemas[0] : undefined) : []; + if (diagnostics.length) { + this.schemaDiagnosticsByResource[resource] = diagnostics; + } else { + delete this.schemaDiagnosticsByResource[resource]; + } + return schema; + }); + this.cachedSchemaForResource = { resource, resolvedSchema: resolvedSchemaWithDiagnostics }; + return resolvedSchemaWithDiagnostics; + } + delete this.schemaDiagnosticsByResource[resource]; + this.cachedSchemaForResource = { resource, resolvedSchema }; + return resolvedSchema; + }; + if (this.schemaStoreEnabled && !this.schemaStoreCatalogLoaded) { + return this.loadSchemaStoreCatalog().then(resolveSchema); + } + return resolveSchema(); } private createCombinedSchema(resource: string, schemaIds: string[]): ISchemaHandle { diff --git a/src/services/jsonValidation.ts b/src/services/jsonValidation.ts index 70c35270..17bfb977 100644 --- a/src/services/jsonValidation.ts +++ b/src/services/jsonValidation.ts @@ -121,7 +121,8 @@ export class JSONValidation { } public getLanguageStatus(textDocument: TextDocument, jsonDocument: JSONDocument): JSONLanguageStatus { - return { schemas: this.jsonSchemaService.getSchemaURIsForResource(textDocument.uri, jsonDocument) }; + const schemaDiagnostics = this.jsonSchemaService.getSchemaDiagnosticsForResource(textDocument.uri); + return { schemas: this.jsonSchemaService.getSchemaURIsForResource(textDocument.uri, jsonDocument), schemaDiagnostics }; } } diff --git a/src/test/schema.test.ts b/src/test/schema.test.ts index 6d016811..b75e73c4 100644 --- a/src/test/schema.test.ts +++ b/src/test/schema.test.ts @@ -1180,6 +1180,126 @@ suite('JSON Schema', () => { assert.strictEqual(section?.description, 'Meaning of Life'); }); + test('SchemaStore can be disabled', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaUri = 'https://www.schemastore.org/package.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaUri + }] + } as JSONSchema, + [schemaUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ schemaStore: { enable: false } }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.deepStrictEqual(accesses, []); + }); + + test('SchemaStore catalog registers file matches when enabled', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaUri = 'https://www.schemastore.org/package.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaUri + }] + } as JSONSchema, + [schemaUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ schemaStore: { enable: true, url: catalogUri } }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].message, 'Incorrect type. Expected "string".'); + assert.deepStrictEqual(accesses, [catalogUri, schemaUri]); + }); + + test('SchemaStore catalog request failures are reported in language status', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaRequestService: SchemaRequestService = async (): Promise => { + return Promise.reject({ message: 'Blocked by schema allow-list', code: 1 }); + }; + const service = getLanguageService({ schemaRequestService, workspaceContext }); + service.configure({ schemaStore: { enable: true, url: catalogUri } }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.strictEqual(status.schemaDiagnostics?.length, 1); + assertInMessage(status.schemaDiagnostics?.[0].message, 'Blocked by schema allow-list'); + assert.strictEqual(status.schemaDiagnostics?.[0].uri, catalogUri); + assert.strictEqual(status.schemaDiagnostics?.[0].severity, 'warning'); + }); + + test('SchemaStore schema request failures are reported in diagnostics and language status', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaUri = 'https://thirdparty.example/package.schema.json'; + const schemaRequestService: SchemaRequestService = async (uri: string): Promise => { + if (uri === catalogUri) { + return JSON.stringify({ + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaUri + }] + }); + } + return Promise.reject({ message: 'Blocked by schema allow-list', code: 1 }); + }; + const service = getLanguageService({ schemaRequestService, workspaceContext }); + service.configure({ schemaStore: { enable: true, url: catalogUri } }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc, { schemaRequest: 'error' }); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].code, ErrorCode.SchemaResolveError + 1); + assertInMessage(diagnostics[0].message, 'Blocked by schema allow-list'); + assert.strictEqual(status.schemaDiagnostics?.length, 1); + assert.strictEqual(status.schemaDiagnostics?.[0].code, ErrorCode.SchemaResolveError + 1); + assertInMessage(status.schemaDiagnostics?.[0].message, 'Blocked by schema allow-list'); + assert.strictEqual(status.schemaDiagnostics?.[0].uri, schemaUri); + assert.strictEqual(status.schemaDiagnostics?.[0].severity, 'warning'); + }); + test('Resolving in-line $refs', async function () { const service = new SchemaService.JSONSchemaService(newMockRequestService(), workspaceContext); From b3fe1b5667489cf813a312c8c26e2207fd6cf5d6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 15 Jul 2026 10:40:46 -0400 Subject: [PATCH 2/4] Ensure explicit schemas override SchemaStore matches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c5e9b306-5d8c-4cc8-ac55-1b1175cfcedf --- src/services/jsonSchemaService.ts | 7 ++- src/test/schema.test.ts | 90 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/services/jsonSchemaService.ts b/src/services/jsonSchemaService.ts index bb4fe4d0..e394ffab 100644 --- a/src/services/jsonSchemaService.ts +++ b/src/services/jsonSchemaService.ts @@ -1430,7 +1430,8 @@ export class JSONSchemaService implements IJSONSchemaService { } } } - return schemas; + const nonSchemaStoreSchemas = schemas.filter(schemaId => !this.schemaStoreSchemaIds[schemaId]); + return nonSchemaStoreSchemas.length ? nonSchemaStoreSchemas : schemas; } public getSchemaURIsForResource(resource: string, document?: JSONDocument): string[] { @@ -1475,6 +1476,10 @@ export class JSONSchemaService implements IJSONSchemaService { return resolvedSchema; }; if (this.schemaStoreEnabled && !this.schemaStoreCatalogLoaded) { + const schemas = this.getAssociatedSchemas(resource); + if (schemas.length > 0) { + return resolveSchema(); + } return this.loadSchemaStoreCatalog().then(resolveSchema); } return resolveSchema(); diff --git a/src/test/schema.test.ts b/src/test/schema.test.ts index b75e73c4..70d17ef8 100644 --- a/src/test/schema.test.ts +++ b/src/test/schema.test.ts @@ -1247,6 +1247,96 @@ suite('JSON Schema', () => { assert.deepStrictEqual(accesses, [catalogUri, schemaUri]); }); + test('$schema takes precedence over SchemaStore file matches', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaStoreUri = 'https://www.schemastore.org/package.json'; + const explicitSchemaUri = 'https://example.com/custom-package.schema.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaStoreUri + }] + } as JSONSchema, + [schemaStoreUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + }, + [explicitSchemaUri]: { + type: 'object', + properties: { + name: { type: 'number' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ schemaStore: { enable: true, url: catalogUri } }); + const { textDoc, jsonDoc } = toDocument(`{ "$schema": "${explicitSchemaUri}", "name": 1 }`, undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.deepStrictEqual(status.schemas, [explicitSchemaUri]); + assert.deepStrictEqual(accesses, [explicitSchemaUri]); + }); + + test('Configured schemas take precedence over SchemaStore file matches', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaStoreUri = 'https://www.schemastore.org/package.json'; + const configuredSchemaUri = 'https://example.com/configured-package.schema.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaStoreUri + }] + } as JSONSchema, + [schemaStoreUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ + schemaStore: { enable: true, url: catalogUri }, + schemas: [{ + uri: configuredSchemaUri, + fileMatch: ['package.json'], + schema: { + type: 'object', + properties: { + name: { type: 'number' } + } + } + }] + }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.deepStrictEqual(status.schemas, [configuredSchemaUri]); + assert.deepStrictEqual(accesses, []); + }); + test('SchemaStore catalog request failures are reported in language status', async function () { const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaRequestService: SchemaRequestService = async (): Promise => { From 47b7787eb13c03946ea77c038a9748fe8dcec88a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 21 Jul 2026 08:12:15 -0400 Subject: [PATCH 3/4] Add SchemaStore exclusion setting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c5e9b306-5d8c-4cc8-ac55-1b1175cfcedf --- src/jsonLanguageTypes.ts | 4 + src/services/jsonSchemaService.ts | 28 ++++++- src/test/schema.test.ts | 135 ++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/src/jsonLanguageTypes.ts b/src/jsonLanguageTypes.ts index d61873a9..77202bcc 100644 --- a/src/jsonLanguageTypes.ts +++ b/src/jsonLanguageTypes.ts @@ -167,6 +167,10 @@ export interface SchemaStoreSettings { * If set to false, the language service will not load schema associations from the SchemaStore catalog. */ enable?: boolean; + /** + * Relative path selectors for resources that should not receive schema associations from the SchemaStore catalog. + */ + exclude?: string[]; /** * Optional URL for a SchemaStore-compatible catalog. * If not set, https://www.schemastore.org/api/json/catalog.json is used. diff --git a/src/services/jsonSchemaService.ts b/src/services/jsonSchemaService.ts index e394ffab..14912a03 100644 --- a/src/services/jsonSchemaService.ts +++ b/src/services/jsonSchemaService.ts @@ -300,6 +300,7 @@ export class JSONSchemaService implements IJSONSchemaService { private schemaStoreSchemaIds: { [id: string]: boolean }; private schemaStoreCatalogDiagnostics: JSONLanguageStatusDiagnostic[]; private schemaDiagnosticsByResource: { [resource: string]: JSONLanguageStatusDiagnostic[] }; + private schemaStoreExclusions: FilePatternAssociation | undefined; private static traverseSchemaProperties(node: JSONSchema, callback: (schema: JSONSchema) => void): void { // `$defs`/`definitions` are visited first so that a reusable resource they @@ -365,6 +366,7 @@ export class JSONSchemaService implements IJSONSchemaService { this.schemaStoreSchemaIds = {}; this.schemaStoreCatalogDiagnostics = []; this.schemaDiagnosticsByResource = {}; + this.schemaStoreExclusions = undefined; } public getRegisteredSchemaIds(filter?: (scheme: string) => boolean): string[] { @@ -485,9 +487,29 @@ export class JSONSchemaService implements IJSONSchemaService { this.schemaStoreSchemaIds = {}; this.schemaStoreCatalogDiagnostics = []; this.schemaDiagnosticsByResource = {}; + this.schemaStoreExclusions = this.createSchemaStoreExclusions(settings?.exclude); this.cachedSchemaForResource = undefined; } + private createSchemaStoreExclusions(exclusions: string[] | undefined): FilePatternAssociation | undefined { + if (!exclusions) { + return undefined; + } + const sanitizedExclusions = exclusions.map(exclusion => this.sanitizeSchemaStoreExclusion(exclusion)).filter((exclusion): exclusion is string => !!exclusion); + return sanitizedExclusions.length ? new FilePatternAssociation(sanitizedExclusions, undefined, []) : undefined; + } + + private sanitizeSchemaStoreExclusion(exclusion: string): string | undefined { + if (typeof exclusion !== 'string' || !exclusion || exclusion[0] === BANG || exclusion[0] === PATH_SEP || exclusion.indexOf('\\') !== -1 || exclusion.indexOf(':') !== -1 || exclusion.indexOf('..') !== -1) { + return undefined; + } + return exclusion; + } + + private matchesSchemaStoreExclusion(resource: string): boolean { + return !!this.schemaStoreExclusions?.matchesPattern(normalizeResourceForMatching(resource)); + } + public getSchemaDiagnosticsForResource(resource: string): JSONLanguageStatusDiagnostic[] | undefined { const diagnostics: JSONLanguageStatusDiagnostic[] = []; if (this.schemaStoreEnabled) { @@ -1430,7 +1452,11 @@ export class JSONSchemaService implements IJSONSchemaService { } } } + const matchesSchemaStoreExclusion = this.matchesSchemaStoreExclusion(resource); const nonSchemaStoreSchemas = schemas.filter(schemaId => !this.schemaStoreSchemaIds[schemaId]); + if (matchesSchemaStoreExclusion) { + return nonSchemaStoreSchemas; + } return nonSchemaStoreSchemas.length ? nonSchemaStoreSchemas : schemas; } @@ -1477,7 +1503,7 @@ export class JSONSchemaService implements IJSONSchemaService { }; if (this.schemaStoreEnabled && !this.schemaStoreCatalogLoaded) { const schemas = this.getAssociatedSchemas(resource); - if (schemas.length > 0) { + if (schemas.length > 0 || this.matchesSchemaStoreExclusion(resource)) { return resolveSchema(); } return this.loadSchemaStoreCatalog().then(resolveSchema); diff --git a/src/test/schema.test.ts b/src/test/schema.test.ts index 70d17ef8..d185c7e0 100644 --- a/src/test/schema.test.ts +++ b/src/test/schema.test.ts @@ -1337,6 +1337,141 @@ suite('JSON Schema', () => { assert.deepStrictEqual(accesses, []); }); + test('SchemaStore exclusions prevent only SchemaStore file matches', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaStoreUri = 'https://www.schemastore.org/package.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaStoreUri + }] + } as JSONSchema, + [schemaStoreUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ + schemaStore: { + enable: true, + url: catalogUri, + exclude: ['**/package.json'] + } + }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.deepStrictEqual(status.schemas, []); + assert.deepStrictEqual(accesses, []); + }); + + test('SchemaStore exclusions do not prevent configured schemas', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaStoreUri = 'https://www.schemastore.org/package.json'; + const configuredSchemaUri = 'https://example.com/configured-package.schema.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaStoreUri + }] + } as JSONSchema, + [schemaStoreUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ + schemaStore: { + enable: true, + url: catalogUri, + exclude: ['package.json'] + }, + schemas: [{ + uri: configuredSchemaUri, + fileMatch: ['package.json'], + schema: { + type: 'object', + properties: { + name: { type: 'number' } + } + } + }] + }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.deepStrictEqual(diagnostics, []); + assert.deepStrictEqual(status.schemas, [configuredSchemaUri]); + assert.deepStrictEqual(accesses, []); + }); + + test('SchemaStore exclusions ignore absolute and parent-relative selectors', async function () { + const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + const schemaStoreUri = 'https://www.schemastore.org/package.json'; + const accesses: string[] = []; + const service = getLanguageService({ + schemaRequestService: newMockRequestService({ + [catalogUri]: { + version: 1, + schemas: [{ + name: 'package.json', + description: 'Package metadata', + fileMatch: ['package.json'], + url: schemaStoreUri + }] + } as JSONSchema, + [schemaStoreUri]: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + }, accesses), + workspaceContext + }); + service.configure({ + schemaStore: { + enable: true, + url: catalogUri, + exclude: ['../package.json', 'config/../package.json', '/workspace/package.json', 'file:///workspace/package.json', 'C:/workspace/package.json', 'foo\\package.json'] + } + }); + const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); + + const diagnostics = await service.doValidation(textDoc, jsonDoc); + const status = service.getLanguageStatus(textDoc, jsonDoc); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].message, 'Incorrect type. Expected "string".'); + assert.deepStrictEqual(status.schemas, [schemaStoreUri]); + assert.deepStrictEqual(accesses, [catalogUri, schemaStoreUri]); + }); + test('SchemaStore catalog request failures are reported in language status', async function () { const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaRequestService: SchemaRequestService = async (): Promise => { From 1ca200625d2e39f357d02f779c3e3f1a34860487 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 21 Jul 2026 11:02:05 -0400 Subject: [PATCH 4/4] Move SchemaStore catalog loading to extension Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c5e9b306-5d8c-4cc8-ac55-1b1175cfcedf --- src/jsonLanguageService.ts | 1 - src/jsonLanguageTypes.ts | 25 +--- src/services/jsonSchemaService.ts | 159 +--------------------- src/test/schema.test.ts | 212 ++++++------------------------ 4 files changed, 45 insertions(+), 352 deletions(-) diff --git a/src/jsonLanguageService.ts b/src/jsonLanguageService.ts index e2873511..4a0c90df 100644 --- a/src/jsonLanguageService.ts +++ b/src/jsonLanguageService.ts @@ -73,7 +73,6 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi configure: (settings: LanguageSettings) => { jsonSchemaService.clearExternalSchemas(); settings.schemas?.forEach(jsonSchemaService.registerExternalSchema.bind(jsonSchemaService)); - jsonSchemaService.configureSchemaStore(settings.schemaStore); jsonValidation.configure(settings); }, resetSchema: (uri: string) => jsonSchemaService.onResourceChange(uri), diff --git a/src/jsonLanguageTypes.ts b/src/jsonLanguageTypes.ts index 77202bcc..c8bb7bb4 100644 --- a/src/jsonLanguageTypes.ts +++ b/src/jsonLanguageTypes.ts @@ -155,27 +155,6 @@ export interface LanguageSettings { * A list of known schemas and/or associations of schemas to file names. */ schemas?: SchemaConfiguration[]; - /** - * Optional support for loading schema associations from the SchemaStore catalog. - * Enabled by default. Requires a schemaRequestService to load the catalog and schemas. - */ - schemaStore?: SchemaStoreSettings; -} - -export interface SchemaStoreSettings { - /** - * If set to false, the language service will not load schema associations from the SchemaStore catalog. - */ - enable?: boolean; - /** - * Relative path selectors for resources that should not receive schema associations from the SchemaStore catalog. - */ - exclude?: string[]; - /** - * Optional URL for a SchemaStore-compatible catalog. - * If not set, https://www.schemastore.org/api/json/catalog.json is used. - */ - url?: string; } export type SeverityLevel = 'error' | 'warning' | 'ignore'; @@ -238,6 +217,10 @@ export interface SchemaConfiguration { * if the document that is validated has the folderUri as parent */ folderUri?: string; + /** + * The origin of this schema association. + */ + source?: 'schemaStore'; } export interface WorkspaceContextService { diff --git a/src/services/jsonSchemaService.ts b/src/services/jsonSchemaService.ts index 14912a03..1c7717e0 100644 --- a/src/services/jsonSchemaService.ts +++ b/src/services/jsonSchemaService.ts @@ -8,7 +8,7 @@ import { JSONSchema, JSONSchemaRef, MergedJSONSchema, DynamicRefInfo, AnchorMaps import { URI } from 'vscode-uri'; import * as Strings from '../utils/strings.js'; import { asSchema, getSchemaDraftFromId, JSONDocument, normalizeId } from '../parser/jsonParser.js'; -import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, MatchingSchema, TextDocument, SchemaConfiguration, SchemaDraft, ErrorCode, Vocabularies, SchemaStoreSettings, JSONLanguageStatusDiagnostic } from '../jsonLanguageTypes.js'; +import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, MatchingSchema, TextDocument, SchemaConfiguration, SchemaDraft, ErrorCode, Vocabularies, JSONLanguageStatusDiagnostic } from '../jsonLanguageTypes.js'; import * as l10n from '@vscode/l10n'; import { createRegex } from '../utils/glob.js'; @@ -74,25 +74,9 @@ export interface ISchemaHandle { getResolvedSchema(): PromiseLike; } -export const defaultSchemaStoreCatalogUrl = 'https://www.schemastore.org/api/json/catalog.json'; - const BANG = '!'; const PATH_SEP = '/'; -interface SchemaStoreCatalogEntry { - name: string; - description: string; - fileMatch?: string[]; - url: string; - versions?: { [version: string]: string }; -} - -interface SchemaStoreCatalog { - $schema?: string; - version: number; - schemas: SchemaStoreCatalogEntry[]; -} - interface IGlobWrapper { regexp: RegExp; include: boolean; @@ -293,14 +277,8 @@ export class JSONSchemaService implements IJSONSchemaService { private callOnDispose: Function[]; private requestService: SchemaRequestService | undefined; private promiseConstructor: PromiseConstructor; - private schemaStoreEnabled: boolean; - private schemaStoreCatalogUrl: string; - private schemaStoreCatalogPromise: PromiseLike | undefined; - private schemaStoreCatalogLoaded: boolean; private schemaStoreSchemaIds: { [id: string]: boolean }; - private schemaStoreCatalogDiagnostics: JSONLanguageStatusDiagnostic[]; private schemaDiagnosticsByResource: { [resource: string]: JSONLanguageStatusDiagnostic[] }; - private schemaStoreExclusions: FilePatternAssociation | undefined; private static traverseSchemaProperties(node: JSONSchema, callback: (schema: JSONSchema) => void): void { // `$defs`/`definitions` are visited first so that a reusable resource they @@ -360,13 +338,8 @@ export class JSONSchemaService implements IJSONSchemaService { this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; - this.schemaStoreEnabled = true; - this.schemaStoreCatalogUrl = defaultSchemaStoreCatalogUrl; - this.schemaStoreCatalogLoaded = false; this.schemaStoreSchemaIds = {}; - this.schemaStoreCatalogDiagnostics = []; this.schemaDiagnosticsByResource = {}; - this.schemaStoreExclusions = undefined; } public getRegisteredSchemaIds(filter?: (scheme: string) => boolean): string[] { @@ -456,6 +429,9 @@ export class JSONSchemaService implements IJSONSchemaService { if (config.fileMatch && config.fileMatch.length) { this.addFilePatternAssociation(config.fileMatch, config.folderUri, [id]); } + if (config.source === 'schemaStore') { + this.schemaStoreSchemaIds[id] = true; + } return config.schema ? this.addSchemaHandle(id, config.schema) : this.getOrAddSchemaHandle(id); } @@ -464,10 +440,7 @@ export class JSONSchemaService implements IJSONSchemaService { this.filePatternAssociations = []; this.registeredSchemasIds = {}; this.cachedSchemaForResource = undefined; - this.schemaStoreCatalogPromise = undefined; - this.schemaStoreCatalogLoaded = false; this.schemaStoreSchemaIds = {}; - this.schemaStoreCatalogDiagnostics = []; this.schemaDiagnosticsByResource = {}; for (const id in this.contributionSchemas) { @@ -479,42 +452,8 @@ export class JSONSchemaService implements IJSONSchemaService { } } - public configureSchemaStore(settings: SchemaStoreSettings | undefined): void { - this.schemaStoreEnabled = settings?.enable !== false; - this.schemaStoreCatalogUrl = normalizeId(settings?.url || defaultSchemaStoreCatalogUrl); - this.schemaStoreCatalogPromise = undefined; - this.schemaStoreCatalogLoaded = false; - this.schemaStoreSchemaIds = {}; - this.schemaStoreCatalogDiagnostics = []; - this.schemaDiagnosticsByResource = {}; - this.schemaStoreExclusions = this.createSchemaStoreExclusions(settings?.exclude); - this.cachedSchemaForResource = undefined; - } - - private createSchemaStoreExclusions(exclusions: string[] | undefined): FilePatternAssociation | undefined { - if (!exclusions) { - return undefined; - } - const sanitizedExclusions = exclusions.map(exclusion => this.sanitizeSchemaStoreExclusion(exclusion)).filter((exclusion): exclusion is string => !!exclusion); - return sanitizedExclusions.length ? new FilePatternAssociation(sanitizedExclusions, undefined, []) : undefined; - } - - private sanitizeSchemaStoreExclusion(exclusion: string): string | undefined { - if (typeof exclusion !== 'string' || !exclusion || exclusion[0] === BANG || exclusion[0] === PATH_SEP || exclusion.indexOf('\\') !== -1 || exclusion.indexOf(':') !== -1 || exclusion.indexOf('..') !== -1) { - return undefined; - } - return exclusion; - } - - private matchesSchemaStoreExclusion(resource: string): boolean { - return !!this.schemaStoreExclusions?.matchesPattern(normalizeResourceForMatching(resource)); - } - public getSchemaDiagnosticsForResource(resource: string): JSONLanguageStatusDiagnostic[] | undefined { const diagnostics: JSONLanguageStatusDiagnostic[] = []; - if (this.schemaStoreEnabled) { - diagnostics.push(...this.schemaStoreCatalogDiagnostics); - } const resourceDiagnostics = this.schemaDiagnosticsByResource[resource]; if (resourceDiagnostics) { diagnostics.push(...resourceDiagnostics); @@ -522,30 +461,6 @@ export class JSONSchemaService implements IJSONSchemaService { return diagnostics.length ? diagnostics : undefined; } - private setSchemaStoreCatalogDiagnostic(message: string, code: ErrorCode = ErrorCode.SchemaResolveError): void { - this.schemaStoreCatalogDiagnostics = [{ - message, - code, - severity: 'warning', - uri: this.schemaStoreCatalogUrl - }]; - } - - private getErrorMessage(error: any): string { - if (error && typeof error.message === 'string') { - return error.message; - } - let errorMessage = error?.toString ? error.toString() as string : String(error); - const errorSplit = errorMessage.split('Error: '); - if (errorSplit.length > 1) { - errorMessage = errorSplit[1]; - } - if (Strings.endsWith(errorMessage, '.')) { - errorMessage = errorMessage.substr(0, errorMessage.length - 1); - } - return errorMessage; - } - private toStatusDiagnostics(diagnostics: readonly SchemaDiagnostic[], uri: string | undefined): JSONLanguageStatusDiagnostic[] { return diagnostics.map(diagnostic => ({ message: diagnostic.message, @@ -555,61 +470,6 @@ export class JSONSchemaService implements IJSONSchemaService { })); } - private loadSchemaStoreCatalog(): PromiseLike { - if (!this.schemaStoreEnabled || this.schemaStoreCatalogLoaded) { - return this.promise.resolve(undefined); - } - if (!this.requestService) { - this.schemaStoreCatalogLoaded = true; - return this.promise.resolve(undefined); - } - if (!this.schemaStoreCatalogPromise) { - this.schemaStoreCatalogDiagnostics = []; - this.schemaStoreCatalogPromise = this.requestService(this.schemaStoreCatalogUrl).then(content => { - if (!content) { - this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to load SchemaStore catalog from \'{0}\': No content.', toDisplayString(this.schemaStoreCatalogUrl))); - this.schemaStoreCatalogLoaded = true; - return; - } - - const jsonErrors: Json.ParseError[] = []; - const catalog = Json.parse(content, jsonErrors) as SchemaStoreCatalog; - if (jsonErrors.length) { - this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to parse SchemaStore catalog from \'{0}\': Parse error at offset {1}.', toDisplayString(this.schemaStoreCatalogUrl), jsonErrors[0].offset)); - this.schemaStoreCatalogLoaded = true; - return; - } - if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.schemas)) { - this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to parse SchemaStore catalog from \'{0}\': Expected a catalog object with a schemas array.', toDisplayString(this.schemaStoreCatalogUrl))); - this.schemaStoreCatalogLoaded = true; - return; - } - - for (const schema of catalog.schemas) { - if (!schema || typeof schema !== 'object' || typeof schema.url !== 'string' || !Array.isArray(schema.fileMatch)) { - continue; - } - - const fileMatch = schema.fileMatch.filter((pattern): pattern is string => typeof pattern === 'string'); - if (fileMatch.length) { - this.registerExternalSchema({ uri: schema.url, fileMatch }); - this.schemaStoreSchemaIds[normalizeId(schema.url)] = true; - } - } - - this.schemaStoreCatalogLoaded = true; - }, error => { - let errorCode = ErrorCode.SchemaResolveError; - if (typeof error?.code === 'number' && error.code < 0x10000) { - errorCode += error.code; - } - this.setSchemaStoreCatalogDiagnostic(l10n.t('Unable to load SchemaStore catalog from \'{0}\': {1}.', toDisplayString(this.schemaStoreCatalogUrl), this.getErrorMessage(error)), errorCode); - this.schemaStoreCatalogLoaded = true; - }); - } - return this.schemaStoreCatalogPromise; - } - public getResolvedSchema(schemaId: string): PromiseLike { const id = normalizeId(schemaId); const schemaHandle = this.schemasById[id]; @@ -1452,11 +1312,7 @@ export class JSONSchemaService implements IJSONSchemaService { } } } - const matchesSchemaStoreExclusion = this.matchesSchemaStoreExclusion(resource); const nonSchemaStoreSchemas = schemas.filter(schemaId => !this.schemaStoreSchemaIds[schemaId]); - if (matchesSchemaStoreExclusion) { - return nonSchemaStoreSchemas; - } return nonSchemaStoreSchemas.length ? nonSchemaStoreSchemas : schemas; } @@ -1501,13 +1357,6 @@ export class JSONSchemaService implements IJSONSchemaService { this.cachedSchemaForResource = { resource, resolvedSchema }; return resolvedSchema; }; - if (this.schemaStoreEnabled && !this.schemaStoreCatalogLoaded) { - const schemas = this.getAssociatedSchemas(resource); - if (schemas.length > 0 || this.matchesSchemaStoreExclusion(resource)) { - return resolveSchema(); - } - return this.loadSchemaStoreCatalog().then(resolveSchema); - } return resolveSchema(); } diff --git a/src/test/schema.test.ts b/src/test/schema.test.ts index d185c7e0..e8ada0a7 100644 --- a/src/test/schema.test.ts +++ b/src/test/schema.test.ts @@ -1180,21 +1180,11 @@ suite('JSON Schema', () => { assert.strictEqual(section?.description, 'Meaning of Life'); }); - test('SchemaStore can be disabled', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + test('SchemaStore source associations validate file matches', async function () { const schemaUri = 'https://www.schemastore.org/package.json'; const accesses: string[] = []; const service = getLanguageService({ schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaUri - }] - } as JSONSchema, [schemaUri]: { type: 'object', properties: { @@ -1204,65 +1194,28 @@ suite('JSON Schema', () => { }, accesses), workspaceContext }); - service.configure({ schemaStore: { enable: false } }); - const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); - - const diagnostics = await service.doValidation(textDoc, jsonDoc); - - assert.deepStrictEqual(diagnostics, []); - assert.deepStrictEqual(accesses, []); - }); - - test('SchemaStore catalog registers file matches when enabled', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; - const schemaUri = 'https://www.schemastore.org/package.json'; - const accesses: string[] = []; - const service = getLanguageService({ - schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaUri - }] - } as JSONSchema, - [schemaUri]: { - type: 'object', - properties: { - name: { type: 'string' } - } - } - }, accesses), - workspaceContext + service.configure({ + schemas: [{ + uri: schemaUri, + fileMatch: ['package.json'], + source: 'schemaStore' + }] }); - service.configure({ schemaStore: { enable: true, url: catalogUri } }); const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); const diagnostics = await service.doValidation(textDoc, jsonDoc); assert.strictEqual(diagnostics.length, 1); assert.strictEqual(diagnostics[0].message, 'Incorrect type. Expected "string".'); - assert.deepStrictEqual(accesses, [catalogUri, schemaUri]); + assert.deepStrictEqual(accesses, [schemaUri]); }); test('$schema takes precedence over SchemaStore file matches', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaStoreUri = 'https://www.schemastore.org/package.json'; const explicitSchemaUri = 'https://example.com/custom-package.schema.json'; const accesses: string[] = []; const service = getLanguageService({ schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaStoreUri - }] - } as JSONSchema, [schemaStoreUri]: { type: 'object', properties: { @@ -1278,7 +1231,13 @@ suite('JSON Schema', () => { }, accesses), workspaceContext }); - service.configure({ schemaStore: { enable: true, url: catalogUri } }); + service.configure({ + schemas: [{ + uri: schemaStoreUri, + fileMatch: ['package.json'], + source: 'schemaStore' + }] + }); const { textDoc, jsonDoc } = toDocument(`{ "$schema": "${explicitSchemaUri}", "name": 1 }`, undefined, 'file:///workspace/package.json'); const diagnostics = await service.doValidation(textDoc, jsonDoc); @@ -1290,21 +1249,11 @@ suite('JSON Schema', () => { }); test('Configured schemas take precedence over SchemaStore file matches', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaStoreUri = 'https://www.schemastore.org/package.json'; const configuredSchemaUri = 'https://example.com/configured-package.schema.json'; const accesses: string[] = []; const service = getLanguageService({ schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaStoreUri - }] - } as JSONSchema, [schemaStoreUri]: { type: 'object', properties: { @@ -1315,8 +1264,11 @@ suite('JSON Schema', () => { workspaceContext }); service.configure({ - schemaStore: { enable: true, url: catalogUri }, schemas: [{ + uri: schemaStoreUri, + fileMatch: ['package.json'], + source: 'schemaStore' + }, { uri: configuredSchemaUri, fileMatch: ['package.json'], schema: { @@ -1337,21 +1289,11 @@ suite('JSON Schema', () => { assert.deepStrictEqual(accesses, []); }); - test('SchemaStore exclusions prevent only SchemaStore file matches', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; + test('SchemaStore source associations support exclusion file matches', async function () { const schemaStoreUri = 'https://www.schemastore.org/package.json'; const accesses: string[] = []; const service = getLanguageService({ schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaStoreUri - }] - } as JSONSchema, [schemaStoreUri]: { type: 'object', properties: { @@ -1362,11 +1304,11 @@ suite('JSON Schema', () => { workspaceContext }); service.configure({ - schemaStore: { - enable: true, - url: catalogUri, - exclude: ['**/package.json'] - } + schemas: [{ + uri: schemaStoreUri, + fileMatch: ['package.json', '!**/package.json'], + source: 'schemaStore' + }] }); const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); @@ -1379,21 +1321,11 @@ suite('JSON Schema', () => { }); test('SchemaStore exclusions do not prevent configured schemas', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaStoreUri = 'https://www.schemastore.org/package.json'; const configuredSchemaUri = 'https://example.com/configured-package.schema.json'; const accesses: string[] = []; const service = getLanguageService({ schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaStoreUri - }] - } as JSONSchema, [schemaStoreUri]: { type: 'object', properties: { @@ -1404,12 +1336,11 @@ suite('JSON Schema', () => { workspaceContext }); service.configure({ - schemaStore: { - enable: true, - url: catalogUri, - exclude: ['package.json'] - }, schemas: [{ + uri: schemaStoreUri, + fileMatch: ['package.json', '!package.json'], + source: 'schemaStore' + }, { uri: configuredSchemaUri, fileMatch: ['package.json'], schema: { @@ -1430,86 +1361,17 @@ suite('JSON Schema', () => { assert.deepStrictEqual(accesses, []); }); - test('SchemaStore exclusions ignore absolute and parent-relative selectors', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; - const schemaStoreUri = 'https://www.schemastore.org/package.json'; - const accesses: string[] = []; - const service = getLanguageService({ - schemaRequestService: newMockRequestService({ - [catalogUri]: { - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaStoreUri - }] - } as JSONSchema, - [schemaStoreUri]: { - type: 'object', - properties: { - name: { type: 'string' } - } - } - }, accesses), - workspaceContext - }); - service.configure({ - schemaStore: { - enable: true, - url: catalogUri, - exclude: ['../package.json', 'config/../package.json', '/workspace/package.json', 'file:///workspace/package.json', 'C:/workspace/package.json', 'foo\\package.json'] - } - }); - const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); - - const diagnostics = await service.doValidation(textDoc, jsonDoc); - const status = service.getLanguageStatus(textDoc, jsonDoc); - - assert.strictEqual(diagnostics.length, 1); - assert.strictEqual(diagnostics[0].message, 'Incorrect type. Expected "string".'); - assert.deepStrictEqual(status.schemas, [schemaStoreUri]); - assert.deepStrictEqual(accesses, [catalogUri, schemaStoreUri]); - }); - - test('SchemaStore catalog request failures are reported in language status', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; - const schemaRequestService: SchemaRequestService = async (): Promise => { - return Promise.reject({ message: 'Blocked by schema allow-list', code: 1 }); - }; - const service = getLanguageService({ schemaRequestService, workspaceContext }); - service.configure({ schemaStore: { enable: true, url: catalogUri } }); - const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); - - const diagnostics = await service.doValidation(textDoc, jsonDoc); - const status = service.getLanguageStatus(textDoc, jsonDoc); - - assert.deepStrictEqual(diagnostics, []); - assert.strictEqual(status.schemaDiagnostics?.length, 1); - assertInMessage(status.schemaDiagnostics?.[0].message, 'Blocked by schema allow-list'); - assert.strictEqual(status.schemaDiagnostics?.[0].uri, catalogUri); - assert.strictEqual(status.schemaDiagnostics?.[0].severity, 'warning'); - }); - test('SchemaStore schema request failures are reported in diagnostics and language status', async function () { - const catalogUri = 'https://www.schemastore.org/api/json/catalog.json'; const schemaUri = 'https://thirdparty.example/package.schema.json'; - const schemaRequestService: SchemaRequestService = async (uri: string): Promise => { - if (uri === catalogUri) { - return JSON.stringify({ - version: 1, - schemas: [{ - name: 'package.json', - description: 'Package metadata', - fileMatch: ['package.json'], - url: schemaUri - }] - }); - } - return Promise.reject({ message: 'Blocked by schema allow-list', code: 1 }); - }; + const schemaRequestService: SchemaRequestService = async (): Promise => Promise.reject({ message: 'Blocked by schema allow-list', code: 1 }); const service = getLanguageService({ schemaRequestService, workspaceContext }); - service.configure({ schemaStore: { enable: true, url: catalogUri } }); + service.configure({ + schemas: [{ + uri: schemaUri, + fileMatch: ['package.json'], + source: 'schemaStore' + }] + }); const { textDoc, jsonDoc } = toDocument('{ "name": 1 }', undefined, 'file:///workspace/package.json'); const diagnostics = await service.doValidation(textDoc, jsonDoc, { schemaRequest: 'error' });