Skip to content

Commit e54b87c

Browse files
Fix formatter adding extra escapes to remote call functions (#13960)
1 parent ef56e81 commit e54b87c

File tree

6 files changed

+46
-24
lines changed

6 files changed

+46
-24
lines changed

lib/elixir/lib/code/formatter.ex

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -582,15 +582,9 @@ defmodule Code.Formatter do
582582
{left, state} =
583583
case left_arg do
584584
{:__block__, _, [atom]} when is_atom(atom) ->
585-
iodata =
586-
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
587-
[Atom.to_string(atom), ?:]
588-
else
589-
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
590-
end
591-
592-
{iodata
593-
|> IO.iodata_to_binary()
585+
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
586+
587+
{formatted
594588
|> string()
595589
|> color_doc(:atom, state.inspect_opts), state}
596590

@@ -1010,7 +1004,7 @@ defmodule Code.Formatter do
10101004
)
10111005
when is_atom(fun) and is_integer(arity) do
10121006
{target_doc, state} = remote_target_to_algebra(target, state)
1013-
fun = Macro.inspect_atom(:remote_call, fun)
1007+
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
10141008
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
10151009
end
10161010

@@ -1057,7 +1051,9 @@ defmodule Code.Formatter do
10571051
{target_doc, state} = remote_target_to_algebra(target, state)
10581052

10591053
fun_doc =
1060-
Macro.inspect_atom(:remote_call, fun) |> string() |> color_doc(:call, state.inspect_opts)
1054+
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
1055+
|> string()
1056+
|> color_doc(:call, state.inspect_opts)
10611057

10621058
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
10631059

@@ -2441,6 +2437,10 @@ defmodule Code.Formatter do
24412437
meta[:closing][:line] || @min_line
24422438
end
24432439

2440+
defp escape_atom(string, char) do
2441+
String.replace(string, <<char>>, <<?\\, char>>)
2442+
end
2443+
24442444
## Algebra helpers
24452445

24462446
# Relying on the inner document is brittle and error prone.

lib/elixir/lib/code/normalizer.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ defmodule Code.Normalizer do
126126
{:., meta, [Access, :get]}
127127
end
128128

129-
# Only normalize the left side of the dot operator
130129
# The right hand side is an atom in the AST but it's not an atom literal, so
131-
# it should not be wrapped
132-
defp do_normalize({:., meta, [left, right]}, state) do
130+
# it should not be wrapped. However, it should be escaped if applicable.
131+
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
133132
meta = patch_meta_line(meta, state.parent_meta)
134133

135134
left = do_normalize(left, %{state | parent_meta: meta})
135+
right = maybe_escape_literal(right, state)
136136

137137
{:., meta, [left, right]}
138138
end

lib/elixir/lib/macro.ex

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,6 +2328,15 @@ defmodule Macro do
23282328
as a key (`:key`), or as the function name of a remote call
23292329
(`:remote_call`).
23302330
2331+
## Options
2332+
2333+
* `:escape` - a two-arity function used to escape a quoted
2334+
atom content, if necessary. The function receives the atom
2335+
content as string and a quote delimiter character, which
2336+
should always be escaped. By default the content is escaped
2337+
such that the inspected sequence would be parsed as the
2338+
given atom.
2339+
23312340
## Examples
23322341
23332342
### As a literal
@@ -2376,14 +2385,14 @@ defmodule Macro do
23762385
23772386
"""
23782387
@doc since: "1.14.0"
2379-
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
2380-
def inspect_atom(source_format, atom)
2388+
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
2389+
def inspect_atom(source_format, atom, opts \\ [])
23812390

2382-
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
2391+
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
23832392
Atom.to_string(atom)
23842393
end
23852394

2386-
def inspect_atom(:literal, atom) when is_atom(atom) do
2395+
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
23872396
binary = Atom.to_string(atom)
23882397

23892398
case classify_atom(atom) do
@@ -2395,31 +2404,31 @@ defmodule Macro do
23952404
end
23962405

23972406
:quoted ->
2398-
{escaped, _} = Code.Identifier.escape(binary, ?")
2407+
escaped = inspect_atom_escape(opts, binary, ?")
23992408
IO.iodata_to_binary([?:, ?", escaped, ?"])
24002409

24012410
_ ->
24022411
":" <> binary
24032412
end
24042413
end
24052414

2406-
def inspect_atom(:key, atom) when is_atom(atom) do
2415+
def inspect_atom(:key, atom, opts) when is_atom(atom) do
24072416
binary = Atom.to_string(atom)
24082417

24092418
case classify_atom(atom) do
24102419
:alias ->
24112420
IO.iodata_to_binary([?", binary, ?", ?:])
24122421

24132422
:quoted ->
2414-
{escaped, _} = Code.Identifier.escape(binary, ?")
2423+
escaped = inspect_atom_escape(opts, binary, ?")
24152424
IO.iodata_to_binary([?", escaped, ?", ?:])
24162425

24172426
_ ->
24182427
IO.iodata_to_binary([binary, ?:])
24192428
end
24202429
end
24212430

2422-
def inspect_atom(:remote_call, atom) when is_atom(atom) do
2431+
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
24232432
binary = Atom.to_string(atom)
24242433

24252434
case inner_classify(atom) do
@@ -2431,13 +2440,22 @@ defmodule Macro do
24312440
if type in [:not_callable, :alias] do
24322441
binary
24332442
else
2434-
elem(Code.Identifier.escape(binary, ?"), 0)
2443+
inspect_atom_escape(opts, binary, ?")
24352444
end
24362445

24372446
IO.iodata_to_binary([?", escaped, ?"])
24382447
end
24392448
end
24402449

2450+
defp inspect_atom_escape(opts, string, char) do
2451+
if escape = opts[:escape] do
2452+
escape.(string, char)
2453+
else
2454+
{escaped, _} = Code.Identifier.escape(string, char)
2455+
escaped
2456+
end
2457+
end
2458+
24412459
# Classifies the given atom into one of the following categories:
24422460
#
24432461
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on

lib/elixir/src/elixir_tokenizer.erl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,9 @@ handle_dot([$., H | T] = Original, Line, Column, DotInfo, Scope, Tokens) when ?i
914914
InterScope
915915
end,
916916

917-
case unsafe_to_atom(Part, Line, Column, NewScope) of
917+
{ok, [UnescapedPart]} = unescape_tokens([Part], Line, Column, NewScope),
918+
919+
case unsafe_to_atom(UnescapedPart, Line, Column, NewScope) of
918920
{ok, Atom} ->
919921
Token = check_call_identifier(Line, Column, Part, Atom, Rest),
920922
TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens),

lib/elixir/test/elixir/code_formatter/calls_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ defmodule Code.Formatter.CallsTest do
595595
assert_same ~S[Kernel.+(1, 2)]
596596
assert_same ~S[:erlang.+(1, 2)]
597597
assert_same ~S[foo."bar baz"(1, 2)]
598+
assert_same ~S[foo."bar\nbaz"(1, 2)]
598599
end
599600

600601
test "splits on arguments and dot on line limit" do

lib/elixir/test/elixir/code_formatter/operators_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,7 @@ defmodule Code.Formatter.OperatorsTest do
948948
assert_format "&(Mod.foo/1)", "&Mod.foo/1"
949949
assert_format "&(Mod.++/1)", "&Mod.++/1"
950950
assert_format ~s[&(Mod."foo bar"/1)], ~s[&Mod."foo bar"/1]
951+
assert_format ~S[&(Mod."foo\nbar"/1)], ~S[&Mod."foo\nbar"/1]
951952

952953
# Invalid
953954
assert_format "& Mod.foo/bar", "&(Mod.foo() / bar)"

0 commit comments

Comments
 (0)