From c746549f6821a3a2f0eda03e7b60463b33426d61 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 11 Oct 2025 13:26:52 +0900 Subject: [PATCH 1/3] Macro.escape/2 honors unquote: true when nested in maps Close #14829 --- lib/elixir/src/elixir_quote.erl | 3 +++ lib/elixir/test/elixir/macro_test.exs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 68a8483cb8..8d41865ce6 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -165,6 +165,9 @@ escape(Expr, Op, Unquote) -> erlang:raise(Kind, Reason, Pruned) end. +do_escape({unquote, Meta, [Expr]}, #elixir_quote{unquote=true} = Q) when is_list(Meta) -> + do_quote({unquote, Meta, [Expr]}, Q); + 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_escape(Left, Q), diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 36eb3ba393..e9ef23ee0e 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -82,6 +82,9 @@ defmodule MacroTest do contents = quote(unquote: false, do: unquote(x)) assert Macro.escape(contents, unquote: true) == {:x, [], MacroTest} + + contents = %{foo: quote(unquote: false, do: unquote(1))} + assert Macro.escape(contents, unquote: true) == {:%{}, [], [foo: 1]} end test "with generated" do From 849a592636e47628992e75965c10449593d79c81 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 11 Oct 2025 14:26:50 +0900 Subject: [PATCH 2/3] Also fix nested remote unquotes --- lib/elixir/src/elixir_quote.erl | 23 +++++++++++++++++++---- lib/elixir/test/elixir/macro_test.exs | 2 ++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 8d41865ce6..5271f676ef 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -152,10 +152,7 @@ escape(Expr, Op, Unquote) -> op=Op, unquote=Unquote }, - case Unquote of - true -> do_quote(Expr, Q); - false -> do_escape(Expr, Q) - end + do_escape(Expr, Q) catch Kind:Reason:Stacktrace -> Pruned = lists:dropwhile(fun @@ -165,9 +162,22 @@ escape(Expr, Op, Unquote) -> erlang:raise(Kind, Reason, Pruned) end. +%% Quote - unquote + +do_escape({quote, Meta, Args}, #elixir_quote{unquote=true} = Q) when is_list(Meta), is_list(Args) -> + do_quote({quote, Meta, Args}, Q#elixir_quote{unquote=false}); + do_escape({unquote, Meta, [Expr]}, #elixir_quote{unquote=true} = Q) when is_list(Meta) -> do_quote({unquote, Meta, [Expr]}, Q); +do_escape({{{'.', Meta, [Left, unquote]}, _, [Expr]}, _, Args}, #elixir_quote{unquote=true} = Q) when is_list(Meta) -> + do_escape_call(Left, Meta, Expr, Args, Q); + +do_escape({{'.', Meta, [Left, unquote]}, _, [Expr]}, #elixir_quote{unquote=true} = Q) when is_list(Meta) -> + do_escape_call(Left, Meta, Expr, nil, Q); + +% Tuples + 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_escape(Left, Q), @@ -253,6 +263,11 @@ do_escape(Fun, _) when is_function(Fun) -> do_escape(Other, _) -> bad_escape(Other). +do_escape_call(Left, Meta, Expr, Args, Q) -> + All = [Left, {unquote, Meta, [Expr]}, Args, Q#elixir_quote.context], + TAll = [do_escape(X, Q) || X <- All], + {{'.', Meta, [elixir_quote, dot]}, Meta, [meta(Meta, Q) | TAll]}. + escape_map_key_value(K, V, Map, Q) -> MaybeRef = if is_reference(V) -> V; diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index e9ef23ee0e..024000ad25 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -100,6 +100,8 @@ defmodule MacroTest do test "with remote unquote" do contents = quote(unquote: false, do: Kernel.unquote(:is_atom)(:ok)) assert eval_escaped(contents) == quote(do: Kernel.is_atom(:ok)) + + assert eval_escaped(%{foo: contents}) == %{foo: quote(do: Kernel.is_atom(:ok))} end test "with nested unquote" do From c2aafb4b3d7d44dc8ed38a01dd93b9514e7e6f11 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sun, 12 Oct 2025 09:44:53 +0900 Subject: [PATCH 3/3] Add test for unquote_splicing --- lib/elixir/test/elixir/macro_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 024000ad25..acfe657b5a 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -145,6 +145,9 @@ defmodule MacroTest do quote(unquote: false, do: [1, unquote_splicing([2]), 3, unquote_splicing([4]) | [5]]) assert eval_escaped(contents) == [1, 2, 3, 4, 5] + + contents = %{foo: quote(unquote: false, do: [1, 2, unquote_splicing([3, 4, 5])])} + assert eval_escaped(contents) == %{foo: [1, 2, 3, 4, 5]} end test "does not add context to quote" do