From 3cfb19f4229b5a689b7ea5647eda3e4f953b79ea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 1 May 2026 21:51:15 +0200 Subject: [PATCH 1/3] Improve ambiguous import error message --- lib/elixir/lib/macro/env.ex | 3 ++ lib/elixir/src/elixir_dispatch.erl | 47 +++++++++++------- lib/elixir/test/elixir/kernel/errors_test.exs | 48 +++++++++++++++++++ lib/iex/test/iex/interaction_test.exs | 2 +- 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/macro/env.ex b/lib/elixir/lib/macro/env.ex index 123d5c6a591..c91b6128d20 100644 --- a/lib/elixir/lib/macro/env.ex +++ b/lib/elixir/lib/macro/env.ex @@ -607,6 +607,9 @@ defmodule Macro.Env do {:function, receiver, name} -> {:function, receiver, name} + {:ambiguous, tagged} -> + {:error, {:ambiguous, for({_kind, mod} <- tagged, do: mod)}} + error -> {:error, error} end diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index eb1a85ce668..30560b4b64d 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -47,10 +47,10 @@ find_imports(Meta, Name, E) -> Macs = ?key(E, macros), Acc0 = #{}, - Acc1 = find_imports_by_name(Funs, Acc0, Name, Meta, E), - Acc2 = find_imports_by_name(Macs, Acc1, Name, Meta, E), + Acc1 = find_imports_by_name(function, Funs, Acc0, Name, Meta, E), + Acc2 = find_imports_by_name(macro, Macs, Acc1, Name, Meta, E), - lists:sort(maps:to_list(Acc2)). + [{Arity, Mod} || {Arity, {_Kind, Mod}} <- lists:sort(maps:to_list(Acc2))]. %% Function retrieval @@ -287,24 +287,24 @@ caller(Line, E) -> %% Helpers -find_imports_by_name([{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> - NewAcc = find_imports_by_name(Name, Imports, Acc, Mod, Meta, E), - find_imports_by_name(ModImports, NewAcc, Name, Meta, E); -find_imports_by_name([], Acc, _Name, _Meta, _E) -> +find_imports_by_name(Kind, [{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> + NewAcc = find_imports_by_name(Kind, Name, Imports, Acc, Mod, Meta, E), + find_imports_by_name(Kind, ModImports, NewAcc, Name, Meta, E); +find_imports_by_name(_Kind, [], Acc, _Name, _Meta, _E) -> Acc. -find_imports_by_name(Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> +find_imports_by_name(Kind, Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> case Acc of - #{Arity := OtherMod} -> - Error = {import, {ambiguous, [Mod, OtherMod]}, Name, Arity}, + #{Arity := {OtherKind, OtherMod}} -> + Error = {import, {ambiguous, [{Kind, Mod}, {OtherKind, OtherMod}]}, Name, Arity}, elixir_errors:file_error(Meta, E, ?MODULE, Error); #{} -> - find_imports_by_name(Name, Imports, Acc#{Arity => Mod}, Mod, Meta, E) + find_imports_by_name(Kind, Name, Imports, Acc#{Arity => {Kind, Mod}}, Mod, Meta, E) end; -find_imports_by_name(Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> - find_imports_by_name(Name, Imports, Acc, Mod, Meta, E); -find_imports_by_name(_Name, _Imports, Acc, _Mod, _Meta, _E) -> +find_imports_by_name(Kind, Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> + find_imports_by_name(Kind, Name, Imports, Acc, Mod, Meta, E); +find_imports_by_name(_Kind, _Name, _Imports, Acc, _Mod, _Meta, _E) -> Acc. find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> @@ -321,7 +321,9 @@ find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> {[], [Receiver]} -> {macro, Receiver}; {[Receiver], []} -> {function, Receiver}; {[], []} -> false; - _ -> {ambiguous, FunMatch ++ MacMatch} + _ -> + Tagged = [{function, M} || M <- FunMatch] ++ [{macro, M} || M <- MacMatch], + {ambiguous, Tagged} end end. @@ -362,9 +364,18 @@ format_error({import, {conflict, Receiver}, Name, Arity}) -> io_lib:format("call to local macro ~ts/~B conflicts with imported ~ts.~ts/~B, " "please rename the local macro or remove the conflicting import", [Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]); -format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) -> - io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous", - [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); +format_error({import, {ambiguous, [{Kind1, Mod1}, {Kind2, Mod2} | _]}, Name, Arity}) -> + case {Kind1, Kind2} of + {function, function} -> + io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous", + [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); + {macro, macro} -> + io_lib:format("macro ~ts/~B imported from both ~ts and ~ts, call is ambiguous", + [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); + _ -> + io_lib:format("~ts/~B is ambiguous, it is imported as a ~ts from ~ts and as a ~ts from ~ts", + [Name, Arity, Kind1, elixir_aliases:inspect(Mod1), Kind2, elixir_aliases:inspect(Mod2)]) + end; format_error({compile_env, Name, Arity}) -> io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]); format_error({deprecated, Mod, '__using__', 1, Message}) -> diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 7718b4fedc1..4cd6ee44f47 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -694,6 +694,54 @@ defmodule Kernel.ErrorsTest do ) end + test "macro/function import conflict" do + assert_compile_error( + [ + "nofile:12:", + "foo/1 is ambiguous, it is imported as a function from Kernel.ErrorsTest.MacroFunImportB and as a macro from Kernel.ErrorsTest.MacroFunImportA" + ], + ~c""" + defmodule Kernel.ErrorsTest.MacroFunImportA do + defmacro foo(x), do: x + end + + defmodule Kernel.ErrorsTest.MacroFunImportB do + def foo(x), do: x + end + + defmodule Kernel.ErrorsTest.MacroFunImportConflict do + import Kernel.ErrorsTest.MacroFunImportA + import Kernel.ErrorsTest.MacroFunImportB + foo(1) + end + """ + ) + end + + test "macro/macro import conflict" do + assert_compile_error( + [ + "nofile:12:", + "macro foo/1 imported from both Kernel.ErrorsTest.MacroMacroImportB and Kernel.ErrorsTest.MacroMacroImportA, call is ambiguous" + ], + ~c""" + defmodule Kernel.ErrorsTest.MacroMacroImportA do + defmacro foo(x), do: x + end + + defmodule Kernel.ErrorsTest.MacroMacroImportB do + defmacro foo(x), do: x + end + + defmodule Kernel.ErrorsTest.MacroMacroImportConflict do + import Kernel.ErrorsTest.MacroMacroImportA + import Kernel.ErrorsTest.MacroMacroImportB + foo(1) + end + """ + ) + end + test "ensure valid import :only option" do assert_compile_error( [ diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index f351da9345e..9ceda9c1762 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -185,7 +185,7 @@ defmodule IEx.InteractionTest do assert capture_io(:stderr, fn -> capture_iex("open('README.md')", [], env: __ENV__) end) =~ - ~r"function open/1 imported from both File and IEx.Helpers" + ~r"open/1 is ambiguous, it is imported as a function from File and as a macro from IEx.Helpers" end test "receive exit" do From 6243b48dbff4e3d1c00a1260e0807d3f61bb09fa Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 6 May 2026 11:58:35 +0200 Subject: [PATCH 2/3] Apply PR comments --- lib/elixir/lib/macro/env.ex | 3 -- lib/elixir/src/elixir_dispatch.erl | 47 +++++++------------ lib/elixir/test/elixir/kernel/errors_test.exs | 8 ++-- lib/iex/test/iex/interaction_test.exs | 2 +- 4 files changed, 23 insertions(+), 37 deletions(-) diff --git a/lib/elixir/lib/macro/env.ex b/lib/elixir/lib/macro/env.ex index c91b6128d20..123d5c6a591 100644 --- a/lib/elixir/lib/macro/env.ex +++ b/lib/elixir/lib/macro/env.ex @@ -607,9 +607,6 @@ defmodule Macro.Env do {:function, receiver, name} -> {:function, receiver, name} - {:ambiguous, tagged} -> - {:error, {:ambiguous, for({_kind, mod} <- tagged, do: mod)}} - error -> {:error, error} end diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 30560b4b64d..ea13a3b5bab 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -47,10 +47,10 @@ find_imports(Meta, Name, E) -> Macs = ?key(E, macros), Acc0 = #{}, - Acc1 = find_imports_by_name(function, Funs, Acc0, Name, Meta, E), - Acc2 = find_imports_by_name(macro, Macs, Acc1, Name, Meta, E), + Acc1 = find_imports_by_name(Funs, Acc0, Name, Meta, E), + Acc2 = find_imports_by_name(Macs, Acc1, Name, Meta, E), - [{Arity, Mod} || {Arity, {_Kind, Mod}} <- lists:sort(maps:to_list(Acc2))]. + lists:sort(maps:to_list(Acc2)). %% Function retrieval @@ -287,24 +287,24 @@ caller(Line, E) -> %% Helpers -find_imports_by_name(Kind, [{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> - NewAcc = find_imports_by_name(Kind, Name, Imports, Acc, Mod, Meta, E), - find_imports_by_name(Kind, ModImports, NewAcc, Name, Meta, E); -find_imports_by_name(_Kind, [], Acc, _Name, _Meta, _E) -> +find_imports_by_name([{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> + NewAcc = find_imports_by_name(Name, Imports, Acc, Mod, Meta, E), + find_imports_by_name(ModImports, NewAcc, Name, Meta, E); +find_imports_by_name([], Acc, _Name, _Meta, _E) -> Acc. -find_imports_by_name(Kind, Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> +find_imports_by_name(Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> case Acc of - #{Arity := {OtherKind, OtherMod}} -> - Error = {import, {ambiguous, [{Kind, Mod}, {OtherKind, OtherMod}]}, Name, Arity}, + #{Arity := OtherMod} -> + Error = {import, {ambiguous, [Mod, OtherMod]}, Name, Arity}, elixir_errors:file_error(Meta, E, ?MODULE, Error); #{} -> - find_imports_by_name(Kind, Name, Imports, Acc#{Arity => {Kind, Mod}}, Mod, Meta, E) + find_imports_by_name(Name, Imports, Acc#{Arity => Mod}, Mod, Meta, E) end; -find_imports_by_name(Kind, Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> - find_imports_by_name(Kind, Name, Imports, Acc, Mod, Meta, E); -find_imports_by_name(_Kind, _Name, _Imports, Acc, _Mod, _Meta, _E) -> +find_imports_by_name(Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> + find_imports_by_name(Name, Imports, Acc, Mod, Meta, E); +find_imports_by_name(_Name, _Imports, Acc, _Mod, _Meta, _E) -> Acc. find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> @@ -321,9 +321,7 @@ find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> {[], [Receiver]} -> {macro, Receiver}; {[Receiver], []} -> {function, Receiver}; {[], []} -> false; - _ -> - Tagged = [{function, M} || M <- FunMatch] ++ [{macro, M} || M <- MacMatch], - {ambiguous, Tagged} + _ -> {ambiguous, FunMatch ++ MacMatch} end end. @@ -364,18 +362,9 @@ format_error({import, {conflict, Receiver}, Name, Arity}) -> io_lib:format("call to local macro ~ts/~B conflicts with imported ~ts.~ts/~B, " "please rename the local macro or remove the conflicting import", [Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]); -format_error({import, {ambiguous, [{Kind1, Mod1}, {Kind2, Mod2} | _]}, Name, Arity}) -> - case {Kind1, Kind2} of - {function, function} -> - io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous", - [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); - {macro, macro} -> - io_lib:format("macro ~ts/~B imported from both ~ts and ~ts, call is ambiguous", - [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); - _ -> - io_lib:format("~ts/~B is ambiguous, it is imported as a ~ts from ~ts and as a ~ts from ~ts", - [Name, Arity, Kind1, elixir_aliases:inspect(Mod1), Kind2, elixir_aliases:inspect(Mod2)]) - end; +format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) -> + io_lib:format("conflicting ~ts/~B import from modules ~ts and ~ts", + [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); format_error({compile_env, Name, Arity}) -> io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]); format_error({deprecated, Mod, '__using__', 1, Message}) -> diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 4cd6ee44f47..9aa7b9f43e4 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -674,7 +674,7 @@ defmodule Kernel.ErrorsTest do test "function import conflict" do assert_compile_error( - ["nofile:3:16", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"], + ["nofile:3:16", "conflicting exit/1 import from modules :erlang and Kernel"], ~c""" defmodule Kernel.ErrorsTest.FunctionImportConflict do import :erlang, only: [exit: 1], warn: false @@ -684,7 +684,7 @@ defmodule Kernel.ErrorsTest do ) assert_compile_error( - ["nofile:3:17", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"], + ["nofile:3:17", "conflicting exit/1 import from modules :erlang and Kernel"], ~c""" defmodule Kernel.ErrorsTest.FunctionImportConflict do import :erlang, only: [exit: 1], warn: false @@ -698,7 +698,7 @@ defmodule Kernel.ErrorsTest do assert_compile_error( [ "nofile:12:", - "foo/1 is ambiguous, it is imported as a function from Kernel.ErrorsTest.MacroFunImportB and as a macro from Kernel.ErrorsTest.MacroFunImportA" + "conflicting foo/1 import from modules Kernel.ErrorsTest.MacroFunImportB and Kernel.ErrorsTest.MacroFunImportA" ], ~c""" defmodule Kernel.ErrorsTest.MacroFunImportA do @@ -722,7 +722,7 @@ defmodule Kernel.ErrorsTest do assert_compile_error( [ "nofile:12:", - "macro foo/1 imported from both Kernel.ErrorsTest.MacroMacroImportB and Kernel.ErrorsTest.MacroMacroImportA, call is ambiguous" + "conflicting foo/1 import from modules Kernel.ErrorsTest.MacroMacroImportB and Kernel.ErrorsTest.MacroMacroImportA" ], ~c""" defmodule Kernel.ErrorsTest.MacroMacroImportA do diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 9ceda9c1762..4ef9c715416 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -185,7 +185,7 @@ defmodule IEx.InteractionTest do assert capture_io(:stderr, fn -> capture_iex("open('README.md')", [], env: __ENV__) end) =~ - ~r"open/1 is ambiguous, it is imported as a function from File and as a macro from IEx.Helpers" + ~r"conflicting open/1 import from modules File and IEx.Helpers" end test "receive exit" do From 2b398889259a6a4229d0b786b5961eae033f8219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 6 May 2026 12:02:51 +0200 Subject: [PATCH 3/3] Apply suggestion from @josevalim --- lib/elixir/test/elixir/kernel/errors_test.exs | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 9aa7b9f43e4..f784870d8bb 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -694,54 +694,6 @@ defmodule Kernel.ErrorsTest do ) end - test "macro/function import conflict" do - assert_compile_error( - [ - "nofile:12:", - "conflicting foo/1 import from modules Kernel.ErrorsTest.MacroFunImportB and Kernel.ErrorsTest.MacroFunImportA" - ], - ~c""" - defmodule Kernel.ErrorsTest.MacroFunImportA do - defmacro foo(x), do: x - end - - defmodule Kernel.ErrorsTest.MacroFunImportB do - def foo(x), do: x - end - - defmodule Kernel.ErrorsTest.MacroFunImportConflict do - import Kernel.ErrorsTest.MacroFunImportA - import Kernel.ErrorsTest.MacroFunImportB - foo(1) - end - """ - ) - end - - test "macro/macro import conflict" do - assert_compile_error( - [ - "nofile:12:", - "conflicting foo/1 import from modules Kernel.ErrorsTest.MacroMacroImportB and Kernel.ErrorsTest.MacroMacroImportA" - ], - ~c""" - defmodule Kernel.ErrorsTest.MacroMacroImportA do - defmacro foo(x), do: x - end - - defmodule Kernel.ErrorsTest.MacroMacroImportB do - defmacro foo(x), do: x - end - - defmodule Kernel.ErrorsTest.MacroMacroImportConflict do - import Kernel.ErrorsTest.MacroMacroImportA - import Kernel.ErrorsTest.MacroMacroImportB - foo(1) - end - """ - ) - end - test "ensure valid import :only option" do assert_compile_error( [