Skip to content

fix(python): find a nested function's file in the typed-receiver visibility check - #4227

Closed
rohit-jsfreaky wants to merge 1 commit into
Graphify-Labs:v8from
rohit-jsfreaky:fix/python-typed-receiver-nested-functions
Closed

rohit-jsfreaky wants to merge 1 commit into
Graphify-Labs:v8from
rohit-jsfreaky:fix/python-typed-receiver-nested-functions

Conversation

@rohit-jsfreaky

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #4226.

_file_node in _resolve_python_member_calls returned the caller's first contains (or method) parent. It now climbs those parents to the top, which is always the file node (no contains edge 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

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

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:

repo new edges correct (jedi goto at the call site: same file, class, method) edges lost real calls found vs jedi
click +10 10 0 80.3% -> 81.5%
rich +4 4 0 —
httpx 0 — 0 78.5% (same)
flask 0 — 0 66.7% (same)
typer 0 — 0 —
  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test: 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.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

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:

python -m pytest tests/test_python_typed_receiver_calls.py -q     # 11 passed (on 0.9.80: the new test fails, 10 pass)
python -m pytest tests -q                                          # 19 failed, 6763 passed, 27 skipped: the same 18 as clean 0.9.80 on this machine, plus
#   test_incremental_mtime_collision.py::test_same_size_rewrite_in_one_tick_is_requeued, which passes 5/5 alone on both 0.9.80 and this branch
#   (it pins file mtimes; it does not reach the resolver)
python -m ruff check graphify/extract.py tests/test_python_typed_receiver_calls.py   # All checks passed
pyright graphify/extract.py tests/test_python_typed_receiver_calls.py               # 120 errors, same 120 as 0.9.80, 0 new
bandit -ll graphify/extract.py                                    # 5 findings, same 5 as 0.9.80, 0 new

Graphify-specific checklist

  • I updated generated skill artifacts — not needed, no skill fragments touched.
  • I confirmed that AST/structural extraction remains deterministic.
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed that no API keys or local-only graph data are included.
  • I disclosed AI authorship in my commit messages.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf

…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
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

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.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_blade_extractor.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_cache.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_cpp_method_declarations.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_tuple_type_refs.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_elixir_unqualified_call_scope.py — impact
  • tests/test_erlang_extractor.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_path_memo.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_import_repoint.py — impact
  • tests/test_go_interface_methods.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/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).

@safishamsi

Copy link
Copy Markdown
Member

Landed in v0.9.81 via an authorship-preserving cherry-pick, so your commit is on v8 with you credited as the author. Closing as shipped — thanks @rohit-jsfreaky!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Python typed-receiver calls get no edge inside a nested function or a nested class's method

2 participants