Skip to content

fix(elixir): resolve qualified and aliased remote calls - #4213

Closed
Mpasha17 wants to merge 1 commit into
Graphify-Labs:v8from
Mpasha17:fix/elixir-qualified-calls-4206
Closed

Mpasha17 wants to merge 1 commit into
Graphify-Labs:v8from
Mpasha17:fix/elixir-qualified-calls-4206

Conversation

@Mpasha17

@Mpasha17 Mpasha17 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Closes #4206

The Elixir extractor threw away the module part of a remote call and kept only the function name. So App.Accounts.get_user(id) and Accounts.get_user(id) (after alias App.Accounts) got no calls edge at all, and App.Repo.insert(x) inside a module with its own insert/1 was linked to that local function as EXTRACTED. The same went for Map.update(...), Enum.map(...), conn.assigns and so on whenever the caller's module had a function with that name.

Now walk_calls keeps the receiver when it's a module name and expands it through the alias table in scope (alias A.B, alias A.B, as: C, alias A.{B, C}, aliases inside a function body, the implicit alias of a nested defmodule, and __MODULE__). If that module is in the same file the edge is made right there. Otherwise the call goes to a new elixir_qualified_calls resolver, which links it only when exactly one top-level module in the corpus has that name and that module defines the function. Modules outside the corpus get nothing. A qualified call never goes through the bare-name lookup anymore, and calls on a variable (mod.fun(), conn.assigns, fun.(x)) or an Erlang module (:ets.new) stay unlinked. Unqualified, import/use and local calls work the same as before.

Limits: arity isn't checked, so Mod.fun/1 and Mod.fun/2 both point at the fun node. A nested module defined in another file (App.Web.Helpers declared inside App.Web) isn't a cross-file target, same as for the existing import-target pass. require A, as: B isn't treated as an alias, and dynamic receivers like Module.concat(...) aren't resolved.

Testing: new tests/test_elixir_qualified_calls.py with 11 tests. 9 of them fail on current v8; the nested-module and import/local control tests pass on both. The full suite on 3.10/3.12/3.13/3.14 shows the same failures as v8. Ruff is clean and pyright shows no new findings. The issue's repro now gives exactly the expected edges, also after an incremental graphify update. On hexpm, plausible/analytics and phoenix the Elixir calls edges went from 3464 to 4811, 2892 to 5091 and 1163 to 1439. Nothing outside .ex/.exs changed. I spot-checked a sample of the new edges and they're correct. Every removed edge was a remote or variable call that had been bound to a same-named local function.

AI-assisted (Grok Bot); each commit carries a Co-Authored-By line.

@Mpasha17
Mpasha17 requested a review from safishamsi as a code owner October 8, 2026 03:34
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Thanks for the pull request, @Mpasha17. 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.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. PR-changed functions: 1/1 verified (0 proven, 1 may-equivalent, 0 distinguished) · 0 not verified.


Graphify review — findings

Adds cross-file calls edges for Elixir module-qualified calls like Accounts.get_user(id): extract_elixir now expands the receiver through alias tables (including as:, function-local aliases, nested-module auto-aliases, and __MODULE__) and links same-file targets directly. Everything else goes to the new _resolve_elixir_qualified_calls pass, which only adds an edge when the module matches exactly one top-level corpus module that defines the function. External modules like Repo or Enum get no edge rather than a same-named guess.

Worth a look

  • defmodule __MODULE__.Child is registered under an unexpandable name — graphify/extractors/elixir.py:163 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2543 functions depend on the 313 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 800 callers, 51 callees
  • new: _rebuild_code() — 160 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 42 more — each is listed as a finding

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

Test selection

Test selection

153 of 345 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_qualified_calls.py — impact, changed-test
  • 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
  • … and 103 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.

Docs that may be stale (advisory)

…and 10 more.

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; 49 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/extractors/elixir.py
@Mpasha17

Mpasha17 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Checked the defmodule __MODULE__.Child finding live. tree-sitter parses __MODULE__.Child as a dot node, not an alias, so _get_alias_text returns None and the defmodule branch returns before _register_module runs. Same as on v8: that module isn't registered at all, so nothing ends up stored under an unexpandable name. On a small repro with defmodule __MODULE__.Child plus calls to App.Parent.Child.pong(x) and __MODULE__.Child.pong(x), this branch and v8 give the same graph, with no wrong edges. Supporting __MODULE__.Child modules would be a separate change, so I kept it out of this one.

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

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: qualified remote calls (Mod.fun(), Alias.fun()) produce no calls edge, or bind to the caller's own same-named function

2 participants