Skip to content

Commit 5bad452

Browse files
committed
Show undefined function errors even on variable failure, closes #13465
1 parent ec8156a commit 5bad452

3 files changed

Lines changed: 84 additions & 9 deletions

File tree

lib/elixir/src/elixir.hrl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
-define(tracker, 'Elixir.Kernel.LexicalTracker').
1212

1313
-record(elixir_ex, {
14+
%% Stores that the function will fail, which can be used
15+
%% to run more expensive checks upfront
16+
tainted_function=false,
1417
%% Stores if __CALLER__ is allowed
1518
caller=false,
1619
%% Stores the variables available before a match.

lib/elixir/src/elixir_expand.erl

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,20 @@ expand({'^', Meta, [Arg]}, #elixir_ex{prematch={Prematch, _, _}, vars={_, Write}
379379

380380
_ ->
381381
function_error(Meta, E, ?MODULE, {invalid_arg_for_pin, Arg}),
382-
{{'^', Meta, [Arg]}, S, E}
382+
{{'^', Meta, [Arg]}, S#elixir_ex{tainted_function=true}, E}
383383
end;
384384
expand({'^', Meta, [Arg]}, S, E) ->
385385
function_error(Meta, E, ?MODULE, {pin_outside_of_match, Arg}),
386-
{{'^', Meta, [Arg]}, S, E};
386+
{{'^', Meta, [Arg]}, S#elixir_ex{tainted_function=true}, E};
387387

388388
expand({'_', Meta, Kind} = Var, S, #{context := Context} = E) when is_atom(Kind) ->
389-
(Context /= match) andalso function_error(Meta, E, ?MODULE, unbound_underscore),
390-
{Var, S, E};
389+
case Context of
390+
match ->
391+
{Var, S, E};
392+
_ ->
393+
function_error(Meta, E, ?MODULE, unbound_underscore),
394+
{Var, S#elixir_ex{tainted_function=true}, E}
395+
end;
391396

392397
expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_atom(Kind) ->
393398
#elixir_ex{
@@ -476,7 +481,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
476481
%% TODO: Remove this clause on v2.0 as we will raise by default
477482
{if_undefined, raise} ->
478483
function_error(Meta, E, ?MODULE, {undefined_var, Name, Kind}),
479-
{{Name, Meta, Kind}, S, E};
484+
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
480485

481486
%% TODO: Remove this clause on v2.0 as we will no longer support warn
482487
_ when Error == warn ->
@@ -485,12 +490,12 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
485490

486491
_ when Error == pin ->
487492
function_error(Meta, E, ?MODULE, {undefined_var_pin, Name, Kind}),
488-
{{Name, Meta, Kind}, S, E};
493+
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
489494

490495
_ when Error == raise ->
491496
SpanMeta = elixir_env:calculate_span(Meta, Name),
492497
function_error(SpanMeta, E, ?MODULE, {undefined_var, Name, Kind}),
493-
{{Name, SpanMeta, Kind}, S, E}
498+
{{Name, SpanMeta, Kind}, S#elixir_ex{tainted_function=true}, E}
494499
end
495500
end;
496501

@@ -950,6 +955,11 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context}
950955
Context =:= nil ->
951956
AttachedMeta = attach_runtime_module(Receiver, Meta, S, E),
952957
{EArgs, {SA, _}, EA} = mapfold(fun expand_arg/3, {SL, S}, E, Args),
958+
959+
SA#elixir_ex.tainted_function andalso is_atom(Receiver) andalso
960+
(not is_loaded_and_exported(Receiver, Right, Args)) andalso
961+
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_function, Receiver, Right, length(Args)}),
962+
953963
Rewritten = elixir_rewrite:rewrite(Receiver, DotMeta, Right, AttachedMeta, EArgs),
954964
{Rewritten, elixir_env:close_write(SA, S), EA};
955965

@@ -970,6 +980,10 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) ->
970980
Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args},
971981
file_error(Meta, E, ?MODULE, {invalid_call, Call}).
972982

983+
is_loaded_and_exported(Receiver, Fun, Args) ->
984+
(code:ensure_loaded(Receiver) =:= {module, Receiver}) andalso
985+
erlang:function_exported(Receiver, Fun, length(Args)).
986+
973987
attach_runtime_module(Receiver, Meta, S, _E) ->
974988
case lists:member(Receiver, S#elixir_ex.runtime_modules) of
975989
true -> [{runtime_module, true} | Meta];
@@ -1127,6 +1141,11 @@ assert_no_underscore_clause_in_cond(_Other, _E) ->
11271141

11281142
%% Errors
11291143

1144+
format_error({undefined_function, Module, Fun, Arity}) ->
1145+
Opts = [{module, Module}, {function, Fun}, {arity, Arity}],
1146+
Exception = 'Elixir.UndefinedFunctionError':exception(Opts),
1147+
{BlamedException, _} = 'Elixir.UndefinedFunctionError':blame(Exception, []),
1148+
'Elixir.UndefinedFunctionError':message(BlamedException);
11301149
format_error(invalid_match_on_zero_float) ->
11311150
"pattern matching on 0.0 is equivalent to matching only on +0.0. Instead you must match on +0.0 or -0.0";
11321151
format_error({useless_literal, Term}) ->

lib/elixir/test/elixir/kernel/errors_test.exs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Code.require_file("../test_helper.exs", __DIR__)
77
defmodule Kernel.ErrorsTest do
88
use ExUnit.Case, async: true
99

10-
defmacro hello do
10+
defmacro hello(_) do
1111
quote location: :keep do
1212
def hello, do: :world
1313
end
@@ -159,6 +159,59 @@ defmodule Kernel.ErrorsTest do
159159
)
160160
end
161161

162+
test "cascading from undefined variables" do
163+
# Test that we show undefined modules/functions/macros on variable failure,
164+
# as sometimes the variable failure come from a missing module or require
165+
assert_compile_error(
166+
[
167+
"nofile:3:23",
168+
"undefined variable \"bar\"",
169+
"nofile:3:19",
170+
"function UnknownModule.foo/1 is undefined (module UnknownModule is not available)"
171+
],
172+
~c"""
173+
defmodule Sample do
174+
def foo do
175+
UnknownModule.foo(bar)
176+
end
177+
end
178+
"""
179+
)
180+
181+
assert_compile_error(
182+
[
183+
"nofile:3:20",
184+
"undefined variable \"bar\"",
185+
"nofile:3:16",
186+
"function Enumerable.foo/1 is undefined or private"
187+
],
188+
~c"""
189+
defmodule Sample do
190+
def foo do
191+
Enumerable.foo(bar)
192+
end
193+
end
194+
"""
195+
)
196+
197+
assert_compile_error(
198+
[
199+
"nofile:3:29",
200+
"undefined variable \"bar\"",
201+
"nofile:3:23",
202+
"function Kernel.ErrorsTest.hello/1 is undefined or private",
203+
"there is a macro with the same name and arity"
204+
],
205+
~c"""
206+
defmodule Sample do
207+
def foo do
208+
Kernel.ErrorsTest.hello(bar)
209+
end
210+
end
211+
"""
212+
)
213+
end
214+
162215
test "recursive variables on definition" do
163216
assert_compile_error(
164217
[
@@ -677,7 +730,7 @@ defmodule Kernel.ErrorsTest do
677730
defmodule Kernel.ErrorsTest.DefDefmacroClauseChange do
678731
require Kernel.ErrorsTest
679732
defp hello, do: :world
680-
Kernel.ErrorsTest.hello()
733+
Kernel.ErrorsTest.hello(:ok)
681734
end
682735
""")
683736
end

0 commit comments

Comments
 (0)