From 7406aae5e64703ba1ac9154ae1e1c6f0e18694d8 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Wed, 16 Sep 2026 15:36:40 +0530 Subject: [PATCH] Add a regression test for the issue's own repro A module aliased from a different file now resolves to that module's real, non dangling node. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit 53877c6874be8482f4a676886185d18b5e567f11) --- tests/test_elixir_import_resolution.py | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/test_elixir_import_resolution.py diff --git a/tests/test_elixir_import_resolution.py b/tests/test_elixir_import_resolution.py new file mode 100644 index 000000000..595e7612d --- /dev/null +++ b/tests/test_elixir_import_resolution.py @@ -0,0 +1,73 @@ +"""Elixir cross-file alias/import/require/use resolution (#2556). + +`extract_elixir` mints a module's own node id with the defining file's stem +(`_make_id(stem, module_name)`) but an alias/import/require/use target with +just the bare module name (`_make_id(module_name)`) -- the two can only +match when a module refers to itself, so a reference to a module declared +in ANY other file was silently dropped as dangling at build time. On a real +900-file Elixir/Phoenix project this discarded 13% of extracted edges -- +the entire internal module dependency graph. +""" +from __future__ import annotations + +from pathlib import Path + +from graphify.extract import extract + + +def _extract(tmp_path: Path, files: dict[str, str]): + paths = [] + for name, body in files.items(): + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(body, encoding="utf-8") + paths.append(path) + return extract(paths, cache_root=tmp_path / "graphify-out") + + +def _find(result: dict, label: str, id_contains: str = "") -> str: + return next( + node["id"] + for node in result["nodes"] + if node.get("label") == label and id_contains in node["id"] + ) + + +def _find_file(result: dict, filename: str) -> str: + """An `imports` edge's source is the FILE node (extract_elixir emits it + at file scope), not the module node the file happens to declare.""" + return next( + node["id"] + for node in result["nodes"] + if node.get("label") == filename + ) + + +_REPRO_CORPUS = { + "lib/demo/accounts.ex": ( + "defmodule Demo.Accounts do\n" + " def list_users, do: []\n" + "end\n" + ), + "lib/demo/web.ex": ( + "defmodule Demo.Web do\n" + " alias Demo.Accounts\n" + "\n" + " def index, do: Accounts.list_users()\n" + "end\n" + ), +} + + +def test_alias_resolves_to_the_module_declared_in_another_file(tmp_path: Path): + result = _extract(tmp_path, _REPRO_CORPUS) + web_file = _find_file(result, "web.ex") + accounts = _find(result, "Demo.Accounts") + imports = { + (e["source"], e["target"]) + for e in result["edges"] + if e["relation"] == "imports" + } + assert (web_file, accounts) in imports + node_ids = {n["id"] for n in result["nodes"]} + assert accounts in node_ids, "the import target must be a real, non-dangling node"