Skip to content

Commit e0d04e1

Browse files
committed
Warn on nullary remote calls, related to #9510
1 parent 0c2b763 commit e0d04e1

File tree

5 files changed

+31
-18
lines changed

5 files changed

+31
-18
lines changed

lib/elixir/pages/references/syntax-reference.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ end
433433
434434
### Optional parentheses
435435
436-
Elixir provides optional parentheses:
436+
Elixir provides optional parentheses on local and remote calls with one or more arguments:
437437
438438
```elixir
439439
quote do

lib/elixir/src/elixir_expand.erl

+14-7
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,16 @@ expand({Atom, Meta, Args}, S, E) when is_atom(Atom), is_list(Meta), is_list(Args
396396

397397
%% Remote calls
398398

399-
expand({{'.', DotMeta, [Left, Right]}, Meta, Args}, S, E)
399+
expand({{'.', DotMeta, [Left, Right]}, Meta, Args} = Expr, S, E)
400400
when (is_tuple(Left) orelse is_atom(Left)), is_atom(Right), is_list(Meta), is_list(Args) ->
401401
{ELeft, SL, EL} = expand(Left, elixir_env:prepare_write(S), E),
402+
NoParens = lists:keyfind(no_parens, 1, Meta),
403+
404+
(is_atom(ELeft) and (Args =:= []) and (NoParens =:= {no_parens, true})) andalso
405+
elixir_errors:file_warn(Meta, E, ?MODULE, {remote_nullary_no_parens, Expr}),
402406

403407
elixir_dispatch:dispatch_require(Meta, ELeft, Right, Args, S, EL, fun(AR, AF, AA) ->
404-
expand_remote(AR, DotMeta, AF, Meta, AA, S, SL, EL)
408+
expand_remote(AR, DotMeta, AF, Meta, NoParens, AA, S, SL, EL)
405409
end);
406410

407411
%% Anonymous calls
@@ -844,17 +848,17 @@ expand_local(Meta, Name, Args, _S, #{function := nil} = E) ->
844848

845849
%% Remote
846850

847-
expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context} = E) when is_atom(Receiver) or is_tuple(Receiver) ->
851+
expand_remote(Receiver, DotMeta, Right, Meta, NoParens, Args, S, SL, #{context := Context} = E) when is_atom(Receiver) or is_tuple(Receiver) ->
848852
assert_no_clauses(Right, Meta, Args, E),
849853

850-
case {Context, lists:keyfind(no_parens, 1, Meta)} of
851-
{guard, NoParens} when is_tuple(Receiver) ->
854+
if
855+
Context =:= guard, is_tuple(Receiver) ->
852856
(NoParens /= {no_parens, true}) andalso
853857
function_error(Meta, E, ?MODULE, {parens_map_lookup, Receiver, Right, guard_context(S)}),
854858

855859
{{{'.', DotMeta, [Receiver, Right]}, Meta, []}, SL, E};
856860

857-
_ ->
861+
true ->
858862
AttachedDotMeta = attach_context_module(Receiver, DotMeta, E),
859863

860864
is_atom(Receiver) andalso
@@ -871,7 +875,7 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context}
871875
file_error(Meta, E, elixir_rewrite, Error)
872876
end
873877
end;
874-
expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) ->
878+
expand_remote(Receiver, DotMeta, Right, Meta, _NoParens, Args, _, _, E) ->
875879
Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args},
876880
file_error(Meta, E, ?MODULE, {invalid_call, Call}).
877881

@@ -1159,6 +1163,9 @@ assert_no_underscore_clause_in_cond(_Other, _E) ->
11591163
guard_context(#elixir_ex{prematch={_, _, {bitsize, _}}}) -> "bitstring size specifier";
11601164
guard_context(_) -> "guards".
11611165

1166+
format_error({remote_nullary_no_parens, Expr}) ->
1167+
String = 'Elixir.String':replace_suffix('Elixir.Macro':to_string(Expr), <<"()">>, <<>>),
1168+
io_lib:format("parentheses are required for function calls with no arguments, got: ~ts", [String]);
11621169
format_error({useless_literal, Term}) ->
11631170
io_lib:format("code block contains unused literal ~ts "
11641171
"(remove the literal or assign it to _ to avoid warnings)",

lib/elixir/test/elixir/kernel/cli_test.exs

+4-2
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@ defmodule Kernel.CLITest do
6565
{output, 0} = System.cmd(elixir_executable(), ["--eval", "IO.puts :hello_world123"])
6666
assert output =~ "hello_world123"
6767

68-
{output, 0} = System.cmd(iex_executable(), ["--eval", "IO.puts :hello_world123; System.halt"])
68+
{output, 0} =
69+
System.cmd(iex_executable(), ["--eval", "IO.puts :hello_world123; System.halt()"])
70+
6971
assert output =~ "hello_world123"
7072

7173
{output, 0} = System.cmd(elixir_executable(), ["-e", "IO.puts :hello_world123"])
7274
assert output =~ "hello_world123"
7375

74-
{output, 0} = System.cmd(iex_executable(), ["-e", "IO.puts :hello_world123; System.halt"])
76+
{output, 0} = System.cmd(iex_executable(), ["-e", "IO.puts :hello_world123; System.halt()"])
7577
assert output =~ "hello_world123"
7678
end
7779

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

+12
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,18 @@ defmodule Kernel.WarningTest do
11751175
purge(Sample)
11761176
end
11771177

1178+
test "parens on nullary remote call" do
1179+
assert_warn_eval(
1180+
[
1181+
"nofile:1:8",
1182+
"parentheses are required for function calls with no arguments, got: System.version"
1183+
],
1184+
"System.version"
1185+
)
1186+
after
1187+
purge(Sample)
1188+
end
1189+
11781190
test "parens with module attribute" do
11791191
assert_warn_eval(
11801192
[

lib/elixir/test/elixir/module/types/expr_test.exs

-8
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,6 @@ defmodule Module.Types.ExprTest do
5252
assert quoted_expr({:a, 123}) == {:ok, {:tuple, 2, [{:atom, :a}, :integer]}}
5353
end
5454

55-
# Use module attribute to avoid formatter adding parentheses
56-
@mix_module Mix
57-
58-
test "module call" do
59-
assert quoted_expr(@mix_module.shell) == {:ok, :dynamic}
60-
assert quoted_expr(@mix_module.shell.info) == {:ok, {:var, 0}}
61-
end
62-
6355
describe "binary" do
6456
test "literal" do
6557
assert quoted_expr(<<"foo"::binary>>) == {:ok, :binary}

0 commit comments

Comments
 (0)