Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions lib/elixir/lib/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
27 changes: 25 additions & 2 deletions lib/elixir/src/elixir_quote.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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([], _) ->
[];
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion lib/elixir/test/elixir/macro_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down