mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
test(kotlin): cover companion members + incremental persistence (#1698 follow-up)
#3598 resolves Kotlin object/class-qualified member calls across files. Adds two regression tests the PR omitted: - a `companion object` member call (`Service.create()`), pinning the #2565 enclosing-class attribution the resolver relies on; - an incremental-rebuild test routed through the real watch._rebuild_code, so it actually exercises the context builder's allow-list carrying _callable_class + method edges (a hand-fed resolution_context_nodes list would bypass exactly the path that silently regressed in sibling PRs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4afdfcbaaa
commit
4e811363a6
1 file changed
+80
@@ -577,3 +577,83 @@ def test_multiline_kotlin_unchanged(tmp_path, capsys):
|
||||
if e["relation"] == "references" and e.get("context") == "field"}
|
||||
assert (cart, inv) in field_refs
|
||||
assert "syntax errors" not in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_kotlin_companion_member_call_resolves_across_files(tmp_path):
|
||||
"""A `companion object` member is attributed to its enclosing class (#2565),
|
||||
so `Service.create()` in another file resolves to the class's method — the
|
||||
resolver relies on that attribution, so pin it directly (#1698)."""
|
||||
r = _extract(tmp_path, {
|
||||
"svc/Service.kt": (
|
||||
"package com.demo.svc\n"
|
||||
"\n"
|
||||
"class Service {\n"
|
||||
" companion object {\n"
|
||||
" fun create() { }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
),
|
||||
"app/Main.kt": (
|
||||
"package com.demo.app\n"
|
||||
"\n"
|
||||
"import com.demo.svc.Service\n"
|
||||
"\n"
|
||||
"fun run() {\n"
|
||||
" Service.create()\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
run = _find(r, "run()")
|
||||
create = _find(r, ".create()")
|
||||
calls = _edges(r, "calls")
|
||||
assert (run, create) in calls, \
|
||||
"`Service.create()` must resolve to the companion member on the enclosing class"
|
||||
edge = next(e for e in r["edges"] if e["relation"] == "calls"
|
||||
and e["source"] == run and e["target"] == create)
|
||||
assert edge["confidence"] == "EXTRACTED"
|
||||
|
||||
|
||||
def test_kotlin_object_member_call_survives_incremental_rebuild(tmp_path):
|
||||
"""The cross-file resolution must hold on the real `graphify update` / watch
|
||||
path, where the unchanged receiver-type file arrives as a resolution-context
|
||||
node. This exercises the actual watch context builder — it only works if the
|
||||
`_callable_class` marker and `method` edges ride through its allow-list
|
||||
(#1698). Routes through the real rebuild, not a hand-fed context list."""
|
||||
import json
|
||||
from graphify.watch import _rebuild_code
|
||||
|
||||
corpus = tmp_path / "corpus"
|
||||
(corpus / "config").mkdir(parents=True)
|
||||
(corpus / "app").mkdir(parents=True)
|
||||
(corpus / "config" / "Config.kt").write_text(
|
||||
"package com.demo.config\n\nobject Config {\n fun load() { }\n}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
app = corpus / "app" / "App.kt"
|
||||
|
||||
def _app(extra: str = "") -> str:
|
||||
return ("package com.demo.app\n\nimport com.demo.config.Config\n\n"
|
||||
"fun Start() {\n Config.load()\n" + extra + "}\n")
|
||||
|
||||
app.write_text(_app(), encoding="utf-8")
|
||||
graph_path = corpus / "graphify-out" / "graph.json"
|
||||
|
||||
def _resolves() -> bool:
|
||||
data = json.loads(graph_path.read_text(encoding="utf-8"))
|
||||
start = next((n["id"] for n in data["nodes"] if n.get("label") == "Start()"), None)
|
||||
load = next((n["id"] for n in data["nodes"] if n.get("label") == ".load()"), None)
|
||||
if start is None or load is None:
|
||||
return False
|
||||
return any(e.get("relation") == "calls"
|
||||
and {e.get("source"), e.get("target")} == {start, load}
|
||||
for e in data["links"])
|
||||
|
||||
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
|
||||
assert _resolves(), "full build resolves the cross-file object member call"
|
||||
|
||||
# Change ONLY the caller: Config.kt is unchanged, so its object node + method
|
||||
# edge are fed back as resolution context. The call must still resolve.
|
||||
app.write_text(_app(" Config.load()\n"), encoding="utf-8")
|
||||
assert _rebuild_code(corpus, changed_paths=[app], no_cluster=True,
|
||||
acquire_lock=False) is True
|
||||
assert _resolves(), "call stays resolved after an incremental rebuild"
|
||||
Reference in new issue
Block a user