Skip to content

Commit 444eab6

Browse files
committed
Improve UndefinedFunctionError for mispelled module
This is a proof of concept to see if we can improve error messages when a module name is mispelled or otherwise incorrect. This can happen when a user misspells a module, or does not fully qualify it. Ideally, the elixir compiler can provide suggestions for what they meant. If we were to merge a version of this PR, we would get error messages like ```elixir iex(1)> Enu.map([1, 2, 3], & &1 + 1) ** (UndefinedFunctionError) function Enu.map/2 is undefined (module Enu is not available). Did you mean: * Enum.map/2 Enu.map([1, 2, 3], #Function<42.125776118/1 in :erl_eval.expr/6>) iex:1: (file) ``` ```elixir iex(1)> defmodule Namespace.SomeModule do ...(1)> def foo, do: :bar ...(1)> end {:module, Namespace.SomeModule, <<70, 79, 82, 49, 0, 0, 5, 36, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 186, 0, 0, 0, 18, 27, 69, 108, 105, 120, 105, 114, 46, 78, 97, 109, 101, 115, 112, 97, 99, 101, 46, 83, 111, 109, 101, 77, ...>>, {:foo, 0}} iex(2)> SomeModule.foo() ** (UndefinedFunctionError) function SomeModule.foo/0 is undefined (module SomeModule is not available). Did you mean: * Namespace.SomeModule.foo/0 SomeModule.foo() iex:2: (file) ``` I saw two experienced developers who are new two Elixir get stuck on this issue on the same day, not including a mention on Twitter! [Twitter Conversation](https://twitter.com/davydog187/status/1687628454240428034?s=20) - [ ] Better heuristic that is accurate for most usecases - [ ] Consider other edge cases and measure results (forgetten alias?) - [ ] Possibly turn off outside of dev? - [ ] Remove `to_atom/1` - [ ] Write more tests * https://groups.google.com/g/elixir-lang-core/c/-IHneszMheI/m/cCCxHB9PBwAJ * #5665
1 parent 146fb4e commit 444eab6

File tree

2 files changed

+23
-2
lines changed

2 files changed

+23
-2
lines changed

lib/elixir/lib/exception.ex

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,8 +1342,21 @@ defmodule UndefinedFunctionError do
13421342
hint_for_loaded_module(module, function, arity, nil)
13431343
end
13441344

1345-
defp hint(_module, _function, _arity, _loaded?) do
1346-
""
1345+
defp hint(module, function, arity, _loaded?) do
1346+
{module, _distance} =
1347+
Enum.map(:code.all_available(), fn {name, _, _} ->
1348+
{name, String.jaro_distance(to_string(module), to_string(name))}
1349+
end)
1350+
|> Enum.sort_by(&elem(&1, 1), :desc)
1351+
|> Enum.take(3)
1352+
|> Enum.find(fn {module, _distance} ->
1353+
module
1354+
|> to_string()
1355+
|> String.to_atom()
1356+
|> function_exported?(function, arity)
1357+
end)
1358+
1359+
". Did you mean:\n\n * #{Exception.format_mfa(String.to_existing_atom(to_string(module)), function, arity)}\n"
13471360
end
13481361

13491362
@doc false

lib/elixir/test/elixir/exception_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,14 @@ defmodule ExceptionTest do
595595
assert message =~ "* set_cookie/2"
596596
end
597597

598+
test "annotates undefined function error with module suggestions" do
599+
assert blame_message(Enu, & &1.map(&1, 1)) == """
600+
function Enu.map/2 is undefined (module Enu is not available). Did you mean:
601+
602+
* Enum.map/2
603+
"""
604+
end
605+
598606
test "annotates undefined function clause error with macro hints" do
599607
assert blame_message(Integer, & &1.is_odd(1)) ==
600608
"function Integer.is_odd/1 is undefined or private. However, there is " <>

0 commit comments

Comments
 (0)