diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index c652a0099a..c2476ea290 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5185,7 +5185,7 @@ defmodule Kernel do quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer))) %{} -> - :elixir_quote.escape(block, :none, false) + :elixir_quote.escape(block, :escape, false) end versioned_vars = env.versioned_vars @@ -5465,7 +5465,7 @@ defmodule Kernel do store = case unquoted_expr or unquoted_call do true -> - :elixir_quote.escape({call, expr}, :none, true) + :elixir_quote.escape({call, expr}, :escape, true) false -> key = :erlang.unique_integer() diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index c126ef10cf..1850fd8ce9 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -126,7 +126,7 @@ defmodule Kernel.Utils do key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition") try do - :elixir_quote.escape(val, :none, false) + :elixir_quote.escape(val, :escape, false) rescue e in [ArgumentError] -> raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e) @@ -171,7 +171,7 @@ defmodule Kernel.Utils do :lists.foreach(foreach, enforce_keys) struct = :maps.from_list([__struct__: module] ++ fields) - escaped_struct = :elixir_quote.escape(struct, :none, false) + escaped_struct = :elixir_quote.escape(struct, :escape, false) body = case bootstrapped? do diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 2bb45d2a46..ed31255c4a 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -803,7 +803,9 @@ defmodule Macro do * `:unquote` - when `true`, this function leaves `unquote/1` and `unquote_splicing/1` expressions unescaped, effectively unquoting the contents on escape. This option is useful only when escaping - ASTs which may have quoted fragments in them. Defaults to `false`. + ASTs which may have quoted fragments in them. Note this option + will give a special meaning to `quote`/`unquote` nodes, which need + to be valid AST before escaping. Defaults to `false`. * `:prune_metadata` - when `true`, removes most metadata from escaped AST nodes. Note this option changes the semantics of escaped code and @@ -912,7 +914,7 @@ defmodule Macro do @spec escape(term, escape_opts) :: t() def escape(expr, opts \\ []) do unquote = Keyword.get(opts, :unquote, false) - kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none + kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape generated = Keyword.get(opts, :generated, false) case :elixir_quote.escape(expr, kind, unquote) do diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 095e402625..c68c485bd3 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -18,7 +18,7 @@ line=false, file=nil, context=nil, - op=none, % none | prune_metadata | add_context + op=escape, % escape | escape_and_prune | quote aliases_hygiene=nil, imports_hygiene=nil, unquote=true, @@ -145,21 +145,27 @@ do_tuple_linify(Fun, Meta, Left, Right, Var, Gen) -> %% Escapes the given expression. It is similar to quote, but %% lines are kept and hygiene mechanisms are disabled. escape(Expr, Op, Unquote) -> - do_quote(Expr, #elixir_quote{ + Q = #elixir_quote{ line=true, file=nil, op=Op, unquote=Unquote - }). + }, + case Unquote of + true -> do_quote(Expr, Q); + false -> do_escape(Expr, Q) + end. -do_escape({Left, Meta, Right}, #elixir_quote{op=prune_metadata} = Q) when is_list(Meta) -> +do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) -> TM = [{K, V} || {K, V} <- Meta, (K == no_parens) orelse (K == line) orelse (K == delimiter)], - TL = do_quote(Left, Q), - TR = do_quote(Right, Q), + TL = do_escape(Left, Q), + TR = do_escape(Right, Q), {'{}', [], [TL, TM, TR]}; +do_escape({Left, Right}, Q) -> + {do_escape(Left, Q), do_escape(Right, Q)}; do_escape(Tuple, Q) when is_tuple(Tuple) -> - TT = do_quote(tuple_to_list(Tuple), Q), + TT = do_escape(tuple_to_list(Tuple), Q), {'{}', [], TT}; do_escape(BitString, _) when is_bitstring(BitString) -> @@ -193,7 +199,7 @@ do_escape([], _) -> []; do_escape([H | T], #elixir_quote{unquote=false} = Q) -> - do_quote_simple_list(T, do_quote(H, Q), Q); + do_quote_simple_list(T, do_escape(H, Q), Q); do_escape([H | T], Q) -> %% The improper case is inefficient, but improper lists are rare. @@ -203,7 +209,7 @@ do_escape([H | T], Q) -> _:_ -> {L, R} = reverse_improper(T, [H]), TL = do_quote_splice(L, Q, [], []), - TR = do_quote(R, Q), + TR = do_escape(R, Q), update_last(TL, fun(X) -> {'|', [], [X, TR]} end) end; @@ -232,7 +238,7 @@ escape_map_key_value(K, V, Map, Q) -> ('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ", "(it must be defined within a function instead). ", (bad_escape_hint())/binary>>); true -> - {do_quote(K, Q), do_quote(V, Q)} + {do_escape(K, Q), do_escape(V, Q)} end. find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil; @@ -261,7 +267,7 @@ build(Meta, Line, File, Context, Unquote, Generated, E) -> validate_runtime(generated, Generated), Q = #elixir_quote{ - op=add_context, + op=quote, aliases_hygiene=E, imports_hygiene=E, line=VLine, @@ -341,7 +347,7 @@ do_quote({quote, Meta, [Arg]}, Q) when is_list(Meta) -> TArg = do_quote(Arg, Q#elixir_quote{unquote=false}), NewMeta = case Q of - #elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context); + #elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context); _ -> Meta end, @@ -352,7 +358,7 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) -> TArg = do_quote(Arg, Q#elixir_quote{unquote=false}), NewMeta = case Q of - #elixir_quote{op=add_context, context=Context} -> keystore(context, Meta, Context); + #elixir_quote{op=quote, context=Context} -> keystore(context, Meta, Context); _ -> Meta end, @@ -379,7 +385,7 @@ do_quote({'__aliases__', Meta, [H | T]}, #elixir_quote{aliases_hygiene=(#{}=E)} %% Vars -do_quote({Name, Meta, nil}, #elixir_quote{op=add_context} = Q) +do_quote({Name, Meta, nil}, #elixir_quote{op=quote} = Q) when is_atom(Name), is_list(Meta) -> ImportMeta = case Q#elixir_quote.imports_hygiene of nil -> Meta; @@ -435,7 +441,7 @@ do_quote({Left, Right}, Q) -> %% Everything else -do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= add_context -> +do_quote(Other, #elixir_quote{op=Op} = Q) when Op =/= quote -> do_escape(Other, Q); do_quote({_, _, _} = Tuple, Q) -> diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index feaeebe3c7..0c09b31887 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -146,6 +146,11 @@ defmodule MacroTest do assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]} end + test "escapes the content of :quote tuples" do + assert Macro.escape({:quote, [%{}], [{}]}) == + {:{}, [], [:quote, [{:%{}, [], []}], [{:{}, [], []}]]} + end + test "escape container when a reference cannot be escaped" do assert_raise ArgumentError, ~r"contains a reference", fn -> Macro.escape(%{re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})