Skip to content

Commit 2ee1d0e

Browse files
authored
Bugfix: Macro.escape/1 properly escapes meta in :quote tuples (#14773)
Close #14771 Also internally renames the `op` field inside `elixir_quote`: `none -> escape`, `prune_metadata` -> `escape_and_prune`, `add_context -> quote`.
1 parent 2e7ee76 commit 2ee1d0e

File tree

5 files changed

+34
-21
lines changed

5 files changed

+34
-21
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5185,7 +5185,7 @@ defmodule Kernel do
51855185
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
51865186

51875187
%{} ->
5188-
:elixir_quote.escape(block, :none, false)
5188+
:elixir_quote.escape(block, :escape, false)
51895189
end
51905190

51915191
versioned_vars = env.versioned_vars
@@ -5465,7 +5465,7 @@ defmodule Kernel do
54655465
store =
54665466
case unquoted_expr or unquoted_call do
54675467
true ->
5468-
:elixir_quote.escape({call, expr}, :none, true)
5468+
:elixir_quote.escape({call, expr}, :escape, true)
54695469

54705470
false ->
54715471
key = :erlang.unique_integer()

lib/elixir/lib/kernel/utils.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ 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, :none, false)
129+
:elixir_quote.escape(val, :escape, false)
130130
rescue
131131
e in [ArgumentError] ->
132132
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
@@ -171,7 +171,7 @@ defmodule Kernel.Utils do
171171

172172
:lists.foreach(foreach, enforce_keys)
173173
struct = :maps.from_list([__struct__: module] ++ fields)
174-
escaped_struct = :elixir_quote.escape(struct, :none, false)
174+
escaped_struct = :elixir_quote.escape(struct, :escape, false)
175175

176176
body =
177177
case bootstrapped? do

lib/elixir/lib/macro.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,9 @@ defmodule Macro do
803803
* `:unquote` - when `true`, this function leaves `unquote/1` and
804804
`unquote_splicing/1` expressions unescaped, effectively unquoting
805805
the contents on escape. This option is useful only when escaping
806-
ASTs which may have quoted fragments in them. Defaults to `false`.
806+
ASTs which may have quoted fragments in them. Note this option
807+
will give a special meaning to `quote`/`unquote` nodes, which need
808+
to be valid AST before escaping. Defaults to `false`.
807809
808810
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
809811
nodes. Note this option changes the semantics of escaped code and
@@ -912,7 +914,7 @@ defmodule Macro do
912914
@spec escape(term, escape_opts) :: t()
913915
def escape(expr, opts \\ []) do
914916
unquote = Keyword.get(opts, :unquote, false)
915-
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
917+
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
916918
generated = Keyword.get(opts, :generated, false)
917919

918920
case :elixir_quote.escape(expr, kind, unquote) do

lib/elixir/src/elixir_quote.erl

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
line=false,
1919
file=nil,
2020
context=nil,
21-
op=none, % none | prune_metadata | add_context
21+
op=escape, % escape | escape_and_prune | quote
2222
aliases_hygiene=nil,
2323
imports_hygiene=nil,
2424
unquote=true,
@@ -145,21 +145,27 @@ do_tuple_linify(Fun, Meta, Left, Right, Var, Gen) ->
145145
%% Escapes the given expression. It is similar to quote, but
146146
%% lines are kept and hygiene mechanisms are disabled.
147147
escape(Expr, Op, Unquote) ->
148-
do_quote(Expr, #elixir_quote{
148+
Q = #elixir_quote{
149149
line=true,
150150
file=nil,
151151
op=Op,
152152
unquote=Unquote
153-
}).
153+
},
154+
case Unquote of
155+
true -> do_quote(Expr, Q);
156+
false -> do_escape(Expr, Q)
157+
end.
154158

155-
do_escape({Left, Meta, Right}, #elixir_quote{op=prune_metadata} = Q) when is_list(Meta) ->
159+
do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) ->
156160
TM = [{K, V} || {K, V} <- Meta, (K == no_parens) orelse (K == line) orelse (K == delimiter)],
157-
TL = do_quote(Left, Q),
158-
TR = do_quote(Right, Q),
161+
TL = do_escape(Left, Q),
162+
TR = do_escape(Right, Q),
159163
{'{}', [], [TL, TM, TR]};
160164

