Extract direct recursive calls as calls self loops - #3364
Conversation
_extract_generic's call resolution required tgt_nid != caller_nid before emitting a calls edge, so a function calling itself by name was silently dropped no matter which of the many languages sharing this engine it was written in (Python, Java, Ruby, C#, PHP, and the rest of the LanguageConfig set). build_from_json already preserves a supplied recursive calls self loop rather than stripping it (test_recursive_call_self_loop_is_preserved in test_import_self_loops.py), so the design intent to keep genuine recursion visible already existed one layer up; extraction just never produced the edge for that layer to preserve. Name resolution binds a self call the same way as any other call, so there is nothing special casing a caller equal to its own target should do beyond what seen_call_pairs already dedupes. Scoped to the shared engine only: several dedicated per language extractors (Go, Rust, CommonLisp, DreamMaker, Elixir, PowerShell, Zig) carry the same tgt_nid != caller_nid guard in their own files and were left untouched here, since that is a separate, larger change across files this report did not exercise. Fixes Graphify-Labs#3350.
|
@safishamsi opened this against a report of direct Python recursion never producing a calls self-loop (#3350), traced to a shared tgt_nid != caller_nid guard in _extract_generic used by many languages, not just Python. Verified the same fix also restores recursion for Java and Ruby. Flagging the scope choice in the PR description: a few dedicated per-language extractors carry the same guard separately and weren't touched here. Happy to address any feedback. |
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. No changes could be formally verified in this run.
Graphify review — findings
Preserves recursive call self-loops during extraction by dropping the tgt_nid != caller_nid guard in _extract_generic, so a function or method that calls itself now emits a calls edge to itself instead of having it silently discarded (#3350). Adds test_recursive_call_produces_self_loop and test_recursive_method_call_produces_self_loop covering plain and self.<method>() recursion, and documents in test_calls_no_self_loops that its fixture just has no recursive functions.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1009 functions depend on the 608 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_extract_generic()— 18 callers, 26 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - new:
extract_cpp()— 27 callers, 3 callees - new:
extract_vue()— 10 callers, 7 callees - new:
walk()— 1 callers, 58 callees - …and 8 more — each is listed as a finding
Verification — 1009 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: 949 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 16 more finding(s) on lines outside this diff (see the check run).
What
_extract_generic's call resolution requiredtgt_nid != caller_nidbefore emitting acallsedge, so a function calling itself by name was silently dropped, no matter which language sharing this engine it was written in.build_from_jsonalready preserves a supplied recursivecallsself loop rather than stripping it (test_recursive_call_self_loop_is_preserved), so the intent to keep genuine recursion visible already existed one layer up. Extraction just never produced the edge for that layer to preserve.Repro (from the issue, reproduced before this fix)
Before:
calls: [('repro_entry', 'repro_factorial')](recursive call missing)After:
calls: [('repro_factorial', 'repro_factorial'), ('repro_entry', 'repro_factorial')]Verified this also fixes recursion for Java, Ruby, and other
_extract_generic-routed languages, since the guard removed was language agnostic.Scope
Several dedicated per-language extractors (Go, Rust, CommonLisp, DreamMaker, Elixir, PowerShell, Zig) carry the same
tgt_nid != caller_nidguard in their own separate files. Left those untouched here since the issue's repro and my verification are both Python/_extract_generic-scoped, and fixing those too would be a materially larger, separate change across files this report didn't exercise. Happy to open a follow-up if that's wanted.Fixes #3350.