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
35 changes: 29 additions & 6 deletions language-server/package-lock.json

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

1 change: 1 addition & 0 deletions language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "^10.0.0",
Expand Down
8 changes: 7 additions & 1 deletion language-server/src/build-server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import "@hyperjump/json-schema/draft-2020-12";
import "@hyperjump/json-schema/draft-2019-09";
import "@hyperjump/json-schema/draft-07";
import "@hyperjump/json-schema/draft-06";
import "@hyperjump/json-schema/draft-04";
import { TextDocuments } from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { Server } from "./services/server.ts";
import { Diagnostics } from "./features/Diagnostics.ts";
import { SyntaxValidation } from "./features/SyntaxValidation.ts";
import { SchemaValidation } from "./features/SchemaValidation.ts";

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

Expand All @@ -17,7 +22,8 @@ export const buildServer = (connection: Connection): Connection => {
documents.listen(server);

new Diagnostics(server, documents, [
new SyntaxValidation()
new SyntaxValidation(),
new SchemaValidation()
]);

return server;
Expand Down
4 changes: 2 additions & 2 deletions language-server/src/features/Diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Server } from "../services/server.ts";
import type { ServerCapabilities, Diagnostic } from "vscode-languageserver";

export type DiagnosticsProvider = {
getDiagnostics(textDocument: TextDocument): Diagnostic[];
getDiagnostics(textDocument: TextDocument): Promise<Diagnostic[]>;
};

export class Diagnostics {
Expand All @@ -27,7 +27,7 @@ export class Diagnostics {
documents.onDidChangeContent(async (change) => {
const diagnostics = [];
for (const provider of this.providers) {
diagnostics.push(...provider.getDiagnostics(change.document));
diagnostics.push(...await provider.getDiagnostics(change.document));
}

await server.sendDiagnostics({
Expand Down
67 changes: 67 additions & 0 deletions language-server/src/features/SchemaValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import * as jsonc from "jsonc-parser";
import { validate } from "@hyperjump/json-schema-errors";
import { pointerSegments } from "@hyperjump/json-pointer";

import type { ErrorObject } from "@hyperjump/json-schema-errors";
import type { DiagnosticsProvider } from "./Diagnostics.ts";

export class SchemaValidation implements DiagnosticsProvider {
async getDiagnostics(textDocument: TextDocument) {
const text = textDocument.getText();
const parseErrors: jsonc.ParseError[] = [];

const tree = jsonc.parseTree(text, parseErrors);
const schemaDiagnostics: Diagnostic[] = [];

const schemaNode = tree ? jsonc.findNodeAtLocation(tree, ["$schema"]) : undefined;
const schemaUri = schemaNode?.value;

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

if (!result.valid) {
const errors = result.errors;
errors.forEach((error) => {
const node = tree ? findNodeByPointer(tree, error.instanceLocation) : 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 findNodeByPointer = (node: jsonc.Node, pointer: string) => {
if (pointer === "#") {
return node;
}
const segments = [...pointerSegments(pointer.slice(1))];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that pointer is a URI and could have URI escaped characters that need to be decoded. pointerSegments will only handle JSON Pointer escaped characters, not URI escaped characters.

const path = segments.map((s) => /^\d+$/.test(s) ? parseInt(s) : s);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite right. Consider,

{
  "42": ["foo"]
}

Given the pointer, /42/0, the path should be ["42", 0], not [42, 0]. So, you can't just match anything that looks like an integer and parse it. You have to take the type at that location into account.

So, you won't be able to use fineNodeAtLocation for this. You'll need to walk the AST for each segment and only parse to a number if the current node is an array.

return jsonc.findNodeAtLocation(node, path);
};

const formatError = (error: ErrorObject, depth = 0): string => {
let msg = error.message;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's best to avoid abbreviations.

Suggested change
let msg = error.message;
let message = 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;
};
4 changes: 2 additions & 2 deletions language-server/src/features/SyntaxValidation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver";
import { DiagnosticSeverity } from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import * as jsonc from "jsonc-parser";

import type { DiagnosticsProvider } from "./Diagnostics.ts";

export class SyntaxValidation implements DiagnosticsProvider {
getDiagnostics(textDocument: TextDocument): Diagnostic[] {
async getDiagnostics(textDocument: TextDocument) {
const text = textDocument.getText();
const parseErrors: jsonc.ParseError[] = [];

Expand Down
Loading
Loading