Skip to content

Commit b6d3f29

Browse files
josevalimJosé Valim
authored andcommitted
Allow to prune metadata while escaping (#7949)
We use this option in ExUnit as we are escaping the code to eventually convert it to a string representation and therefore the metadata is not relevant. Signed-off-by: José Valim <[email protected]>
1 parent bfdaf3f commit b6d3f29

File tree

8 files changed

+128
-93
lines changed

8 files changed

+128
-93
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2776,7 +2776,7 @@ defmodule Kernel do
27762776
end
27772777

27782778
try do
2779-
:elixir_quote.escape(value, false)
2779+
:elixir_quote.escape(value, :default, false)
27802780
rescue
27812781
ex in [ArgumentError] ->
27822782
raise ArgumentError,
@@ -3698,7 +3698,7 @@ defmodule Kernel do
36983698
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
36993699

37003700
%{} ->
3701-
{escaped, _} = :elixir_quote.escape(block, false)
3701+
{escaped, _} = :elixir_quote.escape(block, :default, false)
37023702
escaped
37033703
end
37043704

@@ -3972,8 +3972,8 @@ defmodule Kernel do
39723972
module = assert_module_scope(env, kind, 2)
39733973
assert_no_function_scope(env, kind, 2)
39743974

3975-
{escaped_call, unquoted_call} = :elixir_quote.escape(call, true)
3976-
{escaped_expr, unquoted_expr} = :elixir_quote.escape(expr, true)
3975+
{escaped_call, unquoted_call} = :elixir_quote.escape(call, :default, true)
3976+
{escaped_expr, unquoted_expr} = :elixir_quote.escape(expr, :default, true)
39773977

39783978
escaped_expr =
39793979
case unquoted_expr do

lib/elixir/lib/macro.ex

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,12 +356,7 @@ defmodule Macro do
356356
def decompose_call(_), do: :error
357357

358358
@doc """
359-
Recursively escapes a value so it can be inserted
360-
into a syntax tree.
361-
362-
One may pass `unquote: true` to `escape/2`
363-
which leaves `unquote/1` statements unescaped, effectively
364-
unquoting the contents on escape.
359+
Recursively escapes a value so it can be inserted into a syntax tree.
365360
366361
## Examples
367362
@@ -374,6 +369,26 @@ defmodule Macro do
374369
iex> Macro.escape({:unquote, [], [1]}, unquote: true)
375370
1
376371
372+
## Options
373+
374+
* `:unquote` - when true, this function leaves `unquote/1` and
375+
`unquote_splicing/1` statements unescaped, effectively unquoting
376+
the contents on escape. This option is useful only when escaping
377+
ASTs which may have quoted fragments in them. Defaults to false.
378+
379+
* `:prune_metadata` - when true, removes metadata from escaped AST
380+
nodes. Note this option changes the semantics of escaped code and
381+
it should only be used when escaping ASTs, never values. Defaults
382+
to false.
383+
384+
As an example, `ExUnit` stores the AST of every assertion, so when
385+
an assertion fails we can show code snippets to users. Without this
386+
option, each time the test module is compiled, we get a different
387+
MD5 of the module byte code, because the AST contains metadata,
388+
such as counters, specific to the compilation environment. By pruning
389+
the metadata, we ensure that the module is deterministic and reduce
390+
the amount of data `ExUnit` needs to keep around.
391+
377392
## Comparison to `Kernel.quote/2`
378393
379394
The `escape/2` function is sometimes confused with `Kernel.SpecialForms.quote/2`,
@@ -402,7 +417,9 @@ defmodule Macro do
402417
"""
403418
@spec escape(term, keyword) :: Macro.t()
404419
def escape(expr, opts \\ []) do
405-
elem(:elixir_quote.escape(expr, Keyword.get(opts, :unquote, false)), 0)
420+
unquote = Keyword.get(opts, :unquote, false)
421+
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :default
422+
elem(:elixir_quote.escape(expr, kind, unquote), 0)
406423
end
407424

408425
@doc """

lib/elixir/src/elixir.hrl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
imports_hygiene=true,
2525
unquote=true,
2626
unquoted=false,
27-
escape=false,
2827
generated=false
2928
}).
3029

lib/elixir/src/elixir_bootstrap.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
'MACRO-defmacrop'(Caller, Call, Expr) -> define(Caller, defmacrop, Call, Expr).
2020

2121
'MACRO-defmodule'(_Caller, Alias, [{do, Block}]) ->
22-
{Escaped, _} = elixir_quote:escape(Block, false),
22+
{Escaped, _} = elixir_quote:escape(Block, default, false),
2323
Args = [Alias, Escaped, [], env()],
2424
{{'.', [], [elixir_module, compile]}, [], Args}.
2525

@@ -36,8 +36,8 @@
3636
{defp, 2}].
3737

3838
define({Line, E}, Kind, Call, Expr) ->
39-
{EscapedCall, UC} = elixir_quote:escape(Call, true),
40-
{EscapedExpr, UE} = elixir_quote:escape(Expr, true),
39+
{EscapedCall, UC} = elixir_quote:escape(Call, default, true),
40+
{EscapedExpr, UE} = elixir_quote:escape(Expr, default, true),
4141
Args = [Kind, not(UC or UE), EscapedCall, EscapedExpr, elixir_locals:cache_env(E#{line := Line})],
4242
{{'.', [], [elixir_def, store_definition]}, [], Args}.
4343

lib/elixir/src/elixir_map.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, #{context := Context} = E)
3333
Struct = load_struct(Meta, ELeft, [Assocs], InContext, EE),
3434
assert_struct_keys(Meta, ELeft, Struct, Assocs, EE),
3535
Keys = ['__struct__'] ++ [K || {K, _} <- Assocs],
36-
{StructAssocs, _} = elixir_quote:escape(maps:to_list(maps:without(Keys, Struct)), false),
36+
WithoutKeys = maps:to_list(maps:without(Keys, Struct)),
37+
{StructAssocs, _} = elixir_quote:escape(WithoutKeys, default, false),
3738
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, EE};
3839

3940
{_, _, Assocs} -> %% Update or match

lib/elixir/src/elixir_quote.erl

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-module(elixir_quote).
2-
-export([escape/2, linify/2, linify/3, linify_with_context_counter/3, quote/4]).
2+
-export([escape/3, linify/2, linify/3, linify_with_context_counter/3, quote/4]).
33
-export([dot/5, tail_list/3, list/2]). %% Quote callbacks
44

55
-include("elixir.hrl").
@@ -132,16 +132,15 @@ annotate(Tree, _Context) -> Tree.
132132

133133
%% Escapes the given expression. It is similar to quote, but
134134
%% lines are kept and hygiene mechanisms are disabled.
135-
escape(Expr, Unquote) ->
135+
escape(Expr, Kind, Unquote) ->
136136
{Res, Q} = quote(Expr, nil, #elixir_quote{
137137
line=true,
138138
file=nil,
139139
vars_hygiene=false,
140140
aliases_hygiene=false,
141141
imports_hygiene=false,
142-
unquote=Unquote,
143-
escape=true
144-
}, nil),
142+
unquote=Unquote
143+
}, Kind),
145144
{Res, Q#elixir_quote.unquoted}.
146145

147146
%% Quotes an expression and return its quoted Elixir AST.
@@ -161,7 +160,7 @@ quote(Expr, Binding, Q, E) ->
161160
{'{}', [], [K, [], Context]},
162161
V
163162
] ]
164-
} || {K, V} <- Binding],
163+
} || {K, V} <- Binding],
165164

166165
{TExprs, TQ} = do_quote(Expr, Q, E),
167166
{{'{}', [], ['__block__', [], Vars ++ [TExprs] ]}, TQ}.
@@ -211,11 +210,7 @@ do_quote({'&', Meta, [{'/', _, [{F, _, C}, A]}] = Args},
211210
do_quote({Name, Meta, ArgsOrAtom}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name) ->
212211
do_quote_import(Name, Meta, ArgsOrAtom, Q, E);
213212

214-
do_quote({_, _, _} = Tuple, #elixir_quote{escape=false} = Q, E) ->
215-
Annotated = annotate(Tuple, Q#elixir_quote.context),
216-
do_quote_tuple(Annotated, Q, E);
217-
218-
%% Literals
213+
%% Two element tuples
219214

220215
do_quote({Left, Right}, #elixir_quote{unquote=true} = Q, E) when
221216
is_tuple(Left) andalso (element(1, Left) == unquote_splicing);
@@ -227,7 +222,33 @@ do_quote({Left, Right}, Q, E) ->
227222
{TRight, RQ} = do_quote(Right, LQ, E),
228223
{{TLeft, TRight}, RQ};
229224

230-
do_quote(BitString, #elixir_quote{escape=true} = Q, _) when is_bitstring(BitString) ->
225+
%% Everything else
226+
227+
do_quote(Other, Q, E) when is_atom(E) ->
228+
do_escape(Other, Q, E);
229+
230+
do_quote({_, _, _} = Tuple, Q, E) ->
231+
Annotated = annotate(Tuple, Q#elixir_quote.context),
232+
do_quote_tuple(Annotated, Q, E);
233+
234+
do_quote(List, Q, E) when is_list(List) ->
235+
do_quote_splice(lists:reverse(List), Q, E);
236+
237+
do_quote(Other, Q, _) ->
238+
{Other, Q}.
239+
240+
%% do_escape
241+
242+
do_escape({Left, _Meta, Right}, Q, E = prune_metadata) ->
243+
{TL, QL} = do_quote(Left, Q, E),
244+
{TR, QR} = do_quote(Right, QL, E),
245+
{{'{}', [], [TL, [], TR]}, QR};
246+
247+
do_escape(Tuple, Q, E) when is_tuple(Tuple) ->
248+
{TT, TQ} = do_quote(tuple_to_list(Tuple), Q, E),
249+
{{'{}', [], TT}, TQ};
250+
251+
do_escape(BitString, Q, _) when is_bitstring(BitString) ->
231252
case bit_size(BitString) rem 8 of
232253
0 ->
233254
{BitString, Q};
@@ -236,51 +257,41 @@ do_quote(BitString, #elixir_quote{escape=true} = Q, _) when is_bitstring(BitStri
236257
{{'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], []}]}]}, Q}
237258
end;
238259

239-
do_quote(Map, #elixir_quote{escape=true} = Q, E) when is_map(Map) ->
260+
do_escape(Map, Q, E) when is_map(Map) ->
240261
{TT, TQ} = do_quote(maps:to_list(Map), Q, E),
241262
{{'%{}', [], TT}, TQ};
242263

243-
do_quote(Tuple, #elixir_quote{escape=true} = Q, E) when is_tuple(Tuple) ->
244-
{TT, TQ} = do_quote(tuple_to_list(Tuple), Q, E),
245-
{{'{}', [], TT}, TQ};
246-
247-
do_quote(List, #elixir_quote{escape=true} = Q, E) when is_list(List) ->
264+
do_escape(List, Q, E) when is_list(List) ->
248265
%% The improper case is a bit inefficient, but improper lists are rare.
249266
case reverse_improper(List) of
250-
{L} -> do_splice(L, Q, E);
251-
{L, R} ->
252-
{TL, QL} = do_splice(L, Q, E, [], []),
267+
{L} -> do_quote_splice(L, Q, E);
268+
{L, R} ->
269+
{TL, QL} = do_quote_splice(L, Q, E, [], []),
253270
{TR, QR} = do_quote(R, QL, E),
254271
{update_last(TL, fun(X) -> {'|', [], [X, TR]} end), QR}
255272
end;
256273

257-
do_quote(Other, #elixir_quote{escape=true} = Q, _)
274+
do_escape(Other, Q, _)
258275
when is_number(Other); is_pid(Other); is_atom(Other) ->
259276
{Other, Q};
260277

261-
do_quote(Fun, #elixir_quote{escape=true} = Q, _) when is_function(Fun) ->
278+
do_escape(Fun, Q, _) when is_function(Fun) ->
262279
case (erlang:fun_info(Fun, env) == {env, []}) andalso
263280
(erlang:fun_info(Fun, type) == {type, external}) of
264281
true -> {Fun, Q};
265282
false -> bad_escape(Fun)
266283
end;
267284

268-
do_quote(Other, #elixir_quote{escape=true}, _) ->
269-
bad_escape(Other);
270-
271-
do_quote(List, Q, E) when is_list(List) ->
272-
do_splice(lists:reverse(List), Q, E);
273-
274-
do_quote(Other, Q, _) ->
275-
{Other, Q}.
276-
277-
%% Quote helpers
285+
do_escape(Other, _, _) ->
286+
bad_escape(Other).
278287

279288
bad_escape(Arg) ->
280289
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary, ". ",
281290
"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
282291
"PIDs and remote functions in the format &Mod.fun/arity">>).
283292

293+
%% do_quote_*
294+
284295
do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q, E) ->
285296
Arity = case is_atom(ArgsOrAtom) of
286297
true -> 0;
@@ -332,10 +343,41 @@ do_quote_tuple(Left, Meta, [{{unquote, _, _}, _, _}, _] = Right, Q, E) when ?def
332343
{{'{}', [], [TLeft, meta(Meta, Q), [NewHead, Body]]}, RQ};
333344

334345
do_quote_tuple(Left, Meta, Right, Q, E) ->
335-
{TLeft, LQ} = do_quote(Left, Q, E),
346+
{TLeft, LQ} = do_quote(Left, Q, E),
336347
{TRight, RQ} = do_quote(Right, LQ, E),
337348
{{'{}', [], [TLeft, meta(Meta, Q), TRight]}, RQ}.
338349

350+
do_quote_splice([{'|', Meta, [{unquote_splicing, _, [Left]}, Right]} | T], #elixir_quote{unquote=true} = Q, E) ->
351+
%% Process the remaining entries on the list.
352+
%% For [1, 2, 3, unquote_splicing(arg) | tail], this will quote
353+
%% 1, 2 and 3, which could even be unquotes.
354+
{TT, QT} = do_quote_splice(T, Q, E, [], []),
355+
{TR, QR} = do_quote(Right, QT, E),
356+
{do_runtime_list(Meta, tail_list, [Left, TR, TT]), QR#elixir_quote{unquoted=true}};
357+
358+
do_quote_splice(List, Q, E) ->
359+
do_quote_splice(List, Q, E, [], []).
360+
361+
do_quote_splice([{unquote_splicing, Meta, [Expr]} | T], #elixir_quote{unquote=true} = Q, E, Buffer, Acc) ->
362+
Runtime = do_runtime_list(Meta, list, [Expr, do_list_concat(Buffer, Acc)]),
363+
do_quote_splice(T, Q#elixir_quote{unquoted=true}, E, [], Runtime);
364+
365+
do_quote_splice([H | T], Q, E, Buffer, Acc) ->
366+
{TH, TQ} = do_quote(H, Q, E),
367+
do_quote_splice(T, TQ, E, [TH | Buffer], Acc);
368+
369+
do_quote_splice([], Q, _E, Buffer, Acc) ->
370+
{do_list_concat(Buffer, Acc), Q}.
371+
372+
do_list_concat(Left, []) -> Left;
373+
do_list_concat([], Right) -> Right;
374+
do_list_concat(Left, Right) -> {{'.', [], [erlang, '++']}, [], [Left, Right]}.
375+
376+
do_runtime_list(Meta, Fun, Args) ->
377+
{{'.', Meta, [elixir_quote, Fun]}, Meta, Args}.
378+
379+
%% Helpers
380+
339381
meta(Meta, Q) ->
340382
generated(keep(Meta, Q), Q).
341383

@@ -381,33 +423,3 @@ keynew(Key, Meta, Value) ->
381423
{Key, _} -> Meta;
382424
_ -> keystore(Key, Meta, Value)
383425
end.
384-
385-
%% Quote splicing
386-
387-
do_splice([{'|', Meta, [{unquote_splicing, _, [Left]}, Right]} | T], #elixir_quote{unquote=true} = Q, E) ->
388-
%% Process the remaining entries on the list.
389-
%% For [1, 2, 3, unquote_splicing(arg) | tail], this will quote
390-
%% 1, 2 and 3, which could even be unquotes.
391-
{TT, QT} = do_splice(T, Q, E, [], []),
392-
{TR, QR} = do_quote(Right, QT, E),
393-
{do_runtime_list(Meta, tail_list, [Left, TR, TT]), QR#elixir_quote{unquoted=true}};
394-
395-
do_splice(List, Q, E) ->
396-
do_splice(List, Q, E, [], []).
397-
398-
do_splice([{unquote_splicing, Meta, [Expr]} | T], #elixir_quote{unquote=true} = Q, E, Buffer, Acc) ->
399-
do_splice(T, Q#elixir_quote{unquoted=true}, E, [], do_runtime_list(Meta, list, [Expr, do_join(Buffer, Acc)]));
400-
401-
do_splice([H | T], Q, E, Buffer, Acc) ->
402-
{TH, TQ} = do_quote(H, Q, E),
403-
do_splice(T, TQ, E, [TH | Buffer], Acc);
404-
405-
do_splice([], Q, _E, Buffer, Acc) ->
406-
{do_join(Buffer, Acc), Q}.
407-
408-
do_join(Left, []) -> Left;
409-
do_join([], Right) -> Right;
410-
do_join(Left, Right) -> {{'.', [], [erlang, '++']}, [], [Left, Right]}.
411-
412-
do_runtime_list(Meta, Fun, Args) ->
413-
{{'.', Meta, [elixir_quote, Fun]}, Meta, Args}.

lib/elixir/test/elixir/macro_test.exs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,38 +24,44 @@ defmodule MacroTest do
2424
import Macro.ExternalTest
2525

2626
describe "escape/2" do
27-
test "handles tuples with size different than two" do
28-
assert Macro.escape({:a}) == {:{}, [], [:a]}
29-
assert Macro.escape({:a, :b, :c}) == {:{}, [], [:a, :b, :c]}
30-
assert Macro.escape({:a, {1, 2, 3}, :c}) == {:{}, [], [:a, {:{}, [], [1, 2, 3]}, :c]}
31-
end
32-
33-
test "simply returns tuples with size equal to two" do
27+
test "returns tuples with size equal to two" do
3428
assert Macro.escape({:a, :b}) == {:a, :b}
3529
end
3630

37-
test "simply returns any other structure" do
31+
test "returns lists" do
3832
assert Macro.escape([1, 2, 3]) == [1, 2, 3]
3933
end
4034

41-
test "handles maps" do
35+
test "escapes tuples with size different than two" do
36+
assert Macro.escape({:a}) == {:{}, [], [:a]}
37+
assert Macro.escape({:a, :b, :c}) == {:{}, [], [:a, :b, :c]}
38+
assert Macro.escape({:a, {1, 2, 3}, :c}) == {:{}, [], [:a, {:{}, [], [1, 2, 3]}, :c]}
39+
end
40+
41+
test "escapes maps" do
4242
assert Macro.escape(%{a: 1}) == {:%{}, [], [a: 1]}
4343
end
4444

45-
test "handles bitstring" do
45+
test "escapes bitstring" do
4646
assert {:<<>>, [], args} = Macro.escape(<<300::12>>)
4747
assert [{:::, [], [1, {:size, [], [4]}]}, {:::, [], [",", {:binary, [], []}]}] = args
4848
end
4949

50-
test "works recursively" do
50+
test "escapes recursively" do
5151
assert Macro.escape([1, {:a, :b, :c}, 3]) == [1, {:{}, [], [:a, :b, :c]}, 3]
5252
end
5353

54-
test "with improper lists" do
54+
test "escapes improper lists" do
5555
assert Macro.escape([1 | 2]) == [{:|, [], [1, 2]}]
5656
assert Macro.escape([1, 2 | 3]) == [1, {:|, [], [2, 3]}]
5757
end
5858

59+
test "prunes metadata" do
60+
meta = [nothing: :important, counter: 1]
61+
assert Macro.escape({:foo, meta, []}) == {:{}, [], [:foo, meta, []]}
62+
assert Macro.escape({:foo, meta, []}, prune_metadata: true) == {:{}, [], [:foo, [], []]}
63+
end
64+
5965
test "with unquote" do
6066
contents = quote(unquote: false, do: unquote(1))
6167
assert Macro.escape(contents, unquote: true) == 1

0 commit comments

Comments
 (0)