feat(lsp): type hierarchy for interfaces and implementors (0.15)
prepareTypeHierarchy plus supertypes/subtypes via extend Type for Iface. Smoke covers Drawable ↔ Circle/Square.
This commit is contained in:
@@ -191,6 +191,9 @@ test-lsp: lsp
|
|||||||
@echo "==> LSP workspace import index smoke"
|
@echo "==> LSP workspace import index smoke"
|
||||||
@chmod +x tools/smoke_lsp_workspace_imports.sh
|
@chmod +x tools/smoke_lsp_workspace_imports.sh
|
||||||
@tools/smoke_lsp_workspace_imports.sh
|
@tools/smoke_lsp_workspace_imports.sh
|
||||||
|
@echo "==> LSP type hierarchy smoke"
|
||||||
|
@chmod +x tools/smoke_lsp_type_hierarchy.sh
|
||||||
|
@tools/smoke_lsp_type_hierarchy.sh
|
||||||
|
|
||||||
.PHONY: test-registry
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: build
|
||||||
|
|||||||
+17
-5
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-19
|
||||||
> **Текущо:** v0.5.x — **CI `make test`**, selfhost fixed-point, LSP 0.14
|
> **Текущо:** v0.5.x — **LSP 0.15 type hierarchy**, CI `make test`, fixed-point
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -838,9 +838,21 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Сесия 54 (LSP 0.15 type hierarchy)
|
||||||
|
|
||||||
|
1. **`typeHierarchyProvider`** + prepare / supertypes / subtypes
|
||||||
|
2. **prepare** on `struct` / `enum` / `interface` / `type` → TypeHierarchyItem
|
||||||
|
3. **subtypes** of interface → types with `extend T for I` (Circle, Square)
|
||||||
|
4. **supertypes** of type → interfaces it implements
|
||||||
|
5. Uses open-doc `impls` + `workspaceImpls` + symbol resolve
|
||||||
|
6. Smoke: `tools/smoke_lsp_type_hierarchy.sh`
|
||||||
|
7. Version **bux-lsp 0.15.0**; `make test-lsp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. LSP type hierarchy / prepareTypeHierarchy (optional)
|
1. Macro / quote hygiene using Expr.sourceFile grafts
|
||||||
2. Macro / quote hygiene using Expr.sourceFile grafts
|
2. Parenthesize binary ops in CBE for full C precedence safety
|
||||||
3. Parenthesize binary ops in CBE for full C precedence safety
|
3. CI matrix (macOS) or split jobs for faster PR feedback
|
||||||
4. CI matrix (macOS) or split jobs for faster PR feedback
|
4. Type hierarchy for multi-file closed docs without open (workspace type index)
|
||||||
|
|||||||
+210
-2
@@ -16,6 +16,7 @@
|
|||||||
# v0.12.0: module-path segment rename (import Std::Io / Std::Io::{…}).
|
# v0.12.0: module-path segment rename (import Std::Io / Std::Io::{…}).
|
||||||
# v0.13.0: textDocument/implementation (interface → types / methods).
|
# v0.13.0: textDocument/implementation (interface → types / methods).
|
||||||
# v0.14.0: workspace-wide import path index (no open-doc required).
|
# v0.14.0: workspace-wide import path index (no open-doc required).
|
||||||
|
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
|
||||||
|
|
||||||
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
|
||||||
@@ -2911,6 +2912,203 @@ proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode)
|
|||||||
})
|
})
|
||||||
sendResponse(stream, id, arr)
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Type hierarchy (v0.15) — interface ↔ implementors via `extend T for I`
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc isTypeHierarchyKind(kind: string): bool =
|
||||||
|
kind in ["struct", "enum", "union", "interface", "type"]
|
||||||
|
|
||||||
|
proc typeHierarchySymbolKind(kind: string): int =
|
||||||
|
## LSP SymbolKind
|
||||||
|
case kind
|
||||||
|
of "interface": 11
|
||||||
|
of "struct", "union": 23 # Struct
|
||||||
|
of "enum": 10
|
||||||
|
of "class": 5
|
||||||
|
else: 5 # Class / type alias
|
||||||
|
|
||||||
|
proc resolveTypeSymbol(name: string, preferUri: string = ""): tuple[ok: bool, uri: string, info: SymbolInfo] =
|
||||||
|
result.ok = false
|
||||||
|
if preferUri.len > 0 and documents.hasKey(preferUri):
|
||||||
|
let doc = documents[preferUri]
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
if doc.symbols.hasKey(name) and isTypeHierarchyKind(doc.symbols[name].kind):
|
||||||
|
result.ok = true
|
||||||
|
result.uri = preferUri
|
||||||
|
result.info = doc.symbols[name]
|
||||||
|
return
|
||||||
|
if workspaceSymbols.hasKey(name) and isTypeHierarchyKind(workspaceSymbols[name].info.kind):
|
||||||
|
result.ok = true
|
||||||
|
result.uri = workspaceSymbols[name].uri
|
||||||
|
result.info = workspaceSymbols[name].info
|
||||||
|
return
|
||||||
|
for uri, doc in documents.pairs:
|
||||||
|
if uri == preferUri: continue
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
if doc.symbols.hasKey(name) and isTypeHierarchyKind(doc.symbols[name].kind):
|
||||||
|
result.ok = true
|
||||||
|
result.uri = uri
|
||||||
|
result.info = doc.symbols[name]
|
||||||
|
return
|
||||||
|
|
||||||
|
proc typeHierarchyItem(uri: string, name: string, info: SymbolInfo): JsonNode =
|
||||||
|
let nlen = name.len
|
||||||
|
%*{
|
||||||
|
"name": name,
|
||||||
|
"kind": typeHierarchySymbolKind(info.kind),
|
||||||
|
"detail": info.detail,
|
||||||
|
"uri": uri,
|
||||||
|
"range": {
|
||||||
|
"start": {"line": info.line, "character": 0},
|
||||||
|
"end": {"line": info.line, "character": info.col + nlen}
|
||||||
|
},
|
||||||
|
"selectionRange": {
|
||||||
|
"start": {"line": info.line, "character": info.col},
|
||||||
|
"end": {"line": info.line, "character": info.col + nlen}
|
||||||
|
},
|
||||||
|
"data": name
|
||||||
|
}
|
||||||
|
|
||||||
|
proc typeHierarchyItemSynthetic(uri: string, name: string, kind: string, line: int): JsonNode =
|
||||||
|
## When we only know the type from `extend` relation, not a full SymbolInfo.
|
||||||
|
%*{
|
||||||
|
"name": name,
|
||||||
|
"kind": typeHierarchySymbolKind(kind),
|
||||||
|
"detail": kind & " " & name,
|
||||||
|
"uri": uri,
|
||||||
|
"range": {
|
||||||
|
"start": {"line": line, "character": 0},
|
||||||
|
"end": {"line": line, "character": max(1, name.len)}
|
||||||
|
},
|
||||||
|
"selectionRange": {
|
||||||
|
"start": {"line": line, "character": 0},
|
||||||
|
"end": {"line": line, "character": max(1, name.len)}
|
||||||
|
},
|
||||||
|
"data": name
|
||||||
|
}
|
||||||
|
|
||||||
|
proc collectSubtypeItems(iface: string): seq[JsonNode] =
|
||||||
|
## Types that `extend Type for iface`.
|
||||||
|
result = @[]
|
||||||
|
var seen = initHashSet[string]()
|
||||||
|
for uri, doc in documents.pairs:
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
for impl in doc.impls:
|
||||||
|
if impl.iface != iface: continue
|
||||||
|
let key = uri & "#" & impl.typeName
|
||||||
|
if seen.contains(key): continue
|
||||||
|
seen.incl(key)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(impl.typeName, uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, impl.typeName, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(uri, impl.typeName, "struct", impl.line))
|
||||||
|
# workspaceImpls: "Iface.Method" → (uri, typeName, meth)
|
||||||
|
for wkey, impls in workspaceImpls.pairs:
|
||||||
|
if not wkey.startsWith(iface & "."): continue
|
||||||
|
for impl in impls:
|
||||||
|
let key = impl.uri & "#" & impl.typeName
|
||||||
|
if seen.contains(key): continue
|
||||||
|
seen.incl(key)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(impl.typeName, impl.uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, impl.typeName, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(impl.uri, impl.typeName, "struct", 0))
|
||||||
|
|
||||||
|
proc collectSupertypeItems(typeName: string): seq[JsonNode] =
|
||||||
|
## Interfaces that `typeName` implements via `extend typeName for I`.
|
||||||
|
result = @[]
|
||||||
|
var seen = initHashSet[string]()
|
||||||
|
for uri, doc in documents.pairs:
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
for impl in doc.impls:
|
||||||
|
if impl.typeName != typeName: continue
|
||||||
|
if seen.contains(impl.iface): continue
|
||||||
|
seen.incl(impl.iface)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(impl.iface, uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, impl.iface, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(uri, impl.iface, "interface", impl.line))
|
||||||
|
for wkey, impls in workspaceImpls.pairs:
|
||||||
|
for impl in impls:
|
||||||
|
if impl.typeName != typeName: continue
|
||||||
|
# keys are "Iface.Method"
|
||||||
|
let dot = wkey.find('.')
|
||||||
|
if dot < 0: continue
|
||||||
|
let iface = wkey[0 ..< dot]
|
||||||
|
if seen.contains(iface): continue
|
||||||
|
seen.incl(iface)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(iface, impl.uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, iface, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(impl.uri, iface, "interface", 0))
|
||||||
|
|
||||||
|
proc handlePrepareTypeHierarchy(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
|
let position = paramsNode["position"]
|
||||||
|
let lineNum = position["line"].getInt()
|
||||||
|
let col = position["character"].getInt()
|
||||||
|
let doc = getDoc(uri)
|
||||||
|
if doc.content == "":
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
let word = findWordAt(doc.content, lineNum, col)
|
||||||
|
if word.len == 0:
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
# Prefer symbol under cursor that is a type / interface
|
||||||
|
if doc.symbols.hasKey(word) and isTypeHierarchyKind(doc.symbols[word].kind):
|
||||||
|
let info = doc.symbols[word]
|
||||||
|
# If cursor is on decl name, good; also allow any occurrence of type name
|
||||||
|
sendResponse(stream, id, %*[typeHierarchyItem(uri, word, info)])
|
||||||
|
return
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(word, uri)
|
||||||
|
if ok:
|
||||||
|
sendResponse(stream, id, %*[typeHierarchyItem(u, word, info)])
|
||||||
|
return
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
|
||||||
|
proc typeHierarchyItemName(item: JsonNode): string =
|
||||||
|
if item.hasKey("data") and item["data"].kind == JString:
|
||||||
|
let d = item["data"].getStr()
|
||||||
|
if d.len > 0: return d
|
||||||
|
if item.hasKey("name"):
|
||||||
|
return item["name"].getStr()
|
||||||
|
""
|
||||||
|
|
||||||
|
proc handleTypeHierarchySupertypes(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
## Interfaces implemented by this type (`extend Type for Iface`).
|
||||||
|
if not paramsNode.hasKey("item"):
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
let name = typeHierarchyItemName(paramsNode["item"])
|
||||||
|
if name.len == 0:
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
var arr = newJArray()
|
||||||
|
for it in collectSupertypeItems(name):
|
||||||
|
arr.add(it)
|
||||||
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
|
proc handleTypeHierarchySubtypes(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
## Implementors of an interface (`extend Type for Iface`).
|
||||||
|
if not paramsNode.hasKey("item"):
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
let name = typeHierarchyItemName(paramsNode["item"])
|
||||||
|
if name.len == 0:
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
var arr = newJArray()
|
||||||
|
for it in collectSubtypeItems(name):
|
||||||
|
arr.add(it)
|
||||||
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main message loop
|
# Main message loop
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2936,9 +3134,10 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"renameProvider": {"prepareProvider": true},
|
"renameProvider": {"prepareProvider": true},
|
||||||
"workspaceSymbolProvider": true,
|
"workspaceSymbolProvider": true,
|
||||||
"callHierarchyProvider": true,
|
"callHierarchyProvider": true,
|
||||||
"implementationProvider": true
|
"implementationProvider": true,
|
||||||
|
"typeHierarchyProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.14.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.15.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()
|
||||||
@@ -3041,6 +3240,15 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
of "textDocument/implementation":
|
of "textDocument/implementation":
|
||||||
handleImplementation(stream, id, paramsNode)
|
handleImplementation(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/prepareTypeHierarchy":
|
||||||
|
handlePrepareTypeHierarchy(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "typeHierarchy/supertypes":
|
||||||
|
handleTypeHierarchySupertypes(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "typeHierarchy/subtypes":
|
||||||
|
handleTypeHierarchySubtypes(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)
|
||||||
|
|||||||
Executable
+124
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: type hierarchy prepare / subtypes / supertypes (bux-lsp 0.15)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
struct Square {
|
||||||
|
side: int;
|
||||||
|
}
|
||||||
|
extend Circle for Drawable {
|
||||||
|
func Draw(self: &Circle) {
|
||||||
|
let r: int = self.radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extend Square for Drawable {
|
||||||
|
func Draw(self: &Square) {
|
||||||
|
let s: int = self.side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Drawable ~ line 0 col 10; Circle ~ line 3 col 7
|
||||||
|
{
|
||||||
|
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"'}}}'
|
||||||
|
# prepare on interface Drawable
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/prepareTypeHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":10}}}'
|
||||||
|
# subtypes of Drawable
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"typeHierarchy/subtypes","params":{"item":{"name":"Drawable","kind":11,"uri":"'"$URI"'","data":"Drawable","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":18}},"selectionRange":{"start":{"line":0,"character":10},"end":{"line":0,"character":18}}}}}'
|
||||||
|
# prepare on Circle
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"textDocument/prepareTypeHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":3,"character":7}}}'
|
||||||
|
# supertypes of Circle → Drawable
|
||||||
|
rpc '{"jsonrpc":"2.0","id":5,"method":"typeHierarchy/supertypes","params":{"item":{"name":"Circle","kind":23,"uri":"'"$URI"'","data":"Circle","range":{"start":{"line":3,"character":0},"end":{"line":3,"character":13}},"selectionRange":{"start":{"line":3,"character":7},"end":{"line":3,"character":13}}}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":6,"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.15.0' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: version not 0.15.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q 'typeHierarchyProvider' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: missing typeHierarchyProvider capability"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<'PY' "$TMP/out.txt"
|
||||||
|
import json, sys, re
|
||||||
|
raw = open(sys.argv[1]).read()
|
||||||
|
parts = re.split(r'Content-Length:\s*\d+\s*', raw)
|
||||||
|
got = {}
|
||||||
|
for p in parts:
|
||||||
|
p = p.strip()
|
||||||
|
if not p.startswith('{'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
j = json.loads(p)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'id' in j and 'result' in j:
|
||||||
|
got[j['id']] = j['result']
|
||||||
|
|
||||||
|
# id 2: prepare Drawable
|
||||||
|
r2 = got.get(2) or []
|
||||||
|
if not any(isinstance(x, dict) and x.get('name') == 'Drawable' for x in r2):
|
||||||
|
print('FAIL: prepare on Drawable missing item')
|
||||||
|
print(got.get(2))
|
||||||
|
sys.exit(1)
|
||||||
|
print(' prepare Drawable: OK')
|
||||||
|
|
||||||
|
# id 3: subtypes ≥2 (Circle, Square)
|
||||||
|
r3 = got.get(3) or []
|
||||||
|
names = {x.get('name') for x in r3 if isinstance(x, dict)}
|
||||||
|
if 'Circle' not in names or 'Square' not in names:
|
||||||
|
print(f'FAIL: subtypes of Drawable expected Circle+Square, got {names}')
|
||||||
|
print(json.dumps(r3, indent=2)[:600])
|
||||||
|
sys.exit(1)
|
||||||
|
print(f' subtypes Drawable → {sorted(names)}')
|
||||||
|
|
||||||
|
# id 4: prepare Circle
|
||||||
|
r4 = got.get(4) or []
|
||||||
|
if not any(isinstance(x, dict) and x.get('name') == 'Circle' for x in r4):
|
||||||
|
print('FAIL: prepare on Circle missing item')
|
||||||
|
sys.exit(1)
|
||||||
|
print(' prepare Circle: OK')
|
||||||
|
|
||||||
|
# id 5: supertypes → Drawable
|
||||||
|
r5 = got.get(5) or []
|
||||||
|
snames = {x.get('name') for x in r5 if isinstance(x, dict)}
|
||||||
|
if 'Drawable' not in snames:
|
||||||
|
print(f'FAIL: supertypes of Circle expected Drawable, got {snames}')
|
||||||
|
sys.exit(1)
|
||||||
|
print(f' supertypes Circle → {sorted(snames)}')
|
||||||
|
|
||||||
|
print('PASS: LSP type hierarchy (0.15)')
|
||||||
|
PY
|
||||||
Reference in New Issue
Block a user