mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 08:11:54 +00:00
fix(python): find a nested function's file in the typed-receiver visibility check
The typed-receiver arm of _resolve_python_member_calls checks that the receiver's class is defined in or imported into the caller's file. _file_node returned the caller's first contains/method parent, which is the file for a top-level function or method but the enclosing function for a nested def and the outer class for a nested class's method, so those calls (click's `def callback(ctx: Context, ...): ctx.abort()`) never got an edge. Climb the parents to the top, which is always the file node. 0.9.80 vs this change: click +10, rich +4 call edges, all 14 confirmed by jedi at the call site, 0 lost; httpx, flask and typer unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
This commit is contained in:
1 parent
6478eb7123
commit
56634ac22d
2 files changed
+29
-3
No files matched your search
+8
-3
@@ -4245,12 +4245,17 @@ def _resolve_python_member_calls(
|
||||
method_class[tgt] = src
|
||||
|
||||
def _file_node(nid: str) -> "str | None":
|
||||
# Climb `contains` / `method` parents to the top, which is the file: a
|
||||
# nested function's `contains` parent is its enclosing function, not the
|
||||
# file, so stopping at the first parent compared a function to a file.
|
||||
seen: set[str] = set()
|
||||
parent: "str | None" = None
|
||||
while nid and nid not in seen:
|
||||
seen.add(nid)
|
||||
if nid in file_of_node:
|
||||
return file_of_node[nid]
|
||||
nid = method_class.get(nid, "")
|
||||
up = file_of_node.get(nid) or method_class.get(nid)
|
||||
if not up:
|
||||
return parent
|
||||
parent = nid = up
|
||||
return None
|
||||
|
||||
def _emit_call(
|
||||
|
||||
@@ -144,6 +144,27 @@ def test_class_not_visible_from_caller_emits_no_edge(tmp_path):
|
||||
assert not _has(calls, "run")
|
||||
|
||||
|
||||
def test_nested_functions_and_nested_class_methods_reach_the_file(tmp_path):
|
||||
# A nested def's `contains` parent is its enclosing function, and a nested
|
||||
# class's is its outer class: the visibility check must climb to the file.
|
||||
calls, _, _ = _calls(tmp_path, {
|
||||
"client.py": _CLIENT,
|
||||
"use.py": (
|
||||
"from .client import Client\n\n"
|
||||
"def outer():\n def inner(c: Client):\n return c.send()\n return inner\n\n"
|
||||
"class K:\n def run(self):\n def callback(c: Client):\n return c.send()\n"
|
||||
" return callback\n\n"
|
||||
"def a():\n def b():\n def deep():\n c = Client()\n return c.send()\n"
|
||||
" return deep\n return b\n\n"
|
||||
"class Outer:\n class Inner:\n def go(self, c: Client):\n return c.send()\n\n"
|
||||
"def hidden():\n def nope(c: 'Unknown'):\n return c.send()\n return nope\n"
|
||||
),
|
||||
})
|
||||
for caller in ("inner", "callback", "deep", ".go"): # methods are labeled .name()
|
||||
assert _has(calls, caller), caller
|
||||
assert not _has(calls, "nope")
|
||||
|
||||
|
||||
def test_duplicate_class_name_emits_no_edge(tmp_path):
|
||||
calls, _, _ = _calls(tmp_path, {
|
||||
"a/__init__.py": "",
|
||||
|
||||
Reference in new issue
Block a user