diff --git a/language-server/src/features/diagnostics/SchemaValidationDiagnosticsProvider.test.ts b/language-server/src/features/diagnostics/SchemaValidationDiagnosticsProvider.test.ts index c593ad1..5568420 100644 --- a/language-server/src/features/diagnostics/SchemaValidationDiagnosticsProvider.test.ts +++ b/language-server/src/features/diagnostics/SchemaValidationDiagnosticsProvider.test.ts @@ -899,6 +899,168 @@ describe("Schema Validation", () => { await expect(updatedDiagnostics).resolves.toEqual([]); }); + test("a key failing propertyNames gets a diagnostic on that key and none on $schema", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "pattern": "^[a-z$]+$" + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "123": "value" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + { + message: "Expected a string matching the regular expression /^[a-z$]+$/", + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 11 } + }, + severity: 1, + source: "hyperjump-json-language-server" + } + ]); + }); + + test("multiple failing keys each get their own diagnostic", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "pattern": "^[a-z$]+$" + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "123": "value1", + "456": "value2" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + { + message: "Expected a string matching the regular expression /^[a-z$]+$/", + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 11 } + }, + severity: 1, + source: "hyperjump-json-language-server" + }, + { + message: "Expected a string matching the regular expression /^[a-z$]+$/", + range: { + start: { line: 3, character: 6 }, + end: { line: 3, character: 11 } + }, + severity: 1, + source: "hyperjump-json-language-server" + } + ]); + }); + + test("a nested object with a failing key is placed correctly", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "nested": { + "type": "object", + "propertyNames": { + "pattern": "^[a-z]+$" + } + } + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "nested": { + "123": "value" + } + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + { + message: "Expected a string matching the regular expression /^[a-z]+$/", + range: { + start: { line: 3, character: 8 }, + end: { line: 3, character: 13 } + }, + severity: 1, + source: "hyperjump-json-language-server" + } + ]); + }); + + test("a key that needs JSON Pointer escaping (contains / or ~) is placed correctly", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "pattern": "^[a-z$]+$" + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "foo/bar": "value1", + "foo~bar": "value2" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toEqual([ + { + message: "Expected a string matching the regular expression /^[a-z$]+$/", + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 15 } + }, + severity: 1, + source: "hyperjump-json-language-server" + }, + { + message: "Expected a string matching the regular expression /^[a-z$]+$/", + range: { + start: { line: 3, character: 6 }, + end: { line: 3, character: 15 } + }, + severity: 1, + source: "hyperjump-json-language-server" + } + ]); + }); + + test("a document whose keys all pass propertyNames produces no diagnostics", async () => { + fixtureSchemaUri = await client.writeDocument("schema.json", `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "pattern": "^[a-z$]+$" + } + }`); + + await client.writeDocument("instance.json", `{ + "$schema": "${fixtureSchemaUri}", + "foo": "value" + }`); + const diagnostics = client.getDiagnostics("instance.json"); + await client.openDocument("instance.json"); + + await expect(diagnostics).resolves.toHaveLength(0); + }); + test("should validate against the unsaved content of an open schema", async () => { fixtureSchemaUri = await client.writeDocument("schema.json", `{ "$schema": "https://json-schema.org/draft/2020-12/schema", diff --git a/language-server/src/models/JsonDocument.ts b/language-server/src/models/JsonDocument.ts index a55481a..c332660 100644 --- a/language-server/src/models/JsonDocument.ts +++ b/language-server/src/models/JsonDocument.ts @@ -94,7 +94,14 @@ export class JsonDocument implements TextDocument { return this.parseErrors; } - findNodeAtPointer(pointer: string) { + // Property name locations are prefixed with "*" and find the property's key + // rather than its value + findNodeAtPointer(pointer: string): jsonc.Node | undefined { + if (pointer.startsWith("*")) { + const node = this.findNodeAtPointer(pointer.slice(1)); + return node?.parent?.type === "property" ? node.parent.children![0] : undefined; + } + let node = this.ast; for (let segment of JsonPointer.pointerSegments(pointer)) {