165+
do_escape({Left, Right}, Q) ->
166+
{do_escape(Left, Q), do_escape(Right, Q)};
161167
do_escape(Tuple, Q) when is_tuple(Tuple) ->
162-
TT = do_quote(tuple_to_list(Tuple), Q),
168+
TT = do_escape(tuple_to_list(Tuple), Q),
163169
{'{}', [], TT};
164170

165171
do_escape(BitString, _) when is_bitstring(BitString) ->
@@ -193,7 +199,7 @@ do_escape([], _) ->
193199
[];
194200

195201
do_escape([H | T], #elixir_quote{unquote=false} = Q) ->
196-
do_quote_simple_list(T, do_quote(H, Q), Q);
202+
do_quote_simple_list(T, do_escape(H, Q), Q);
197203

198204
do_escape([H | T], Q) ->
199205
%% The improper case is inefficient, but improper lists are rare.
@@ -203,7 +209,7 @@ do_escape([H | T], Q) ->
203209
_:_ ->
204210
{L, R} = reverse_improper(T, [H]),
205211
TL = do_quote_splice(L, Q, [], []),
206-
TR = do_quote(R, Q),
212+
TR = do_escape(R, Q),
207213
update_last(TL, fun(X) -> {'|', [], [X, TR]} end)
208214
end;
209215

@@ -232,7 +238,7 @@ escape_map_key_value(K, V, Map, Q) ->
232238
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
233239
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
234240
true ->
235-
{do_quote(K, Q), do_quote(V, Q)}
241+
{do_escape(K, Q), do_escape(V, Q)}
236242
end.
237243

238244
find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil;
@@ -261,7 +267,7 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
261267
validate_runtime(generated, Generated),
262268

263269
Q = #elixir_quote{
264-
op=add_context,
270+
op=quote,
265271
aliases_hygiene=E,
266272
imports_hygiene=E,
267273
line=VLine,
@@ -341,7 +347,7 @@ do_quote({quote, Meta, [Arg]}, Q) when is_list(Meta) ->
341347
TArg = do_quote(Arg, Q#elixir_quote{unquote=false}),
342348

343349
NewMeta = case Q of
344-
#elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context);
350+
#elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context);
345351
_ -> Meta
346352
end,
347353

@@ -352,7 +358,7 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
352358
TArg = do_quote(Arg, Q#elixir_quote{unquote=false}),
353359

354360
NewMeta = case Q of
355-
#elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context);
361+
#elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context);
356362
_ -> Meta
357363
end,
358364

@@ -379,7 +385,7 @@ do_quote({'__aliases__', Meta, [H | T]}, #elixir_quote{aliases_hygiene=(#{}=E)}
379385

380386
%% Vars
381387

382-
do_quote({Name, Meta, nil}, #elixir_quote{op=add_context} = Q)
388+
do_quote({Name, Meta, nil}, #elixir_quote{op=quote} = Q)
383389
when is_atom(Name), is_list(Meta) ->
384390
ImportMeta = case Q#elixir_quote.imports_hygiene of
385391
nil -> Meta;
@@ -435,7 +441,7 @@ do_quote({Left, Right}, Q) ->
435441

436442
%% Everything else
437443

438-
do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= add_context ->
444+
do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= quote ->
439445
do_escape(Other, Q);
440446

441447
do_quote({_, _, _} = Tuple, Q) ->

lib/elixir/test/elixir/macro_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ defmodule MacroTest do
146146
assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]}
147147
end
148148

149+
test "escapes the content of :quote tuples" do
150+
assert Macro.escape({:quote, [%{}], [{}]}) ==
151+
{:{}, [], [:quote, [{:%{}, [], []}], [{:{}, [], []}]]}
152+
end
153+
149154
test "escape container when a reference cannot be escaped" do
150155
assert_raise ArgumentError, ~r"contains a reference", fn ->
151156
Macro.escape(%{re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})

0 commit comments

Comments
 (0)