mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
fix(elixir): expand multi-alias brace form into per-module imports edges
`alias Foo.{Bar, Baz}` (and the same import/require/use brace form) emitted
NO imports edges. tree-sitter-elixir represents it as a `dot` node holding the
base alias plus a trailing `tuple` of member aliases, but the import handler
only matched a bare `alias` child, so every multi-alias import was silently
dropped.
Add `_get_alias_modules`, which expands the brace form to `Foo.Bar`,
`Foo.Baz`, … while leaving the single form (`alias Foo.Bar`) unchanged. Adds a
fixture line + regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,35 @@ def extract_elixir(path: Path) -> dict:
|
||||
return source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
||||
return None
|
||||
|
||||
def _get_alias_modules(node) -> list[str]:
|
||||
"""Every module named by an alias/import/require/use argument.
|
||||
|
||||
Handles the single form (``alias Foo.Bar`` -> ``["Foo.Bar"]``) and the
|
||||
multi-alias brace form (``alias Foo.{Bar, Baz}`` ->
|
||||
``["Foo.Bar", "Foo.Baz"]``), which the grammar represents as a ``dot``
|
||||
node holding the base alias and a trailing ``tuple`` of member aliases.
|
||||
"""
|
||||
def _text(n) -> str:
|
||||
return source[n.start_byte:n.end_byte].decode("utf-8", errors="replace")
|
||||
|
||||
for child in node.children:
|
||||
if child.type == "alias":
|
||||
return [_text(child)]
|
||||
if child.type == "dot":
|
||||
base = None
|
||||
tuple_node = None
|
||||
for sub in child.children:
|
||||
if sub.type == "alias" and base is None:
|
||||
base = _text(sub)
|
||||
elif sub.type == "tuple":
|
||||
tuple_node = sub
|
||||
if base and tuple_node is not None:
|
||||
members = [_text(m) for m in tuple_node.children if m.type == "alias"]
|
||||
if members:
|
||||
return [f"{base}.{m}" for m in members]
|
||||
return [_text(child)]
|
||||
return []
|
||||
|
||||
def walk(node, parent_module_nid: str | None = None) -> None:
|
||||
if node.type != "call":
|
||||
for child in node.children:
|
||||
@@ -121,8 +150,7 @@ def extract_elixir(path: Path) -> dict:
|
||||
return
|
||||
|
||||
if keyword in _IMPORT_KEYWORDS and arguments_node:
|
||||
module_name = _get_alias_text(arguments_node)
|
||||
if module_name:
|
||||
for module_name in _get_alias_modules(arguments_node):
|
||||
tgt_nid = _make_id(module_name)
|
||||
add_edge(file_nid, tgt_nid, "imports", line, context="import")
|
||||
return
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@ defmodule MyApp.Accounts.User do
|
||||
"""
|
||||
|
||||
alias MyApp.Repo
|
||||
alias MyApp.Schemas.{Account, Token}
|
||||
import Ecto.Query
|
||||
|
||||
defstruct [:id, :name, :email]
|
||||
|
||||
@@ -975,6 +975,23 @@ def test_elixir_import_edges_have_import_context():
|
||||
assert import_edges
|
||||
assert all(e.get("context") == "import" for e in import_edges)
|
||||
|
||||
|
||||
def test_elixir_multi_alias_expands():
|
||||
"""`alias Foo.{Bar, Baz}` must emit one imports edge per expanded module.
|
||||
|
||||
The brace form is a `dot` node with a trailing `tuple`; the single-alias
|
||||
handler only matched a bare `alias` child, so every multi-alias import was
|
||||
silently dropped.
|
||||
"""
|
||||
r = extract_elixir(FIXTURES / "sample.ex")
|
||||
import_segs = [
|
||||
e["target"].rsplit("_", 1)[-1]
|
||||
for e in r["edges"] if e["relation"] == "imports"
|
||||
]
|
||||
# from `alias MyApp.Schemas.{Account, Token}`
|
||||
assert "account" in import_segs, "MyApp.Schemas.Account import missing"
|
||||
assert "token" in import_segs, "MyApp.Schemas.Token import missing"
|
||||
|
||||
def test_elixir_finds_calls():
|
||||
r = extract_elixir(FIXTURES / "sample.ex")
|
||||
calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
|
||||
|
||||
Reference in New Issue
Block a user