Repository navigation
fix(python): find a nested function's file in the typed-receiver visibility check - #4227
rohit-jsfreaky wants to merge 1 commit into
Conversation
…bility 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
|
Thanks for the pull request, @rohit-jsfreaky. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. PR-changed functions: 0/1 verified (0 proven, 0 may-equivalent, 0 distinguished) · 1 not verified (1 vacuous).
Not verified on this run: \_resolve\_python\_member\_calls (vacuous: never exercised).
Graphify review — findings
Fixes typed-receiver call resolution for nested Python code: _file_node now climbs the full chain of contains/method parents to the file instead of stopping at the first one, which for a nested def or nested class is a function or outer class. Calls from nested functions, closures inside methods, deeply nested defs, and nested-class methods now pass the visibility check and emit edges, while unknown receiver types still emit none.
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2497 functions depend on the 290 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 798 callers, 51 callees - new:
_rebuild_code()— 158 callers, 56 callees - new:
extract_js()— 87 callers, 5 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
main()— 102 callers, 3 callees - new:
dispatch_command()— 2 callers, 129 callees - new:
extract_cpp()— 36 callers, 5 callees - new:
_get_extractor()— 27 callers, 6 callees - …and 40 more — each is listed as a finding
Verification — 2497 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2302 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
152 of 344 test file(s) selected (44%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_blade_extractor.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_cache.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cobol_extractor.py— impacttests/test_cpp_method_declarations.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_external_call_guards.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_tuple_type_refs.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_elixir_import_resolution.py— impacttests/test_elixir_unqualified_call_scope.py— impacttests/test_erlang_extractor.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_extract_path_memo.py— impacttests/test_extract_php_closures.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_import_repoint.py— impacttests/test_go_interface_methods.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impact- … and 102 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_resolve\_python\_member\_calls.
The verifier did not have enough to check \_resolve\_python\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 48 more finding(s) on lines outside this diff (see the check run).
|
Landed in v0.9.81 via an authorship-preserving cherry-pick, so your commit is on |
What does this PR do?
Fixes #4226.
_file_nodein_resolve_python_member_callsreturned the caller's firstcontains(ormethod) parent. It now climbs those parents to the top, which is always the file node (nocontainsedge ever targets a file). For top-level functions and methods the result is unchanged; for a nested function (parent: the enclosing function) and a nested class's method (parent: the outer class) it is now the file, so the #4198 visibility check compares like with like.The check itself is unchanged: the class must still be unique by name and defined in, imported into, or in a module imported by the caller's file.
Type of change
Verification & Invariants
Invariant: the typed-receiver arm decides visibility by the caller's file, at any nesting depth.
Persisted state it could invalidate: none (resolver only; the extractor output and cache are unchanged).
0.9.80 vs this branch, cold
graphify update <dir> --no-cluster --force:gotoat the call site: same file, class, method)test_nested_functions_and_nested_class_methods_reach_the_file(function in function, function in method, three levels with a constructor binding, nested-class method, and an unimported annotation that must still give no edge). Fails on 0.9.80, passes here; the other 10 tests in the file are unchanged and pass.Limitations: none new; the arm's existing limits from #4198 apply unchanged.
How was this tested?
Project venv (Python 3.12.12, graphify from this branch via PYTHONPATH), Windows 11:
Graphify-specific checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf