From 613da727899ad07e59b0c592e6e14fcac6e58049 Mon Sep 17 00:00:00 2001 From: Diya Date: Sat, 30 May 2026 02:24:06 +0530 Subject: [PATCH 1/4] feat: add schema validation with human-readable error messages --- language-server/package-lock.json | 27 ++++ language-server/package.json | 1 + .../src/features/JsonValidation.ts | 28 ++-- .../src/features/SchemaValidation.ts | 92 ++++++++++++ .../src/features/SyntaxValidation.ts | 15 ++ language-server/src/language-server.test.ts | 141 +++++++++++++++++- 6 files changed, 291 insertions(+), 13 deletions(-) create mode 100644 language-server/src/features/SchemaValidation.ts create mode 100644 language-server/src/features/SyntaxValidation.ts diff --git a/language-server/package-lock.json b/language-server/package-lock.json index 0659212..2e77cc5 100644 --- a/language-server/package-lock.json +++ b/language-server/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@hyperjump/json-schema": "^1.17.6", + "@hyperjump/json-schema-errors": "github:hyperjump-io/json-schema-errors", "jsonc-parser": "^3.3.1", "merge-anything": "^6.0.6", "vscode-languageserver": "^9.0.1", @@ -17,6 +18,15 @@ "vscode-uri": "^3.1.0" } }, + "node_modules/@fluent/bundle": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.19.1.tgz", + "integrity": "sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==", + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@hyperjump/browser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.3.1.tgz", @@ -67,6 +77,23 @@ "@hyperjump/browser": "^1.1.0" } }, + "node_modules/@hyperjump/json-schema-errors": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/hyperjump-io/json-schema-errors.git#52de8c3c78e4514eac3a525399b323544d4b533c", + "license": "MIT", + "dependencies": { + "@fluent/bundle": "^0.19.1", + "@hyperjump/json-pointer": "^1.1.1", + "@hyperjump/json-schema": "^1.17.2", + "@hyperjump/pact": "^1.4.0", + "@hyperjump/uri": "^1.3.2", + "json-stringify-deterministic": "^1.0.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/hyperjump-io" + } + }, "node_modules/@hyperjump/json-schema-formats": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@hyperjump/json-schema-formats/-/json-schema-formats-1.0.1.tgz", diff --git a/language-server/package.json b/language-server/package.json index 22603d3..7f8d870 100644 --- a/language-server/package.json +++ b/language-server/package.json @@ -20,6 +20,7 @@ "keywords": [], "dependencies": { "@hyperjump/json-schema": "^1.17.6", + "@hyperjump/json-schema-errors": "github:hyperjump-io/json-schema-errors", "jsonc-parser": "^3.3.1", "merge-anything": "^6.0.6", "vscode-languageserver": "^9.0.1", diff --git a/language-server/src/features/JsonValidation.ts b/language-server/src/features/JsonValidation.ts index 7a4ab26..5685545 100644 --- a/language-server/src/features/JsonValidation.ts +++ b/language-server/src/features/JsonValidation.ts @@ -1,9 +1,13 @@ -import { TextDocuments, TextDocumentSyncKind, Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; +import { TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import * as jsonc from "jsonc-parser"; import { Server } from "../services/server.ts"; +import { getSyntaxDiagnostics } from "./SyntaxValidation.ts"; import type { ServerCapabilities } from "vscode-languageserver"; +import { getSchemaDiagnostics, type MatchingSchemaCollector } from "./SchemaValidation.ts"; + +const documentMap = new Map(); export class JsonValidation { constructor(server: Server, documents: TextDocuments) { @@ -17,27 +21,27 @@ export class JsonValidation { }; }); + // single onDidChangeContent call to prevent overwriting by multiple features documents.onDidChangeContent(async (change) => { const textDocument = change.document; const text = textDocument.getText(); const parseErrors: jsonc.ParseError[] = []; - jsonc.parseTree(text, parseErrors); + const tree = jsonc.parseTree(text, parseErrors); // for syntax errors - const syntaxDiagnostics: Diagnostic[] = parseErrors.map((error) => ({ - severity: DiagnosticSeverity.Error, - range: { - start: textDocument.positionAt(error.offset), - end: textDocument.positionAt(error.offset + error.length) - }, - message: jsonc.printParseErrorCode(error.error), - source: "json-language-server" - })); + const syntaxDiagnostics = getSyntaxDiagnostics(textDocument, parseErrors); + + // for schema validation errors + const schemaNode = tree ? jsonc.findNodeAtLocation(tree, ["$schema"]) : undefined; + const schemaUri = schemaNode?.value; + const schemaDiagnostics = schemaUri && parseErrors.length === 0 && tree + ? await getSchemaDiagnostics(textDocument, tree, schemaUri, documentMap) + : []; void server.sendDiagnostics({ uri: textDocument.uri, - diagnostics: [...syntaxDiagnostics] + diagnostics: [...syntaxDiagnostics, ...schemaDiagnostics] }); }); } diff --git a/language-server/src/features/SchemaValidation.ts b/language-server/src/features/SchemaValidation.ts new file mode 100644 index 0000000..fb8f691 --- /dev/null +++ b/language-server/src/features/SchemaValidation.ts @@ -0,0 +1,92 @@ +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import * as jsonc from "jsonc-parser"; +import { validate } from "@hyperjump/json-schema/draft-2020-12"; +import { BASIC } from "@hyperjump/json-schema/experimental"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import { jsonSchemaErrors } from "@hyperjump/json-schema-errors"; + +import type { Node } from "@hyperjump/json-schema/experimental"; +import type { ErrorObject } from "@hyperjump/json-schema-errors"; + +export class MatchingSchemaCollector { + matches: Map; + + constructor() { + this.matches = new Map(); + } + + afterKeyword(node: Node, instance: any, keywordContext: any, valid: any, schemaContext: any, keyword: any) { + const [keywordId, schemaUri, keywordValue] = node; + const instanceLocation = Instance.uri(instance); + + const skipKeyword = new Set([ + "https://json-schema.org/keyword/allOf", + "https://json-schema.org/keyword/anyOf", + "https://json-schema.org/keyword/oneOf" + ]); + + if (!this.matches.has(instanceLocation)) { + this.matches.set(instanceLocation, []); + } + if (!skipKeyword.has(keywordId)) { + this.matches.get(instanceLocation)?.push({ + keywordId, + schemaUri, + keywordValue, + value: instance.value, + valid, + keywordContext, + schemaContext, + keyword + }); + } + } +} + +export const getSchemaDiagnostics = async (textDocument: TextDocument, tree: jsonc.Node, schemaUri: string, documentMap: Map): Promise => { + const text = textDocument.getText(); + const schemaDiagnostics: Diagnostic[] = []; + + if (schemaUri) { + let instance = JSON.parse(text); + const collector = new MatchingSchemaCollector(); + const result = await validate(schemaUri, instance, { + outputFormat: BASIC, + plugins: [collector] + }); + + documentMap.set(textDocument.uri, collector); + + if (!result.valid) { + const errors = await jsonSchemaErrors(result, schemaUri, instance); + errors.forEach((error) => { + const path = error.instanceLocation === "#" ? [] : error.instanceLocation.slice(2).split("/"); + const node = tree ? jsonc.findNodeAtLocation(tree, path) : undefined; + + if (node) { + schemaDiagnostics.push({ + severity: DiagnosticSeverity.Error, + range: { + start: textDocument.positionAt(node.offset), + end: textDocument.positionAt(node.offset + node.length) + }, + message: formatError(error), + source: "json-language-server" + }); + } + }); + } + } + return schemaDiagnostics; +}; + +const formatError = (error: ErrorObject, depth = 0): string => { + let msg = error.message; + if (error.alternatives && error.alternatives.length > 0) { + const indent = " ".repeat(depth + 1); + const lines = error.alternatives.flatMap((alt) => alt.map((subErr) => `${indent}- ${formatError(subErr, depth + 1)}`)); + msg += `:\n${lines.join("\n")}`; + } + return msg; +}; diff --git a/language-server/src/features/SyntaxValidation.ts b/language-server/src/features/SyntaxValidation.ts new file mode 100644 index 0000000..4bdc36f --- /dev/null +++ b/language-server/src/features/SyntaxValidation.ts @@ -0,0 +1,15 @@ +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import * as jsonc from "jsonc-parser"; + +export const getSyntaxDiagnostics = (textDocument: TextDocument, parseErrors: jsonc.ParseError[]): Diagnostic[] => { + return parseErrors.map((error) => ({ + severity: DiagnosticSeverity.Error, + range: { + start: textDocument.positionAt(error.offset), + end: textDocument.positionAt(error.offset + error.length) + }, + message: jsonc.printParseErrorCode(error.error), + source: "json-language-server" + })); +}; diff --git a/language-server/src/language-server.test.ts b/language-server/src/language-server.test.ts index e56838e..4b72636 100644 --- a/language-server/src/language-server.test.ts +++ b/language-server/src/language-server.test.ts @@ -1,9 +1,11 @@ -import { describe, test, expect, beforeAll, afterAll } from "vitest"; +import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest"; import { TextDocumentSyncKind } from "vscode-languageserver"; import { TestClient } from "./test/test-client.ts"; +import { registerSchema, unregisterSchema } from "@hyperjump/json-schema"; describe("JSON Language Server", () => { let client: TestClient; + const fixtureSchemaUri = "https://example.com/person"; beforeAll(async () => { client = new TestClient(); @@ -14,6 +16,10 @@ describe("JSON Language Server", () => { await client.stop(); }); + afterEach(() => { + unregisterSchema(fixtureSchemaUri); + }); + test("textDocumentSync = Incremental", () => { expect(client.serverCapabilities?.textDocumentSync).to.equal(TextDocumentSyncKind.Incremental); }); @@ -45,4 +51,137 @@ describe("JSON Language Server", () => { const diagnostics = await diagnosticsPromise; expect(diagnostics).toHaveLength(0); }); + + test("JSON Validation using Hyperjump - Valid Case", async () => { + const diagnosticsPromise = new Promise((resolve) => { + client.onNotification("textDocument/publishDiagnostics", (params) => { + resolve(params.diagnostics); + }); + }); + + const testSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" } + } + }; + + registerSchema(testSchema, fixtureSchemaUri); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "name": "Alice", + "age" : 39 + }`); + await client.openDocument("instance.json"); + + const diagnostics = await diagnosticsPromise; + expect(diagnostics).toHaveLength(0); + }); + + test("JSON Validation using Hyperjump - Invalid Case", async () => { + const diagnosticsPromise = new Promise((resolve) => { + client.onNotification("textDocument/publishDiagnostics", (params) => { + resolve(params.diagnostics); + }); + }); + + const testSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" } + } + }; + + registerSchema(testSchema, fixtureSchemaUri); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "name": 1234, + "age" : "hello" + }`); + await client.openDocument("instance.json"); + + const diagnostics = (await diagnosticsPromise) as any[]; + expect(diagnostics).toHaveLength(2); + const messages = diagnostics.map((d) => d.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringMatching(/Expected a.*string/), + expect.stringMatching(/Expected a.*number/) + ]) + ); + }); + + test("JSON Validation using Hyperjump - anyOf Formatting Case", async () => { + const diagnosticsPromise = new Promise((resolve) => { + client.onNotification("textDocument/publishDiagnostics", (params) => { + resolve(params.diagnostics); + }); + }); + + const testSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + value: { + anyOf: [ + { type: "string" }, + { type: "number" } + ] + } + } + }; + + registerSchema(testSchema, fixtureSchemaUri); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "value": true + }`); + await client.openDocument("instance.json"); + + const diagnostics = (await diagnosticsPromise) as any[]; + expect(diagnostics).toHaveLength(1); + const cleanMessage = diagnostics[0].message.replace(/[\u2068\u2069]/g, ""); + expect(cleanMessage).toBe("Expected the value to match at least one alternative:\n - Expected a string\n - Expected a number"); + }); + + test("JSON Validation using Hyperjump - oneOf Formatting Case", async () => { + const diagnosticsPromise = new Promise((resolve) => { + client.onNotification("textDocument/publishDiagnostics", (params) => { + resolve(params.diagnostics); + }); + }); + + const testSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + value: { + oneOf: [ + { type: "string" }, + { type: "number" } + ] + } + } + }; + + registerSchema(testSchema, fixtureSchemaUri); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "value": true + }`); + await client.openDocument("instance.json"); + + const diagnostics = (await diagnosticsPromise) as any[]; + expect(diagnostics).toHaveLength(1); + const cleanMessage = diagnostics[0].message.replace(/[\u2068\u2069]/g, ""); + expect(cleanMessage).toBe("Expected the value to match exactly one alternative, but none matched:\n - Expected a string\n - Expected a number"); + }); }); From cf89b5c6d6ba8e027f639aaf37c6d04ffbd1dba5 Mon Sep 17 00:00:00 2001 From: Diya Srivastava Date: Sat, 30 May 2026 18:38:34 +0530 Subject: [PATCH 2/4] Clarify onDidChangeContent comment in JsonValidation Updated comment for clarity on onDidChangeContent handler. --- language-server/src/features/JsonValidation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language-server/src/features/JsonValidation.ts b/language-server/src/features/JsonValidation.ts index 5685545..284bc82 100644 --- a/language-server/src/features/JsonValidation.ts +++ b/language-server/src/features/JsonValidation.ts @@ -21,7 +21,7 @@ export class JsonValidation { }; }); - // single onDidChangeContent call to prevent overwriting by multiple features + // Single onDidChangeContent handler to ensure one combined sendDiagnostics call per change, preventing multiple diagnostic pushes per keystroke from separate features documents.onDidChangeContent(async (change) => { const textDocument = change.document; const text = textDocument.getText(); From 1e514957ee40b01702bf915279af08834f7df9d2 Mon Sep 17 00:00:00 2001 From: Diya Date: Mon, 1 Jun 2026 22:48:31 +0530 Subject: [PATCH 3/4] refactor: add Diagnostics provider --- language-server/src/build-server.ts | 7 +- language-server/src/features/Diagnostics.ts | 56 +++++++ .../src/features/JsonValidation.ts | 48 ------ .../src/features/SchemaValidation.ts | 92 ------------ .../src/features/SyntaxValidation.ts | 31 ++-- language-server/src/language-server.test.ts | 141 +----------------- 6 files changed, 82 insertions(+), 293 deletions(-) create mode 100644 language-server/src/features/Diagnostics.ts delete mode 100644 language-server/src/features/JsonValidation.ts delete mode 100644 language-server/src/features/SchemaValidation.ts diff --git a/language-server/src/build-server.ts b/language-server/src/build-server.ts index c36d51a..522de59 100644 --- a/language-server/src/build-server.ts +++ b/language-server/src/build-server.ts @@ -2,7 +2,8 @@ import "@hyperjump/json-schema/draft-2020-12"; import { TextDocuments } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Server } from "./services/server.ts"; -import { JsonValidation } from "./features/JsonValidation.ts"; +import { Diagnostics } from "./features/Diagnostics.ts"; +import { SyntaxValidation } from "./features/SyntaxValidation.ts"; import type { Connection } from "vscode-languageserver"; @@ -15,7 +16,9 @@ export const buildServer = (connection: Connection): Connection => { const documents = new TextDocuments(TextDocument); documents.listen(server); - new JsonValidation(server, documents); + new Diagnostics(server, documents, [ + new SyntaxValidation() + ]); return server; }; diff --git a/language-server/src/features/Diagnostics.ts b/language-server/src/features/Diagnostics.ts new file mode 100644 index 0000000..116313c --- /dev/null +++ b/language-server/src/features/Diagnostics.ts @@ -0,0 +1,56 @@ +import { TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { Server } from "../services/server.ts"; + +import type { ServerCapabilities, Diagnostic } from "vscode-languageserver"; + +export type DiagnosticsProvider = { + getDiagnostics(textDocument: TextDocument): Diagnostic[]; +}; + +export class Diagnostics { + private providers: DiagnosticsProvider[]; + + constructor(server: Server, documents: TextDocuments, providers: DiagnosticsProvider[]) { + this.providers = providers; + + server.onInitialize(() => { + const serverCapabilities: ServerCapabilities = { + textDocumentSync: TextDocumentSyncKind.Incremental + }; + + return { + capabilities: serverCapabilities + }; + }); + + // Single onDidChangeContent handler to ensure one combined sendDiagnostics call per change, preventing multiple diagnostic pushes per keystroke from separate features + documents.onDidChangeContent(async (change) => { + const diagnostics = []; + for (const provider of this.providers) { + diagnostics.push(...provider.getDiagnostics(change.document)); + } + + // const textDocument = change.document; + // const text = textDocument.getText(); + // const parseErrors: jsonc.ParseError[] = []; + + // const tree = jsonc.parseTree(text, parseErrors); + + // // for syntax errors + // const syntaxDiagnostics = getSyntaxDiagnostics(textDocument, parseErrors); + + // // for schema validation errors + // const schemaNode = tree ? jsonc.findNodeAtLocation(tree, ["$schema"]) : undefined; + // const schemaUri = schemaNode?.value; + // const schemaDiagnostics = schemaUri && parseErrors.length === 0 && tree + // ? await getSchemaDiagnostics(textDocument, tree, schemaUri, documentMap) + // : []; + + await server.sendDiagnostics({ + uri: change.document.uri, + diagnostics: diagnostics + }); + }); + } +} diff --git a/language-server/src/features/JsonValidation.ts b/language-server/src/features/JsonValidation.ts deleted file mode 100644 index 284bc82..0000000 --- a/language-server/src/features/JsonValidation.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import * as jsonc from "jsonc-parser"; -import { Server } from "../services/server.ts"; -import { getSyntaxDiagnostics } from "./SyntaxValidation.ts"; - -import type { ServerCapabilities } from "vscode-languageserver"; -import { getSchemaDiagnostics, type MatchingSchemaCollector } from "./SchemaValidation.ts"; - -const documentMap = new Map(); - -export class JsonValidation { - constructor(server: Server, documents: TextDocuments) { - server.onInitialize(() => { - const serverCapabilities: ServerCapabilities = { - textDocumentSync: TextDocumentSyncKind.Incremental - }; - - return { - capabilities: serverCapabilities - }; - }); - - // Single onDidChangeContent handler to ensure one combined sendDiagnostics call per change, preventing multiple diagnostic pushes per keystroke from separate features - documents.onDidChangeContent(async (change) => { - const textDocument = change.document; - const text = textDocument.getText(); - const parseErrors: jsonc.ParseError[] = []; - - const tree = jsonc.parseTree(text, parseErrors); - - // for syntax errors - const syntaxDiagnostics = getSyntaxDiagnostics(textDocument, parseErrors); - - // for schema validation errors - const schemaNode = tree ? jsonc.findNodeAtLocation(tree, ["$schema"]) : undefined; - const schemaUri = schemaNode?.value; - const schemaDiagnostics = schemaUri && parseErrors.length === 0 && tree - ? await getSchemaDiagnostics(textDocument, tree, schemaUri, documentMap) - : []; - - void server.sendDiagnostics({ - uri: textDocument.uri, - diagnostics: [...syntaxDiagnostics, ...schemaDiagnostics] - }); - }); - } -} diff --git a/language-server/src/features/SchemaValidation.ts b/language-server/src/features/SchemaValidation.ts deleted file mode 100644 index fb8f691..0000000 --- a/language-server/src/features/SchemaValidation.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import * as jsonc from "jsonc-parser"; -import { validate } from "@hyperjump/json-schema/draft-2020-12"; -import { BASIC } from "@hyperjump/json-schema/experimental"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { jsonSchemaErrors } from "@hyperjump/json-schema-errors"; - -import type { Node } from "@hyperjump/json-schema/experimental"; -import type { ErrorObject } from "@hyperjump/json-schema-errors"; - -export class MatchingSchemaCollector { - matches: Map; - - constructor() { - this.matches = new Map(); - } - - afterKeyword(node: Node, instance: any, keywordContext: any, valid: any, schemaContext: any, keyword: any) { - const [keywordId, schemaUri, keywordValue] = node; - const instanceLocation = Instance.uri(instance); - - const skipKeyword = new Set([ - "https://json-schema.org/keyword/allOf", - "https://json-schema.org/keyword/anyOf", - "https://json-schema.org/keyword/oneOf" - ]); - - if (!this.matches.has(instanceLocation)) { - this.matches.set(instanceLocation, []); - } - if (!skipKeyword.has(keywordId)) { - this.matches.get(instanceLocation)?.push({ - keywordId, - schemaUri, - keywordValue, - value: instance.value, - valid, - keywordContext, - schemaContext, - keyword - }); - } - } -} - -export const getSchemaDiagnostics = async (textDocument: TextDocument, tree: jsonc.Node, schemaUri: string, documentMap: Map): Promise => { - const text = textDocument.getText(); - const schemaDiagnostics: Diagnostic[] = []; - - if (schemaUri) { - let instance = JSON.parse(text); - const collector = new MatchingSchemaCollector(); - const result = await validate(schemaUri, instance, { - outputFormat: BASIC, - plugins: [collector] - }); - - documentMap.set(textDocument.uri, collector); - - if (!result.valid) { - const errors = await jsonSchemaErrors(result, schemaUri, instance); - errors.forEach((error) => { - const path = error.instanceLocation === "#" ? [] : error.instanceLocation.slice(2).split("/"); - const node = tree ? jsonc.findNodeAtLocation(tree, path) : undefined; - - if (node) { - schemaDiagnostics.push({ - severity: DiagnosticSeverity.Error, - range: { - start: textDocument.positionAt(node.offset), - end: textDocument.positionAt(node.offset + node.length) - }, - message: formatError(error), - source: "json-language-server" - }); - } - }); - } - } - return schemaDiagnostics; -}; - -const formatError = (error: ErrorObject, depth = 0): string => { - let msg = error.message; - if (error.alternatives && error.alternatives.length > 0) { - const indent = " ".repeat(depth + 1); - const lines = error.alternatives.flatMap((alt) => alt.map((subErr) => `${indent}- ${formatError(subErr, depth + 1)}`)); - msg += `:\n${lines.join("\n")}`; - } - return msg; -}; diff --git a/language-server/src/features/SyntaxValidation.ts b/language-server/src/features/SyntaxValidation.ts index 4bdc36f..4d38626 100644 --- a/language-server/src/features/SyntaxValidation.ts +++ b/language-server/src/features/SyntaxValidation.ts @@ -2,14 +2,23 @@ import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import * as jsonc from "jsonc-parser"; -export const getSyntaxDiagnostics = (textDocument: TextDocument, parseErrors: jsonc.ParseError[]): Diagnostic[] => { - return parseErrors.map((error) => ({ - severity: DiagnosticSeverity.Error, - range: { - start: textDocument.positionAt(error.offset), - end: textDocument.positionAt(error.offset + error.length) - }, - message: jsonc.printParseErrorCode(error.error), - source: "json-language-server" - })); -}; +import type { DiagnosticsProvider } from "./Diagnostics.ts"; + +export class SyntaxValidation implements DiagnosticsProvider { + getDiagnostics(textDocument: TextDocument): Diagnostic[] { + const text = textDocument.getText(); + const parseErrors: jsonc.ParseError[] = []; + + jsonc.parseTree(text, parseErrors); + + return parseErrors.map((error) => ({ + severity: DiagnosticSeverity.Error, + range: { + start: textDocument.positionAt(error.offset), + end: textDocument.positionAt(error.offset + error.length) + }, + message: jsonc.printParseErrorCode(error.error), + source: "json-language-server" + })); + } +} diff --git a/language-server/src/language-server.test.ts b/language-server/src/language-server.test.ts index 4b72636..e56838e 100644 --- a/language-server/src/language-server.test.ts +++ b/language-server/src/language-server.test.ts @@ -1,11 +1,9 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest"; +import { describe, test, expect, beforeAll, afterAll } from "vitest"; import { TextDocumentSyncKind } from "vscode-languageserver"; import { TestClient } from "./test/test-client.ts"; -import { registerSchema, unregisterSchema } from "@hyperjump/json-schema"; describe("JSON Language Server", () => { let client: TestClient; - const fixtureSchemaUri = "https://example.com/person"; beforeAll(async () => { client = new TestClient(); @@ -16,10 +14,6 @@ describe("JSON Language Server", () => { await client.stop(); }); - afterEach(() => { - unregisterSchema(fixtureSchemaUri); - }); - test("textDocumentSync = Incremental", () => { expect(client.serverCapabilities?.textDocumentSync).to.equal(TextDocumentSyncKind.Incremental); }); @@ -51,137 +45,4 @@ describe("JSON Language Server", () => { const diagnostics = await diagnosticsPromise; expect(diagnostics).toHaveLength(0); }); - - test("JSON Validation using Hyperjump - Valid Case", async () => { - const diagnosticsPromise = new Promise((resolve) => { - client.onNotification("textDocument/publishDiagnostics", (params) => { - resolve(params.diagnostics); - }); - }); - - const testSchema = { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" } - } - }; - - registerSchema(testSchema, fixtureSchemaUri); - - await client.writeDocument("instance.json", `{ - "$schema": "${fixtureSchemaUri}", - "name": "Alice", - "age" : 39 - }`); - await client.openDocument("instance.json"); - - const diagnostics = await diagnosticsPromise; - expect(diagnostics).toHaveLength(0); - }); - - test("JSON Validation using Hyperjump - Invalid Case", async () => { - const diagnosticsPromise = new Promise((resolve) => { - client.onNotification("textDocument/publishDiagnostics", (params) => { - resolve(params.diagnostics); - }); - }); - - const testSchema = { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" } - } - }; - - registerSchema(testSchema, fixtureSchemaUri); - - await client.writeDocument("instance.json", `{ - "$schema": "${fixtureSchemaUri}", - "name": 1234, - "age" : "hello" - }`); - await client.openDocument("instance.json"); - - const diagnostics = (await diagnosticsPromise) as any[]; - expect(diagnostics).toHaveLength(2); - const messages = diagnostics.map((d) => d.message); - expect(messages).toEqual( - expect.arrayContaining([ - expect.stringMatching(/Expected a.*string/), - expect.stringMatching(/Expected a.*number/) - ]) - ); - }); - - test("JSON Validation using Hyperjump - anyOf Formatting Case", async () => { - const diagnosticsPromise = new Promise((resolve) => { - client.onNotification("textDocument/publishDiagnostics", (params) => { - resolve(params.diagnostics); - }); - }); - - const testSchema = { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - value: { - anyOf: [ - { type: "string" }, - { type: "number" } - ] - } - } - }; - - registerSchema(testSchema, fixtureSchemaUri); - - await client.writeDocument("instance.json", `{ - "$schema": "${fixtureSchemaUri}", - "value": true - }`); - await client.openDocument("instance.json"); - - const diagnostics = (await diagnosticsPromise) as any[]; - expect(diagnostics).toHaveLength(1); - const cleanMessage = diagnostics[0].message.replace(/[\u2068\u2069]/g, ""); - expect(cleanMessage).toBe("Expected the value to match at least one alternative:\n - Expected a string\n - Expected a number"); - }); - - test("JSON Validation using Hyperjump - oneOf Formatting Case", async () => { - const diagnosticsPromise = new Promise((resolve) => { - client.onNotification("textDocument/publishDiagnostics", (params) => { - resolve(params.diagnostics); - }); - }); - - const testSchema = { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - value: { - oneOf: [ - { type: "string" }, - { type: "number" } - ] - } - } - }; - - registerSchema(testSchema, fixtureSchemaUri); - - await client.writeDocument("instance.json", `{ - "$schema": "${fixtureSchemaUri}", - "value": true - }`); - await client.openDocument("instance.json"); - - const diagnostics = (await diagnosticsPromise) as any[]; - expect(diagnostics).toHaveLength(1); - const cleanMessage = diagnostics[0].message.replace(/[\u2068\u2069]/g, ""); - expect(cleanMessage).toBe("Expected the value to match exactly one alternative, but none matched:\n - Expected a string\n - Expected a number"); - }); }); From 39ab34505702d6adaff9602d6b0e91704230dc4f Mon Sep 17 00:00:00 2001 From: Diya Date: Mon, 1 Jun 2026 22:52:28 +0530 Subject: [PATCH 4/4] remove comments and unused package --- language-server/package-lock.json | 27 --------------------- language-server/package.json | 1 - language-server/src/features/Diagnostics.ts | 17 ------------- 3 files changed, 45 deletions(-) diff --git a/language-server/package-lock.json b/language-server/package-lock.json index 2e77cc5..0659212 100644 --- a/language-server/package-lock.json +++ b/language-server/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@hyperjump/json-schema": "^1.17.6", - "@hyperjump/json-schema-errors": "github:hyperjump-io/json-schema-errors", "jsonc-parser": "^3.3.1", "merge-anything": "^6.0.6", "vscode-languageserver": "^9.0.1", @@ -18,15 +17,6 @@ "vscode-uri": "^3.1.0" } }, - "node_modules/@fluent/bundle": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.19.1.tgz", - "integrity": "sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==", - "engines": { - "node": ">=18.0.0", - "npm": ">=7.0.0" - } - }, "node_modules/@hyperjump/browser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.3.1.tgz", @@ -77,23 +67,6 @@ "@hyperjump/browser": "^1.1.0" } }, - "node_modules/@hyperjump/json-schema-errors": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/hyperjump-io/json-schema-errors.git#52de8c3c78e4514eac3a525399b323544d4b533c", - "license": "MIT", - "dependencies": { - "@fluent/bundle": "^0.19.1", - "@hyperjump/json-pointer": "^1.1.1", - "@hyperjump/json-schema": "^1.17.2", - "@hyperjump/pact": "^1.4.0", - "@hyperjump/uri": "^1.3.2", - "json-stringify-deterministic": "^1.0.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/hyperjump-io" - } - }, "node_modules/@hyperjump/json-schema-formats": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@hyperjump/json-schema-formats/-/json-schema-formats-1.0.1.tgz", diff --git a/language-server/package.json b/language-server/package.json index 7f8d870..22603d3 100644 --- a/language-server/package.json +++ b/language-server/package.json @@ -20,7 +20,6 @@ "keywords": [], "dependencies": { "@hyperjump/json-schema": "^1.17.6", - "@hyperjump/json-schema-errors": "github:hyperjump-io/json-schema-errors", "jsonc-parser": "^3.3.1", "merge-anything": "^6.0.6", "vscode-languageserver": "^9.0.1", diff --git a/language-server/src/features/Diagnostics.ts b/language-server/src/features/Diagnostics.ts index 116313c..4228586 100644 --- a/language-server/src/features/Diagnostics.ts +++ b/language-server/src/features/Diagnostics.ts @@ -24,29 +24,12 @@ export class Diagnostics { }; }); - // Single onDidChangeContent handler to ensure one combined sendDiagnostics call per change, preventing multiple diagnostic pushes per keystroke from separate features documents.onDidChangeContent(async (change) => { const diagnostics = []; for (const provider of this.providers) { diagnostics.push(...provider.getDiagnostics(change.document)); } - // const textDocument = change.document; - // const text = textDocument.getText(); - // const parseErrors: jsonc.ParseError[] = []; - - // const tree = jsonc.parseTree(text, parseErrors); - - // // for syntax errors - // const syntaxDiagnostics = getSyntaxDiagnostics(textDocument, parseErrors); - - // // for schema validation errors - // const schemaNode = tree ? jsonc.findNodeAtLocation(tree, ["$schema"]) : undefined; - // const schemaUri = schemaNode?.value; - // const schemaDiagnostics = schemaUri && parseErrors.length === 0 && tree - // ? await getSchemaDiagnostics(textDocument, tree, schemaUri, documentMap) - // : []; - await server.sendDiagnostics({ uri: change.document.uri, diagnostics: diagnostics