@@ -153,9 +153,37 @@ def _callers_of(backend: dict, fn: str) -> set:
153153"""
154154
155155
156+ _MODULE_LEVEL_CALL_PY = """\
157+ # Guards #291: Python calls at module top level (not inside any def/class) must
158+ # count toward the callee's rc — analogous to #219 for TS/JS. Before #291 the
159+ # Python parser only emitted edges for calls inside a function body, so a
160+ # function called ONLY at module level got rc=0 / status=dead (false positive).
161+ def setup_app():
162+ return 1
163+
164+
165+ def helper():
166+ return 2
167+
168+
169+ def caller():
170+ return helper()
171+
172+
173+ def py_never_called():
174+ # Control: genuinely dead — never called anywhere. Must STAY dead.
175+ return 99
176+
177+
178+ setup_app() # module-level call -> synthetic <module> caller
179+ caller() # module-level call -> synthetic <module> caller
180+ """
181+
182+
156183def _build_workspace (tmp_path ) -> str :
157184 ws = tmp_path / "golden_ws"
158185 (ws / "src" ).mkdir (parents = True )
186+ (ws / "src" / "mod_level.py" ).write_text (_MODULE_LEVEL_CALL_PY )
159187 (ws / "src" / "mod_level.ts" ).write_text (_MODULE_LEVEL_CALL_TS )
160188 (ws / "src" / "handler.ts" ).write_text (_ASYNC_HANDLER_TS )
161189 (ws / "src" / "svc.ts" ).write_text (_SVC_TS )
@@ -279,6 +307,63 @@ def test_same_file_rust_const_not_dead(self, scanned):
279307 f"#220 regression: same-file-used Rust const RED flagged dead by the engine: { red_hits [:1 ]} "
280308 )
281309
310+ def test_python_module_level_call_counts_toward_rc (self , backend ):
311+ """#291: Python `setup_app`/`caller` called only at module top level.
312+
313+ Both are called via a bare top-level statement (not inside any function
314+ body). Before #291 the Python parser emitted no edge for module-level
315+ calls, so both had rc=0 / status=dead (false positive). Each must now
316+ have rc>=1 with a caller from mod_level.py.
317+ """
318+ for fn in ("setup_app" , "caller" ):
319+ rc = _rc (backend , fn )
320+ assert rc >= 1 , f"#291 regression: Python { fn } rc={ rc } , expected >=1 (module-level call)"
321+ callers = _callers_of (backend , fn )
322+ assert any ("mod_level.py" in c for c in callers ), (
323+ f"#291: expected a caller from mod_level.py for { fn } , got { callers } "
324+ )
325+
326+ def test_python_module_level_caller_visible_in_trace_up (self , scanned ):
327+ """#291/#223: `trace --direction up setup_app` surfaces the <module> caller.
328+
329+ The module-level caller uses the synthetic `<file>:0:<module>` id (same
330+ format as TS/JS), so `graph_model.is_module_level_source_id()` recognises
331+ it and trace-up emits a `module_level=True` / `fn="<module>"` entry.
332+ """
333+ ws , _ = scanned
334+ from commands .trace import execute
335+
336+ class _Args :
337+ name = "setup_app"
338+ direction = "up"
339+ depth = 10
340+ domain = "auto"
341+ limit = 20
342+ offset = 0
343+ max_results = 1000
344+ use_graph = True
345+ deep = False
346+ format = "json"
347+
348+ result = execute (_Args (), ws )
349+ up = result .get ("chains" , {}).get ("up" , [])
350+ module_callers = [c for c in up if c .get ("module_level" ) or c .get ("fn" ) == "<module>" ]
351+ assert module_callers , (
352+ f"#291 regression: module-level caller of setup_app dropped from trace-up. up callers: { up } "
353+ )
354+
355+ def test_python_genuinely_dead_still_detected (self , backend ):
356+ """#291 control: an unreferenced Python function IS still dead (rc 0).
357+
358+ Guards against a fix that inflates rc to hide false positives — a
359+ genuinely-never-called Python function must retain rc 0.
360+ """
361+ nodes = _nodes_named (backend , "py_never_called" )
362+ assert nodes , "control node py_never_called missing"
363+ assert nodes [0 ].get ("ref_count" , 0 ) == 0 , (
364+ f"#291 control: py_never_called should have rc 0, got { nodes [0 ].get ('ref_count' )} "
365+ )
366+
282367 def test_genuinely_dead_still_detected (self , backend ):
283368 """Control: an unreferenced internal function IS still dead (rc 0)."""
284369 nodes = _nodes_named (backend , "trulyUnusedInternal" )
0 commit comments