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:
rohit-jsfreakyandClaude Opus 5.5 committed 2026-10-08 16:09:56 +05:30
1 parent 6478eb7123
commit 56634ac22d
2 files changed
+29 -3

No files matched your search

+8 -3
View File
@@ -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(
+21
View File
@@ -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": "",