Skip to content

Commit 134f320

Browse files
committed
Improve UndefinedFunctionError for unqualified module
This PR builds off of elixir-lang#12839, handling the case where a developer forgets to alias a module, resulting in UndefinedFunctionError. Imagine we have the following modules in our Elixir program. ```elixir def MyAppWeb.Context.Event do def foo, do: :bar end ``` If the developer attempts to reference this module, but forgets to type the alias, they will now get module suggestions like. ```elixir ** (UndefinedFunctionError) function Event.foo/0 is undefined (module Event is not available). Did you mean: * MyAppWeb.Context.Event.foo/0 Event.foo() iex:2: (file) ```
1 parent 52158d7 commit 134f320

File tree

2 files changed

+53
-8
lines changed

2 files changed

+53
-8
lines changed

lib/elixir/lib/exception.ex

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,17 +1345,25 @@ defmodule UndefinedFunctionError do
13451345
defp hint(module, function, arity, _loaded?) do
13461346
downcased_module = downcase_module_name(module)
13471347

1348-
candidate =
1349-
Enum.find(:code.all_available(), fn {name, _, _} ->
1350-
downcase_module_name(name) == downcased_module
1348+
candidates =
1349+
Enum.filter(:code.all_available(), fn {name, _, _} = candidate ->
1350+
if downcase_module_name(name) == downcased_module or
1351+
String.ends_with?(to_string(name), strip_elixir_prefix(module)) do
1352+
with {:module, module} <- load_module(candidate),
1353+
true <- function_exported?(module, function, arity) do
1354+
true
1355+
else
1356+
_ -> false
1357+
end
1358+
else
1359+
false
1360+
end
13511361
end)
13521362

1353-
with {_, _, _} <- candidate,
1354-
{:module, module} <- load_module(candidate),
1355-
true <- function_exported?(module, function, arity) do
1356-
". Did you mean:\n\n * #{Exception.format_mfa(module, function, arity)}\n"
1363+
if candidates != [] do
1364+
". Did you mean:\n#{Enum.map(candidates, fn {module, _, _} -> "\n * #{Exception.format_mfa(String.to_atom(to_string(module)), function, arity)}" end)}\n"
13571365
else
1358-
_ -> ""
1366+
""
13591367
end
13601368
end
13611369

@@ -1365,6 +1373,12 @@ defmodule UndefinedFunctionError do
13651373
|> Code.ensure_loaded()
13661374
end
13671375

1376+
defp strip_elixir_prefix(module) do
1377+
module
1378+
|> to_string()
1379+
|> String.replace_leading("Elixir.", "")
1380+
end
1381+
13681382
defp downcase_module_name(module) do
13691383
module
13701384
|> to_string()

lib/elixir/test/elixir/exception_test.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,25 @@ defmodule ExceptionTest do
596596
end
597597

598598
test "annotates undefined function error with module suggestions" do
599+
import PathHelpers
600+
601+
modules = [
602+
Namespace.A.One,
603+
Namespace.A.Two,
604+
Namespace.A.Three,
605+
Namespace.B.One,
606+
Namespace.B.Two,
607+
Namespace.B.Three
608+
]
609+
610+
for module <- modules do
611+
write_beam(
612+
defmodule module do
613+
def foo, do: :bar
614+
end
615+
)
616+
end
617+
599618
assert blame_message(ENUM, & &1.map(&1, 1)) == """
600619
function ENUM.map/2 is undefined (module ENUM is not available). Did you mean:
601620
@@ -604,6 +623,18 @@ defmodule ExceptionTest do
604623

605624
assert blame_message(ENUM, & &1.not_a_function(&1, 1)) ==
606625
"function ENUM.not_a_function/2 is undefined (module ENUM is not available)"
626+
627+
assert blame_message(One, & &1.foo()) == """
628+
function One.foo/0 is undefined (module One is not available). Did you mean:
629+
630+
* Namespace.A.One.foo/0
631+
* Namespace.B.One.foo/0
632+
"""
633+
634+
for module <- modules do
635+
:code.delete(module)
636+
:code.purge(module)
637+
end
607638
end
608639

609640
test "annotates undefined function clause error with macro hints" do

0 commit comments

Comments
 (0)