Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 12 additions & 11 deletions lib/elixir/lib/code/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -582,15 +582,9 @@ defmodule Code.Formatter do
{left, state} =
case left_arg do
{:__block__, _, [atom]} when is_atom(atom) ->
iodata =
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
[Atom.to_string(atom), ?:]
else
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
end

{iodata
|> IO.iodata_to_binary()
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)

{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}

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

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

fun_doc =
Macro.inspect_atom(:remote_call, fun) |> string() |> color_doc(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)

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

Expand Down Expand Up @@ -2441,6 +2437,11 @@ defmodule Code.Formatter do
meta[:closing][:line] || @min_line
end

defp escape_atom(string, char) do
char = List.to_string([char])
String.replace(string, char, "\\#{char}")
end

## Algebra helpers

# Relying on the inner document is brittle and error prone.
Expand Down
6 changes: 3 additions & 3 deletions lib/elixir/lib/code/normalizer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]}
end

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

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

{:., meta, [left, right]}
end
Expand Down
37 changes: 28 additions & 9 deletions lib/elixir/lib/macro.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2328,6 +2328,15 @@ defmodule Macro do
as a key (`:key`), or as the function name of a remote call
(`:remote_call`).

## Options

* `:escape` - a two-arity function used to escape a quoted
atom content, if necessary. The function receives the atom
content as string and a quote delimiter character, which
should always be escaped. By default the content is escaped
such that the inspected sequence would be parsed as the
given atom.

## Examples

### As a literal
Expand Down Expand Up @@ -2376,14 +2385,19 @@ defmodule Macro do

"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
def inspect_atom(source_format, atom)
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
def inspect_atom(source_format, atom, opts \\ []) do
opts = Keyword.validate!(opts, [:escape])

def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
escape = Keyword.get(opts, :escape, &inspect_atom_escape/2)
do_inspect_atom(source_format, atom, escape)
end

def do_inspect_atom(:literal, atom, _escape) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end

def inspect_atom(:literal, atom) when is_atom(atom) do
def do_inspect_atom(:literal, atom, escape) when is_atom(atom) do
binary = Atom.to_string(atom)

case classify_atom(atom) do
Expand All @@ -2395,31 +2409,31 @@ defmodule Macro do
end

:quoted ->
{escaped, _} = Code.Identifier.escape(binary, ?")
escaped = escape.(binary, ?")
IO.iodata_to_binary([?:, ?", escaped, ?"])

_ ->
":" <> binary
end
end

def inspect_atom(:key, atom) when is_atom(atom) do
def do_inspect_atom(:key, atom, escape) when is_atom(atom) do
binary = Atom.to_string(atom)

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

:quoted ->
{escaped, _} = Code.Identifier.escape(binary, ?")
escaped = escape.(binary, ?")
IO.iodata_to_binary([?", escaped, ?", ?:])

_ ->
IO.iodata_to_binary([binary, ?:])
end
end

def inspect_atom(:remote_call, atom) when is_atom(atom) do
def do_inspect_atom(:remote_call, atom, escape) when is_atom(atom) do
binary = Atom.to_string(atom)

case inner_classify(atom) do
Expand All @@ -2431,13 +2445,18 @@ defmodule Macro do
if type in [:not_callable, :alias] do
binary
else
elem(Code.Identifier.escape(binary, ?"), 0)
escape.(binary, ?")
end

IO.iodata_to_binary([?", escaped, ?"])
end
end

defp inspect_atom_escape(string, char) do
{escaped, _} = Code.Identifier.escape(string, char)
escaped
end

# Classifies the given atom into one of the following categories:
#
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
Expand Down
6 changes: 4 additions & 2 deletions lib/elixir/src/elixir_tokenizer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,11 @@ handle_dot([$., H | T] = Original, Line, Column, DotInfo, Scope, Tokens) when ?i
InterScope
end,

case unsafe_to_atom(Part, Line, Column, NewScope) of
{ok, [UnescapedPart]} = unescape_tokens([Part], Line, Column, NewScope),

case unsafe_to_atom(UnescapedPart, Line, Column, NewScope) of
{ok, Atom} ->
Token = check_call_identifier(Line, Column, Part, Atom, Rest),
Token = check_call_identifier(Line, Column, UnescapedPart, Atom, Rest),
TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens),
tokenize(Rest, NewLine, NewColumn, NewScope, [Token | TokensSoFar]);

Expand Down
1 change: 1 addition & 0 deletions lib/elixir/test/elixir/code_formatter/calls_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ defmodule Code.Formatter.CallsTest do
assert_same ~S[Kernel.+(1, 2)]
assert_same ~S[:erlang.+(1, 2)]
assert_same ~S[foo."bar baz"(1, 2)]
assert_same ~S[foo."bar\nbaz"(1, 2)]
end

test "splits on arguments and dot on line limit" do
Expand Down
1 change: 1 addition & 0 deletions lib/elixir/test/elixir/code_formatter/operators_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ defmodule Code.Formatter.OperatorsTest do
assert_format "&(Mod.foo/1)", "&Mod.foo/1"
assert_format "&(Mod.++/1)", "&Mod.++/1"
assert_format ~s[&(Mod."foo bar"/1)], ~s[&Mod."foo bar"/1]
assert_format ~S[&(Mod."foo\nbar"/1)], ~S[&Mod."foo\nbar"/1]

# Invalid
assert_format "& Mod.foo/bar", "&(Mod.foo() / bar)"
Expand Down
Loading