Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
154 changes: 137 additions & 17 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
JSONSchemaService,
SchemaDependencies,
ISchemaContributions,
SchemaHandle,
} from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';

import { URI } from 'vscode-uri';
Expand All @@ -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();
Expand Down Expand Up @@ -160,11 +160,9 @@ export class YAMLSchemaService extends JSONSchemaService {
return result;
}

async resolveSchemaContent(
schemaToResolve: UnresolvedSchema,
schemaURL: string,
dependencies: SchemaDependencies
): Promise<ResolvedSchema> {
async resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaHandle: SchemaHandle): Promise<ResolvedSchema> {
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;
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -438,6 +436,7 @@ export class YAMLSchemaService extends JSONSchemaService {
(schemas) => {
return {
errors: [],
warnings: [],
schema: {
allOf: schemas.map((schemaObj) => {
return schemaObj.schema;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -711,25 +720,44 @@ 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 {
super.clearExternalSchemas();
}

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
Expand All @@ -738,14 +766,62 @@ export class YAMLSchemaService extends JSONSchemaService {
}

getResolvedSchema(schemaId: string): Promise<ResolvedSchema> {
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);
Expand All @@ -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<string, JSONSchema> | undefined;
private resolvedSchema: Promise<ResolvedSchema> | undefined;
private unresolvedSchema: Promise<UnresolvedSchema> | 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<UnresolvedSchema> {
if (!this.unresolvedSchema) {
this.unresolvedSchema = this.service.loadSchema(this.uri);
}
return this.unresolvedSchema;
}

public getResolvedSchema(): Promise<ResolvedSchema> {
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;
}
}
8 changes: 5 additions & 3 deletions src/languageservice/yamlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/jsonParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -1483,6 +1485,7 @@ describe('JSON Parser', () => {
assert.strictEqual(semanticErrors.length, 1);
}
schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'array',
items: [
{
Expand Down
22 changes: 13 additions & 9 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ describe('JSON Schema', () => {
},
};

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

service
.getSchemaForResource('test.json', undefined)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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'],
},
},
},
});
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading
Loading