From d7f22b71fd9d8d02535d68e39b461222c0e2fdaf Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 13 Jun 2026 18:46:57 +0900 Subject: [PATCH 1/3] Disallow use of unquote when quote is used in pattern --- lib/elixir/src/elixir_expand.erl | 7 ++- lib/elixir/src/elixir_quote.erl | 8 ++- lib/elixir/test/elixir/kernel/errors_test.exs | 60 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 97e72d52da8..ef4487870d7 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -263,7 +263,12 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) -> {[], true} end, - Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote), + Unquote = case E of + #{context := nil} -> proplists:get_value(unquote, EOpts, DefaultUnquote); + % unquote=raise when quote/1 is called in a guard or pattern + _ -> raise + end, + Generated = proplists:get_value(generated, EOpts, false), {Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET), diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 8972d066818..c08bcbba709 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -315,7 +315,7 @@ is_valid(line, Line) -> is_integer(Line); is_valid(file, File) -> is_binary(File); is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil); is_valid(generated, Generated) -> is_boolean(Generated); -is_valid(unquote, Unquote) -> is_boolean(Unquote). +is_valid(unquote, Unquote) -> is_boolean(Unquote) orelse Unquote == raise. quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) -> argument_error(<<"unquote_splicing only works inside arguments and block contexts, " @@ -325,6 +325,12 @@ quote(Expr, Q) -> %% quote/unquote +do_quote({unquote, _, [_]}, #elixir_quote{unquote=raise}) -> + argument_error(<<"unquote/1 is not allowed when quote/1 is used inside a pattern or guard">>); + +do_quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=raise}) -> + argument_error(<<"unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard">>); + do_quote({quote, Meta, [Arg]}, Q) when is_list(Meta) -> TArg = do_quote(Arg, Q#elixir_quote{unquote=false}), diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 2c1dc605ae7..6eb70cd6da2 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -456,6 +456,66 @@ defmodule Kernel.ErrorsTest do """ end + test "invalid unquote when quote/1 is in a pattern" do + assert_eval_raise ArgumentError, + ["unquote/1 is not allowed when quote/1 is used inside a pattern or guard"], + ~c""" + defmodule Kernel.ErrorsTest.InvalidUnquoteInQuotePattern do + def my_fun(ast) do + case ast do + quote(do: foo(unquote(x))) -> x + end + end + end + """ + end + + test "invalid unquote when quote/1 is in a guard" do + assert_eval_raise ArgumentError, + ["unquote/1 is not allowed when quote/1 is used inside a pattern or guard"], + ~c""" + defmodule Kernel.ErrorsTest.InvalidUnquoteInQuoteGuard do + def my_fun(ast, x) do + case ast do + ast when ast == quote(do: foo(unquote(x))) -> x + end + end + end + """ + end + + test "invalid unquote_splicing when quote/1 is in a pattern" do + assert_eval_raise ArgumentError, + [ + "unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard" + ], + ~c""" + defmodule Kernel.ErrorsTest.InvalidUnquoteSplicingInQuotePattern do + def my_fun(ast) do + case ast do + quote(do: foo(unquote_splicing(x))) -> x + end + end + end + """ + end + + test "invalid unquote_splicing when quote/1 is in a guard" do + assert_eval_raise ArgumentError, + [ + "unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard" + ], + ~c""" + defmodule Kernel.ErrorsTest.InvalidUnquoteSplicingInQuoteGuard do + def my_fun(ast, x) do + case ast do + ast when ast == quote(do: foo(unquote_splicing(x))) -> x + end + end + end + """ + end + test "invalid attribute" do msg = ~r"cannot inject attribute @foo into function/macro because cannot escape " From 10c6b9b63bd1dd7b5fad95f5bbd10a391c70bd35 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 13 Jun 2026 20:41:38 +0900 Subject: [PATCH 2/3] More robust approach --- lib/elixir/src/elixir_expand.erl | 19 +++-- lib/elixir/src/elixir_quote.erl | 8 +-- lib/elixir/test/elixir/kernel/errors_test.exs | 69 ++++--------------- 3 files changed, 26 insertions(+), 70 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index ef4487870d7..aced6632520 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -3,6 +3,9 @@ %% SPDX-FileCopyrightText: 2012 Plataformatec -module(elixir_expand). + +-feature(maybe_expr, enable). + -export([expand/3, expand_args/3, expand_arg/3, format_error/1]). -import(elixir_errors, [file_error/4, module_error/4, function_error/4]). -include("elixir.hrl"). @@ -263,14 +266,16 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) -> {[], true} end, - Unquote = case E of - #{context := nil} -> proplists:get_value(unquote, EOpts, DefaultUnquote); - % unquote=raise when quote/1 is called in a guard or pattern - _ -> raise - end, - + Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote), Generated = proplists:get_value(generated, EOpts, false), + maybe + true ?= map_get(context, E) /= nil, + true ?= Unquote, + true ?= elixir_quote:has_unquotes(Exprs), + file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote) + end, + {Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET), {EPrelude, SP, EP} = expand(QPrelude, ST, ET), {EContext, SC, EC} = expand(QContext, SP, EP), @@ -1221,6 +1226,8 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) -> format_error({unquote_outside_quote, Unquote}) -> %% Unquote can be "unquote" or "unquote_splicing". io_lib:format("~p called outside quote", [Unquote]); +format_error(quote_in_pattern_with_unquote) -> + "unquote is not allowed when quote is used inside a pattern or guard"; format_error({invalid_bind_quoted_for_quote, BQ}) -> io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts", ['Elixir.Macro':to_string(BQ)]); diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index c08bcbba709..8972d066818 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -315,7 +315,7 @@ is_valid(line, Line) -> is_integer(Line); is_valid(file, File) -> is_binary(File); is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil); is_valid(generated, Generated) -> is_boolean(Generated); -is_valid(unquote, Unquote) -> is_boolean(Unquote) orelse Unquote == raise. +is_valid(unquote, Unquote) -> is_boolean(Unquote). quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) -> argument_error(<<"unquote_splicing only works inside arguments and block contexts, " @@ -325,12 +325,6 @@ quote(Expr, Q) -> %% quote/unquote -do_quote({unquote, _, [_]}, #elixir_quote{unquote=raise}) -> - argument_error(<<"unquote/1 is not allowed when quote/1 is used inside a pattern or guard">>); - -do_quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=raise}) -> - argument_error(<<"unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard">>); - do_quote({quote, Meta, [Arg]}, Q) when is_list(Meta) -> TArg = do_quote(Arg, Q#elixir_quote{unquote=false}), diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 6eb70cd6da2..06d1ed68e03 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -457,63 +457,18 @@ defmodule Kernel.ErrorsTest do end test "invalid unquote when quote/1 is in a pattern" do - assert_eval_raise ArgumentError, - ["unquote/1 is not allowed when quote/1 is used inside a pattern or guard"], - ~c""" - defmodule Kernel.ErrorsTest.InvalidUnquoteInQuotePattern do - def my_fun(ast) do - case ast do - quote(do: foo(unquote(x))) -> x - end - end - end - """ - end - - test "invalid unquote when quote/1 is in a guard" do - assert_eval_raise ArgumentError, - ["unquote/1 is not allowed when quote/1 is used inside a pattern or guard"], - ~c""" - defmodule Kernel.ErrorsTest.InvalidUnquoteInQuoteGuard do - def my_fun(ast, x) do - case ast do - ast when ast == quote(do: foo(unquote(x))) -> x - end - end - end - """ - end - - test "invalid unquote_splicing when quote/1 is in a pattern" do - assert_eval_raise ArgumentError, - [ - "unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard" - ], - ~c""" - defmodule Kernel.ErrorsTest.InvalidUnquoteSplicingInQuotePattern do - def my_fun(ast) do - case ast do - quote(do: foo(unquote_splicing(x))) -> x - end - end - end - """ - end - - test "invalid unquote_splicing when quote/1 is in a guard" do - assert_eval_raise ArgumentError, - [ - "unquote_splicing/1 is not allowed when quote/1 is used inside a pattern or guard" - ], - ~c""" - defmodule Kernel.ErrorsTest.InvalidUnquoteSplicingInQuoteGuard do - def my_fun(ast, x) do - case ast do - ast when ast == quote(do: foo(unquote_splicing(x))) -> x - end - end - end - """ + assert_compile_error( + ["unquote is not allowed when quote is used inside a pattern or guard"], + ~c""" + defmodule Kernel.ErrorsTest.InvalidUnquoteInQuotePattern do + def my_fun(ast) do + case ast do + quote(do: foo(unquote(x))) -> x + end + end + end + """ + ) end test "invalid attribute" do From 6a7275ecdbcd1fad6977309ea1cc5513dcf51f56 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 13 Jun 2026 20:55:24 +0900 Subject: [PATCH 3/3] Apply @josevalim's suggestion --- lib/elixir/src/elixir_expand.erl | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index aced6632520..e52bceb3827 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -3,9 +3,6 @@ %% SPDX-FileCopyrightText: 2012 Plataformatec -module(elixir_expand). - --feature(maybe_expr, enable). - -export([expand/3, expand_args/3, expand_arg/3, format_error/1]). -import(elixir_errors, [file_error/4, module_error/4, function_error/4]). -include("elixir.hrl"). @@ -269,12 +266,8 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) -> Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote), Generated = proplists:get_value(generated, EOpts, false), - maybe - true ?= map_get(context, E) /= nil, - true ?= Unquote, - true ?= elixir_quote:has_unquotes(Exprs), - file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote) - end, + (map_get(context, E) /= nil) andalso Unquote andalso elixir_quote:has_unquotes(Exprs) andalso + file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote), {Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET), {EPrelude, SP, EP} = expand(QPrelude, ST, ET),