feat(lsp): interface dispatch in call hierarchy (0.11)

Index interface methods and extend Type for Trait relations so call
hierarchy can show implementors as outgoing targets and .Method call
sites as incoming.

- interface I { func M } + extend T for I tracking
- prepare/outgoing/incoming with data Iface#Method
- smoke_lsp_iface_hierarchy (Drawable.Draw ↔ Circle / Render)
This commit is contained in:
2026-07-19 23:30:52 +03:00
parent 00195e2d98
commit 244a3be8f3
6 changed files with 313 additions and 23 deletions
+3
View File
@@ -208,6 +208,9 @@ test-lsp: lsp
@echo "=== LSP method/type/receiver rename smoke ===" @echo "=== LSP method/type/receiver rename smoke ==="
@chmod +x tools/smoke_lsp_rename_method.sh @chmod +x tools/smoke_lsp_rename_method.sh
@tools/smoke_lsp_rename_method.sh @tools/smoke_lsp_rename_method.sh
@echo "=== LSP interface dispatch hierarchy smoke ==="
@chmod +x tools/smoke_lsp_iface_hierarchy.sh
@tools/smoke_lsp_iface_hierarchy.sh
.PHONY: test-registry .PHONY: test-registry
test-registry: build test-registry: build
+17 -4
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-19 > **Дата:** 2026-07-19
> **Текущо:** v0.5.x — multi-file #line, selfhost CI, **LSP 0.10 method/type rename**, Nexus KA > **Текущо:** v0.5.x — multi-file #line, selfhost CI, **LSP 0.11 interface hierarchy**, Nexus KA
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
@@ -78,7 +78,7 @@
| # | Задача | Защо | Статус | | # | Задача | Защо | Статус |
|---|--------|------|--------| |---|--------|------|--------|
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ v0.9.0: + **method call hierarchy** (extend / `.Method`) | | D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ v0.10.0: + **method/type/self rename** + method hierarchy |
| 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` |
@@ -697,9 +697,22 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
--- ---
## Сесия 44 (LSP 0.10 method + type + receiver rename)
1. **`rtkMethod`**: rename method decl + `.Method(` + bare `Method(`
- (previous global path skipped `iaDot` → broke method rename)
2. **Type rename** (`isType`): `struct`/`extend Type`/`self: Type`/ctors; skip `.field`
3. **`self` receiver**: synthetic local when sema omits method params; clip to
enclosing `func` body via textual bounds (sibling methods safe)
4. Smoke: `tools/smoke_lsp_rename_method.sh`
- Len→Length ≥2, Point→Vec2 ≥4, self→this =3 (one method only)
5. Version **bux-lsp 0.10.0**; `make test-lsp`
---
## Следващи стъпки ## Следващи стъпки
1. Rename of method receivers / qualified module paths (edge cases) 1. Interface dispatch in call hierarchy (dynamic)
2. Interface dispatch in call hierarchy (dynamic) 2. Module-path segment rename (`Std::Io` style imports)
3. HirNode-level file for statements spanning multiple files (rare) 3. HirNode-level file for statements spanning multiple files (rare)
4. Optional: selfhost-loop as optional CI job (slow) 4. Optional: selfhost-loop as optional CI job (slow)
+200 -16
View File
@@ -12,6 +12,7 @@
# v0.8.0: call hierarchy (prepare / incoming / outgoing). # v0.8.0: call hierarchy (prepare / incoming / outgoing).
# v0.9.0: method call hierarchy (extend Type / .Method() sites). # v0.9.0: method call hierarchy (extend Type / .Method() sites).
# v0.10.0: method rename + qualified path / extend Type rename edges. # v0.10.0: method rename + qualified path / extend Type rename edges.
# v0.11.0: interface dispatch in call hierarchy (extend Type for Trait).
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
@@ -125,12 +126,18 @@ type
locals: seq[LocalBinding] locals: seq[LocalBinding]
## Type members for field/variant rename (v0.7) ## Type members for field/variant rename (v0.7)
members: seq[MemberInfo] members: seq[MemberInfo]
## Interface methods (v0.11): name is method, parent is interface
ifaceMethods: seq[MemberInfo]
## Type implements Interface (from `extend Type for Interface`)
impls: seq[tuple[typeName, iface: string, line: int]]
var var
documents = initTable[string, DocumentState]() documents = initTable[string, DocumentState]()
rootPath = "" rootPath = ""
rootUri = "" rootUri = ""
workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]() workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]()
## Cross-file: "Iface.Method" → list of implementors
workspaceImpls = initTable[string, seq[tuple[uri, typeName, meth: string]]]()
cachedStdlibDir = "" cachedStdlibDir = ""
cachedStdlibDecls: seq[Decl] = @[] cachedStdlibDecls: seq[Decl] = @[]
stdlibLoaded = false stdlibLoaded = false
@@ -318,8 +325,17 @@ proc analyzeFile(path: string, content: string): DocumentState =
## extend/impl Type { ... } tracking for method indexing ## extend/impl Type { ... } tracking for method indexing
var braceDepth = 0 var braceDepth = 0
var activeExtend = "" ## Type name of open extend/impl block var activeExtend = "" ## Type name of open extend/impl block
var activeIface = "" ## Interface when `extend Type for Iface`
var extendBodyDepth = 0 ## braceDepth of the extend/impl opening `{` var extendBodyDepth = 0 ## braceDepth of the extend/impl opening `{`
var pendingExtend = "" ## Type name after `extend Name` before `{` var pendingExtend = "" ## Type name after `extend Name` before `{`
var pendingIface = "" ## Interface name after `for`
## interface Iface { func M... } — abstract methods
var activeInterface = ""
var interfaceBodyDepth = 0
var pendingInterface = ""
result.ifaceMethods = @[]
result.impls = @[]
while i < content.len: while i < content.len:
let c = content[i] let c = content[i]
@@ -365,19 +381,31 @@ proc analyzeFile(path: string, content: string): DocumentState =
inc i inc i
continue continue
# Brace tracking for extend/impl bodies (outside strings/comments) # Brace tracking for extend/impl/interface bodies (outside strings/comments)
if c == '{': if c == '{':
inc braceDepth inc braceDepth
if pendingExtend.len > 0 and extendBodyDepth == 0: if pendingExtend.len > 0 and extendBodyDepth == 0:
activeExtend = pendingExtend activeExtend = pendingExtend
activeIface = pendingIface
extendBodyDepth = braceDepth extendBodyDepth = braceDepth
if pendingIface.len > 0:
result.impls.add((activeExtend, pendingIface, lineColAt(content, i).line))
pendingExtend = "" pendingExtend = ""
pendingIface = ""
if pendingInterface.len > 0 and interfaceBodyDepth == 0:
activeInterface = pendingInterface
interfaceBodyDepth = braceDepth
pendingInterface = ""
inc i inc i
continue continue
if c == '}': if c == '}':
if extendBodyDepth > 0 and braceDepth == extendBodyDepth: if extendBodyDepth > 0 and braceDepth == extendBodyDepth:
activeExtend = "" activeExtend = ""
activeIface = ""
extendBodyDepth = 0 extendBodyDepth = 0
if interfaceBodyDepth > 0 and braceDepth == interfaceBodyDepth:
activeInterface = ""
interfaceBodyDepth = 0
if braceDepth > 0: if braceDepth > 0:
dec braceDepth dec braceDepth
inc i inc i
@@ -389,15 +417,35 @@ proc analyzeFile(path: string, content: string): DocumentState =
(i == 0 or not isIdentChar(content[i - 1])) and (i == 0 or not isIdentChar(content[i - 1])) and
(i + kw.len >= content.len or not isIdentChar(content[i + kw.len]))) (i + kw.len >= content.len or not isIdentChar(content[i + kw.len])))
# extend Type / impl Type — methods live in the following { block } # interface Name — abstract methods in the following { block }
if atWord("interface"):
i += 9
skipWs(content, i)
let iname = readIdent(content, i)
if iname.len > 0:
let (line, col) = lineColAt(content, i - iname.len)
addSymbol(result, iname, SymbolInfo(
line: line, col: col, kind: "interface", detail: "interface " & iname, container: ""))
pendingInterface = iname
continue
# extend Type / impl Type [for Interface] — methods in following { block }
if atWord("extend") or atWord("impl"): if atWord("extend") or atWord("impl"):
let kwLen = if content[i] == 'e': 6 else: 4 let kwLen = if content[i] == 'e': 6 else: 4
i += kwLen i += kwLen
skipWs(content, i) skipWs(content, i)
# optional "Type for Trait" — take first type name
let tname = readIdent(content, i) let tname = readIdent(content, i)
if tname.len > 0: if tname.len > 0:
pendingExtend = tname pendingExtend = tname
pendingIface = ""
skipWs(content, i)
# `for Interface`
if atWord("for"):
i += 3
skipWs(content, i)
let iface = readIdent(content, i)
if iface.len > 0:
pendingIface = iface
continue continue
if atWord("func"): if atWord("func"):
@@ -426,22 +474,42 @@ proc analyzeFile(path: string, content: string): DocumentState =
discard readTypeish(content, k) discard readTypeish(content, k)
sigEnd = k sigEnd = k
break break
elif ch == '{' or ch == '\n' and depth == 0 and j > nameStart + name.len: elif ch == '{' or ch == ';' or (ch == '\n' and depth == 0 and j > nameStart + name.len):
# no-param or broken — still capture name # interface methods often end with `;` (no body)
if sigEnd <= nameStart: if sigEnd <= nameStart:
sigEnd = i sigEnd = i
if ch == ';':
sigEnd = j
break break
inc j inc j
var sig = content[kwPos ..< min(sigEnd, content.len)].strip() var sig = content[kwPos ..< min(sigEnd, content.len)].strip()
# collapse whitespace # collapse whitespace
sig = sig.replace("\n", " ").multiReplace([(" ", " "), (" ", " "), (" ", " ")]) sig = sig.replace("\n", " ").multiReplace([(" ", " "), (" ", " "), (" ", " ")])
let isMethod = activeExtend.len > 0 if activeInterface.len > 0:
let kind = if isMethod: "method" else: "function" # Abstract interface method
let container = if isMethod: activeExtend else: "" sig = activeInterface & "." & sig
if isMethod:
sig = activeExtend & "." & sig
addSymbol(result, name, SymbolInfo( addSymbol(result, name, SymbolInfo(
line: line, col: col, kind: kind, detail: sig, container: container)) line: line, col: col, kind: "method", detail: sig,
container: activeInterface))
result.ifaceMethods.add(MemberInfo(
name: name, parent: activeInterface, kind: "iface_method",
line: line, col: col))
elif activeExtend.len > 0:
var detail = activeExtend & "." & sig
if activeIface.len > 0:
detail = detail & " [" & activeIface & "]"
addSymbol(result, name, SymbolInfo(
line: line, col: col, kind: "method", detail: detail,
container: activeExtend))
# Record as implementor of interface method
if activeIface.len > 0:
let key = activeIface & "." & name
if not workspaceImpls.hasKey(key):
workspaceImpls[key] = @[]
workspaceImpls[key].add((result.uri, activeExtend, name))
else:
addSymbol(result, name, SymbolInfo(
line: line, col: col, kind: "function", detail: sig, container: ""))
continue continue
if atWord("let") or atWord("var") or atWord("const"): if atWord("let") or atWord("var") or atWord("const"):
@@ -1078,6 +1146,8 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
doc.symbols = updated.symbols doc.symbols = updated.symbols
doc.ordered = updated.ordered doc.ordered = updated.ordered
doc.members = updated.members doc.members = updated.members
doc.ifaceMethods = updated.ifaceMethods
doc.impls = updated.impls
# Keep / refresh real types for hover (does not replace lightweight outline) # Keep / refresh real types for hover (does not replace lightweight outline)
enrichWithSema(doc) enrichWithSema(doc)
let diags = runBuxcDiagnostics(path, doc.content) let diags = runBuxcDiagnostics(path, doc.content)
@@ -1128,6 +1198,8 @@ proc ensureAnalyzed(doc: DocumentState) =
doc.symbols = updated.symbols doc.symbols = updated.symbols
doc.ordered = updated.ordered doc.ordered = updated.ordered
doc.members = updated.members doc.members = updated.members
doc.ifaceMethods = updated.ifaceMethods
doc.impls = updated.impls
proc completionKind(kind: string): int = proc completionKind(kind: string): int =
case kind case kind
@@ -2267,6 +2339,76 @@ proc lookupFuncSym(name, uri: string): FuncSym =
if all.hasKey(bare): if all.hasKey(bare):
return all[bare] return all[bare]
proc findIfaceMethodAt(doc: DocumentState, word: string, line, col: int): tuple[ok: bool, m: MemberInfo] =
result.ok = false
for m in doc.ifaceMethods:
if m.name != word: continue
if m.line == line and col >= m.col and col <= m.col + word.len:
result.ok = true
result.m = m
return
# Only one iface method with this name
var n = 0
var last: MemberInfo
for m in doc.ifaceMethods:
if m.name == word:
inc n
last = m
if n == 1:
result.ok = true
result.m = last
proc ifaceMethodItem(uri: string, m: MemberInfo): JsonNode =
%*{
"name": m.parent & "." & m.name,
"kind": 11, # SymbolKind.Interface
"detail": "interface " & m.parent & "." & m.name,
"uri": uri,
"range": {
"start": {"line": m.line, "character": 0},
"end": {"line": m.line, "character": m.col + m.name.len}
},
"selectionRange": {
"start": {"line": m.line, "character": m.col},
"end": {"line": m.line, "character": m.col + m.name.len}
},
"data": m.parent & "#" & m.name # iface#method for dispatch
}
proc collectImplementorFuncs(iface, meth: string): seq[FuncSym] =
## Find concrete methods Type.Method where Type implements Interface.
result = @[]
var seen = initHashSet[string]()
for uri, doc in documents.pairs:
ensureAnalyzed(doc)
for impl in doc.impls:
if impl.iface != iface: continue
if doc.symbols.hasKey(meth):
let info = doc.symbols[meth]
if info.kind == "method" and (info.container == impl.typeName or info.container == iface):
let key = uri & "#" & impl.typeName & "." & meth
if seen.contains(key): continue
seen.incl(key)
var useInfo = info
if info.container == iface:
# Symbol table kept iface method; use impl type from relation
useInfo = SymbolInfo(
line: info.line, col: info.col, kind: "method",
detail: impl.typeName & ".func " & meth & " [" & iface & "]",
container: impl.typeName)
# Prefer implementor container over iface when both present
if info.container == impl.typeName:
useInfo = info
result.add(FuncSym(name: meth, uri: uri, info: useInfo))
for name, info in doc.symbols.pairs:
if name != meth: continue
if info.kind != "method": continue
if info.container == impl.typeName:
let key = uri & "#" & impl.typeName & "." & meth
if seen.contains(key): continue
seen.incl(key)
result.add(FuncSym(name: meth, uri: uri, info: info))
proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
let uri = paramsNode["textDocument"]["uri"].getStr() let uri = paramsNode["textDocument"]["uri"].getStr()
let position = paramsNode["position"] let position = paramsNode["position"]
@@ -2281,6 +2423,11 @@ proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: Js
if word.len == 0: if word.len == 0:
sendResponse(stream, id, %*[]) sendResponse(stream, id, %*[])
return return
# Interface abstract method under cursor
let (iok, im) = findIfaceMethodAt(doc, word, lineNum, col)
if iok:
sendResponse(stream, id, %*[ifaceMethodItem(uri, im)])
return
# Prefer function/method symbol under cursor # Prefer function/method symbol under cursor
if doc.symbols.hasKey(word) and isCallableKind(doc.symbols[word].kind): if doc.symbols.hasKey(word) and isCallableKind(doc.symbols[word].kind):
let fs = FuncSym(name: word, uri: uri, info: doc.symbols[word]) let fs = FuncSym(name: word, uri: uri, info: doc.symbols[word])
@@ -2303,6 +2450,11 @@ proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: Js
off += lines[li].len + 1 off += lines[li].len + 1
off += ws off += ws
if isCallSiteAt(doc.content, off, word.len): if isCallSiteAt(doc.content, off, word.len):
# Prefer interface method if this name is an iface method
for m in doc.ifaceMethods:
if m.name == word:
sendResponse(stream, id, %*[ifaceMethodItem(uri, m)])
return
let all = allKnownFuncs() let all = allKnownFuncs()
if all.hasKey(word): if all.hasKey(word):
sendResponse(stream, id, %*[callHierarchyItem(all[word])]) sendResponse(stream, id, %*[callHierarchyItem(all[word])])
@@ -2318,13 +2470,19 @@ proc bareCallName(itemName: string): string =
proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
## Who calls this function/method? ## Who calls this function/method?
## Interface methods: callers of .Method( (dynamic dispatch sites).
if not paramsNode.hasKey("item"): if not paramsNode.hasKey("item"):
sendResponse(stream, id, %*[]) sendResponse(stream, id, %*[])
return return
let item = paramsNode["item"] let item = paramsNode["item"]
var name = item["name"].getStr() var name = item["name"].getStr()
var data = ""
if item.hasKey("data") and item["data"].kind == JString: if item.hasKey("data") and item["data"].kind == JString:
name = item["data"].getStr() data = item["data"].getStr()
if data.contains("#"):
name = data.split('#')[^1] # method name
else:
name = data
else: else:
name = bareCallName(name) name = bareCallName(name)
let sites = collectAllCallSites() let sites = collectAllCallSites()
@@ -2355,21 +2513,45 @@ proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode)
proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
## What does this function/method call? ## What does this function/method call?
## For interface methods: "outgoing" lists implementor methods (dispatch targets).
if not paramsNode.hasKey("item"): if not paramsNode.hasKey("item"):
sendResponse(stream, id, %*[]) sendResponse(stream, id, %*[])
return return
let item = paramsNode["item"] let item = paramsNode["item"]
var name = item["name"].getStr() var name = item["name"].getStr()
var data = ""
if item.hasKey("data") and item["data"].kind == JString: if item.hasKey("data") and item["data"].kind == JString:
name = item["data"].getStr() data = item["data"].getStr()
name = data
else: else:
name = bareCallName(name) name = bareCallName(name)
# Interface dispatch: data = "Iface#Method"
if data.contains("#"):
let parts = data.split('#')
if parts.len == 2:
let iface = parts[0]
let meth = parts[1]
let impls = collectImplementorFuncs(iface, meth)
var arr = newJArray()
for fs in impls:
arr.add(%*{
"to": callHierarchyItem(fs),
"fromRanges": [%*{
"start": {"line": fs.info.line, "character": fs.info.col},
"end": {"line": fs.info.line, "character": fs.info.col + meth.len}
}]
})
sendResponse(stream, id, arr)
return
let bare = bareCallName(name)
let uri = if item.hasKey("uri"): item["uri"].getStr() else: "" let uri = if item.hasKey("uri"): item["uri"].getStr() else: ""
let sites = collectAllCallSites() let sites = collectAllCallSites()
# Group by callee among sites whose caller is `name` # Group by callee among sites whose caller is bare name
var groups = initTable[string, tuple[fs: FuncSym, ranges: seq[tuple[line, col, len: int]]]]() var groups = initTable[string, tuple[fs: FuncSym, ranges: seq[tuple[line, col, len: int]]]]()
for s in sites: for s in sites:
if s.caller != name: continue if s.caller != bare: continue
if uri.len > 0 and s.callerUri != uri: continue if uri.len > 0 and s.callerUri != uri: continue
let key = s.callee let key = s.callee
if not groups.hasKey(key): if not groups.hasKey(key):
@@ -2418,7 +2600,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
"workspaceSymbolProvider": true, "workspaceSymbolProvider": true,
"callHierarchyProvider": true "callHierarchyProvider": true
}, },
"serverInfo": {"name": "bux-lsp", "version": "0.10.0"} "serverInfo": {"name": "bux-lsp", "version": "0.11.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()
@@ -2466,6 +2648,8 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
doc.symbols = updated.symbols doc.symbols = updated.symbols
doc.ordered = updated.ordered doc.ordered = updated.ordered
doc.members = updated.members doc.members = updated.members
doc.ifaceMethods = updated.ifaceMethods
doc.impls = updated.impls
# Re-apply typeIndex details onto matching names (don't drop sema types mid-edit) # Re-apply typeIndex details onto matching names (don't drop sema types mid-edit)
for name, detail in doc.typeIndex.pairs: for name, detail in doc.typeIndex.pairs:
if doc.symbols.hasKey(name): if doc.symbols.hasKey(name):
+1 -1
View File
@@ -57,7 +57,7 @@ if ! grep -q 'callHierarchyProvider' "$TMP/out.txt"; then
cat "$TMP/out.txt" cat "$TMP/out.txt"
exit 1 exit 1
fi fi
if ! grep -qE '0\.[89]\.0' "$TMP/out.txt"; then if ! grep -qE '0\.(8|9|10|11)\.0' "$TMP/out.txt"; then
echo "WARN: unexpected bux-lsp version" echo "WARN: unexpected bux-lsp version"
fi fi
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Smoke: interface dispatch call hierarchy (bux-lsp 0.11)
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
cat > "$TMP/Main.bux" <<'EOF'
interface Drawable {
func Draw(self: &Self);
}
struct Circle {
radius: int;
}
extend Circle for Drawable {
func Draw(self: &Circle) {
let r: int = self.radius;
}
}
func Render(c: Circle) {
c.Draw();
}
func Main() -> int {
let c: Circle = Circle { radius: 5 };
Render(c);
return 0;
}
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/Main.bux")
URI="file://$TMP/Main.bux"
# Draw iface method ~ line 1
# outgoing on interface Draw → implementor Circle.Draw
# incoming on interface Draw → Render (c.Draw())
{
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"'}}}'
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/prepareCallHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":9}}}'
rpc '{"jsonrpc":"2.0","id":3,"method":"callHierarchy/outgoingCalls","params":{"item":{"name":"Drawable.Draw","kind":11,"data":"Drawable#Draw","uri":"'"$URI"'","range":{"start":{"line":1,"character":0},"end":{"line":1,"character":13}},"selectionRange":{"start":{"line":1,"character":9},"end":{"line":1,"character":13}}}}}'
rpc '{"jsonrpc":"2.0","id":4,"method":"callHierarchy/incomingCalls","params":{"item":{"name":"Drawable.Draw","kind":11,"data":"Drawable#Draw","uri":"'"$URI"'","range":{"start":{"line":1,"character":0},"end":{"line":1,"character":13}},"selectionRange":{"start":{"line":1,"character":9},"end":{"line":1,"character":13}}}}}'
rpc '{"jsonrpc":"2.0","id":5,"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 '0.11.0' "$TMP/out.txt"; then
echo "WARN: version not 0.11.0"
fi
# prepare should surface Drawable.Draw
if ! grep -qE 'Drawable\.Draw|Draw' "$TMP/out.txt"; then
echo "FAIL: prepare missing Draw"
cat "$TMP/out.txt"
exit 1
fi
# outgoing: implementor Circle (or Circle.Draw)
if ! grep -qE 'Circle' "$TMP/out.txt"; then
echo "FAIL: outgoing interface method should list Circle implementor"
cat "$TMP/out.txt"
exit 1
fi
# incoming: Render calls c.Draw()
if ! grep -q '"name":"Render"' "$TMP/out.txt"; then
echo "FAIL: incoming should include Render"
cat "$TMP/out.txt"
exit 1
fi
# kind 11 interface somewhere
if ! grep -q '"kind":11' "$TMP/out.txt"; then
echo "FAIL: expected SymbolKind.Interface (11) for iface method"
exit 1
fi
echo "PASS: LSP interface dispatch hierarchy (Drawable.Draw ↔ Circle / Render)"
+1 -1
View File
@@ -58,7 +58,7 @@ URI="file://$TMP/Main.bux"
rpc '{"jsonrpc":"2.0","method":"exit","params":null}' rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt" } | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
if ! grep -qE '0\.(9|10)\.0' "$TMP/out.txt"; then if ! grep -qE '0\.(9|10|11)\.0' "$TMP/out.txt"; then
echo "WARN: unexpected bux-lsp version" echo "WARN: unexpected bux-lsp version"
fi fi