Skip to content

Commit bf77434

Browse files
committed
Without cheating: defining __escape__/1
1 parent f47e748 commit bf77434

File tree

4 files changed

+60
-40
lines changed

4 files changed

+60
-40
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6648,8 +6648,7 @@ defmodule Kernel do
66486648
defp compile_regex(binary_or_tuple, options) do
66496649
bin_opts = :binary.list_to_bin(options)
66506650

6651-
# TODO: Remove this when we require Erlang/OTP 28.1+
6652-
case is_binary(binary_or_tuple) and compile_time_regexes_supported?() do
6651+
case is_binary(binary_or_tuple) do
66536652
true ->
66546653
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
66556654

@@ -6658,14 +6657,6 @@ defmodule Kernel do
66586657
end
66596658
end
66606659

6661-
defp compile_time_regexes_supported? do
6662-
# OTP 28.0 introduced refs in patterns, which can't be used in AST anymore
6663-
# OTP 28.1 introduced :re.import/1 which allows us to fix this in Macro.escape
6664-
:erlang.system_info(:otp_release) < [?2, ?8] or
6665-
(Code.ensure_loaded?(:re) and
6666-
function_exported?(:re, :import, 1))
6667-
end
6668-
66696660
@doc ~S"""
66706661
Handles the sigil `~D` for dates.
66716662

lib/elixir/lib/regex.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,4 +1000,45 @@ defmodule Regex do
10001000

10011001
defp translate_options(<<>>, acc), do: acc
10021002
defp translate_options(t, _acc), do: {:error, t}
1003+
1004+
@doc false
1005+
def __escape__(%{__struct__: Regex} = regex) do
1006+
# OTP 28.0 introduced refs in patterns, which can't be used in AST anymore
1007+
# OTP 28.1 introduced :re.import/1 which allows us to work with pre-compiled binaries again
1008+
1009+
pattern_ast =
1010+
cond do
1011+
# TODO: Remove this when we require Erlang/OTP 28+
1012+
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
1013+
:erlang.system_info(:otp_release) < [?2, ?8] ->
1014+
Macro.escape(regex.re_pattern)
1015+
1016+
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
1017+
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
1018+
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
1019+
1020+
quote do
1021+
:re.import(unquote(Macro.escape(exported)))
1022+
end
1023+
1024+
# TODO: Remove this when we require Erlang/OTP 28.1+
1025+
# OTP 28.0 works in degraded mode performance-wise, we need to recompile from the source
1026+
true ->
1027+
quote do
1028+
{:ok, pattern} =
1029+
:re.compile(unquote(Macro.escape(regex.source)), unquote(Macro.escape(regex.opts)))
1030+
1031+
pattern
1032+
end
1033+
end
1034+
1035+
quote do
1036+
%{
1037+
__struct__: unquote(Regex),
1038+
re_pattern: unquote(pattern_ast),
1039+
source: unquote(Macro.escape(regex.source)),
1040+
opts: unquote(Macro.escape(regex.opts))
1041+
}
1042+
end
1043+
end
10031044
end

lib/elixir/src/elixir_quote.erl

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
%% SPDX-FileCopyrightText: 2012 Plataformatec
44

55
-module(elixir_quote).
6+
7+
-feature(maybe_expr, enable).
8+
69
-export([escape/3, linify/3, linify_with_context_counter/3, build/7, quote/2, has_unquotes/1, fun_to_quoted/1]).
710
-export([dot/5, tail_list/3, list/2, validate_runtime/2, shallow_validate_ast/1]). %% Quote callbacks
811

@@ -168,28 +171,18 @@ do_escape(BitString, _) when is_bitstring(BitString) ->
168171
{'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], nil}]}]}
169172
end;
170173

171-
do_escape(#{
172-
'__struct__' := 'Elixir.Regex',
173-
're_pattern' := {re_pattern, _, _, _, Ref},
174-
'source' := Source,
175-
'opts' := Opts
176-
} = Map, Q) when is_reference(Ref), is_binary(Source), is_list(Opts) ->
177-
case erlang:function_exported(re, import, 1) of
178-
true ->
179-
{ok, ExportedPattern} = re:compile(Source, [export | Opts]),
180-
PatternAst = {{'.', [], ['re', 'import']}, [], [do_escape(ExportedPattern, Q)]},
181-
{'%{}', [], [
182-
{'__struct__', 'Elixir.Regex'},
183-
{'re_pattern', PatternAst},
184-
{'source', Source},
185-
{'opts', do_escape(Opts, Q)}
186-
]};
187-
false ->
188-
escape_map(Map, Q)
189-
end;
190-
191174
do_escape(Map, Q) when is_map(Map) ->
192-
escape_map(Map, Q);
175+
maybe
176+
#{'__struct__' := Module} ?= Map,
177+
true ?= is_atom(Module),
178+
{module, Module} ?= code:ensure_loaded(Module),
179+
true ?= erlang:function_exported(Module, '__escape__', 1),
180+
Module:'__escape__'(Map)
181+
else
182+
_ ->
183+
TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))],
184+
{'%{}', [], TT}
185+
end;
193186

194187
do_escape([], _) ->
195188
[];
@@ -222,10 +215,6 @@ do_escape(Fun, _) when is_function(Fun) ->
222215
do_escape(Other, _) ->
223216
bad_escape(Other).
224217

225-
escape_map(Map, Q) ->
226-
TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))],
227-
{'%{}', [], TT}.
228-
229218
escape_map_key_value(K, V, Map, Q) ->
230219
MaybeRef = if
231220
is_reference(V) -> V;
@@ -239,7 +228,7 @@ escape_map_key_value(K, V, Map, Q) ->
239228
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
240229
true ->
241230
{do_quote(K, Q), do_quote(V, Q)}
242-
end.
231+
end.
243232

244233
find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil;
245234
find_tuple_ref(Tuple, Index) ->

lib/elixir/test/elixir/macro_test.exs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,9 @@ defmodule MacroTest do
141141
assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]}
142142
end
143143

144-
@tag skip: System.otp_release() < "28" or function_exported?(:re, :import, 1)
145144
test "escape container when a reference cannot be escaped" do
146-
assert_raise ArgumentError, ~r"~r/foo/ contains a reference", fn ->
147-
Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
145+
assert_raise ArgumentError, ~r"contains a reference", fn ->
146+
Macro.escape(%{re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
148147
end
149148
end
150149

@@ -160,7 +159,7 @@ defmodule MacroTest do
160159
source: "foo",
161160
opts: []
162161
]
163-
} = Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
162+
} = Macro.escape(~r/foo/)
164163
end
165164
end
166165

0 commit comments

Comments
 (0)