e7738de6d2
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
564 lines
21 KiB
TypeScript
Executable File
564 lines
21 KiB
TypeScript
Executable File
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
|
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const zero = "bin/zero";
|
|
const documents = new Map();
|
|
const symbols = new Map();
|
|
const documentFacts = new Map();
|
|
|
|
function uriToPath(uri) {
|
|
return uri && uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
|
}
|
|
|
|
function pathToUri(path) {
|
|
return pathToFileURL(path).href;
|
|
}
|
|
|
|
function graphSidecarPath(path) {
|
|
return path.endsWith(".0") ? `${path.slice(0, -2)}.graph` : null;
|
|
}
|
|
|
|
function diagnosticsFromBody(body) {
|
|
return (body?.diagnostics ?? []).map((diag) => ({
|
|
range: {
|
|
start: { line: Math.max(0, diag.line - 1), character: Math.max(0, diag.column - 1) },
|
|
end: { line: Math.max(0, diag.line - 1), character: Math.max(0, diag.column) },
|
|
},
|
|
severity: 1,
|
|
code: diag.code,
|
|
source: "zero",
|
|
message: diag.message,
|
|
data: { repair: diag.repair, fixSafety: diag.fixSafety },
|
|
}));
|
|
}
|
|
|
|
async function importProjectionSidecar(path) {
|
|
const sidecar = graphSidecarPath(path);
|
|
if (!sidecar) return null;
|
|
const result = await execFileAsync(zero, ["import", "--json", "--format", "binary", "--out", sidecar, path]).catch((error) => error);
|
|
if (!result.stdout) return null;
|
|
try {
|
|
return JSON.parse(result.stdout);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function positionAt(text, offset) {
|
|
const prefix = text.slice(0, offset);
|
|
const lines = prefix.split("\n");
|
|
return { line: lines.length - 1, character: lines[lines.length - 1].length };
|
|
}
|
|
|
|
function offsetAt(text, position) {
|
|
const lines = text.split("\n");
|
|
let offset = 0;
|
|
for (let i = 0; i < Math.min(position.line, lines.length); i++) offset += lines[i].length + 1;
|
|
return offset + Math.min(position.character, lines[position.line]?.length ?? 0);
|
|
}
|
|
|
|
function wordAt(text, position) {
|
|
const offset = offsetAt(text, position);
|
|
let start = offset;
|
|
let end = offset;
|
|
while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1])) start--;
|
|
while (end < text.length && /[A-Za-z0-9_]/.test(text[end])) end++;
|
|
return text.slice(start, end);
|
|
}
|
|
|
|
function rangeForLine(line, start, end) {
|
|
return {
|
|
start: { line, character: start },
|
|
end: { line, character: end },
|
|
};
|
|
}
|
|
|
|
function symbolKind(kind) {
|
|
return {
|
|
function: 12,
|
|
shape: 23,
|
|
enum: 10,
|
|
choice: 23,
|
|
interface: 11,
|
|
const: 14,
|
|
alias: 5,
|
|
"type-alias": 5,
|
|
type: 5,
|
|
}[kind] ?? 13;
|
|
}
|
|
|
|
function analyzeText(uri, text) {
|
|
const found = [];
|
|
const lines = text.split("\n");
|
|
const declaration = /^\s*(?:pub\s+)?(?:(export\s+c)\s+)?(?:(extern|packed)\s+)?(fn|type|enum|choice|interface|const|alias)\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
const match = lines[lineIndex].match(declaration);
|
|
if (!match) continue;
|
|
const kind = match[3] === "fn" ? "function" : match[3];
|
|
const name = match[4];
|
|
const start = lines[lineIndex].indexOf(name);
|
|
found.push({
|
|
name,
|
|
kind,
|
|
detail: lines[lineIndex].trim(),
|
|
uri,
|
|
range: rangeForLine(lineIndex, start, start + name.length),
|
|
selectionRange: rangeForLine(lineIndex, start, start + name.length),
|
|
});
|
|
}
|
|
symbols.set(uri, found);
|
|
return found;
|
|
}
|
|
|
|
function simpleDiagnostics(uri, text) {
|
|
const diagnostics = [];
|
|
let balance = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
if (text[i] === "{") balance++;
|
|
if (text[i] === "}") balance--;
|
|
if (balance < 0) {
|
|
diagnostics.push({
|
|
range: { start: positionAt(text, i), end: positionAt(text, i + 1) },
|
|
severity: 1,
|
|
code: "PAR100",
|
|
source: "zero",
|
|
message: "unmatched closing brace",
|
|
});
|
|
balance = 0;
|
|
}
|
|
}
|
|
if (balance > 0) {
|
|
diagnostics.push({
|
|
range: { start: positionAt(text, text.length), end: positionAt(text, text.length) },
|
|
severity: 1,
|
|
code: "PAR100",
|
|
source: "zero",
|
|
message: "missing closing brace",
|
|
});
|
|
}
|
|
return diagnostics;
|
|
}
|
|
|
|
async function compilerDiagnostics(uri, text) {
|
|
const path = uriToPath(uri);
|
|
try {
|
|
await writeFile(path, text);
|
|
const sidecar = graphSidecarPath(path);
|
|
const imported = await importProjectionSidecar(path);
|
|
if (imported && imported.ok === false) return diagnosticsFromBody(imported);
|
|
const input = sidecar && imported ? sidecar : path;
|
|
const result = await execFileAsync(zero, ["check", "--json", input]).catch((error) => error);
|
|
if (!result.stdout) return simpleDiagnostics(uri, text);
|
|
const body = JSON.parse(result.stdout);
|
|
return diagnosticsFromBody(body);
|
|
} catch {
|
|
return simpleDiagnostics(uri, text);
|
|
}
|
|
}
|
|
|
|
async function compilerFacts(path) {
|
|
const sidecar = graphSidecarPath(path);
|
|
const imported = await importProjectionSidecar(path);
|
|
const input = sidecar && imported ? sidecar : path;
|
|
const [graphResult, sizeResult, memResult, docResult] = await Promise.all([
|
|
execFileAsync(zero, ["inspect", "--json", input]).catch((error) => error),
|
|
execFileAsync(zero, ["size", "--json", input]).catch((error) => error),
|
|
execFileAsync(zero, ["mem", "--json", input]).catch((error) => error),
|
|
execFileAsync(zero, ["doc", "--json", input]).catch((error) => error),
|
|
]);
|
|
const parseBody = (result) => {
|
|
if (!result.stdout) return null;
|
|
try {
|
|
return JSON.parse(result.stdout);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
const graph = parseBody(graphResult);
|
|
const size = parseBody(sizeResult);
|
|
const mem = parseBody(memResult);
|
|
const doc = parseBody(docResult);
|
|
return {
|
|
schemaVersion: 1,
|
|
sourceFile: graph?.sourceFile ?? size?.sourceFile ?? mem?.sourceFile ?? path,
|
|
generatedCBytes: Math.max(graph?.generatedCBytes ?? 0, size?.generatedCBytes ?? 0, mem?.generatedCBytes ?? 0, doc?.generatedCBytes ?? 0),
|
|
cBridgeFallback: Boolean(
|
|
graph?.selfHostRouting?.cBridge?.required ??
|
|
size?.selfHostRouting?.cBridge?.required ??
|
|
mem?.cBridgeFallback ??
|
|
doc?.cBridgeFallback ??
|
|
false
|
|
),
|
|
targetCapabilityFacts: graph?.targetSupport ?? size?.targetSupport ?? null,
|
|
directBackendStatus: {
|
|
graph: graph?.selfHostRouting ?? null,
|
|
objectBackend: size?.objectBackend ?? mem?.objectBackend ?? null,
|
|
},
|
|
generatedBindingPreviews: (graph?.cImports ?? []).map((item) => ({
|
|
header: item.header,
|
|
alias: item.alias,
|
|
functions: item.typedModel?.functions?.map((fn) => fn.name) ?? [],
|
|
constants: item.typedModel?.constants?.map((constant) => constant.name) ?? [],
|
|
structs: item.typedModel?.structs?.map((shape) => shape.name) ?? [],
|
|
})),
|
|
runtimeHelperCosts: {
|
|
usedStdlibHelpers: size?.usedStdlibHelpers ?? [],
|
|
runtimeShims: size?.runtimeShims ?? [],
|
|
compilerRuntimeHelpers: size?.compilerRuntimeHelpers ?? [],
|
|
},
|
|
memoryBudget: {
|
|
sections: mem?.sections ?? [],
|
|
stackBytes: mem?.stackBytes ?? null,
|
|
heapBytes: mem?.heapBytes ?? null,
|
|
staticBytes: mem?.staticBytes ?? null,
|
|
},
|
|
docs: {
|
|
publicSymbols: doc?.symbols ?? [],
|
|
requiresCapabilities: doc?.requiresCapabilities ?? [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function completionItems() {
|
|
const keywordItems = [
|
|
"pub",
|
|
"fn",
|
|
"type",
|
|
"enum",
|
|
"choice",
|
|
"interface",
|
|
"alias",
|
|
"const",
|
|
"let",
|
|
"var",
|
|
"return",
|
|
"if",
|
|
"else",
|
|
"while",
|
|
"for",
|
|
"match",
|
|
"check",
|
|
"rescue",
|
|
"raise",
|
|
"raises",
|
|
"use",
|
|
"extern",
|
|
"packed",
|
|
"static",
|
|
"meta",
|
|
"test",
|
|
"break",
|
|
"continue",
|
|
"defer",
|
|
].map((label) => ({ label, kind: 14 }));
|
|
const symbolItems = [...symbols.values()].flat().map((symbol) => ({ label: symbol.name, kind: symbolKind(symbol.kind), detail: symbol.detail }));
|
|
return [...keywordItems, ...symbolItems];
|
|
}
|
|
|
|
function findSymbol(name) {
|
|
for (const list of symbols.values()) {
|
|
const found = list.find((symbol) => symbol.name === name);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hover(params) {
|
|
const doc = documents.get(params.textDocument.uri);
|
|
const name = doc ? wordAt(doc.text, params.position) : "";
|
|
const symbol = findSymbol(name);
|
|
if (!symbol) return null;
|
|
const facts = documentFacts.get(params.textDocument.uri);
|
|
const directStatus = facts?.directBackendStatus?.objectBackend?.targetFacts?.directStatus ?? facts?.targetCapabilityFacts?.requiredCapabilitySupport?.status ?? "unknown";
|
|
const target = facts?.targetCapabilityFacts?.target ?? "host";
|
|
const capabilities = (facts?.docs?.requiresCapabilities ?? []).map((item) => item.name ?? item).filter(Boolean).join(", ") || "none";
|
|
const bindingPreview = (facts?.generatedBindingPreviews ?? [])
|
|
.map((item) => `${item.alias ?? item.header}: ${(item.functions ?? []).slice(0, 3).join(", ") || "typed header"}`)
|
|
.join("; ") || "none";
|
|
return {
|
|
contents: {
|
|
kind: "markdown",
|
|
value: `\`${symbol.detail}\`\n\nkind: ${symbol.kind}\n\ntarget: ${target}\n\ncapabilities: ${capabilities}\n\ndirect backend: ${directStatus}\n\ngenerated binding previews: ${bindingPreview}\n\ngeneratedCBytes: ${facts?.generatedCBytes ?? 0}`,
|
|
},
|
|
range: symbol.selectionRange,
|
|
};
|
|
}
|
|
|
|
function documentSymbols(params) {
|
|
const uri = params.textDocument.uri;
|
|
return (symbols.get(uri) ?? []).map((symbol) => ({
|
|
name: symbol.name,
|
|
kind: symbolKind(symbol.kind),
|
|
detail: symbol.detail,
|
|
range: symbol.range,
|
|
selectionRange: symbol.selectionRange,
|
|
}));
|
|
}
|
|
|
|
function signatureHelp(params) {
|
|
const doc = documents.get(params.textDocument.uri);
|
|
if (!doc) return { signatures: [], activeSignature: 0, activeParameter: 0 };
|
|
const offset = offsetAt(doc.text, params.position);
|
|
const before = doc.text.slice(0, offset);
|
|
const call = before.match(/([A-Za-z_][A-Za-z0-9_]*)\([^()]*$/);
|
|
const symbol = call ? findSymbol(call[1]) : null;
|
|
return {
|
|
signatures: symbol ? [{ label: symbol.detail, parameters: [] }] : [],
|
|
activeSignature: 0,
|
|
activeParameter: 0,
|
|
};
|
|
}
|
|
|
|
function definition(params) {
|
|
const doc = documents.get(params.textDocument.uri);
|
|
const name = doc ? wordAt(doc.text, params.position) : "";
|
|
const symbol = findSymbol(name);
|
|
return symbol ? { uri: symbol.uri, range: symbol.selectionRange } : null;
|
|
}
|
|
|
|
function references(params) {
|
|
const doc = documents.get(params.textDocument.uri);
|
|
if (!doc) return [];
|
|
const name = wordAt(doc.text, params.position);
|
|
const locations = [];
|
|
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
let match = null;
|
|
while ((match = pattern.exec(doc.text)) !== null) {
|
|
const start = positionAt(doc.text, match.index);
|
|
locations.push({ uri: params.textDocument.uri, range: { start, end: { line: start.line, character: start.character + name.length } } });
|
|
}
|
|
return locations;
|
|
}
|
|
|
|
function rename(params) {
|
|
const refs = references(params);
|
|
return {
|
|
changes: {
|
|
[params.textDocument.uri]: refs.map((location) => ({ range: location.range, newText: params.newName })),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function codeActions(params) {
|
|
const path = uriToPath(params.textDocument.uri);
|
|
const diagnosticActions = (params.context?.diagnostics ?? [])
|
|
.filter((diagnostic) => diagnostic.data?.repair?.id)
|
|
.map((diagnostic) => ({
|
|
title: diagnostic.data.repair.summary ?? `Repair ${diagnostic.code}`,
|
|
kind: "quickfix",
|
|
diagnostics: [diagnostic],
|
|
data: {
|
|
id: diagnostic.data.repair.id,
|
|
diagnosticCode: diagnostic.code,
|
|
safety: diagnostic.data.fixSafety ?? "requires-human-review",
|
|
patches: [],
|
|
},
|
|
}));
|
|
const result = await execFileAsync(zero, ["fix", "--patch", "--json", path]).catch((error) => error);
|
|
if (!result.stdout) return diagnosticActions;
|
|
const body = JSON.parse(result.stdout);
|
|
const patchActions = (body.fixes ?? []).map((fix) => ({
|
|
title: fix.summary,
|
|
kind: "quickfix",
|
|
diagnostics: params.context?.diagnostics ?? [],
|
|
data: {
|
|
id: fix.id,
|
|
diagnosticCode: fix.diagnosticCode,
|
|
safety: fix.safety,
|
|
patches: body.patches ?? [],
|
|
},
|
|
}));
|
|
return [...diagnosticActions, ...patchActions];
|
|
}
|
|
|
|
async function didOpen(params, send) {
|
|
const { uri, text } = params.textDocument;
|
|
documents.set(uri, { text, version: params.textDocument.version ?? 0 });
|
|
analyzeText(uri, text);
|
|
const diagnostics = await compilerDiagnostics(uri, text);
|
|
send("textDocument/publishDiagnostics", { uri, diagnostics });
|
|
const facts = await compilerFacts(uriToPath(uri));
|
|
documentFacts.set(uri, facts);
|
|
send("zero/editorMetadata", { uri, facts });
|
|
}
|
|
|
|
async function didChange(params, send) {
|
|
const uri = params.textDocument.uri;
|
|
const text = params.contentChanges.at(-1)?.text ?? documents.get(uri)?.text ?? "";
|
|
documents.set(uri, { text, version: params.textDocument.version ?? 0 });
|
|
analyzeText(uri, text);
|
|
const diagnostics = await compilerDiagnostics(uri, text);
|
|
send("textDocument/publishDiagnostics", { uri, diagnostics });
|
|
const facts = await compilerFacts(uriToPath(uri));
|
|
documentFacts.set(uri, facts);
|
|
send("zero/editorMetadata", { uri, facts });
|
|
}
|
|
|
|
function workspaceSymbols(query) {
|
|
return [...symbols.values()].flat()
|
|
.filter((symbol) => !query || symbol.name.toLowerCase().includes(query.toLowerCase()))
|
|
.map((symbol) => ({ name: symbol.name, kind: symbolKind(symbol.kind), location: { uri: symbol.uri, range: symbol.selectionRange } }));
|
|
}
|
|
|
|
async function handle(method, params, send) {
|
|
if (method === "initialize") {
|
|
return {
|
|
capabilities: {
|
|
textDocumentSync: 2,
|
|
workspaceSymbolProvider: true,
|
|
completionProvider: { triggerCharacters: ["."] },
|
|
hoverProvider: true,
|
|
signatureHelpProvider: { triggerCharacters: ["("] },
|
|
definitionProvider: true,
|
|
documentSymbolProvider: true,
|
|
referencesProvider: true,
|
|
renameProvider: true,
|
|
codeActionProvider: true,
|
|
},
|
|
};
|
|
}
|
|
if (method === "textDocument/didOpen") return didOpen(params, send);
|
|
if (method === "textDocument/didChange") return didChange(params, send);
|
|
if (method === "workspace/symbol") return workspaceSymbols(params.query ?? "");
|
|
if (method === "textDocument/completion") return completionItems();
|
|
if (method === "textDocument/hover") return hover(params);
|
|
if (method === "textDocument/signatureHelp") return signatureHelp(params);
|
|
if (method === "textDocument/definition") return definition(params);
|
|
if (method === "textDocument/documentSymbol") return documentSymbols(params);
|
|
if (method === "textDocument/references") return references(params);
|
|
if (method === "textDocument/rename") return rename(params);
|
|
if (method === "textDocument/codeAction") return codeActions(params);
|
|
return null;
|
|
}
|
|
|
|
function sendMessage(message) {
|
|
const json = JSON.stringify(message);
|
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`);
|
|
}
|
|
|
|
function startServer() {
|
|
let buffer = Buffer.alloc(0);
|
|
const send = (method, params) => sendMessage({ jsonrpc: "2.0", method, params });
|
|
process.stdin.on("data", async (chunk) => {
|
|
const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
buffer = Buffer.concat([buffer, data]);
|
|
while (true) {
|
|
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
if (headerEnd < 0) return;
|
|
const header = buffer.slice(0, headerEnd).toString("utf8");
|
|
const match = header.match(/Content-Length: (\d+)/i);
|
|
if (!match) return;
|
|
const length = Number(match[1]);
|
|
const bodyStart = headerEnd + 4;
|
|
if (buffer.length < bodyStart + length) return;
|
|
const body = buffer.slice(bodyStart, bodyStart + length).toString("utf8");
|
|
buffer = buffer.slice(bodyStart + length);
|
|
const message = JSON.parse(body);
|
|
if (message.method === "exit") process.exit(0);
|
|
const result = await handle(message.method, message.params ?? {}, send);
|
|
if (Object.hasOwn(message, "id")) sendMessage({ jsonrpc: "2.0", id: message.id, result });
|
|
}
|
|
});
|
|
}
|
|
|
|
async function selfTest() {
|
|
const dir = ".zero/lsp";
|
|
await mkdir(dir, { recursive: true });
|
|
const path = join(dir, "sample.0");
|
|
const uri = pathToUri(path);
|
|
const text = "pub fn add(a: i32, b: i32) -> i32 {\n return a + b\n}\n\npub fn main(world: World) -> Void raises {\n check world.out.write(\"ok\\n\")\n}\n";
|
|
await writeFile(path, text);
|
|
const notifications = [];
|
|
await didOpen({ textDocument: { uri, text, version: 1 } }, (method, params) => notifications.push({ method, params }));
|
|
const metadata = notifications.find((item) => item.method === "zero/editorMetadata")?.params?.facts;
|
|
assert.equal(metadata.generatedCBytes, 0);
|
|
assert.equal(metadata.cBridgeFallback, false);
|
|
assert(metadata.targetCapabilityFacts);
|
|
assert(metadata.runtimeHelperCosts.usedStdlibHelpers.length >= 0);
|
|
assert(Array.isArray(metadata.memoryBudget.sections));
|
|
assert(metadata.directBackendStatus.objectBackend);
|
|
assert(Array.isArray(metadata.generatedBindingPreviews));
|
|
assert(symbols.get(uri).some((symbol) => symbol.name === "add"));
|
|
assert(notifications.some((item) => item.method === "textDocument/publishDiagnostics"));
|
|
assert(workspaceSymbols("add").some((symbol) => symbol.name === "add"));
|
|
const completions = completionItems();
|
|
assert(completions.some((item) => item.label === "add"));
|
|
assert(completions.some((item) => item.label === "fn"));
|
|
assert(completions.some((item) => item.label === "return"));
|
|
assert(!completions.some((item) => item.label === "fun"));
|
|
assert(completions.some((item) => item.label === "raises"));
|
|
const hoverText = hover({ textDocument: { uri }, position: { line: 0, character: 9 } }).contents.value;
|
|
assert(hoverText.includes("add"));
|
|
assert(hoverText.includes("capabilities:"));
|
|
assert(hoverText.includes("generated binding previews:"));
|
|
assert(hoverText.includes("generatedCBytes: 0"));
|
|
assert(documentSymbols({ textDocument: { uri } }).some((symbol) => symbol.name === "add"));
|
|
assert(signatureHelp({ textDocument: { uri }, position: { line: 4, character: 20 } }).signatures.length >= 0);
|
|
assert(definition({ textDocument: { uri }, position: { line: 0, character: 9 } }).uri === uri);
|
|
assert(references({ textDocument: { uri }, position: { line: 0, character: 9 } }).length >= 1);
|
|
assert(rename({ textDocument: { uri }, position: { line: 0, character: 9 }, newName: "sum" }).changes[uri].length >= 1);
|
|
const mutableFixPath = join(dir, "mutable-fix.0");
|
|
const mutableFixUri = pathToUri(mutableFixPath);
|
|
const mutableFixText = "pub fn main() -> Void {\n let dst: [2]u8 = [0_u8; 2]\n let src: [2]u8 = [1_u8; 2]\n std.mem.copy(dst, src)\n}\n";
|
|
await writeFile(mutableFixPath, mutableFixText);
|
|
const mutableNotifications = [];
|
|
await didOpen({ textDocument: { uri: mutableFixUri, text: mutableFixText, version: 1 } }, (method, params) => mutableNotifications.push({ method, params }));
|
|
const mutableDiagnostics = mutableNotifications.find((item) => item.method === "textDocument/publishDiagnostics")?.params?.diagnostics ?? [];
|
|
const actions = await codeActions({ textDocument: { uri: mutableFixUri }, context: { diagnostics: mutableDiagnostics } });
|
|
assert(actions.some((action) => action.data.id === "make-binding-mutable"));
|
|
const targetActions = await codeActions({
|
|
textDocument: { uri },
|
|
context: {
|
|
diagnostics: [{
|
|
code: "TAR002",
|
|
message: "target does not provide hosted filesystem capability",
|
|
data: { repair: { id: "choose-target-with-required-capability", summary: "Build for a target with the required capability or move the code behind a target-specific entry point." }, fixSafety: "requires-human-review" },
|
|
}],
|
|
},
|
|
});
|
|
assert(targetActions.some((action) => action.data.id === "choose-target-with-required-capability"));
|
|
const typeAnnotationActions = await codeActions({
|
|
textDocument: { uri },
|
|
context: {
|
|
diagnostics: [{
|
|
code: "PUB001",
|
|
message: "public declaration requires explicit type metadata",
|
|
data: { repair: { id: "add-public-api-type", summary: "Add an explicit public type annotation so graph and docs metadata stay stable." }, fixSafety: "behavior-preserving" },
|
|
}],
|
|
},
|
|
});
|
|
assert(typeAnnotationActions.some((action) => action.data.id === "add-public-api-type"));
|
|
const errorSetActions = await codeActions({
|
|
textDocument: { uri },
|
|
context: {
|
|
diagnostics: [
|
|
{
|
|
code: "ERR002",
|
|
message: "caller error set is missing a callee error",
|
|
data: { repair: { id: "add-missing-error-name", summary: "Add the missing error name to the caller `raises [...]` set." }, fixSafety: "api-changing" },
|
|
},
|
|
{
|
|
code: "ERR003",
|
|
message: "fallible call requires check or rescue",
|
|
data: { repair: { id: "check-or-rescue-fallible-call", summary: "Wrap the fallible call with check or handle it locally with rescue." }, fixSafety: "api-changing" },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
assert(errorSetActions.some((action) => action.data.id === "add-missing-error-name"));
|
|
assert(errorSetActions.some((action) => action.data.id === "check-or-rescue-fallible-call"));
|
|
console.log("zls self-test ok");
|
|
}
|
|
|
|
if (process.argv.includes("--self-test")) {
|
|
await selfTest();
|
|
} else {
|
|
startServer();
|
|
}
|