Skip to content

Commit 64f69cf

Browse files
committed
Improve error message when escaping default values with custom rules in structs, closes #14817
1 parent 7844c98 commit 64f69cf

File tree

7 files changed

+56
-39
lines changed

7 files changed

+56
-39
lines changed

lib/elixir/lib/kernel/utils.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
126126
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
127127

128128
try do
129-
:elixir_quote.escape(val, :escape, false)
129+
:elixir_quote.escape(val, {:struct, module}, false)
130130
rescue
131131
e in [ArgumentError] ->
132-
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
132+
raise ArgumentError,
133+
"invalid default value for struct field #{key}, " <> Exception.message(e)
133134
else
134135
_ -> {key, val}
135136
end
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
171172

172173
:lists.foreach(foreach, enforce_keys)
173174
struct = :maps.from_list([__struct__: module] ++ fields)
174-
escaped_struct = :elixir_quote.escape(struct, :escape, false)
175+
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
175176

176177
body =
177178
case bootstrapped? do

lib/elixir/lib/regex.ex

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,9 +1017,6 @@ defmodule Regex do
10171017

10181018
pattern_ast =
10191019
cond do
1020-
is_nil(regex.re_pattern) ->
1021-
nil
1022-
10231020
# TODO: Remove this when we require Erlang/OTP 28+
10241021
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
10251022
:erlang.system_info(:otp_release) < [?2, ?8] ->

lib/elixir/src/elixir_bootstrap.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
'MACRO-defmacrop'(Caller, Call, Expr) -> define(Caller, defmacrop, Call, Expr).
2424

2525
'MACRO-defmodule'({Line, _S, _E} = _Caller, Alias, [{do, Block}]) ->
26-
Escaped = elixir_quote:escape(Block, none, false),
26+
Escaped = elixir_quote:escape(Block, escape, false),
2727
Args = [[{line, Line}], Alias, Escaped, [], false, env()],
2828
{{'.', [], [elixir_module, compile]}, [], Args}.
2929

@@ -46,7 +46,7 @@ define({Line, _S, #{module := Module} = E}, Kind, Call, Expr) ->
4646
Store =
4747
case UC or UE of
4848
true ->
49-
elixir_quote:escape({Call, Expr}, none, true);
49+
elixir_quote:escape({Call, Expr}, escape, true);
5050

5151
false ->
5252
Key = erlang:unique_integer(),

lib/elixir/src/elixir_map.erl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} =
3737
Struct = load_struct(Meta, ELeft, Assocs, EE),
3838
Keys = ['__struct__'] ++ AssocKeys,
3939
WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))),
40-
StructAssocs = elixir_quote:escape(WithoutKeys, none, false),
41-
{EStructAssocs, SA, EA} = elixir_expand:expand(StructAssocs, SE, EE),
42-
{{'%', Meta, [ELeft, {'%{}', MapMeta, EStructAssocs ++ Assocs}]}, SA, EA};
40+
StructAssocs = elixir_quote:escape(WithoutKeys, escape, false),
41+
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE};
4342

4443
{'%{}', MapMeta, Assocs} ->
4544
_ = load_struct_info(Meta, ELeft, Assocs, EE),

lib/elixir/src/elixir_quote.erl

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
line=false,
1919
file=nil,
2020
context=nil,
21-
op=escape, % escape | escape_and_prune | quote
21+
op=escape, % escape | escape_and_prune | {struct, Module} | quote
2222
aliases_hygiene=nil,
2323
imports_hygiene=nil,
2424
unquote=true,
@@ -140,15 +140,24 @@ do_tuple_linify(Fun, Meta, Left, Right, Var) ->
140140
%% Escapes the given expression. It is similar to quote, but
141141
%% lines are kept and hygiene mechanisms are disabled.
142142
escape(Expr, Op, Unquote) ->
143-
Q = #elixir_quote{
144-
line=true,
145-
file=nil,
146-
op=Op,
147-
unquote=Unquote
148-
},
149-
case Unquote of
150-
true -> do_quote(Expr, Q);
151-
false -> do_escape(Expr, Q)
143+
try
144+
Q = #elixir_quote{
145+
line=true,
146+
file=nil,
147+
op=Op,
148+
unquote=Unquote
149+
},
150+
case Unquote of
151+
true -> do_quote(Expr, Q);
152+
false -> do_escape(Expr, Q)
153+
end
154+
catch
155+
Kind:Reason:Stacktrace ->
156+
Pruned = lists:dropwhile(fun
157+
({?MODULE, _, _, _}) -> true;
158+
(_) -> false
159+
end, Stacktrace),
160+
erlang:raise(Kind, Reason, Pruned)
152161
end.
153162

