Skip to content

Commit 7be008c

Browse files
committed
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 499a9c5 commit 7be008c

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
@@ -5186,7 +5186,7 @@ defmodule Kernel do
51865186
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
51875187

51885188
%{} ->
5189-
:elixir_quote.escape(block, :none, false)
5189+
:elixir_quote.escape(block, :escape, false)
51905190
end
51915191

51925192
versioned_vars = env.versioned_vars
@@ -5466,7 +5466,7 @@ defmodule Kernel do
54665466
store =
54675467
case unquoted_expr or unquoted_call do
54685468
true ->
5469-
:elixir_quote.escape({call, expr}, :none, true)
5469+
:elixir_quote.escape({call, expr}, :escape, true)
54705470

54715471
false ->
54725472
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,
@@ -140,21 +140,27 @@ 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-
do_quote(Expr, #elixir_quote{
143+
Q = #elixir_quote{
144144
line=true,
145145
file=nil,
146146
op=Op,
147147
unquote=Unquote
148-
}).
148+
},
149+
case Unquote of
150+
true -> do_quote(Expr, Q);
151+
false -> do_escape(Expr, Q)
152+
end.
149153

150-
do_escape({Left, Meta, Right}, #elixir_quote{op=prune_metadata} = Q) when is_list(Meta) ->
154+
do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) ->
151155
TM = [{K, V} || {K, V} <- Meta, (K == no_parens) orelse (K == line) orelse (K == delimiter)],
152-
TL = do_quote(Left, Q),
153-
TR = do_quote(Right, Q),
156+
TL = do_escape(Left, Q),
157+
TR = do_escape(Right, Q),
154158
{'{}', [], [TL, TM, TR]};
155159

160+
do_escape({Left, Right}, Q) ->
161+
{do_escape(Left, Q), do_escape(Right, Q)};
156162
do_escape(Tuple, Q) when is_tuple(Tuple) ->
157-
TT = do_quote(tuple_to_list(Tuple), Q),
163+
TT = do_escape(tuple_to_list(Tuple), Q),
158164
{'{}', [], TT};
159165

160166
do_escape(BitString, _) when is_bitstring(BitString) ->
@@ -188,7 +194,7 @@ do_escape([], _) ->
188194
[];
189195

190196
do_escape([H | T], #elixir_quote{unquote=false} = Q) ->
191-
do_quote_simple_list(T, do_quote(H, Q), Q);
197+
do_quote_simple_list(T, do_escape(H, Q), Q);
192198

193199
do_escape([H | T], Q) ->
194200
%% The improper case is inefficient, but improper lists are rare.
@@ -198,7 +204,7 @@ do_escape([H | T], Q) ->
198204
_:_ ->
199205
{L, R} = reverse_improper(T, [H]),
200206
TL = do_quote_splice(L, Q, [], []),
201-
TR = do_quote(R, Q),
207+
TR = do_escape(R, Q),
202208
update_last(TL, fun(X) -> {'|', [], [X, TR]} end)
203209
end;
204210

@@ -227,7 +233,7 @@ escape_map_key_value(K, V, Map, Q) ->
227233
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
228234
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
229235
true ->
230-
{do_quote(K, Q), do_quote(V, Q)}
236+
{do_escape(K, Q), do_escape(V, Q)}
231237
end.
232238

233239
find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil;
@@ -256,7 +262,7 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
256262
validate_runtime(generated, Generated),
257263

258264
Q = #elixir_quote{
259-
op=add_context,
265+
op=quote,
260266
aliases_hygiene=E,
261267
imports_hygiene=E,
262268
line=VLine,
@@ -336,7 +342,7 @@ do_quote({quote, Meta, [Arg]}, Q) when is_list(Meta) ->
336342
TArg = do_quote(Arg, Q#elixir_quote{unquote=false}),
337343

338344
NewMeta = case Q of
339-
#elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context);
345+
#elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context);
340346
_ -> Meta
341347
end,
342348

@@ -347,7 +353,7 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
347353
TArg = do_quote(Arg, Q#elixir_quote{unquote=false}),
348354

349355
NewMeta = case Q of
350-
#elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context);
356+
#elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context);
351357
_ -> Meta
352358
end,
353359

@@ -374,7 +380,7 @@ do_quote({'__aliases__', Meta, [H | T]}, #elixir_quote{aliases_hygiene=(#{}=E)}
374380

375381
%% Vars
376382

377-
do_quote({Name, Meta, nil}, #elixir_quote{op=add_context} = Q)
383+
do_quote({Name, Meta, nil}, #elixir_quote{op=quote} = Q)
378384
when is_atom(Name), is_list(Meta) ->
379385
ImportMeta = case Q#elixir_quote.imports_hygiene of
380386
nil -> Meta;
@@ -430,7 +436,7 @@ do_quote({Left, Right}, Q) ->
430436

431437
%% Everything else
432438

433-
do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= add_context ->
439+
do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= quote ->
434440
do_escape(Other, Q);
435441

436442
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)