Skip to content
Closed
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
19 changes: 17 additions & 2 deletions graphify/extractors/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5607,7 +5607,22 @@ def walk_calls(
# Try reading the node directly (e.g. Java name field is the callee)
callee_name = _read_text(func_node, source)

if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS:
# _LANGUAGE_BUILTIN_GLOBALS is one union across every language, right for
# a BARE call (String(x) really would become a god node) but wrong for a
# MEMBER call: `open` is a Python builtin and `Set` a JavaScript one, so
# session.open() in Swift or _server.Set() in C# named after another
# language's builtin was silently discarded outright -- no same-file
# edge, but also no raw_calls entry, so cross-file resolution never even
# got a chance to try (#3381). A member call carries a receiver, so it
# is not the ambiguous case the union guards against; letting it through
# here while forcing tgt_nid = None below (same as the existing
# receiver-typed defers just past this comment) means it can only ever
# reach an edge through a guarded, receiver-typed resolver, never the
# unguarded bare-name path a real god node would need.
_builtin_member_call = is_member_call and callee_name in _LANGUAGE_BUILTIN_GLOBALS
if callee_name and (
callee_name not in _LANGUAGE_BUILTIN_GLOBALS or _builtin_member_call
):
# Python member calls defer to receiver-based resolution unless the
# receiver is known to stay in the current class. Falling back to a
# bare method name for an unresolved/lowercase receiver (`d.get()` or
Expand Down Expand Up @@ -5636,7 +5651,7 @@ def walk_calls(
_java_defer = (
config.ts_module == "tree_sitter_java" and is_member_call
)
if _python_defer or _java_defer or (
if _python_defer or _java_defer or _builtin_member_call or (
is_member_call
and member_receiver
and (
Expand Down
58 changes: 58 additions & 0 deletions tests/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,64 @@ def test_python_qualified_class_method_call_resolves_extracted(tmp_path):
assert call_edges[0]["confidence"] == "EXTRACTED"


def test_builtin_named_member_call_still_resolves_cross_file(tmp_path):
"""#3381: _LANGUAGE_BUILTIN_GLOBALS is one union across every language, right
for a BARE call (String(x) really would become a god node) but wrong for a
MEMBER call -- `open` is a Python builtin, so Session.open() used to be
silently discarded outright: no same-file edge, but also no raw_calls entry,
so cross-file resolution never got a chance to try it. A member call carries
a receiver, so it isn't the ambiguous case the union guards against."""
session = tmp_path / "session.py"
user = tmp_path / "user.py"
session.write_text(
"class Session:\n"
" @staticmethod\n"
" def open():\n"
" return 'opened'\n"
)
user.write_text(
"from session import Session\n\n"
"def start():\n"
" Session.open()\n"
)
result = extract([user, session], cache_root=tmp_path)
nodes = {n["id"]: n for n in result["nodes"]}
call_edges = [
e for e in result["edges"]
if e["relation"] == "calls"
and "start" in nodes[e["source"]]["label"]
and "open" in nodes[e["target"]]["label"]
and "session.py" in (nodes[e["target"]].get("source_file") or "")
]
assert len(call_edges) == 1, f"expected one start->open edge, got {call_edges}"
assert call_edges[0]["confidence"] == "EXTRACTED"


def test_builtin_named_member_call_does_not_bind_to_unrelated_bare_function(tmp_path):
"""#3381 follow-up: the god-node guard the builtin filter exists for must
still hold. A member call named after a builtin must never fall back to an
unrelated same-file bare function sharing that name -- it may only ever
resolve through a guarded, receiver-typed path (or not resolve at all)."""
p = tmp_path / "sample.py"
p.write_text(
"def open():\n"
" return 'unrelated top-level function also named open'\n"
"\n"
"class Session:\n"
" def start(self, other):\n"
" other.open()\n"
" f = open('file.txt')\n"
)
result = extract([p], cache_root=tmp_path)
nodes = {n["id"]: n for n in result["nodes"]}
open_fn = next(n for n in result["nodes"] if n["label"] == "open()")
bad_edges = [
e for e in result["edges"]
if e["relation"] == "calls" and e["target"] == open_fn["id"]
]
assert bad_edges == [], f"member/bare builtin-named calls bound to unrelated open(): {bad_edges}"


def test_degenerate_symbol_name_does_not_leak_absolute_id(tmp_path):
"""#1899 variant B: a symbol whose name normalizes to nothing (a minified `$`
function, a JSONC `"//"` key) must not be minted — `_make_id(stem, "")`
Expand Down
Loading