154163
do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) ->
@@ -176,13 +185,24 @@ do_escape(Map, Q) when is_map(Map) ->
176185
maybe
177186
#{'__struct__' := Module} ?= Map,
178187
true ?= is_atom(Module),
188+
% We never escape ourselves (it can only happen during Elixir bootstrapping)
189+
true ?= (Q#elixir_quote.op /= {struct, Module}),
179190
{module, Module} ?= code:ensure_loaded(Module),
180191
true ?= erlang:function_exported(Module, '__escape__', 1),
181-
Expr = Module:'__escape__'(Map),
182-
case shallow_valid_ast(Expr) of
183-
true -> Expr;
184-
false -> argument_error(
185-
<<('Elixir.Kernel':inspect(Module))/binary, ".__escape__/1 returned invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary>>)
192+
case Q#elixir_quote.op of
193+
{struct, _Module} ->
194+
argument_error(<<('Elixir.Kernel':inspect(Module))/binary,
195+
" defines custom escaping rules which are not supported in struct defaults",
196+
(bad_escape_hint())/binary>>);
197+
198+
_ ->
199+
Expr = Module:'__escape__'(Map),
200+
case shallow_valid_ast(Expr) of
201+
true -> Expr;
202+
false -> argument_error(
203+
<<('Elixir.Kernel':inspect(Module))/binary, ".__escape__/1 returned invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary>>
204+
)
205+
end
186206
end
187207
else
188208
_ ->
@@ -234,8 +254,8 @@ escape_map_key_value(K, V, Map, Q) ->
234254
if
235255
is_reference(MaybeRef) ->
236256
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
237-
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
238-
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
257+
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped",
258+
(bad_escape_hint())/binary>>);
239259
true ->
240260
{do_escape(K, Q), do_escape(V, Q)}
241261
end.
@@ -248,11 +268,11 @@ find_tuple_ref(Tuple, Index) ->
248268
end.
249269

250270
bad_escape(Arg) ->
251-
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary, ". ",
271+
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary,
252272
(bad_escape_hint())/binary>>).
253273

254274
bad_escape_hint() ->
255-
<<"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
275+
<<". The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
256276
"PIDs and remote functions in the format &Mod.fun/arity">>.
257277

258278
%% Quote entry points

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ defmodule Kernel.ErrorsTest do
435435
end
436436

437437
test "invalid struct field value" do
438-
msg = ~r"invalid value for struct field baz, cannot escape "
438+
msg = ~r"invalid default value for struct field baz, cannot escape "
439439

440440
assert_raise ArgumentError, msg, fn ->
441441
defmodule InvalidStructFieldValue do

lib/elixir/test/elixir/macro_test.exs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,14 @@ defmodule MacroTest do
177177
end
178178

179179
@tag :re_import
180-
test "escape works within structs fields" do
181-
defmodule Test do
182-
# Structs have their own escape logic, which now needs to be expanded
183-
defstruct my_regex: ~r/^hi$/
184-
def init, do: %__MODULE__{}
185-
end
186-
187-
assert %Regex{} = Test.init().my_regex
180+
test "escape raises within structs fields" do
181+
assert_raise ArgumentError,
182+
~r"Regex defines custom escaping rules which are not supported in struct defaults",
183+
fn ->
184+
defmodule Test do
185+
defstruct my_regex: ~r/^hi$/
186+
end
187+
end
188188
end
189189

190190
defmodule EscapedStruct do

0 commit comments

Comments
 (0)