mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
feat(go): park a member call whose receiver type is in another repo (#3152)
The Go resolver had the receiver's type in hand and dropped the call when nothing
in the corpus declared it, so `graph.json` — the only artifact `merge-graphs` and
`global add` read — recorded nothing and no merge-time pass could recover it. A
merged graph of a Go service and a Go library was missing exactly the edges that
make it a call graph.
Parks the pair on the caller node like the Java/C++/C#/Swift resolvers already do,
and adds `"go": {".go"}` to the merge pass's language guard. `.go` alone: Go and
Java both turn up in one backend merge, and a shared type name across them is a
coincidence, not a namespace.
The `_key` case preservation the merge pass already documents is what makes this
correct for Go — `greet` is a different method from `Greet` and is not reachable
from another package at all.
This commit is contained in:
1 parent
deca9fa366
commit
f7d2a50603
3 files changed
+48
-3
No files matched your search
@@ -41,6 +41,7 @@ UNRESOLVED_CALLS_KEY = "unresolved_calls"
|
||||
_LANG_SUFFIXES: dict[str, frozenset[str]] = {
|
||||
"cpp": frozenset({".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h", ".cu", ".cuh"}),
|
||||
"csharp": frozenset({".cs"}),
|
||||
"go": frozenset({".go"}),
|
||||
"java": frozenset({".java"}),
|
||||
"swift": frozenset({".swift"}),
|
||||
}
|
||||
|
||||
+10
-1
@@ -6812,6 +6812,9 @@ def _resolve_go_member_calls(
|
||||
Names are matched case-sensitively, unlike the sibling resolvers. Go exports by
|
||||
capitalisation, so `Run` and `run` on one type are two different methods with two
|
||||
different visibilities, and folding them would pick whichever came last.
|
||||
|
||||
A receiver typed to a type this corpus declares nowhere is parked on the caller for
|
||||
a merged graph to finish (#3152).
|
||||
"""
|
||||
raw = [
|
||||
rc
|
||||
@@ -6857,7 +6860,13 @@ def _resolve_go_member_calls(
|
||||
if not type_name or type_name in _LANGUAGE_BUILTIN_GLOBALS:
|
||||
continue
|
||||
type_defs = type_def_nids.get(_key(type_name), [])
|
||||
if len(type_defs) != 1: # ambiguous or absent -> bail (god-node guard)
|
||||
if not type_defs:
|
||||
# Declared nowhere here — usually "in a repo this build does not contain",
|
||||
# so park it for the merge (#3152). The extractor's language tag already
|
||||
# says who is asking, so no suffix sniff is needed.
|
||||
_park_unresolved_member_call(node_by_id.get(caller), callee, type_name, "go", rc)
|
||||
continue
|
||||
if len(type_defs) != 1: # ambiguous -> bail (god-node guard)
|
||||
continue
|
||||
targets = method_index.get((type_defs[0], _key(callee)), [])
|
||||
if len(targets) != 1:
|
||||
|
||||
@@ -6,8 +6,8 @@ type already in hand and nothing about it reached `graph.json` — the only arti
|
||||
`merge-graphs` and `global add` read. The two-repo graph was missing precisely the
|
||||
edges that make it a call graph.
|
||||
|
||||
The Java, C++, C# and Swift resolvers now park those calls on the caller node and
|
||||
this pass finishes them after the merge. The cases below pin what it must NOT do
|
||||
The Java, C++, C#, Swift and Go resolvers now park those calls on the caller node
|
||||
and this pass finishes them after the merge. The cases below pin what it must NOT do
|
||||
as much as what it must: the single-definition guard, the cross-repo-only scope,
|
||||
and the language guard are what keep it from fabricating an edge from a name
|
||||
collision.
|
||||
@@ -45,6 +45,7 @@ needs_java = _needs("tree_sitter_java")
|
||||
needs_cpp = _needs("tree_sitter_cpp")
|
||||
needs_csharp = _needs("tree_sitter_c_sharp")
|
||||
needs_swift = _needs("tree_sitter_swift")
|
||||
needs_go = _needs("tree_sitter_go")
|
||||
|
||||
|
||||
def _caller(repo: str, parked: list[dict], node_id: str = "app_run",
|
||||
@@ -250,6 +251,30 @@ def test_a_repo_that_stops_declaring_the_type_loses_the_edge():
|
||||
assert _added_calls(G) == set()
|
||||
|
||||
|
||||
PARKED_GO = [{"callee": "Greet", "receiver_type": "Greeter", "lang": "go", "line": "L7"}]
|
||||
|
||||
|
||||
def test_a_go_call_does_not_bind_to_a_java_declaration():
|
||||
# Go's suffix set is `.go` alone. Two backend services in one merge often hold one
|
||||
# of each, and a shared type name there is a coincidence, not a namespace.
|
||||
G = _graph(
|
||||
caller=_caller("a", PARKED_GO, source_file="src/app.go"),
|
||||
declarations=[(_declaration("b", "Greeter"), _method("b", ".Greet()"))],
|
||||
)
|
||||
assert link_cross_repo_member_calls(G) == 0
|
||||
|
||||
|
||||
def test_a_go_call_does_not_bind_to_a_differently_cased_method():
|
||||
# Go exports by capitalisation, so `greet` is a different method from `Greet` and
|
||||
# is unreachable from another package at all.
|
||||
G = _graph(
|
||||
caller=_caller("a", PARKED_GO, source_file="src/app.go"),
|
||||
declarations=[(_declaration("b", "Greeter", "greeter.go"),
|
||||
_method("b", ".greet()", source_file="greeter.go"))],
|
||||
)
|
||||
assert link_cross_repo_member_calls(G) == 0
|
||||
|
||||
|
||||
def _write_graph(path: Path, nodes: list[dict], links: list[dict]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"directed": False, "multigraph": False, "graph": {},
|
||||
@@ -374,6 +399,16 @@ def test_a_java_build_parks_the_call_and_the_merge_finishes_it(tmp_path: Path):
|
||||
("src/Greeter.swift", "class Greeter { func greet() {} }\n"),
|
||||
marks=needs_swift, id="swift-property-receiver",
|
||||
),
|
||||
pytest.param(
|
||||
# A struct field used from a method is the shape Go dependency injection takes,
|
||||
# and the field's declared type is the only place the receiver is named.
|
||||
"go", "Greet",
|
||||
("src/app.go", "package svc\n\ntype App struct {\n\tgreeter *Greeter\n}\n\n"
|
||||
"func (a *App) Run() { a.greeter.Greet() }\n"),
|
||||
("src/greeter.go", "package svc\n\ntype Greeter struct{}\n\n"
|
||||
"func (g *Greeter) Greet() {}\n"),
|
||||
marks=needs_go, id="go-struct-field",
|
||||
),
|
||||
])
|
||||
def test_each_language_parks_the_call_and_the_merge_finishes_it(
|
||||
tmp_path: Path, lang: str, callee: str, app_file: tuple[str, str],
|
||||
|
||||
Reference in new issue
Block a user