Skip to content
Open
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
4 changes: 3 additions & 1 deletion language-server/src/build-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Server } from "./services/server.ts";
import { JsonDocuments } from "./services/JsonDocuments.ts";
import { SchemaStore } from "./services/SchemaStore.ts";
import { Diagnostics } from "./features/Diagnostics.ts";
import { SyntaxValidation } from "./features/SyntaxValidation.ts";
import { SchemaValidation } from "./features/SchemaValidation.ts";
Expand All @@ -17,8 +18,9 @@ export type LanguageServerSettings = {

export const buildServer = (connection: Connection): Connection => {
const server = new Server(connection);
const schemaStore = new SchemaStore(server);

const documents = new JsonDocuments(server);
const documents = new JsonDocuments(server, schemaStore);
documents.listen(server);

new Diagnostics(server, documents, [
Expand Down
35 changes: 28 additions & 7 deletions language-server/src/features/Diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Server } from "../services/server.ts";
import { JsonDocuments } from "../services/JsonDocuments.ts";
import { JsonDocument } from "../models/JsonDocument.ts";
import { normalizeIri } from "@hyperjump/uri";

import type { Diagnostic } from "vscode-languageserver";

Expand All @@ -9,21 +10,41 @@ export type DiagnosticsProvider = {
};

export class Diagnostics {
private server: Server;
private providers: DiagnosticsProvider[];

constructor(server: Server, documents: JsonDocuments, providers: DiagnosticsProvider[]) {
this.server = server;
this.providers = providers;

documents.onDidChangeContent(async (change) => {
const diagnostics = [];
for (const provider of this.providers) {
diagnostics.push(...await provider.getDiagnostics(change.document));
await this.sendDiagnostics(change.document);
});

server.onDidChangeWatchedFiles(async (params) => {
const changedUris = new Set<string>();
for (const change of params.changes) {
changedUris.add(normalizeIri(change.uri));
}

for (const document of documents.all()) {
if (document.dependsOn(changedUris)) {
document.validateSchema();
await this.sendDiagnostics(document);
}
}
});
}

private async sendDiagnostics(document: JsonDocument) {
const diagnostics = [];
for (const provider of this.providers) {
diagnostics.push(...await provider.getDiagnostics(document));
}

await server.sendDiagnostics({
uri: change.document.uri,
diagnostics: diagnostics
});
await this.server.sendDiagnostics({
uri: document.uri,
diagnostics: diagnostics
});
}
}
151 changes: 148 additions & 3 deletions language-server/src/features/SchemaValidation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest";
import { describe, test, expect, afterEach, beforeEach } from "vitest";
import { TestClient } from "../test/test-client.ts";
import { unregisterSchema } from "@hyperjump/json-schema";

Expand All @@ -8,12 +8,12 @@
let client: TestClient;
let fixtureSchemaUri: string;

beforeAll(async () => {
beforeEach(async () => {
client = new TestClient();
await client.start();
});

afterAll(async () => {
afterEach(async () => {
await client.stop();
});

Expand All @@ -21,7 +21,7 @@
unregisterSchema(fixtureSchemaUri);
});

test("JSON Validation using Hyperjump - Valid Case", async () => {

Check failure on line 24 in language-server/src/features/SchemaValidation.test.ts

View workflow job for this annotation

GitHub Actions / build (windows-latest)

src/features/SchemaValidation.test.ts > Schema Validation > JSON Validation using Hyperjump - Valid Case

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ src/features/SchemaValidation.test.ts:24:3
const diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
resolve(params.diagnostics);
Expand Down Expand Up @@ -322,4 +322,149 @@
const diagnostics2 = await diagnosticsPromise2;
expect(diagnostics2).toHaveLength(0);
});

test("changing the schema should invalidate the cache", async () => {
const diagnosticsPromise1 = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
resolve(params.diagnostics);
});
});

fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" }
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "${fixtureSchemaUri}",
"name": "Alice",
"age" : "not a number"
}`);
const instanceUri = await client.openDocument("instance.json");

const diagnostics1 = await diagnosticsPromise1;
expect(diagnostics1).toHaveLength(1);

const diagnosticsPromise2 = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params) => {
if (params.uri === instanceUri) {
resolve(params.diagnostics);
}
});
});

await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "string" }
}
}`);

const diagnostics2 = await diagnosticsPromise2;
expect(diagnostics2).toHaveLength(0);
});

test("changing a referenced schema revalidates dependents", async () => {
const diagnosticsPromise1 = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
resolve(params.diagnostics);
});
});

