mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(vbnet): resolve type/module-qualified calls to their target method
The invocation resolver only fired for self/unqualified receivers (Me, the
enclosing type, or a bare name). A call qualified by a type or module name —
Helpers.Log(), the idiomatic way to reach a shared Module Sub — matched
nothing and produced no edge at all, so the caller was disconnected from the
method it invoked.
Map each declared type/module's short name to its owner key, and resolve a
qualified call against the receiver's owner (falling back to raw_calls for
cross-file partial-type merging). A call through a value receiver (a local
variable) still has no known owner and is left unresolved rather than
guessed (fail-closed).
(cherry picked from commit 1297c18f50)
This commit is contained in:
1 parent
85390c83a4
commit
4dc58d17a7
2 files changed
+67
-4
No files matched your search
@@ -100,6 +100,10 @@ def extract_vbnet(path: Path) -> dict:
|
||||
seen_ids: set[str] = set()
|
||||
seen_edges: set[tuple[str, str, str]] = set()
|
||||
types_by_name: dict[str, str] = {}
|
||||
# Short type/module name -> its owner_key, so a qualified call such as
|
||||
# Helpers.Log() can be resolved back to the owner that keys `methods`
|
||||
# (which is the namespace-qualified name, not the bare receiver token).
|
||||
owner_by_short: dict[str, str] = {}
|
||||
methods: dict[tuple[str, str, int], list[str]] = {}
|
||||
events: dict[tuple[str, str], str] = {}
|
||||
bodies: list[tuple[Node, str, str, str]] = []
|
||||
@@ -209,6 +213,7 @@ def extract_vbnet(path: Path) -> dict:
|
||||
)
|
||||
add_edge(parent_id, type_id, "contains", block)
|
||||
types_by_name[name.casefold()] = type_id
|
||||
owner_by_short.setdefault(name.casefold(), owner_key)
|
||||
|
||||
for clause in block.named_children:
|
||||
if clause.type not in {"inherits_clause", "implements_clause"}:
|
||||
@@ -378,14 +383,24 @@ def extract_vbnet(path: Path) -> dict:
|
||||
parts = written.split(".")
|
||||
receiver = ".".join(parts[:-1]).casefold()
|
||||
callee = parts[-1]
|
||||
known_receiver = (
|
||||
self_receiver = (
|
||||
not receiver
|
||||
or receiver in {"me", "myclass", type_name.casefold()}
|
||||
)
|
||||
if known_receiver:
|
||||
# A self/unqualified call targets the caller's own type; a
|
||||
# qualified call (Helpers.Log(), OtherType.Foo()) targets the
|
||||
# named receiver type/module. Only these two forms can be
|
||||
# resolved by name — a call through a value receiver (a local
|
||||
# variable) has no known owner, so it is left unresolved.
|
||||
target_owner = None
|
||||
if self_receiver:
|
||||
target_owner = owner_key
|
||||
else:
|
||||
target_owner = owner_by_short.get(receiver.split(".")[-1])
|
||||
if target_owner is not None:
|
||||
arity = len(arguments.named_children) if arguments is not None else 0
|
||||
candidates = methods.get(
|
||||
(owner_key, callee.casefold(), arity), []
|
||||
(target_owner, callee.casefold(), arity), []
|
||||
)
|
||||
if len(candidates) == 1:
|
||||
add_edge(caller_id, candidates[0], "calls", node)
|
||||
@@ -394,7 +409,7 @@ def extract_vbnet(path: Path) -> dict:
|
||||
"caller_nid": caller_id,
|
||||
"callee": callee,
|
||||
"arity": arity,
|
||||
"owner": owner_key,
|
||||
"owner": target_owner,
|
||||
"is_member_call": True,
|
||||
"language": "vbnet",
|
||||
"source_file": source_file,
|
||||
|
||||
@@ -59,6 +59,54 @@ def test_vbnet_class_methods_and_case_insensitive_calls(tmp_path):
|
||||
assert ("Run()", "Helper()") in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
def test_vbnet_module_qualified_call_resolves_to_the_module_method(tmp_path):
|
||||
# A call qualified by a type/module name (Helpers.Log()) — the idiomatic way
|
||||
# to reach a shared Module Sub — was dropped entirely: the invocation
|
||||
# resolver only fired for self/unqualified receivers, so the caller had no
|
||||
# edge to the module method it invoked.
|
||||
source = tmp_path / "App.vb"
|
||||
source.write_text(
|
||||
"Namespace Demo\n"
|
||||
" Public Module Helpers\n"
|
||||
" Public Sub Log()\n"
|
||||
" End Sub\n"
|
||||
" End Module\n"
|
||||
" Public Class Service\n"
|
||||
" Public Sub Run()\n"
|
||||
" Helpers.Log()\n"
|
||||
" End Sub\n"
|
||||
" End Class\n"
|
||||
"End Namespace\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
assert ("Run()", "Log()") in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
def test_vbnet_call_through_a_value_receiver_stays_unresolved(tmp_path):
|
||||
# A call through a value receiver (a local variable) has no statically known
|
||||
# owner, so it must NOT be guessed into a false edge (fail-closed).
|
||||
source = tmp_path / "App.vb"
|
||||
source.write_text(
|
||||
"Public Class Other\n"
|
||||
" Public Sub Save()\n"
|
||||
" End Sub\n"
|
||||
"End Class\n"
|
||||
"Public Class Service\n"
|
||||
" Public Sub Run()\n"
|
||||
" obj.Save()\n"
|
||||
" End Sub\n"
|
||||
"End Class\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
assert ("Run()", "Save()") not in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
def test_vbnet_types_members_relationships_and_partial_calls(tmp_path):
|
||||
first = tmp_path / "Counter.vb"
|
||||
first.write_text(
|
||||
|
||||
Reference in new issue
Block a user