Skip to content

Commit 457d52c

Browse files
sabiwarajosevalim
authored andcommitted
Add 'E' modifier to Regex for :export option (#14907)
1 parent 6684dbd commit 457d52c

File tree

7 files changed

+94
-3
lines changed

7 files changed

+94
-3
lines changed

lib/elixir/lib/inspect.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ defimpl Inspect, for: Regex do
569569
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
570570
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
571571
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
572+
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
572573
defp translate_options([], acc), do: acc
573574
defp translate_options(_t, _acc), do: :error
574575

lib/elixir/lib/regex.ex

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ defmodule Regex do
101101
* `:ungreedy` (U) - inverts the "greediness" of the regexp
102102
(the previous `r` option is deprecated in favor of `U`)
103103
104+
* `:export` (E) (since Elixir 1.20) - uses an exported pattern
105+
which can be shared across nodes or through config, at the cost of a runtime
106+
overhead every time to re-import it every time it is executed.
107+
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
108+
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
109+
and do not need to be exported in order to be shared in these versions.
110+
104111
## Captures
105112
106113
Many functions in this module handle what to capture in a regex
@@ -515,7 +522,7 @@ defmodule Regex do
515522
"""
516523
@spec names(t) :: [String.t()]
517524
def names(%Regex{re_pattern: re_pattern}) do
518-
{:namelist, names} = :re.inspect(re_pattern, :namelist)
525+
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
519526
names
520527
end
521528

@@ -585,10 +592,16 @@ defmodule Regex do
585592
%Regex{source: source, opts: compile_opts} = regex
586593
:re.run(string, source, compile_opts ++ options)
587594
else
588-
_ -> :re.run(string, re_pattern, options)
595+
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
589596
end
590597
end
591598

599+
@compile {:inline, maybe_import_pattern: 1}
600+
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
601+
do: :re.import(exported)
602+
603+
defp maybe_import_pattern(pattern), do: pattern
604+
592605
@typedoc """
593606
Options for regex functions that capture matches.
594607
"""
@@ -1007,6 +1020,16 @@ defmodule Regex do
10071020
translate_options(t, [:ungreedy | acc])
10081021
end
10091022

1023+
defp translate_options(<<?E, t::binary>>, acc) do
1024+
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
1025+
# (regexes aren't using references and can be shared across nodes or stored in config)
1026+
# TODO: remove this check on Erlang/OTP 28+ and update docs
1027+
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
1028+
true -> translate_options(t, [:export | acc])
1029+
false -> translate_options(t, acc)
1030+
end
1031+
end
1032+
10101033
defp translate_options(<<>>, acc), do: acc
10111034
defp translate_options(t, _acc), do: {:error, t}
10121035

@@ -1022,6 +1045,9 @@ defmodule Regex do
10221045
:erlang.system_info(:otp_release) < [?2, ?8] ->
10231046
Macro.escape(regex.re_pattern)
10241047

1048+
:lists.member(:export, regex.opts) ->
1049+
Macro.escape(regex.re_pattern)
1050+
10251051
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
10261052
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
10271053
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)

lib/elixir/test/elixir/inspect_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,11 @@ defmodule Inspect.OthersTest do
906906
assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])'
907907
end
908908

909+
@tag :re_import
910+
test "exported regex" do
911+
assert inspect(~r/foo/E) == "~r/foo/E"
912+
end
913+
909914
test "inspect_fun" do
910915
fun = fn
911916
integer, _opts when is_integer(integer) ->

lib/elixir/test/elixir/regex_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ defmodule RegexTest do
5252
assert Regex.match?(~r/^b$/m, "a\nb\nc")
5353
end
5454

55+
@tag :re_import
56+
test "export" do
57+
# exported patterns have no structs, so these are structurally equal
58+
assert ~r/foo/E == Regex.compile!("foo", [:export])
59+
60+
assert Regex.match?(~r/foo/E, "foo")
61+
refute Regex.match?(~r/foo/E, "Foo")
62+
63+
assert Regex.run(~r/c(d)/E, "abcd") == ["cd", "d"]
64+
assert Regex.run(~r/e/E, "abcd") == nil
65+
66+
assert Regex.names(~r/(?<FOO>foo)/E) == ["FOO"]
67+
end
68+
5569
test "precedence" do
5670
assert {"aa", :unknown} |> elem(0) =~ ~r/(a)\1/
5771
end

lib/mix/lib/mix/tasks/compile.app.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ defmodule Mix.Tasks.Compile.App do
221221
end
222222
end
223223

224+
defp to_erl_term(%Regex{re_pattern: {:re_pattern, _, _, _, ref}} = regex)
225+
when is_reference(ref) do
226+
Mix.raise("""
227+
\"def application\" has a term which cannot be written to .app files: #{inspect(regex)}.
228+
Use the E modifier to store regexes in application config.
229+
""")
230+
end
231+
224232
defp to_erl_term(map) when is_map(map) do
225233
inner =
226234
Enum.map_intersperse(

lib/mix/test/mix/tasks/compile.app_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,33 @@ defmodule Mix.Tasks.Compile.AppTest do
263263
end)
264264
end
265265

266+
@tag :re_import
267+
test "accepts only regexes without a reference" do
268+
in_fixture("no_mixfile", fn ->
269+
Mix.Project.push(CustomProject)
270+
271+
Process.put(:application, env: [regex: ~r/foo/])
272+
273+
message = """
274+
"def application" has a term which cannot be written to \.app files: ~r\/foo\/.
275+
Use the E modifier to store regexes in application config.
276+
"""
277+
278+
assert_raise Mix.Error, message, fn ->
279+
Mix.Tasks.Compile.App.run([])
280+
end
281+
282+
Process.put(:application, env: [exported: ~r/foo/E])
283+
284+
Mix.Tasks.Compile.Elixir.run([])
285+
Mix.Tasks.Compile.App.run([])
286+
287+
properties = parse_resource_file(:custom_project)
288+
289+
assert properties[:env] == [exported: ~r/foo/E]
290+
end)
291+
end
292+
266293
test ".app contains description and registered (as required by systools)" do
267294
in_fixture("no_mixfile", fn ->
268295
Mix.Project.push(MixTest.Case.Sample)

lib/mix/test/test_helper.exs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,22 @@ cover_exclude =
4343
[]
4444
end
4545

46+
# OTP 28.1+
47+
re_import_exclude =
48+
if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
49+
[]
50+
else
51+
[:re_import]
52+
end
53+
4654
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
4755
CoverageRecorder.maybe_record("mix")
4856

4957
ExUnit.start(
5058
trace: !!System.get_env("TRACE"),
51-
exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude,
59+
exclude:
60+
epmd_exclude ++
61+
os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude,
5262
include: line_include,
5363
assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300"))
5464
)

0 commit comments

Comments
 (0)