Skip to content

Commit ec65f8d

Browse files
devonestesericmj
authored andcommitted
Always capture delimiter metadata for sigils (#9671)
This makes it so we're always capturing delimiter metadata for sigils instead of requiring the `token_metadata` option. This means that we can now show the correct sigils in ExUnit diffs (and elsewhere) for code examples.
1 parent f0fbf02 commit ec65f8d

File tree

5 files changed

+96
-55
lines changed

5 files changed

+96
-55
lines changed

lib/elixir/lib/macro.ex

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,13 @@ defmodule Macro do
10071007
false
10081008
end
10091009

1010-
defp interpolate({:<<>>, _, parts}, fun) do
1010+
defp interpolate(ast, fun), do: interpolate(ast, "\"", "\"", fun)
1011+
1012+
defp interpolate({:<<>>, _, [parts]}, left, right, _) when left in [~s["""\n], ~s['''\n]] do
1013+
<<left::binary, parts::binary, right::binary>>
1014+
end
1015+
1016+
defp interpolate({:<<>>, _, parts}, left, right, fun) do
10111017
parts =
10121018
Enum.map_join(parts, "", fn
10131019
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [arg]}, {:binary, _, _}]} ->
@@ -1018,9 +1024,16 @@ defmodule Macro do
10181024
binary_part(binary, 1, byte_size(binary) - 2)
10191025
end)
10201026

1021-
<<?", parts::binary, ?">>
1027+
escaped = escape_sigil(parts, left)
1028+
<<left::binary, escaped::binary, right::binary>>
10221029
end
10231030

1031+
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
1032+
defp escape_sigil(parts, "{"), do: String.replace(parts, "}", ~S"\}")
1033+
defp escape_sigil(parts, "["), do: String.replace(parts, "]", ~S"\]")
1034+
defp escape_sigil(parts, "<"), do: String.replace(parts, ">", ~S"\>")
1035+
defp escape_sigil(parts, delimiter), do: String.replace(parts, delimiter, "\\#{delimiter}")
1036+
10241037
defp module_to_string(atom, _fun) when is_atom(atom) do
10251038
inspect_no_limit(atom)
10261039
end
@@ -1080,25 +1093,22 @@ defmodule Macro do
10801093
:error
10811094
end
10821095

1083-
defp sigil_call({sigil, _, [{:<<>>, _, _} = parts, args]} = ast, fun)
1096+
defp sigil_call({sigil, meta, [{:<<>>, _, _} = parts, args]} = ast, fun)
10841097
when is_atom(sigil) and is_list(args) do
1098+
delimiter = Keyword.get(meta, :delimiter, "\"")
1099+
{left, right} = delimiter_pair(delimiter)
1100+
10851101
case Atom.to_string(sigil) do
10861102
<<"sigil_", name>> when name >= ?A and name <= ?Z ->
1103+
args = sigil_args(args, fun)
10871104
{:<<>>, _, [binary]} = parts
1088-
1089-
formatted =
1090-
if :binary.last(binary) == ?\n do
1091-
binary = String.replace(binary, ~s["""], ~s["\\""])
1092-
<<?~, name, ~s["""\n], binary::binary, ~s["""], sigil_args(args, fun)::binary>>
1093-
else
1094-
{left, right} = select_sigil_container(binary)
1095-
<<?~, name, left, binary::binary, right, sigil_args(args, fun)::binary>>
1096-
end
1097-
1105+
formatted = <<?~, name, left::binary, binary::binary, right::binary, args::binary>>
10981106
{:ok, fun.(ast, formatted)}
10991107

11001108
<<"sigil_", name>> when name >= ?a and name <= ?z ->
1101-
{:ok, fun.(ast, "~" <> <<name>> <> interpolate(parts, fun) <> sigil_args(args, fun))}
1109+
args = sigil_args(args, fun)
1110+
formatted = "~" <> <<name>> <> interpolate(parts, left, right, fun) <> args
1111+
{:ok, fun.(ast, formatted)}
11021112

11031113
_ ->
11041114
:error
@@ -1109,17 +1119,13 @@ defmodule Macro do
11091119
:error
11101120
end
11111121

1112-
defp select_sigil_container(binary) do
1113-
cond do
1114-
:binary.match(binary, ["\""]) == :nomatch -> {?", ?"}
1115-
:binary.match(binary, ["\'"]) == :nomatch -> {?', ?'}
1116-
:binary.match(binary, ["(", ")"]) == :nomatch -> {?(, ?)}
1117-
:binary.match(binary, ["[", "]"]) == :nomatch -> {?[, ?]}
1118-
:binary.match(binary, ["{", "}"]) == :nomatch -> {?{, ?}}
1119-
:binary.match(binary, ["<", ">"]) == :nomatch -> {?<, ?>}
1120-
true -> {?/, ?/}
1121-
end
1122-
end
1122+
defp delimiter_pair("["), do: {"[", "]"}
1123+
defp delimiter_pair("{"), do: {"{", "}"}
1124+
defp delimiter_pair("("), do: {"(", ")"}
1125+
defp delimiter_pair("<"), do: {"<", ">"}
1126+
defp delimiter_pair("\"\"\""), do: {"\"\"\"\n", "\"\"\""}
1127+
defp delimiter_pair("'''"), do: {"'''\n", "'''"}
1128+
defp delimiter_pair(str), do: {str, str}
11231129

11241130
defp sigil_args([], _fun), do: ""
11251131
defp sigil_args(args, fun), do: fun.(args, List.to_string(args))

lib/elixir/src/elixir_parser.yrl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -868,10 +868,7 @@ build_access(Expr, {List, Location}) ->
868868

869869
build_sigil({sigil, Location, Sigil, Parts, Modifiers, Delimiter}) ->
870870
Meta = meta_from_location(Location),
871-
MetaWithDelimiter = case ?token_metadata() of
872-
true -> [{delimiter, Delimiter} | Meta];
873-
false -> Meta
874-
end,
871+
MetaWithDelimiter = [{delimiter, Delimiter} | Meta],
875872
{list_to_atom("sigil_" ++ [Sigil]),
876873
MetaWithDelimiter,
877874
[{'<<>>', Meta, string_parts(Parts)}, Modifiers]}.

lib/elixir/test/elixir/code_test.exs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,27 @@ defmodule CodeTest do
236236
Code.string_to_quoted!("1 +")
237237
end
238238
end
239+
240+
test "delimiter information for sigils is included" do
241+
string_to_quoted = &Code.string_to_quoted!(&1, token_metadata: false)
242+
243+
assert string_to_quoted.("~r/foo/") ==
244+
{:sigil_r, [delimiter: "/", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
245+
246+
assert string_to_quoted.("~r[foo]") ==
247+
{:sigil_r, [delimiter: "[", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
248+
249+
assert string_to_quoted.("~r\"foo\"") ==
250+
{:sigil_r, [delimiter: "\"", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
251+
252+
meta = [delimiter: "\"\"\"", line: 1]
253+
args = {:sigil_S, meta, [{:<<>>, [line: 1], ["sigil heredoc\n"]}, []]}
254+
assert string_to_quoted.("~S\"\"\"\nsigil heredoc\n\"\"\"") == args
255+
256+
meta = [delimiter: "'''", line: 1]
257+
args = {:sigil_S, meta, [{:<<>>, [line: 1], ["sigil heredoc\n"]}, []]}
258+
assert string_to_quoted.("~S'''\nsigil heredoc\n'''") == args
259+
end
239260
end
240261

241262
describe "string_to_quoted/2 with :token_metadata" do
@@ -301,27 +322,6 @@ defmodule CodeTest do
301322
{:foo, [do: [line: 2], end: [line: 3], newlines: 1, closing: [line: 2], line: 1],
302323
[[do: {:__block__, [], []}]]}
303324
end
304-
305-
test "adds delimiter information to sigils" do
306-
string_to_quoted = &Code.string_to_quoted!(&1, token_metadata: true)
307-
308-
assert string_to_quoted.("~r/foo/") ==
309-
{:sigil_r, [delimiter: "/", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
310-
311-
assert string_to_quoted.("~r[foo]") ==
312-
{:sigil_r, [delimiter: "[", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
313-
314-
assert string_to_quoted.("~r\"foo\"") ==
315-
{:sigil_r, [delimiter: "\"", line: 1], [{:<<>>, [line: 1], ["foo"]}, []]}
316-
317-
meta = [delimiter: "\"\"\"", line: 1]
318-
args = {:sigil_S, meta, [{:<<>>, [line: 1], ["sigil heredoc\n"]}, []]}
319-
assert string_to_quoted.("~S\"\"\"\nsigil heredoc\n\"\"\"") == args
320-
321-
meta = [delimiter: "'''", line: 1]
322-
args = {:sigil_S, meta, [{:<<>>, [line: 1], ["sigil heredoc\n"]}, []]}
323-
assert string_to_quoted.("~S'''\nsigil heredoc\n'''") == args
324-
end
325325
end
326326

327327
describe "string_to_quoted/2 with :literal_encoder" do

lib/elixir/test/elixir/macro_test.exs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,18 +327,56 @@ defmodule MacroTest do
327327

328328
test "sigil call" do
329329
assert Macro.to_string(quote(do: ~r"123")) == ~S/~r"123"/
330-
assert Macro.to_string(quote(do: ~r"123"u)) == ~S/~r"123"u/
331330
assert Macro.to_string(quote(do: ~r"\n123")) == ~S/~r"\\n123"/
331+
assert Macro.to_string(quote(do: ~r"12\"3")) == ~S/~r"12\\"3"/
332+
assert Macro.to_string(quote(do: ~r/12\/3/u)) == ~S"~r/12\/3/u"
333+
assert Macro.to_string(quote(do: ~r{\n123})) == ~S/~r{\\n123}/
334+
assert Macro.to_string(quote(do: ~r((1\)(2\)3))) == ~S/~r((1\)(2\)3)/
335+
assert Macro.to_string(quote(do: ~r{\n1{1\}23})) == ~S/~r{\\n1{1\}23}/
336+
assert Macro.to_string(quote(do: ~r|12\|3|)) == ~S"~r|12\|3|"
332337

333-
assert Macro.to_string(quote(do: ~r"1#{two}3")) == ~S/~r"1#{two}3"/
334-
assert Macro.to_string(quote(do: ~r"1#{two}3"u)) == ~S/~r"1#{two}3"u/
338+
assert Macro.to_string(quote(do: ~r[1#{two}3])) == ~S/~r[1#{two}3]/
339+
assert Macro.to_string(quote(do: ~r[1[#{two}\]3])) == ~S/~r[1[#{two}\]3]/
340+
assert Macro.to_string(quote(do: ~r'1#{two}3'u)) == ~S/~r'1#{two}3'u/
335341

336342
assert Macro.to_string(quote(do: ~R"123")) == ~S/~R"123"/
337343
assert Macro.to_string(quote(do: ~R"123"u)) == ~S/~R"123"u/
338344
assert Macro.to_string(quote(do: ~R"\n123")) == ~S/~R"\n123"/
339345

340346
assert Macro.to_string(quote(do: ~S["'(123)'"])) == ~S/~S["'(123)'"]/
341347

348+
assert Macro.to_string(
349+
quote do
350+
~s"""
351+
"\""foo"\""
352+
"""
353+
end
354+
) == ~s[~s"""\n"\\""foo"\\""\n"""]
355+
356+
assert Macro.to_string(
357+
quote do
358+
~s'''
359+
'\''foo'\''
360+
'''
361+
end
362+
) == ~s[~s'''\n'\\''foo'\\''\n''']
363+
364+
assert Macro.to_string(
365+
quote do
366+
~s"""
367+
"\"foo\""
368+
"""
369+
end
370+
) == ~s[~s"""\n"\\"foo\\""\n"""]
371+
372+
assert Macro.to_string(
373+
quote do
374+
~s'''
375+
'\"foo\"'
376+
'''
377+
end
378+
) == ~s[~s'''\n'\\"foo\\"'\n''']
379+
342380
assert Macro.to_string(
343381
quote do
344382
~S"""

lib/ex_unit/test/ex_unit/diff_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ defmodule ExUnit.DiffTest do
557557
test "structs with inspect" do
558558
refute_diff(
559559
~D[2017-10-01] = ~D[2017-10-02],
560-
~s/-~D"2017-10-01"-/,
560+
~s/-~D[2017-10-01]-/,
561561
"~D[2017-10-0+2+]"
562562
)
563563

@@ -569,7 +569,7 @@ defmodule ExUnit.DiffTest do
569569

570570
refute_diff(
571571
~D[2017-10-02] = "2017-10-01",
572-
~s/-~D"2017-10-02"-/,
572+
~s/-~D[2017-10-02]-/,
573573
~s/+"2017-10-01"+/
574574
)
575575
end

0 commit comments

Comments
 (0)