Skip to content

Commit 44cce79

Browse files
authored
Improve UndefinedFunctionError for unqualified module (#12859)
This PR builds off of #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 0099c2a commit 44cce79

File tree

2 files changed

+53
-11
lines changed

2 files changed

+53
-11
lines changed

Diff for: lib/elixir/lib/exception.ex

+22-11
Original file line numberDiff line numberDiff line change
@@ -1342,20 +1342,31 @@ defmodule UndefinedFunctionError do
13421342
hint_for_loaded_module(module, function, arity, nil)
13431343
end
13441344

1345+
@max_suggestions 5
13451346
defp hint(module, function, arity, _loaded?) do
13461347
downcased_module = downcase_module_name(module)
1347-
1348-
candidate =
1349-
Enum.find(:code.all_available(), fn {name, _, _} ->
1350-
downcase_module_name(name) == downcased_module
1351-
end)
1352-
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"
1348+
stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "")
1349+
1350+
candidates =
1351+
for {name, _, _} = candidate <- :code.all_available(),
1352+
downcase_module_name(name) == downcased_module or
1353+
String.ends_with?(List.to_string(name), stripped_module),
1354+
{:module, module} <- [load_module(candidate)],
1355+
function_exported?(module, function, arity),
1356+
do: module
1357+
1358+
if candidates != [] do
1359+
suggestions =
1360+
candidates
1361+
|> Enum.take(@max_suggestions)
1362+
|> Enum.sort(:asc)
1363+
|> Enum.map(fn module ->
1364+
["\n * ", Exception.format_mfa(module, function, arity)]
1365+
end)
1366+
1367+
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
13571368
else
1358-
_ -> ""
1369+
""
13591370
end
13601371
end
13611372

Diff for: lib/elixir/test/elixir/exception_test.exs

+31
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)