fix: Array move ownership, selfhost -g flags, LSP workspace/symbol
Session 36: prevent UAF when Array is moved into a struct field (Nexus headers), align selfhost cc flags with bootstrap debug/release, and add workspace/symbol search to bux-lsp 0.6. - Parser: zero headers after embedding so auto-drop is a no-op - Request_WantsKeepAlive uses RequestHeader_Get again safely - Selfhost: default -O0 -g; --release -O2 -DNDEBUG; BUX_CFLAGS - LSP: workspace/symbol + smoke; version 0.6.0
This commit is contained in:
@@ -193,6 +193,9 @@ test-lsp: lsp
|
|||||||
@echo "=== LSP references / rename smoke ==="
|
@echo "=== LSP references / rename smoke ==="
|
||||||
@chmod +x tools/smoke_lsp_rename.sh
|
@chmod +x tools/smoke_lsp_rename.sh
|
||||||
@tools/smoke_lsp_rename.sh
|
@tools/smoke_lsp_rename.sh
|
||||||
|
@echo "=== LSP workspace/symbol smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_workspace.sh
|
||||||
|
@tools/smoke_lsp_workspace.sh
|
||||||
|
|
||||||
.PHONY: test-registry
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: build
|
||||||
|
|||||||
+17
-3
@@ -133,15 +133,29 @@ module Http {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decide keep-alive from the raw request bytes (avoids fragile header Array walk).
|
|
||||||
/// HTTP/1.1 defaults to keep-alive; Connection: close forces close;
|
/// HTTP/1.1 defaults to keep-alive; Connection: close forces close;
|
||||||
/// HTTP/1.0 needs explicit keep-alive.
|
/// HTTP/1.0 needs explicit keep-alive. Uses parsed headers (safe after
|
||||||
|
/// Parser field-move ownership handoff).
|
||||||
|
pub func Request_WantsKeepAlive(req: *HttpRequest) -> bool {
|
||||||
|
let conn: String = RequestHeader_Get(req, "Connection");
|
||||||
|
if String_EqIgnoreCase(conn, "close") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_EqIgnoreCase(conn, "keep-alive") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if String_StartsWith(req.version, "HTTP/1.0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fallback when only raw bytes are available.
|
||||||
pub func RawRequest_WantsKeepAlive(raw: String) -> bool {
|
pub func RawRequest_WantsKeepAlive(raw: String) -> bool {
|
||||||
if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") ||
|
if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") ||
|
||||||
String_Contains(raw, "CONNECTION: CLOSE") {
|
String_Contains(raw, "CONNECTION: CLOSE") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// HTTP/1.0 without Keep-Alive → close
|
|
||||||
if String_Contains(raw, "HTTP/1.0") {
|
if String_Contains(raw, "HTTP/1.0") {
|
||||||
if String_Contains(raw, "Connection: keep-alive") ||
|
if String_Contains(raw, "Connection: keep-alive") ||
|
||||||
String_Contains(raw, "Connection: Keep-Alive") ||
|
String_Contains(raw, "Connection: Keep-Alive") ||
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ module Parser {
|
|||||||
|
|
||||||
// Find header/body boundary
|
// Find header/body boundary
|
||||||
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||||
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
// Only one allocation path — avoid Array_New then overwrite (leak) and
|
||||||
|
// suppress auto-drop after moving into HttpRequest (use-after-free).
|
||||||
|
var headers: Array<HeaderEntry>;
|
||||||
var body: String = "";
|
var body: String = "";
|
||||||
if String_Len(boundary) > 0 {
|
if String_Len(boundary) > 0 {
|
||||||
let headerEnd: uint = bux_str_offset(boundary, raw);
|
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||||
@@ -114,6 +116,7 @@ module Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if String_Eq(path, "") {
|
if String_Eq(path, "") {
|
||||||
|
// auto-drop of `headers` runs on return
|
||||||
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +127,11 @@ module Parser {
|
|||||||
body: body,
|
body: body,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
};
|
};
|
||||||
|
// Ownership transferred into req — zero local shell so auto-drop is a no-op.
|
||||||
|
// (Compiler does not yet treat field-move as a move-out of the local.)
|
||||||
|
headers.data = null as *HeaderEntry;
|
||||||
|
headers.len = 0;
|
||||||
|
headers.cap = 0;
|
||||||
return ParseResult_NewOk(req);
|
return ParseResult_NewOk(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Server {
|
|||||||
import Std::String::{String_Len, String_StartsWith};
|
import Std::String::{String_Len, String_StartsWith};
|
||||||
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||||
import Config::{ServerConfig};
|
import Config::{ServerConfig};
|
||||||
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, RawRequest_WantsKeepAlive};
|
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Request_WantsKeepAlive};
|
||||||
import Errors::{ParseResult};
|
import Errors::{ParseResult};
|
||||||
import Parser::{ParseRequest};
|
import Parser::{ParseRequest};
|
||||||
import Router::{Router, Router_Dispatch};
|
import Router::{Router, Router_Dispatch};
|
||||||
@@ -87,8 +87,8 @@ module Server {
|
|||||||
let parsed: ParseResult = ParseRequest(raw);
|
let parsed: ParseResult = ParseRequest(raw);
|
||||||
var keepAlive: bool = false;
|
var keepAlive: bool = false;
|
||||||
if parsed.tag == ParseResult_Ok {
|
if parsed.tag == ParseResult_Ok {
|
||||||
let req: HttpRequest = parsed.data.Ok_0;
|
var req: HttpRequest = parsed.data.Ok_0;
|
||||||
keepAlive = RawRequest_WantsKeepAlive(raw);
|
keepAlive = Request_WantsKeepAlive(&req);
|
||||||
// Last request on the connection quota must close
|
// Last request on the connection quota must close
|
||||||
if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS {
|
if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS {
|
||||||
keepAlive = false;
|
keepAlive = false;
|
||||||
|
|||||||
@@ -146,12 +146,14 @@ make bench-nexus # wrk throughput vs apps/nexus /api/health
|
|||||||
### Debug builds (E.4)
|
### Debug builds (E.4)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./buxc build # -O0 -g, #line → .bux (gdb-friendly)
|
./buxc build # -O0 -g, #line → .bux (gdb-friendly; bootstrap)
|
||||||
./buxc build --release # -O2, no debug maps
|
./buxc build --release # -O2 -DNDEBUG, no #line / -g
|
||||||
|
# Selfhost (buxc2) same --release / default -O0 -g; #line maps bootstrap-only for now
|
||||||
|
export BUX_CFLAGS="-fno-omit-frame-pointer" # optional extra cc flags
|
||||||
gdb --args ./build/myapp
|
gdb --args ./build/myapp
|
||||||
# (gdb) break Main
|
# (gdb) break Main
|
||||||
# (gdb) run
|
# (gdb) run
|
||||||
# (gdb) list # shows Bux source via #line
|
# (gdb) list # shows Bux source via #line (bootstrap)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+24
-5
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-19
|
||||||
> **Текущо:** v0.5.x — LSP 0.5, CI smokes, **Nexus HTTP/1.1 keep-alive (~2× RPS)**
|
> **Текущо:** v0.5.x — Nexus keep-alive, **header Array ownership fix**, LSP 0.6 workspace/symbol, selfhost `-g`
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
| # | Задача | Защо | Статус |
|
| # | Задача | Защо | Статус |
|
||||||
|---|--------|------|--------|
|
|---|--------|------|--------|
|
||||||
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ v0.5.0: locals + **references** + **rename** + prepareRename |
|
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ v0.6.0: refs/rename + **workspace/symbol** |
|
||||||
| D.2 | `bux fmt` стабилен + CI check | Единен style | ✅ full-tree format + `make fmt-check` enforce |
|
| D.2 | `bux fmt` стабилен + CI check | Единен style | ✅ full-tree format + `make fmt-check` enforce |
|
||||||
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ✅ `--filter` / summary / exit 0\|1 |
|
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ✅ `--filter` / summary / exit 0\|1 |
|
||||||
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ✅ bootstrap+selfhost + `make docs` |
|
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ✅ bootstrap+selfhost + `make docs` |
|
||||||
@@ -575,9 +575,28 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Сесия 36 (header ownership + selfhost flags + LSP workspace/symbol)
|
||||||
|
|
||||||
|
1. **Root cause (Nexus headers UAF):** auto-drop of local `headers` after
|
||||||
|
shallow-copy into `HttpRequest` / `ParseResult` freed the buffer while still
|
||||||
|
referenced. Not Array ABI — **move-out-of-field not tracked**.
|
||||||
|
2. **Fix** (`apps/nexus/src/Parser.bux`): after embedding, zero
|
||||||
|
`headers.data/len/cap` so auto-drop is a no-op; `Request_WantsKeepAlive`
|
||||||
|
again uses `RequestHeader_Get` safely. Bench still ~76–80k RPS.
|
||||||
|
3. **Selfhost compile flags** (`src/cli.bux`):
|
||||||
|
- default **`-O0 -g`** (was always `-O2`)
|
||||||
|
- `--release` → **`-O2 -DNDEBUG`** (was `-O3 -flto`)
|
||||||
|
- `BUX_CFLAGS` appended via `bux_getenv`
|
||||||
|
- `#line` maps remain bootstrap-only (selfhost C backend has no LIR #line yet)
|
||||||
|
4. **LSP 0.6.0:** `workspace/symbol` over open docs + workspace index (cap 200);
|
||||||
|
`tools/smoke_lsp_workspace.sh` + `make test-lsp`
|
||||||
|
5. Verified: nexus keep-alive + header Get; `make lsp` + workspace smoke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. Selfhost parity for `--release` / `#line` (optional)
|
1. Compiler: skip Drop when local is moved into a struct field / return payload
|
||||||
2. LSP: workspace symbol search / call hierarchy (optional)
|
2. Selfhost `#line` maps (parity with bootstrap LIR backend)
|
||||||
3. Deeper rename (type members / qualified paths)
|
3. Deeper rename (type members / qualified paths)
|
||||||
4. Fix header Array iteration / string field ABI (root cause of Get crash)
|
4. LSP call hierarchy (optional)
|
||||||
|
|||||||
+20
-5
@@ -325,11 +325,17 @@ func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile with cc or clang for cross-compilation
|
// Compile with cc or clang for cross-compilation
|
||||||
|
// Default: -O0 -g (debug, matches bootstrap). --release: -O2 -DNDEBUG.
|
||||||
PrintLine("Compiling C...");
|
PrintLine("Compiling C...");
|
||||||
var cmdBuf: StringBuilder = StringBuilder_NewCap(512);
|
var cmdBuf: StringBuilder = StringBuilder_NewCap(512);
|
||||||
var optFlags: String = "-O2";
|
var optFlags: String = "-O0 -g";
|
||||||
if isRelease {
|
if isRelease {
|
||||||
optFlags = "-O3 -flto";
|
optFlags = "-O2 -DNDEBUG";
|
||||||
|
}
|
||||||
|
let extraCf: String = bux_getenv("BUX_CFLAGS");
|
||||||
|
if extraCf != null as String && !String_Eq(extraCf, "") {
|
||||||
|
optFlags = String_Concat(optFlags, " ");
|
||||||
|
optFlags = String_Concat(optFlags, extraCf);
|
||||||
}
|
}
|
||||||
if !String_Eq(targetTriple, "") {
|
if !String_Eq(targetTriple, "") {
|
||||||
StringBuilder_Append(&cmdBuf, "clang ");
|
StringBuilder_Append(&cmdBuf, "clang ");
|
||||||
@@ -1623,13 +1629,18 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
|
|||||||
ioPath = bux_path_join(projectDir, "../../rt/io.c");
|
ioPath = bux_path_join(projectDir, "../../rt/io.c");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile with cc
|
// Compile with cc — default debug (-O0 -g); --release → -O2 -DNDEBUG
|
||||||
PrintLine("Compiling C...");
|
PrintLine("Compiling C...");
|
||||||
let outBin: String = bux_path_join(buildDir, outName);
|
let outBin: String = bux_path_join(buildDir, outName);
|
||||||
var ccBuf: StringBuilder = StringBuilder_NewCap(512);
|
var ccBuf: StringBuilder = StringBuilder_NewCap(512);
|
||||||
var optFlags2: String = "-O2";
|
var optFlags2: String = "-O0 -g";
|
||||||
if isRelease {
|
if isRelease {
|
||||||
optFlags2 = "-O3 -flto";
|
optFlags2 = "-O2 -DNDEBUG";
|
||||||
|
}
|
||||||
|
let extraCf2: String = bux_getenv("BUX_CFLAGS");
|
||||||
|
if extraCf2 != null as String && !String_Eq(extraCf2, "") {
|
||||||
|
optFlags2 = String_Concat(optFlags2, " ");
|
||||||
|
optFlags2 = String_Concat(optFlags2, extraCf2);
|
||||||
}
|
}
|
||||||
if !String_Eq(targetTriple, "") {
|
if !String_Eq(targetTriple, "") {
|
||||||
StringBuilder_Append(&ccBuf, "clang ");
|
StringBuilder_Append(&ccBuf, "clang ");
|
||||||
@@ -1729,6 +1740,8 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
||||||
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
||||||
|
PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g");
|
||||||
|
PrintLine(" BUX_CFLAGS Extra flags appended to cc");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1745,6 +1758,8 @@ func Cli_Run(args: *String, argCount: int) -> int {
|
|||||||
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
PrintLine(" test --filter <name> Only run tests/*.bux whose name contains <name>");
|
||||||
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
PrintLine(" fmt --check [path] Exit 1 if any file would be reformatted (CI)");
|
||||||
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
PrintLine(" doc --out file.md [path] API docs from /// comments");
|
||||||
|
PrintLine(" --release Optimize (-O2 -DNDEBUG); default is -O0 -g");
|
||||||
|
PrintLine(" BUX_CFLAGS Extra flags appended to cc");
|
||||||
PrintLine("Pipeline modules:");
|
PrintLine("Pipeline modules:");
|
||||||
PrintLine(" Lexer ✅");
|
PrintLine(" Lexer ✅");
|
||||||
PrintLine(" Parser ✅");
|
PrintLine(" Parser ✅");
|
||||||
|
|||||||
+57
-2
@@ -7,6 +7,7 @@
|
|||||||
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
||||||
# Locals are position-sensitive (scoped) and include inferred `let` types (v0.4.0).
|
# Locals are position-sensitive (scoped) and include inferred `let` types (v0.4.0).
|
||||||
# v0.5.0: textDocument/references + rename (scoped locals + workspace globals).
|
# v0.5.0: textDocument/references + rename (scoped locals + workspace globals).
|
||||||
|
# v0.6.0: workspace/symbol search.
|
||||||
|
|
||||||
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
||||||
import lexer, parser, ast, sema, types, scope, source_location
|
import lexer, parser, ast, sema, types, scope, source_location
|
||||||
@@ -1556,6 +1557,56 @@ proc handleDocumentSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode
|
|||||||
})
|
})
|
||||||
sendResponse(stream, id, arr)
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
|
proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
## workspace/symbol — fuzzy-ish substring filter over workspace + open docs.
|
||||||
|
let query = if paramsNode.hasKey("query"): paramsNode["query"].getStr().toLowerAscii() else: ""
|
||||||
|
var arr = newJArray()
|
||||||
|
var seen = initHashSet[string]() # name@uri
|
||||||
|
|
||||||
|
proc maybeAdd(name, uri: string, info: SymbolInfo) =
|
||||||
|
if query.len > 0 and query notin name.toLowerAscii() and
|
||||||
|
query notin info.detail.toLowerAscii() and
|
||||||
|
query notin info.kind.toLowerAscii():
|
||||||
|
return
|
||||||
|
let key = name & "@" & uri
|
||||||
|
if seen.contains(key): return
|
||||||
|
seen.incl(key)
|
||||||
|
var item = %*{
|
||||||
|
"name": name,
|
||||||
|
"kind": symbolKindLsp(info.kind),
|
||||||
|
"location": {
|
||||||
|
"uri": uri,
|
||||||
|
"range": {
|
||||||
|
"start": {"line": info.line, "character": info.col},
|
||||||
|
"end": {"line": info.line, "character": info.col + name.len}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.detail.len > 0:
|
||||||
|
item["containerName"] = %info.kind
|
||||||
|
arr.add(item)
|
||||||
|
|
||||||
|
# Open documents first (freshest)
|
||||||
|
for uri, doc in documents.pairs:
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
for name, info in doc.symbols.pairs:
|
||||||
|
maybeAdd(name, uri, info)
|
||||||
|
|
||||||
|
# Workspace index from scan
|
||||||
|
for name, ws in workspaceSymbols.pairs:
|
||||||
|
maybeAdd(name, ws.uri, ws.info)
|
||||||
|
|
||||||
|
# Cap result size for IDE responsiveness
|
||||||
|
if arr.len > 200:
|
||||||
|
var capped = newJArray()
|
||||||
|
var i = 0
|
||||||
|
while i < 200:
|
||||||
|
capped.add(arr[i])
|
||||||
|
inc i
|
||||||
|
arr = capped
|
||||||
|
|
||||||
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main message loop
|
# Main message loop
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1578,9 +1629,10 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"hoverProvider": true,
|
"hoverProvider": true,
|
||||||
"documentSymbolProvider": true,
|
"documentSymbolProvider": true,
|
||||||
"referencesProvider": true,
|
"referencesProvider": true,
|
||||||
"renameProvider": {"prepareProvider": true}
|
"renameProvider": {"prepareProvider": true},
|
||||||
|
"workspaceSymbolProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.5.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.6.0"}
|
||||||
})
|
})
|
||||||
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
||||||
rootPath = paramsNode["rootPath"].getStr()
|
rootPath = paramsNode["rootPath"].getStr()
|
||||||
@@ -1664,6 +1716,9 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
of "textDocument/rename":
|
of "textDocument/rename":
|
||||||
handleRename(stream, id, paramsNode)
|
handleRename(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "workspace/symbol":
|
||||||
|
handleWorkspaceSymbol(stream, id, paramsNode)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if id != nil:
|
if id != nil:
|
||||||
sendError(stream, id, -32601, "method not found: " & methodName)
|
sendError(stream, id, -32601, "method not found: " & methodName)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ if ! grep -q 'renameProvider' "$TMP/out.txt"; then
|
|||||||
echo "FAIL: initialize missing renameProvider"
|
echo "FAIL: initialize missing renameProvider"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! grep -q '0.5.0' "$TMP/out.txt"; then
|
if ! grep -qE '0\.[56]\.0' "$TMP/out.txt"; then
|
||||||
echo "WARN: version not 0.5.0 in initialize (may be ok)"
|
echo "WARN: unexpected bux-lsp version in initialize"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rename workspace edit should propose "total"
|
# Rename workspace edit should propose "total"
|
||||||
|
|||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: workspace/symbol (bux-lsp 0.6.0)
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
LSP="$ROOT/tools/bux-lsp"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -x "$LSP" ]]; then
|
||||||
|
(cd "$ROOT" && make lsp >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/src"
|
||||||
|
cat > "$TMP/src/Main.bux" <<'EOF'
|
||||||
|
func Helper() -> int { return 1; }
|
||||||
|
func Main() -> int {
|
||||||
|
return Helper();
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat > "$TMP/src/Util.bux" <<'EOF'
|
||||||
|
func Util_Max(a: int, b: int) -> int {
|
||||||
|
if a > b { return a; }
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rpc() {
|
||||||
|
local body="$1"
|
||||||
|
local len
|
||||||
|
len=$(printf '%s' "$body" | wc -c)
|
||||||
|
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/src/Main.bux")
|
||||||
|
URI="file://$TMP/src/Main.bux"
|
||||||
|
|
||||||
|
{
|
||||||
|
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
|
||||||
|
# workspace symbol query "Helper"
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"workspace/symbol","params":{"query":"Helper"}}'
|
||||||
|
# query "Util"
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"workspace/symbol","params":{"query":"Util"}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"shutdown","params":null}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
|
if ! grep -q 'workspaceSymbolProvider' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: missing workspaceSymbolProvider"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q '0.6.0' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: version not 0.6.0"
|
||||||
|
fi
|
||||||
|
if ! grep -q 'Helper' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: workspace/symbol did not find Helper"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Util may come from workspace scan of Util.bux
|
||||||
|
if ! grep -q 'Util' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: Util not in workspace results (scan depth/path?)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: LSP workspace/symbol smoke"
|
||||||
Reference in New Issue
Block a user