const referencedSchema = await client.writeDocument("B.schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number"
}`);

fixtureSchemaUri = await client.writeDocument("A.schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"age": { "$ref": "${referencedSchema}" }
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "${fixtureSchemaUri}",
"age": "not a number"
}`);
const instanceUri = await client.openDocument("instance.json");

const diagnostics1 = await diagnosticsPromise1;
expect(diagnostics1).toHaveLength(1);

const diagnosticsPromise2 = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params) => {
if (params.uri === instanceUri) {
resolve(params.diagnostics);
}
});
});

await client.writeDocument("B.schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string"
}`);

const diagnostics2 = await diagnosticsPromise2;
expect(diagnostics2).toHaveLength(0);
});

test("JSON Validation using Hyperjump - Relative $schema case", async () => {
const diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
resolve(params.diagnostics);
});
});

fixtureSchemaUri = await client.writeDocument("schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" }
}
}`);

await client.writeDocument("instance.json", `{
"$schema": "schema.json",
"name": 1234
}`);
await client.openDocument("instance.json");

const diagnostics = await diagnosticsPromise;
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].message).toMatch(/Expected a.*string/);
});

test("changing a watched file should not revalidate documents with no $schema", async () => {
const DiagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
resolve(params.diagnostics);
});
});

await client.writeDocument("plain.json", `{ "foo": "bar" }`);
const plainUri = await client.openDocument("plain.json");
await DiagnosticsPromise;

let revalidated = false;
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
if (params.uri === plainUri) {
revalidated = true;
}
});

fixtureSchemaUri = await client.writeDocument("unrelated.schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
}`);

expect(revalidated).toBe(false);
});
});
55 changes: 23 additions & 32 deletions language-server/src/features/SchemaValidation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver";
import { validate } from "@hyperjump/json-schema-errors";
import { JsonDocument } from "../models/JsonDocument.ts";

import type { ErrorObject } from "@hyperjump/json-schema-errors";
Expand All @@ -9,39 +8,31 @@ export class SchemaValidation implements DiagnosticsProvider {
async getDiagnostics(jsonDocument: JsonDocument) {
const schemaDiagnostics: Diagnostic[] = [];

const schemaNode = jsonDocument.findNodeAtPointer("/$schema");
const schemaUri = schemaNode?.value;

// skip schema validation if there are syntax errors hence the parseError.length check
if (schemaUri && jsonDocument.getParseErrors().length === 0) {
let instance = JSON.parse(jsonDocument.getText());
try {
const result = await validate(schemaUri, instance);

if (!result.valid) {
const errors = result.errors;
errors.forEach((error) => {
const pointer = decodeURIComponent(error.instanceLocation.slice(1));
const node = jsonDocument.findNodeAtPointer(pointer);

if (node) {
schemaDiagnostics.push({
severity: DiagnosticSeverity.Error,
range: {
start: jsonDocument.positionAt(node.offset),
end: jsonDocument.positionAt(node.offset + node.length)
},
message: formatError(error),
source: "hyperjump-json-language-server"
});
}
});
}
} catch (_error: unknown) {
Comment thread
srivastava-diya marked this conversation as resolved.
// TODO: Handle invalid or missing schema errors
try {
const result = await jsonDocument.getSchemaErrors();

if (result?.valid === false) {
const errors = result.errors;
errors.forEach((error) => {
const pointer = decodeURIComponent(error.instanceLocation.slice(1));
const node = jsonDocument.findNodeAtPointer(pointer);

if (node) {
schemaDiagnostics.push({
severity: DiagnosticSeverity.Error,
range: {
start: jsonDocument.positionAt(node.offset),
end: jsonDocument.positionAt(node.offset + node.length)
},
message: formatError(error),
source: "hyperjump-json-language-server"
});
}
});
}
} catch (_error: unknown) {
// TODO: Handle invalid or missing schema errors
}

return schemaDiagnostics;
}
}
Expand Down
6 changes: 3 additions & 3 deletions language-server/src/features/SyntaxValidation.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { describe, test, expect, beforeAll, afterAll } from "vitest";
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { TestClient } from "../test/test-client.ts";

describe("Syntax Validation", () => {
let client: TestClient;

beforeAll(async () => {
beforeEach(async () => {
client = new TestClient();
await client.start();
});

afterAll(async () => {
afterEach(async () => {
await client.stop();
});

Expand Down
Loading
Loading