diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 7c8e854549..096408b177 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6647,16 +6647,26 @@ defmodule Kernel do end defp compile_regex(binary_or_tuple, options) do - # TODO: Remove this when we require Erlang/OTP 28+ - case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do + bin_opts = :binary.list_to_bin(options) + + # TODO: Remove this when we require Erlang/OTP 28.1+ + case is_binary(binary_or_tuple) and compile_time_regexes_supported?() do true -> - Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options))) + Macro.escape(Regex.compile!(binary_or_tuple, bin_opts)) false -> - quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options)))) + quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts))) end end + defp compile_time_regexes_supported? do + # OTP 28.0 introduced refs in patterns, which can't be used in AST anymore + # OTP 28.1 introduced :re.import/1 which allows us to fix this in Macro.escape + :erlang.system_info(:otp_release) < [?2, ?8] or + (Code.ensure_loaded?(:re) and + function_exported?(:re, :import, 1)) + end + @doc ~S""" Handles the sigil `~D` for dates. diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 4260985f28..c1199df636 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -168,9 +168,28 @@ do_escape(BitString, _) when is_bitstring(BitString) -> {'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], nil}]}]} end; +do_escape(#{ + '__struct__' := 'Elixir.Regex', + 're_pattern' := {re_pattern, _, _, _, Ref}, + 'source' := Source, + 'opts' := Opts +} = Map, Q) when is_reference(Ref), is_binary(Source), is_list(Opts) -> + case erlang:function_exported(re, import, 1) of + true -> + {ok, ExportedPattern} = re:compile(Source, [export | Opts]), + PatternAst = {{'.', [], ['re', 'import']}, [], [do_escape(ExportedPattern, Q)]}, + {'%{}', [], [ + {'__struct__', 'Elixir.Regex'}, + {'re_pattern', PatternAst}, + {'source', Source}, + {'opts', do_escape(Opts, Q)} + ]}; + false -> + escape_map(Map, Q) + end; + do_escape(Map, Q) when is_map(Map) -> - TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))], - {'%{}', [], TT}; + escape_map(Map, Q); do_escape([], _) -> []; @@ -203,6 +222,10 @@ do_escape(Fun, _) when is_function(Fun) -> do_escape(Other, _) -> bad_escape(Other). +escape_map(Map, Q) -> + TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))], + {'%{}', [], TT}. + 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 3c8944defd..61a48fcc4d 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -141,11 +141,27 @@ defmodule MacroTest do assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]} end - test "inspects container when a reference cannot be escaped" do + @tag skip: System.otp_release() < "28" or function_exported?(:re, :import, 1) + test "escape container when a reference cannot be escaped" do assert_raise ArgumentError, ~r"~r/foo/ contains a reference", fn -> Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}}) end end + + @tag skip: not function_exported?(:re, :import, 1) + test "escape regex will remove references and replace it by a call to :re.import/1" do + assert { + :%{}, + [], + [ + __struct__: Regex, + re_pattern: + {{:., [], [:re, :import]}, [], [{:{}, [], [:re_exported_pattern | _]}]}, + source: "foo", + opts: [] + ] + } = Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}}) + end end describe "expand_once/2" do