Skip to content

Commit 5882a52

Browse files
authored
Deprecate complex module expressions in &mod.fun/arity captures (#14095)
1 parent 52e3aa7 commit 5882a52

File tree

3 files changed

+57
-31
lines changed

3 files changed

+57
-31
lines changed

lib/elixir/src/elixir_fn.erl

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ fn_arity(Args) -> length(Args).
4141
capture(Meta, {'/', _, [{{'.', _, [M, F]} = Dot, RequireMeta, []}, A]}, S, E) when is_atom(F), is_integer(A) ->
4242
Args = args_from_arity(Meta, A, E),
4343
handle_capture_possible_warning(Meta, RequireMeta, M, F, A, E),
44-
capture_require({Dot, RequireMeta, Args}, S, E, true);
44+
capture_require({Dot, RequireMeta, Args}, S, E, arity);
4545

4646
capture(Meta, {'/', _, [{F, ImportMeta, C}, A]}, S, E) when is_atom(F), is_integer(A), is_atom(C) ->
4747
Args = args_from_arity(Meta, A, E),
48-
capture_import({F, ImportMeta, Args}, S, E, true);
48+
capture_import({F, ImportMeta, Args}, S, E, arity);
4949

5050
capture(_Meta, {{'.', _, [_, Fun]}, _, Args} = Expr, S, E) when is_atom(Fun), is_list(Args) ->
51-
capture_require(Expr, S, E, is_sequential_and_not_empty(Args));
51+
capture_require(Expr, S, E, check_sequential_and_not_empty(Args));
5252

5353
capture(Meta, {{'.', _, [_]}, _, Args} = Expr, S, E) when is_list(Args) ->
54-
capture_expr(Meta, Expr, S, E, false);
54+
capture_expr(Meta, Expr, S, E, non_sequential);
5555

5656
capture(Meta, {'__block__', _, [Expr]}, S, E) ->
5757
capture(Meta, Expr, S, E);
@@ -60,31 +60,39 @@ capture(Meta, {'__block__', _, _} = Expr, _S, E) ->
6060
file_error(Meta, E, ?MODULE, {block_expr_in_capture, Expr});
6161

6262
capture(_Meta, {Atom, _, Args} = Expr, S, E) when is_atom(Atom), is_list(Args) ->
63-
capture_import(Expr, S, E, is_sequential_and_not_empty(Args));
63+
capture_import(Expr, S, E, check_sequential_and_not_empty(Args));
6464

6565
capture(Meta, {Left, Right}, S, E) ->
6666
capture(Meta, {'{}', Meta, [Left, Right]}, S, E);
6767

6868
capture(Meta, List, S, E) when is_list(List) ->
69-
capture_expr(Meta, List, S, E, is_sequential_and_not_empty(List));
69+
capture_expr(Meta, List, S, E, check_sequential_and_not_empty(List));
7070

7171
capture(Meta, Integer, _S, E) when is_integer(Integer) ->
7272
file_error(Meta, E, ?MODULE, {capture_arg_outside_of_capture, Integer});
7373

7474
capture(Meta, Arg, _S, E) ->
7575
invalid_capture(Meta, Arg, E).
7676

77-
capture_import({Atom, ImportMeta, Args} = Expr, S, E, Sequential) ->
78-
Res = Sequential andalso
77+
capture_import({Atom, ImportMeta, Args} = Expr, S, E, ArgsType) ->
78+
Res = ArgsType /= non_sequential andalso
7979
elixir_dispatch:import_function(ImportMeta, Atom, length(Args), E),
80-
handle_capture(Res, ImportMeta, ImportMeta, Expr, S, E, Sequential).
80+
handle_capture(Res, ImportMeta, ImportMeta, Expr, S, E, ArgsType).
8181

82-
capture_require({{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, Sequential) ->
82+
capture_require({{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, ArgsType) ->
8383
case escape(Left, E, []) of
8484
{EscLeft, []} ->
8585
{ELeft, SE, EE} = elixir_expand:expand(EscLeft, S, E),
8686

87-
Res = Sequential andalso case ELeft of
87+
case ELeft of
88+
_ when ArgsType /= arity -> ok;
89+
Atom when is_atom(Atom) -> ok;
90+
{Var, _, Ctx} when is_atom(Var), is_atom(Ctx) -> ok;
91+
%% TODO: Raise on Elixir v2.0
92+
_ -> elixir_errors:file_warn(RequireMeta, E, ?MODULE, {complex_module_capture, Left})
93+
end,
94+
95+
Res = ArgsType /= non_sequential andalso case ELeft of
8896
{Name, _, Context} when is_atom(Name), is_atom(Context) ->
8997
{remote, ELeft, Right, length(Args)};
9098
_ when is_atom(ELeft) ->
@@ -94,24 +102,25 @@ capture_require({{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, Sequen
94102
end,
95103

96104
Dot = {{'.', DotMeta, [ELeft, Right]}, RequireMeta, Args},
97-
handle_capture(Res, RequireMeta, DotMeta, Dot, SE, EE, Sequential);
105+
handle_capture(Res, RequireMeta, DotMeta, Dot, SE, EE, ArgsType);
98106

99107
{EscLeft, Escaped} ->
100108
Dot = {{'.', DotMeta, [EscLeft, Right]}, RequireMeta, Args},
101-
capture_expr(RequireMeta, Dot, S, E, Escaped, Sequential)
109+
capture_expr(RequireMeta, Dot, S, E, Escaped, ArgsType)
102110
end.
103111

104-
handle_capture(false, Meta, _DotMeta, Expr, S, E, Sequential) ->
105-
capture_expr(Meta, Expr, S, E, Sequential);
106-
handle_capture(LocalOrRemote, Meta, DotMeta, _Expr, S, E, _Sequential) ->
112+
handle_capture(false, Meta, _DotMeta, Expr, S, E, ArgsType) ->
113+
capture_expr(Meta, Expr, S, E, ArgsType);
114+
handle_capture(LocalOrRemote, Meta, DotMeta, _Expr, S, E, _ArgsType) ->
107115
{LocalOrRemote, Meta, DotMeta, S, E}.
108116

109-
capture_expr(Meta, Expr, S, E, Sequential) ->
110-
capture_expr(Meta, Expr, S, E, [], Sequential).
111-
capture_expr(Meta, Expr, S, E, Escaped, Sequential) ->
117+
capture_expr(Meta, Expr, S, E, ArgsType) ->
118+
capture_expr(Meta, Expr, S, E, [], ArgsType).
119+
capture_expr(Meta, Expr, S, E, Escaped, ArgsType) ->
112120
case escape(Expr, E, Escaped) of
113-
{_, []} when not Sequential ->
121+
{_, []} when ArgsType == non_sequential ->
114122
invalid_capture(Meta, Expr, E);
123+
%% TODO: Remove this clause once we raise on complex module captures like &get_mod().fun/0
115124
{{{'.', _, [_, _]} = Dot, _, Args}, []} ->
116125
Meta2 = lists:keydelete(no_parens, 1, Meta),
117126
Fn = {fn, Meta2, [{'->', Meta2, [[], {Dot, Meta2, Args}]}]},
@@ -166,12 +175,12 @@ args_from_arity(_Meta, A, _E) when is_integer(A), A >= 0, A =< 255 ->
166175
args_from_arity(Meta, A, E) ->
167176
file_error(Meta, E, ?MODULE, {invalid_arity_for_capture, A}).
168177

169-
is_sequential_and_not_empty([]) -> false;
170-
is_sequential_and_not_empty(List) -> is_sequential(List, 1).
178+
check_sequential_and_not_empty([]) -> non_sequential;
179+
check_sequential_and_not_empty(List) -> check_sequential(List, 1).
171180

172-
is_sequential([{'&', _, [Int]} | T], Int) -> is_sequential(T, Int + 1);
173-
is_sequential([], _Int) -> true;
174-
is_sequential(_, _Int) -> false.
181+
check_sequential([{'&', _, [Int]} | T], Int) -> check_sequential(T, Int + 1);
182+
check_sequential([], _Int) -> sequential;
183+
check_sequential(_, _Int) -> non_sequential.
175184

176185
handle_capture_possible_warning(Meta, DotMeta, Mod, Fun, Arity, E) ->
177186
case (Arity =:= 0) andalso (lists:keyfind(no_parens, 1, DotMeta) /= {no_parens, true}) of
@@ -186,6 +195,10 @@ format_error({parens_remote_capture, Mod, Fun}) ->
186195
io_lib:format("extra parentheses on a remote function capture &~ts.~ts()/0 have been "
187196
"deprecated. Please remove the parentheses: &~ts.~ts/0",
188197
['Elixir.Macro':to_string(Mod), Fun, 'Elixir.Macro':to_string(Mod), Fun]);
198+
format_error({complex_module_capture, Mod}) ->
199+
io_lib:format("expected the module in &module.fun/arity to expand to a variable or an atom, got: ~ts\n"
200+
"You can either compute the module name outside of & or convert it to a regular anonymous function.",
201+
['Elixir.Macro':to_string(Mod)]);
189202
format_error(clauses_with_different_arities) ->
190203
"cannot mix clauses with different arities in anonymous functions";
191204
format_error(defaults_in_args) ->

lib/elixir/test/elixir/kernel/fn_test.exs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,6 @@ defmodule Kernel.FnTest do
9595
assert (&mod.flatten/1) == (&List.flatten/1)
9696
end
9797

98-
test "capture with module from local call" do
99-
assert (&math_mod().pi/0).() == :math.pi()
100-
end
101-
102-
defp math_mod, do: :math
103-
10498
test "local partial application" do
10599
assert (&atb(&1, :utf8)).(:a) == "a"
106100
assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a"

lib/elixir/test/elixir/kernel/warning_test.exs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,6 +2194,25 @@ defmodule Kernel.WarningTest do
21942194
)
21952195
end
21962196

2197+
test "deprecate &module.fun/arity captures with complex expressions as modules" do
2198+
assert_warn_eval(
2199+
[
2200+
"nofile:2:",
2201+
"""
2202+
expected the module in &module.fun/arity to expand to a variable or an atom, got: hd(modules)
2203+
You can either compute the module name outside of & or convert it to a regular anonymous function.
2204+
"""
2205+
],
2206+
"""
2207+
defmodule Sample do
2208+
def foo(modules), do: &hd(modules).module_info/0
2209+
end
2210+
"""
2211+
)
2212+
after
2213+
purge(Sample)
2214+
end
2215+
21972216
defp assert_compile_error(messages, string) do
21982217
captured =
21992218
capture_err(fn ->

0 commit comments

Comments
 (0)