Skip to content
Closed
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
7 changes: 7 additions & 0 deletions lib/elixir/lib/regex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1000,4 +1000,11 @@ defmodule Regex do

defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}

if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to check this at runtime, because Elixir compiled for OTP 27 may run on OTP 28.

def __expand_compile__(%Regex{source: source, opts: opts}, :re_pattern) do
{:ok, exported} = :re.compile(source, [:export] ++ opts)
quote do: :re.import(unquote(Macro.escape(exported)))
end
end
end
45 changes: 44 additions & 1 deletion lib/elixir/src/elixir_map.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,42 @@ expand_map(Meta, [{'|', _, [_, _]}] = Args, _S, #{context := Context, file := Fi
expand_map(Meta, Args, S, E) ->
{EArgs, SE, EE} = elixir_expand:expand_args(Args, S, E),
validate_kv(Meta, EArgs, Args, E),
{{'%{}', Meta, EArgs}, SE, EE}.
PArgs = post_process_map_args(EArgs, E),
{{'%{}', Meta, PArgs}, SE, EE}.

post_process_map_args(Args, E) ->
% TODO rewrite with maybe once we require OTP 2X+
case lists:keyfind('__expand_compile__', 2, Args) of
{_Key, '__expand_compile__'} ->
case lists:keyfind('__struct__', 1, Args) of
{'__struct__', Module} ->
case code:ensure_loaded(Module) == {module, Module} andalso erlang:function_exported(Module, '__expand_compile__', 2) andalso is_literal(Args) of
true ->
case E of
#{context := nil} ->
Map = eval_literal({'%{}', [], Args}),
lists:map(fun
({Key, '__expand_compile__'}) -> {Key, Module:'__expand_compile__'(Map, Key)};
({Key, Value}) -> {Key, Value}
end, Args);
#{context := match} ->
% replace it by wildcards in matches
lists:map(fun
({Key, '__expand_compile__'}) -> {Key, {'_,', [], nil}};
({Key, Value}) -> {Key, Value}
end, Args);
_ ->
Args
end;

false ->
Args
end;
_ -> Args
end;
false ->
Args
end.

expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = E) ->
CleanMapArgs = delete_struct_key(Meta, MapArgs, E),
Expand Down Expand Up @@ -103,11 +138,19 @@ validate_not_repeated(Meta, Key, Used, E) ->
Used
end.

is_literal({'{}', _, List}) when is_list(List) -> lists:all(fun is_literal/1, List);
is_literal({'%{}', _, List}) when is_list(List) -> lists:all(fun is_literal/1, List);
is_literal({_, _, _}) -> false;
is_literal({Left, Right}) -> is_literal(Left) andalso is_literal(Right);
is_literal([_ | _] = List) -> lists:all(fun is_literal/1, List);
is_literal(_) -> true.

eval_literal({'{}', _, List}) when is_list(List) -> list_to_tuple(eval_literal(List));
eval_literal({'%{}', _, List}) when is_list(List) -> maps:from_list(eval_literal(List));
eval_literal({Left, Right}) -> {eval_literal(Left), eval_literal(Right)};
eval_literal([_ | _] = List) -> lists:map(fun eval_literal/1, List);
eval_literal(Other) when is_binary(Other); is_atom(Other); is_number(Other); is_pid(Other); Other == [] -> Other.

validate_kv(Meta, KV, Original, #{context := Context} = E) ->
lists:foldl(fun
({K, _V}, {Index, Used}) ->
Expand Down
17 changes: 14 additions & 3 deletions lib/elixir/src/elixir_quote.erl
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,20 @@ escape_map_key_value(K, V, Map, Q) ->
end,
if
is_reference(MaybeRef) ->
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
ExpandCompile = case Map of
#{'__struct__' := Module} ->
code:ensure_loaded(Module) == {module, Module} andalso erlang:function_exported(Module, '__expand_compile__', 2);
_ ->
false
end,
case ExpandCompile of
true ->
{do_quote(K, Q), '__expand_compile__'};
false ->
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>)
end;
true ->
{do_quote(K, Q), do_quote(V, Q)}
end.
Expand Down
17 changes: 16 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,26 @@ 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,
opts: [],
re_pattern: :__expand_compile__,
source: "foo"
]
} = Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
end
end

describe "expand_once/2" do
Expand Down