From 861444b8a3115a99f4b7f0031882a0f4e32778c2 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 20 Aug 2026 18:40:08 +0530 Subject: [PATCH] feat(language-server): flag the removed data blocks Co-Authored-By: Claude Opus 5 --- editors/vscode/src/compiler.cjs | 4 +- editors/vscode/src/extension.bundle.cjs | 2 +- editors/vscode/src/language-server.cjs | 4 +- .../removed-construct-diagnostics.test.ts | 120 ++++++++++++++++++ packages/syntax/src/parser.ts | 2 +- 5 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 packages/language-server/test/removed-construct-diagnostics.test.ts diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index e8078845..689dc78e 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -1,6 +1,6 @@ "use strict"; // Generated by scripts/build-editor-compiler.mjs. Do not edit directly. -// WRN editor compiler source hash: 2987e8e9d91b793818a195395e740d35d0d5c2a320ff1c3a5ae77363850c7b8e +// WRN editor compiler source hash: 4ba6ddaf22c0a33fa1c2a108a97f077a69c0a46de9fe031c0a00e38863fab613 // WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8 // Generated with TypeScript: 6.0.3 const __nodeRequire = require; @@ -6762,7 +6762,7 @@ function parse(source) { break; } if (lx.peek().type === "lbrace") { - throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`); + throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`); } throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`); } diff --git a/editors/vscode/src/extension.bundle.cjs b/editors/vscode/src/extension.bundle.cjs index a7f79288..75be8487 100644 --- a/editors/vscode/src/extension.bundle.cjs +++ b/editors/vscode/src/extension.bundle.cjs @@ -1,4 +1,4 @@ -// WRN editor extension source hash: d0ca0feee6516ae43e902e6aea0424bc5c3d05790708eb8a8c68c98162eb99bb +// WRN editor extension source hash: 566bce98338a6a99c6c5c15017d92b3a5fd8ed376dd6abd932179a1e79c00f70 // WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728 "use strict"; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); diff --git a/editors/vscode/src/language-server.cjs b/editors/vscode/src/language-server.cjs index 18057976..9cb78f44 100644 --- a/editors/vscode/src/language-server.cjs +++ b/editors/vscode/src/language-server.cjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// WRN editor language server source hash: 2e1803dd46a175316931c0390c5af90dc35a5cd2299798014cbe46b075fee15d +// WRN editor language server source hash: a8bac4fc444116cc73f898d51989b058370ba9cfd8413b62974a8ce2d841ab6f // WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72 // @bun @bun-cjs (function(exports, require, module, __filename, __dirname) {var __create = Object.create; @@ -171799,7 +171799,7 @@ function parse(source) { break; } if (lx.peek().type === "lbrace") { - throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`); + throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`); } throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`); } diff --git a/packages/language-server/test/removed-construct-diagnostics.test.ts b/packages/language-server/test/removed-construct-diagnostics.test.ts new file mode 100644 index 00000000..bbe9692c --- /dev/null +++ b/packages/language-server/test/removed-construct-diagnostics.test.ts @@ -0,0 +1,120 @@ +import { expect, test } from "bun:test"; +import { documentDiagnostics } from "../src/index.ts"; + +// documentDiagnostics is the language server's actual diagnostic entry point. +// The plan's draft guessed a `diagnoseWrn` export that does not exist. + +// These two tests are the plan's original assertions, kept verbatim. They +// already passed before this change — the message produced by the parser +// named "apis" from the start — so on their own they are a false-green: +// they would not catch a regression to the fix below. They stay as cheap +// regression guards, not as the load-bearing coverage for this task. +test("an ssr data block is flagged and names the replacement", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///removed-ssr.wrn", + text: `page P { + ssr { api x GET /api/x { response { return data } } } + view {
x
} +} +`, + }); + + expect(diagnostics.length).toBeGreaterThan(0); + expect(diagnostics[0]!.message).toContain("apis"); +}); + +test("client state is not flagged", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///client-state.wrn", + text: `page P { + client state { count = 0 } + view {
x
} +} +`, + }); + + expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]); +}); + +// --- Load-bearing tests ----------------------------------------------- +// +// The message alone doesn't prove the diagnostic is useful: before the fix, +// documentDiagnostics reported this error at line 0 / character 0 for every +// input, no matter where the offending block actually was. These tests pin +// the range to the real position of the "ssr" / "client" keyword, with the +// block moved off line 1 so a hardcoded line number cannot pass. + +test("an ssr block diagnostic lands on the ssr keyword, not offset zero", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///removed-ssr-position.wrn", + text: `page P { + ssr { api x GET /api/x { response { return data } } } + view {
x
} +} +`, + }); + + expect(diagnostics.length).toBeGreaterThan(0); + // " ssr { ... }" — the keyword starts at line 1 (0-indexed), character 2. + expect(diagnostics[0]!.range.start).toEqual({ line: 1, character: 2 }); +}); + +test("a client block diagnostic lands on the client keyword when it is not on line 1", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///removed-client-position.wrn", + text: `page P { + view {
x
} + client { api x GET /api/x { response { return data } } } +} +`, + }); + + expect(diagnostics.length).toBeGreaterThan(0); + // The client block is on line 2 (0-indexed) here, preceded by "view { ... }" + // on line 1 — a diagnostic hardcoded to line 1 would fail this assertion. + expect(diagnostics[0]!.range.start).toEqual({ line: 2, character: 2 }); +}); + +// --- Legal `client` forms stay legal ------------------------------------ +// +// `client` is one word with several jobs. Only the `ssr { }` / `client { }` +// data-block form was removed; these forms must never be flagged. + +test("client state block produces no error-severity diagnostic", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///client-state-2.wrn", + text: `page P { + client state { count = 0 } + view {
x
} +} +`, + }); + + expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]); +}); + +test('runtime = "client" produces no error-severity diagnostic', () => { + const diagnostics = documentDiagnostics({ + uri: "file:///runtime-client.wrn", + text: `page P { + runtime = "client" + view {
x
} +} +`, + }); + + expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]); +}); + +test("client function inside functions { } produces no error-severity diagnostic", () => { + const diagnostics = documentDiagnostics({ + uri: "file:///client-function.wrn", + text: `page P { + functions { client function go() { console.log(1) } } + view {
x
} +} +`, + }); + + expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]); +}); diff --git a/packages/syntax/src/parser.ts b/packages/syntax/src/parser.ts index 658ef296..2631d2e9 100644 --- a/packages/syntax/src/parser.ts +++ b/packages/syntax/src/parser.ts @@ -685,7 +685,7 @@ export function parse(source: string): PageAst { } if (lx.peek().type === "lbrace") { throw new ParseError( - `"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`, + `"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`, ); } throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);