Skip to content

fix(elixir): emit calls edges for keyword-form def bodies - #4210

Closed
rtmalikian wants to merge 1 commit into
Graphify-Labs:v8from
rtmalikian:fix/4207-elixir-keyword-def-calls
Closed

rtmalikian wants to merge 1 commit into
Graphify-Labs:v8from
rtmalikian:fix/4207-elixir-keyword-def-calls

Conversation

@rtmalikian

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #4207.

One-line Elixir definitions (def a(x), do: b(x)) have no do_block node: tree-sitter-elixir puts the keyword body in the arguments node's trailing keywords child, as the value of the pair whose keyword is do:. The extractor only registered do_block bodies in function_bodies for the call-walk pass, so every call inside a keyword-form body was silently dropped from the graph — a real codebase loses a large share of its local call edges.

The fix: when a def/defp/defmacro/defmacrop/defguard/defguardp has no do_block, take the value of the do: pair in its arguments as the body and walk it the same way (new _get_do_keyword_body helper, mirroring the existing _get_defimpl_target pair-walking style).

Changes

File Change
graphify/extractors/elixir.py Add _get_do_keyword_body() helper; in the def-family branch, fall back to the do: keyword-pair value when do_block_node is absent (+32 lines)
tests/test_elixir_keyword_def_calls.py New regression tests: keyword-form call edge, when-guarded keyword head, no dangling edges for undefined callees, block-form unchanged (+70 lines)

Type of change

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

Verification & Invariants

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Invariant protected: every defined function whose body the extractor recognizes gets its calls walked exactly once, so calls edges reflect the source. Limitation: only the do: keyword is handled; else:/rescue: keyword bodies are not walked, matching the block form which only walks do_block children.

How was this tested?

# Reproduced the issue first: calls edges were [] for `def a(x), do: b(x)`
pytest tests/test_elixir_keyword_def_calls.py -q
# 4 passed (2 key assertions fail on the unfixed code — negative control verified via git stash)

pytest tests/test_elixir_keyword_def_calls.py tests/test_elixir_import_resolution.py tests/test_elixir_unqualified_call_scope.py -q
# 15 passed

pytest tests/test_extract.py -q
# 176 passed, 111 failed, 7 skipped — identical counts on the pristine tree
# (pre-existing failures: optional tree-sitter grammars not installed in this env)

ruff check graphify/extractors/elixir.py tests/test_elixir_keyword_def_calls.py   # (ruff 0.15.14, repo-pinned) clean
ruff format --check tests/test_elixir_keyword_def_calls.py                        # clean
# (graphify/extractors/elixir.py was already format-dirty before this change; pre-commit runs ruff lint only)

Environment: Python 3.11, tree-sitter 0.25 + tree-sitter-elixir 0.3 (repo-pinned ranges), offline — no LLM/provider calls.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. — N/A: no skill fragments touched (tools/skillgen has no reference to the Elixir extractor).
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

Additional Notes

Author: Raphael Malikian — rtmalikian@gmail.com. If you'd like similar help with your own project or codebase, I'm open to collaborate — feel free to reach out.

AI assistance disclosure: implementation drafted with Muse (Anthropic), reviewed and verified by the author.

One-line definitions (`def a(x), do: b(x)`) have no do_block node: the
body is the value of the `do:` pair in the arguments keywords node. The
extractor only registered do_block bodies for the call-walk pass, so every
call inside a keyword-form body was silently dropped from the graph. When
there is no do_block, take the `do:` pair value as the body and walk it
the same way (Fixes Graphify-Labs#4207).

AI assistance: implementation drafted with Muse (Anthropic), reviewed and
verified by the author.

Signed-off-by: Raphael Malikian <rtmalikian@gmail.com>
@rtmalikian
rtmalikian requested a review from safishamsi as a code owner October 7, 2026 23:36
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Thanks for the pull request, @rtmalikian. 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: 1/1 verified (0 proven, 1 may-equivalent, 0 distinguished) · 0 not verified.


Graphify review — findings

Fixes dropped calls edges for keyword-form Elixir definitions (def a(x), do: b(x)) by having _get_do_keyword_body pull the do: value out of the arguments keywords and queue it for the call walk whenever a definition has no do_block. This also covers when-guarded heads. Definitions with neither form register no body, same as before. New tests check that keyword bodies produce edges to local functions and no edges to undefined ones.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 37 functions depend on the 25 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract_elixir() — 14 callers, 6 callees
  • new: walk() — 1 callers, 7 callees

Verification — 37 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: 37 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

2 of 345 test file(s) selected (1%) via static blast radius.

  • tests/test_elixir_keyword_def_calls.py — impact, changed-test
  • tests/test_languages.py — impact

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

No difference found (not proven): No behavior difference found in extract\_elixir (not a proof).

The verifier ran both versions of extract\_elixir on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 1 grounded finding(s) anchored inline below; 1 more finding(s) on lines outside this diff (see the check run).

return sub
return None

def walk(node, parent_module_nid: str | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — walk()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@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 @rtmalikian!

@safishamsi safishamsi closed this Oct 8, 2026
@rtmalikian

Copy link
Copy Markdown
Contributor Author

Thanks for landing it, @safishamsi — glad the fix made it into v0.9.81. Happy to help again.

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]: Elixir: calls inside one-line function bodies (def f(x), do: g(x)) are never collected

2 participants