diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 76d412b0801..9ab6352bc81 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -1342,8 +1342,33 @@ defmodule UndefinedFunctionError do hint_for_loaded_module(module, function, arity, nil) end - defp hint(_module, _function, _arity, _loaded?) do - "" + defp hint(module, function, arity, _loaded?) do + downcased_module = downcase_module_name(module) + + candidate = + Enum.find(:code.all_available(), fn {name, _, _} -> + downcase_module_name(name) == downcased_module + end) + + with {_, _, _} <- candidate, + {:module, module} <- load_module(candidate), + true <- function_exported?(module, function, arity) do + ". Did you mean:\n\n * #{Exception.format_mfa(module, function, arity)}\n" + else + _ -> "" + end + end + + defp load_module({name, _path, _loaded?}) do + name + |> List.to_atom() + |> Code.ensure_loaded() + end + + defp downcase_module_name(module) do + module + |> to_string() + |> String.downcase(:ascii) end @doc false diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index 030b68cce02..364ba95f249 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -595,6 +595,17 @@ defmodule ExceptionTest do assert message =~ "* set_cookie/2" end + test "annotates undefined function error with module suggestions" do + assert blame_message(ENUM, & &1.map(&1, 1)) == """ + function ENUM.map/2 is undefined (module ENUM is not available). Did you mean: + + * Enum.map/2 + """ + + assert blame_message(ENUM, & &1.not_a_function(&1, 1)) == + "function ENUM.not_a_function/2 is undefined (module ENUM is not available)" + end + test "annotates undefined function clause error with macro hints" do assert blame_message(Integer, & &1.is_odd(1)) == "function Integer.is_odd/1 is undefined or private. However, there is " <>