diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 6322b8530bc..ac40b4a0065 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -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} @@ -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 @@ -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) @@ -2441,6 +2437,10 @@ defmodule Code.Formatter do meta[:closing][:line] || @min_line end + defp escape_atom(string, char) do + String.replace(string, <>, <>) + end + ## Algebra helpers # Relying on the inner document is brittle and error prone. diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 32189dfc972..b77e056de6b 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -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 diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 6a1923eaf03..312c697c66b 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -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 @@ -2376,14 +2385,14 @@ 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 \\ []) - def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do + def inspect_atom(:literal, atom, _opts) 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 inspect_atom(:literal, atom, opts) when is_atom(atom) do binary = Atom.to_string(atom) case classify_atom(atom) do @@ -2395,7 +2404,7 @@ defmodule Macro do end :quoted -> - {escaped, _} = Code.Identifier.escape(binary, ?") + escaped = inspect_atom_escape(opts, binary, ?") IO.iodata_to_binary([?:, ?", escaped, ?"]) _ -> @@ -2403,7 +2412,7 @@ defmodule Macro do end end - def inspect_atom(:key, atom) when is_atom(atom) do + def inspect_atom(:key, atom, opts) when is_atom(atom) do binary = Atom.to_string(atom) case classify_atom(atom) do @@ -2411,7 +2420,7 @@ defmodule Macro do IO.iodata_to_binary([?", binary, ?", ?:]) :quoted -> - {escaped, _} = Code.Identifier.escape(binary, ?") + escaped = inspect_atom_escape(opts, binary, ?") IO.iodata_to_binary([?", escaped, ?", ?:]) _ -> @@ -2419,7 +2428,7 @@ defmodule Macro do end end - def inspect_atom(:remote_call, atom) when is_atom(atom) do + def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do binary = Atom.to_string(atom) case inner_classify(atom) do @@ -2431,13 +2440,22 @@ defmodule Macro do if type in [:not_callable, :alias] do binary else - elem(Code.Identifier.escape(binary, ?"), 0) + inspect_atom_escape(opts, binary, ?") end IO.iodata_to_binary([?", escaped, ?"]) end end + defp inspect_atom_escape(opts, string, char) do + if escape = opts[:escape] do + escape.(string, char) + else + {escaped, _} = Code.Identifier.escape(string, char) + escaped + end + end + # Classifies the given atom into one of the following categories: # # * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 4a81819d044..45ee82c244e 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -914,7 +914,9 @@ 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), TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens), diff --git a/lib/elixir/test/elixir/code_formatter/calls_test.exs b/lib/elixir/test/elixir/code_formatter/calls_test.exs index 6dc9925efe9..4df2138a9a2 100644 --- a/lib/elixir/test/elixir/code_formatter/calls_test.exs +++ b/lib/elixir/test/elixir/code_formatter/calls_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/code_formatter/operators_test.exs b/lib/elixir/test/elixir/code_formatter/operators_test.exs index ec613a7ff2e..255553d38a6 100644 --- a/lib/elixir/test/elixir/code_formatter/operators_test.exs +++ b/lib/elixir/test/elixir/code_formatter/operators_test.exs @@ -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)"