Skip to content

Commit f244ee3

Browse files
dnsbtymichalmuskala
authored andcommitted
add hint for behaviours on UndefinedFunctionErrors (#8517)
This is the first pass on #8513. I would love any feedback you have on this.
1 parent bd37919 commit f244ee3

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

lib/elixir/lib/exception.ex

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,14 +924,38 @@ defmodule UndefinedFunctionError do
924924

925925
defp message(:"function not exported", module, function, arity) do
926926
formatted_fun = Exception.format_mfa(module, function, arity)
927-
{"function #{formatted_fun} is undefined or private", true}
927+
fun_message = "function #{formatted_fun} is undefined or private"
928+
behaviour_hint = behaviour_hint(module, function, arity)
929+
{fun_message <> behaviour_hint, true}
928930
end
929931

930932
defp message(reason, module, function, arity) do
931933
formatted_fun = Exception.format_mfa(module, function, arity)
932934
{"function #{formatted_fun} is undefined (#{reason})", false}
933935
end
934936

937+
defp behaviour_hint(module, function, arity) do
938+
case behaviours_for(module) do
939+
[] ->
940+
""
941+
942+
behaviours ->
943+
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
944+
nil -> ""
945+
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
946+
end
947+
end
948+
rescue
949+
# In case the module was removed while we are computing this
950+
UndefinedFunctionError ->
951+
[]
952+
end
953+
954+
defp expects_callback?(behaviour, function, arity) do
955+
callbacks = behaviour.behaviour_info(:callbacks)
956+
Enum.member?(callbacks, {function, arity})
957+
end
958+
935959
@impl true
936960
def blame(exception, stacktrace) do
937961
%{reason: reason, module: module, function: function, arity: arity} = exception
@@ -997,6 +1021,12 @@ defmodule UndefinedFunctionError do
9971021
[" * ", Code.Identifier.inspect_as_function(fun), ?/, Integer.to_string(arity), ?\n]
9981022
end
9991023

1024+
defp behaviours_for(module) do
1025+
:attributes
1026+
|> module.module_info()
1027+
|> Keyword.get(:behaviour, [])
1028+
end
1029+
10001030
defp exports_for(module) do
10011031
if function_exported?(module, :__info__, 1) do
10021032
module.__info__(:macros) ++ module.__info__(:functions)

lib/elixir/test/elixir/exception_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,22 @@ defmodule ExceptionTest do
729729
|> message == "function nil.bar/0 is undefined"
730730
end
731731

732+
test "UndefinedFunctionError for a callback" do
733+
defmodule Behaviour do
734+
@callback callback() :: :ok
735+
@optional_callbacks callback: 0
736+
end
737+
738+
defmodule Implementation do
739+
@behaviour Behaviour
740+
end
741+
742+
assert %UndefinedFunctionError{module: Implementation, function: :callback, arity: 0}
743+
|> message ==
744+
"function ExceptionTest.Implementation.callback/0 is undefined or private" <>
745+
", but the behaviour ExceptionTest.Behaviour expects it to be present"
746+
end
747+
732748
test "FunctionClauseError" do
733749
assert %FunctionClauseError{} |> message == "no function clause matches"
734750

lib/elixir/test/elixir/record_test.exs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ defmodule RecordTest do
66
require Record
77
doctest Record
88

9-
import ExUnit.CaptureIO
10-
119
test "extract/2 extracts information from an Erlang file" do
1210
assert Record.extract(:file_info, from_lib: "kernel/include/file.hrl") == [
1311
size: :undefined,

0 commit comments

Comments
 (0)