Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2493,6 +2493,95 @@ def _key(label: str) -> str:
})


def _resolve_java_member_calls(
per_file: list[dict],
all_nodes: list[dict],
all_edges: list[dict],
) -> None:
"""Resolve Java member calls against the receiver's declared type.

Explicit type receivers and ``this`` are exact. Fields declared on the
caller's class plus method parameters and explicit locals are inferred from
the extractor's method-scoped type table. A missing or ambiguous receiver
type is skipped rather than falling back to a bare method-name match.
"""
def key(label: str) -> str:
return str(label).strip().removeprefix(".").removesuffix("()")

contained = {edge.get("target") for edge in all_edges
if edge.get("relation") == "contains"}
node_by_id = {node.get("id"): node for node in all_nodes}

type_def_nids: dict[str, list[str]] = {}
for node in all_nodes:
if (
node.get("source_file")
and node.get("id") in contained
and _is_type_like_definition(node)
):
type_def_nids.setdefault(key(node.get("label", "")), []).append(node["id"])

method_index: dict[tuple[str, str], set[str]] = {}
enclosing_type: dict[str, str] = {}
for edge in all_edges:
if edge.get("relation") != "method":
continue
owner, method = edge.get("source"), edge.get("target")
method_node = node_by_id.get(method)
if method_node is None:
continue
enclosing_type.setdefault(method, owner)
method_index.setdefault((owner, key(method_node.get("label", ""))), set()).add(method)

existing_pairs = {(edge.get("source"), edge.get("target")) for edge in all_edges}
for result in per_file:
for raw_call in result.get("raw_calls", []):
if raw_call.get("lang") != "java" or not raw_call.get("is_member_call"):
continue
receiver = raw_call.get("receiver")
callee = raw_call.get("callee")
caller = raw_call.get("caller_nid")
if not receiver or not callee or not caller:
continue

exact = False
if receiver == "this":
type_nid = enclosing_type.get(caller)
exact = True
if not type_nid:
continue
else:
type_name = raw_call.get("receiver_type")
if not type_name and receiver[:1].isupper():
type_name = receiver
exact = True
if not type_name:
continue
type_defs = type_def_nids.get(key(type_name), [])
if len(type_defs) != 1:
continue
type_nid = type_defs[0]

method_nids = method_index.get((type_nid, key(callee)), set())
if len(method_nids) != 1:
continue
method_nid = next(iter(method_nids))
if method_nid == caller or (caller, method_nid) in existing_pairs:
continue
existing_pairs.add((caller, method_nid))
all_edges.append({
"source": caller,
"target": method_nid,
"relation": "calls",
"context": "call",
"confidence": "EXTRACTED" if exact else "INFERRED",
"confidence_score": 1.0 if exact else 0.8,
"source_file": raw_call.get("source_file", ""),
"source_location": raw_call.get("source_location"),
"weight": 1.0,
})


def _resolve_objc_member_calls(
per_file: list[dict],
all_nodes: list[dict],
Expand Down Expand Up @@ -2643,6 +2732,9 @@ def _key(label: str) -> str:
register_language_resolver(
LanguageResolver("csharp_member_calls", frozenset({".cs"}), _resolve_csharp_member_calls)
)
register_language_resolver(
LanguageResolver("java_member_calls", frozenset({".java"}), _resolve_java_member_calls)
)
# Pascal/Delphi cross-file inherited-method-call resolution: a call from a
# manual descendant class to a method it inherits from an ancestor declared
# in a DIFFERENT file (the common generated-base/manual-descendant split,
Expand Down
Loading
Loading