summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2021-12-05 15:02:06 +0100
committerGitHub <noreply@github.com>2021-12-05 15:02:06 +0100
commitc4527b3ff40ed4e93c4a7096e65b36748e07db69 (patch)
treee7b443f2b39a44c5cf7c6b12f05bbe4a0b727d54
parent74d61e0c8dfe99999edecbc4fc288eb2adaa0d21 (diff)
downloadelixir-c4527b3ff40ed4e93c4a7096e65b36748e07db69.tar.gz
Add Macro.classify_atom/1 and Macro.inspect_atom/2 (#11446)
Today the formatter still uses some private APIs and this commit aims to expose some of them. In particular, Macro.classify_atom/1 was added to expose if an atom is an alias, an identifier, quoted, or unquoted. Note that the API does not say anything about operators, since they can fall within three distinct categories: * unquoted and callable, such as, `+`, `-`, and most operators * unquoted and not callable, such as, `.`, `..`, and `..//`, which would be ambiguous when used as `Module.OP()` * quoted but callable, such as `::`, which is ambiguous in the atom syntax but not as calls This information still remains private, especially because most times operators are used, we often still want to learn about their precedence too. We also exposed `inspect_atom/2` with three distinct clauses to cover the different scenarios atoms can be inspected at runtime and in source code.
-rw-r--r--lib/elixir/lib/code/formatter.ex28
-rw-r--r--lib/elixir/lib/code/identifier.ex143
-rw-r--r--lib/elixir/lib/code/normalizer.ex2
-rw-r--r--lib/elixir/lib/exception.ex10
-rw-r--r--lib/elixir/lib/inspect.ex18
-rw-r--r--lib/elixir/lib/macro.ex313
-rw-r--r--lib/elixir/src/elixir_aliases.erl2
-rw-r--r--lib/elixir/src/elixir_overridable.erl2
-rw-r--r--lib/elixir/test/elixir/code_formatter/integration_test.exs2
-rw-r--r--lib/ex_unit/lib/ex_unit/diff.ex5
-rw-r--r--lib/iex/lib/iex/autocomplete.ex2
-rw-r--r--lib/logger/lib/logger/translator.ex2
-rw-r--r--lib/mix/lib/mix/tasks/compile.protocols.ex2
13 files changed, 311 insertions, 220 deletions
diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex
index de835e814..9532e2c1c 100644
--- a/lib/elixir/lib/code/formatter.ex
+++ b/lib/elixir/lib/code/formatter.ex
@@ -487,12 +487,10 @@ defmodule Code.Formatter do
{:__block__, _, [atom]} when is_atom(atom) ->
key =
- case Code.Identifier.classify(atom) do
- type when type in [:callable_local, :callable_operator, :not_callable] ->
- IO.iodata_to_binary([Atom.to_string(atom), ?:])
-
- _ ->
- IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:])
+ if Macro.classify_atom(atom) in [:identifier, :unquoted] do
+ IO.iodata_to_binary([Atom.to_string(atom), ?:])
+ else
+ IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:])
end
{string(key), state}
@@ -865,7 +863,7 @@ defmodule Code.Formatter do
# @foo(bar)
defp module_attribute_to_algebra(meta, {name, call_meta, [_] = args} = expr, context, state)
when is_atom(name) and name not in [:__block__, :__aliases__] do
- if Code.Identifier.classify(name) == :callable_local do
+ if Macro.classify_atom(name) == :identifier do
{{call_doc, state}, wrap_in_parens?} =
call_args_to_algebra(args, call_meta, context, :skip_unless_many_args, false, state)
@@ -910,7 +908,7 @@ defmodule Code.Formatter do
)
when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state)
- fun = Code.Identifier.inspect_as_function(fun)
+ fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -955,7 +953,7 @@ defmodule Code.Formatter do
defp remote_to_algebra({{:., _, [target, fun]}, meta, args}, context, state)
when is_atom(fun) do
{target_doc, state} = remote_target_to_algebra(target, state)
- fun = Code.Identifier.inspect_as_function(fun)
+ fun = Macro.inspect_atom(:remote_call, fun)
remote_doc = target_doc |> concat(".") |> concat(string(fun))
if args == [] and not remote_target_is_a_module?(target) and not meta?(meta, :closing) do
@@ -1495,12 +1493,10 @@ defmodule Code.Formatter do
string = Atom.to_string(atom)
iodata =
- case Code.Identifier.classify(atom) do
- type when type in [:callable_local, :callable_operator, :not_callable] ->
- [?:, string]
-
- _ ->
- [?:, ?", String.replace(string, "\"", "\\\""), ?"]
+ if Macro.classify_atom(atom) in [:unquoted, :identifier] do
+ [?:, string]
+ else
+ [?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string()
@@ -2042,7 +2038,7 @@ defmodule Code.Formatter do
defp module_attribute_read?({:@, _, [{var, _, var_context}]})
when is_atom(var) and is_atom(var_context) do
- Code.Identifier.classify(var) == :callable_local
+ Macro.classify_atom(var) == :identifier
end
defp module_attribute_read?(_), do: false
diff --git a/lib/elixir/lib/code/identifier.ex b/lib/elixir/lib/code/identifier.ex
index 8f60c69cc..3c09f869d 100644
--- a/lib/elixir/lib/code/identifier.ex
+++ b/lib/elixir/lib/code/identifier.ex
@@ -55,149 +55,6 @@ defmodule Code.Identifier do
end
@doc """
- Classifies the given atom into one of the following categories:
-
- * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
-
- * `:callable_local` - an atom that can be used as a local call;
- this category includes identifiers like `:foo`
-
- * `:callable_operator` - all callable operators, such as `:<>`. Note
- operators such as `:..` are not callable because of ambiguity
-
- * `:not_atomable` - callable operators that must be wrapped in quotes when
- defined as an atom. For example, `::` must be written as `:"::"` to avoid
- the ambiguity between the atom and the keyword identifier
-
- * `:not_callable` - an atom that cannot be used as a function call after the
- `.` operator. Those are typically AST nodes that are special forms (such as
- `:%{}` and `:<<>>>`) as well as nodes that are ambiguous in calls (such as
- `:..` and `:...`). This category also includes atoms like `:Foo`, since
- they are valid identifiers but they need quotes to be used in function
- calls (`Foo."Bar"`)
-
- * `:other` - any other atom (these are usually escaped when inspected, like
- `:"foo and bar"`)
-
- """
- def classify(atom) when is_atom(atom) do
- charlist = Atom.to_charlist(atom)
-
- cond do
- atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
- :not_callable
-
- atom in [:"::", :"//"] ->
- :not_atomable
-
- unary_op(atom) != :error or binary_op(atom) != :error ->
- :callable_operator
-
- valid_alias?(charlist) ->
- :alias
-
- true ->
- case :elixir_config.identifier_tokenizer().tokenize(charlist) do
- {kind, _acc, [], _, _, special} ->
- if kind == :identifier and not :lists.member(?@, special) do
- :callable_local
- else
- :not_callable
- end
-
- _ ->
- :other
- end
- end
- end
-
- defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest)
- defp valid_alias?(_other), do: false
-
- defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
- do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
-
- defp valid_alias_piece?([]), do: true
- defp valid_alias_piece?(_other), do: false
-
- defp trim_leading_while_valid_identifier([char | rest])
- when char >= ?a and char <= ?z
- when char >= ?A and char <= ?Z
- when char >= ?0 and char <= ?9
- when char == ?_ do
- trim_leading_while_valid_identifier(rest)
- end
-
- defp trim_leading_while_valid_identifier(other) do
- other
- end
-
- @doc """
- Inspects the identifier as an atom.
- """
- def inspect_as_atom(atom) when is_nil(atom) or is_boolean(atom) do
- Atom.to_string(atom)
- end
-
- def inspect_as_atom(atom) when is_atom(atom) do
- binary = Atom.to_string(atom)
-
- case classify(atom) do
- :alias ->
- case binary do
- binary when binary in ["Elixir", "Elixir.Elixir"] -> binary
- "Elixir.Elixir." <> _rest -> binary
- "Elixir." <> rest -> rest
- end
-
- type when type in [:callable_local, :callable_operator, :not_callable] ->
- ":" <> binary
-
- _ ->
- {escaped, _} = escape(binary, ?")
- IO.iodata_to_binary([?:, ?", escaped, ?"])
- end
- end
-
- @doc """
- Inspects the given identifier as a key.
- """
- def inspect_as_key(atom) when is_atom(atom) do
- binary = Atom.to_string(atom)
-
- case classify(atom) do
- type when type in [:callable_local, :callable_operator, :not_callable] ->
- IO.iodata_to_binary([binary, ?:])
-
- _ ->
- {escaped, _} = escape(binary, ?")
- IO.iodata_to_binary([?", escaped, ?", ?:])
- end
- end
-
- @doc """
- Inspects the given identifier as a function name.
- """
- def inspect_as_function(atom) when is_atom(atom) do
- binary = Atom.to_string(atom)
-
- case classify(atom) do
- type when type in [:callable_local, :callable_operator, :not_atomable] ->
- binary
-
- type ->
- escaped =
- if type in [:not_callable, :alias] do
- binary
- else
- elem(escape(binary, ?"), 0)
- end
-
- IO.iodata_to_binary([?", escaped, ?"])
- end
- end
-
- @doc """
Extracts the name and arity of the parent from the anonymous function identifier.
"""
# Example of this format: -NAME/ARITY-fun-COUNT-
diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex
index be455f274..89a6e8dcf 100644
--- a/lib/elixir/lib/code/normalizer.ex
+++ b/lib/elixir/lib/code/normalizer.ex
@@ -244,7 +244,7 @@ defmodule Code.Normalizer do
meta = patch_meta_line(meta, state.parent_meta)
literal = maybe_escape_literal(literal, state)
- if is_atom(literal) and Code.Identifier.classify(literal) == :alias and
+ if is_atom(literal) and Macro.classify_atom(literal) == :alias and
is_nil(meta[:delimiter]) do
"Elixir." <> segments = Atom.to_string(literal)
diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex
index 400271b49..7a0c5b028 100644
--- a/lib/elixir/lib/exception.ex
+++ b/lib/elixir/lib/exception.ex
@@ -622,12 +622,12 @@ defmodule Exception do
case Code.Identifier.extract_anonymous_fun_parent(fun) do
{outer_name, outer_arity} ->
"anonymous fn#{format_arity(arity)} in " <>
- "#{Code.Identifier.inspect_as_atom(module)}." <>
- "#{Code.Identifier.inspect_as_function(outer_name)}/#{outer_arity}"
+ "#{Macro.inspect_atom(:literal, module)}." <>
+ "#{Macro.inspect_atom(:remote_call, outer_name)}/#{outer_arity}"
:error ->
- "#{Code.Identifier.inspect_as_atom(module)}." <>
- "#{Code.Identifier.inspect_as_function(fun)}#{format_arity(arity)}"
+ "#{Macro.inspect_atom(:literal, module)}." <>
+ "#{Macro.inspect_atom(:remote_call, fun)}#{format_arity(arity)}"
end
end
@@ -1093,7 +1093,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
- [" * ", Code.Identifier.inspect_as_function(fun), ?/, Integer.to_string(arity), ?\n]
+ [" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp behaviour_hint(module, function, arity) do
diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex
index 52dcb227f..721c77211 100644
--- a/lib/elixir/lib/inspect.ex
+++ b/lib/elixir/lib/inspect.ex
@@ -94,7 +94,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
- color(Identifier.inspect_as_atom(atom), color_key(atom), opts)
+ color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -222,7 +222,7 @@ defimpl Inspect, for: List do
@doc false
def keyword({key, value}, opts) do
- key = color(Identifier.inspect_as_key(key), :atom, opts)
+ key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@@ -351,8 +351,8 @@ defimpl Inspect, for: Function do
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]}>"
fun_info[:type] == :external and fun_info[:env] == [] ->
- inspected_as_atom = Identifier.inspect_as_atom(mod)
- inspected_as_function = Identifier.inspect_as_function(name)
+ inspected_as_atom = Macro.inspect_atom(:literal, mod)
+ inspected_as_function = Macro.inspect_atom(:remote_call, name)
"&#{inspected_as_atom}.#{inspected_as_function}/#{fun_info[:arity]}"
match?('elixir_compiler_' ++ _, Atom.to_charlist(mod)) ->
@@ -368,7 +368,7 @@ defimpl Inspect, for: Function do
end
defp default_inspect(mod, fun_info) do
- inspected_as_atom = Identifier.inspect_as_atom(mod)
+ inspected_as_atom = Macro.inspect_atom(:literal, mod)
extracted_name = extract_name(fun_info[:name])
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in #{inspected_as_atom}#{extracted_name}>"
end
@@ -380,10 +380,10 @@ defimpl Inspect, for: Function do
defp extract_name(name) do
case Identifier.extract_anonymous_fun_parent(name) do
{name, arity} ->
- "." <> Identifier.inspect_as_function(name) <> "/" <> arity
+ "." <> Macro.inspect_atom(:remote_call, name) <> "/" <> arity
:error ->
- "." <> Identifier.inspect_as_function(name)
+ "." <> Macro.inspect_atom(:remote_call, name)
end
end
@@ -463,7 +463,7 @@ defimpl Inspect, for: Any do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(map) = Map.take(var!(struct), unquote(filtered_fields))
- var!(name) = Identifier.inspect_as_atom(unquote(module))
+ var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(map), var!(name), var!(opts))
end
end
@@ -479,7 +479,7 @@ defimpl Inspect, for: Any do
dunder ->
if Map.keys(dunder) == Map.keys(struct) do
pruned = Map.drop(struct, [:__struct__, :__exception__])
- Inspect.Map.inspect(pruned, Identifier.inspect_as_atom(module), opts)
+ Inspect.Map.inspect(pruned, Macro.inspect_atom(:literal, module), opts)
else
Inspect.Map.inspect(struct, opts)
end
diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex
index 78c564495..375943ab9 100644
--- a/lib/elixir/lib/macro.ex
+++ b/lib/elixir/lib/macro.ex
@@ -2,10 +2,14 @@ import Kernel, except: [to_string: 1]
defmodule Macro do
@moduledoc ~S"""
- Macros are compile-time constructs that are invoked with Elixir's AST
- as input and a superset of Elixir's AST as output.
+ Macros are compile-time constructs that receive Elixir's AST as input
+ and return Elixir's AST as output.
- Let's see a simple example that shows the difference between functions and macros:
+ Many of the functions in this module exist precisely to work with Elixir
+ AST, to traverse, query, and transform it.
+
+ Let's see a simple example that shows the difference between functions
+ and macros:
defmodule Example do
defmacro macro_inspect(value) do
@@ -50,6 +54,10 @@ defmodule Macro do
To learn more about Elixir's AST and how to build them programmatically,
see `quote/2`.
+ > Note: the functions in this module do not evaluate code. In fact,
+ > evaluating code from macros is often an anti-pattern. For code
+ > evaluation, see the `Code` module.
+
## Custom Sigils
Macros are also commonly used to implement custom sigils. To create a custom
@@ -286,12 +294,12 @@ defmodule Macro do
def pipe(expr, {op, line, args} = op_args, integer) when is_list(args) do
cond do
- is_atom(op) and Identifier.unary_op(op) != :error ->
+ is_atom(op) and operator?(op, 1) ->
raise ArgumentError,
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
"the #{to_string(op)} operator can only take one argument"
- is_atom(op) and Identifier.binary_op(op) != :error ->
+ is_atom(op) and operator?(op, 2) ->
raise ArgumentError,
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
"the #{to_string(op)} operator can only take two arguments"
@@ -1283,16 +1291,14 @@ defmodule Macro do
end
defp unary_call({op, _, [arg]} = ast, fun) when is_atom(op) do
- case Identifier.unary_op(op) do
- {_, _} ->
- if op == :not or op_expr?(arg) do
- {:ok, fun.(ast, Atom.to_string(op) <> "(" <> to_string(arg, fun) <> ")")}
- else
- {:ok, fun.(ast, Atom.to_string(op) <> to_string(arg, fun))}
- end
-
- :error ->
- :error
+ if operator?(op, 1) do
+ if op == :not or op_expr?(arg) do
+ {:ok, fun.(ast, Atom.to_string(op) <> "(" <> to_string(arg, fun) <> ")")}
+ else
+ {:ok, fun.(ast, Atom.to_string(op) <> to_string(arg, fun))}
+ end
+ else
+ :error
end
end
@@ -1308,15 +1314,13 @@ defmodule Macro do
end
defp op_call({op, _, [left, right]} = ast, fun) when is_atom(op) do
- case Identifier.binary_op(op) do
- {_, _} ->
- left = op_to_string(left, fun, op, :left)
- right = op_to_string(right, fun, op, :right)
- op = if op in [:..], do: "#{op}", else: " #{op} "
- {:ok, fun.(ast, left <> op <> right)}
-
- :error ->
- :error
+ if operator?(op, 2) do
+ left = op_to_string(left, fun, op, :left)
+ right = op_to_string(right, fun, op, :right)
+ op = if op in [:..], do: "#{op}", else: " #{op} "
+ {:ok, fun.(ast, left <> op <> right)}
+ else
+ :error
end
end
@@ -1363,14 +1367,9 @@ defmodule Macro do
defp op_expr?(expr) do
case expr do
- {op, _, [_, _]} ->
- Identifier.binary_op(op) != :error
-
- {op, _, [_]} ->
- Identifier.unary_op(op) != :error
-
- _ ->
- false
+ {op, _, [_, _]} -> operator?(op, 2)
+ {op, _, [_]} -> operator?(op, 1)
+ _ -> false
end
end
@@ -1392,7 +1391,7 @@ defmodule Macro do
end
defp call_to_string_for_atom(atom) do
- Identifier.inspect_as_function(atom)
+ Macro.inspect_atom(:remote_call, atom)
end
defp args_to_string(args, fun) do
@@ -1451,7 +1450,7 @@ defmodule Macro do
defp kw_list_to_string(list, fun) do
Enum.map_join(list, ", ", fn {key, value} ->
- Identifier.inspect_as_key(key) <> " " <> to_string(value, fun)
+ Macro.inspect_atom(:key, key) <> " " <> to_string(value, fun)
end)
end
@@ -1737,9 +1736,19 @@ defmodule Macro do
"""
@doc since: "1.7.0"
@spec operator?(name :: atom(), arity()) :: boolean()
- def operator?(:"..//", 3), do: true
- def operator?(name, 2) when is_atom(name), do: Identifier.binary_op(name) != :error
- def operator?(name, 1) when is_atom(name), do: Identifier.unary_op(name) != :error
+ def operator?(name, arity)
+
+ def operator?(:"..//", 3),
+ do: true
+
+ # Code.Identifier treats :// as a binary operator for precedence
+ # purposes but it isn't really one, so we explicitly skip it.
+ def operator?(name, 2) when is_atom(name),
+ do: Identifier.binary_op(name) != :error and name != :"//"
+
+ def operator?(name, 1) when is_atom(name),
+ do: Identifier.unary_op(name) != :error
+
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
@doc """
@@ -1921,4 +1930,234 @@ defmodule Macro do
defp to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32
defp to_lower_char(char), do: char
+
+ ## Atom handling
+
+ @doc """
+ Classifies the given `atom`.
+
+ It returns one of the following atoms:
+
+ * `:alias` - the atom represents an alias
+
+ * `:identifier` - the atom can be used as a variable or local function call
+
+ * `:unquoted` - the atom can be used in its unquoted form,
+ includes operators and atoms with `@` in them
+
+ * `:quoted` - all other atoms which can only be used in their quoted form
+
+ Note operators are going to either be unquoted, such as `:+` and
+ most operators, or quoted, such as `:"::"`. Use `operator?/2` to
+ check if a given atom is an operator.
+
+ ## Examples
+
+ iex> Macro.classify_atom(:foo)
+ :identifier
+ iex> Macro.classify_atom(Foo)
+ :alias
+ iex> Macro.classify_atom(:foo@bar)
+ :unquoted
+ iex> Macro.classify_atom(:+)
+ :unquoted
+ iex> Macro.classify_atom(:Foo)
+ :unquoted
+ iex> Macro.classify_atom(:"with spaces")
+ :quoted
+
+ """
+ @doc since: "1.14.0"
+ @spec classify_atom(atom) :: :alias | :identifier | :quoted | :unquoted
+ def classify_atom(atom) do
+ case inner_classify(atom) do
+ :alias -> :alias
+ :identifier -> :identifier
+ type when type in [:unquoted_operator, :not_callable] -> :unquoted
+ _ -> :quoted
+ end
+ end
+
+ @doc ~S"""
+ Inspects the given atom.
+
+ The atom can be inspected as a literal (`:literal`),
+ as a key (`:key`), or as the function name in a remote call
+ (`:remote_call`).
+
+ ## Examples
+
+ ### As a literal
+
+ iex> Macro.inspect_atom(:literal, nil)
+ "nil"
+ iex> Macro.inspect_atom(:literal, :foo)
+ ":foo"
+ iex> Macro.inspect_atom(:literal, :<>)
+ ":<>"
+ iex> Macro.inspect_atom(:literal, :Foo)
+ ":Foo"
+ iex> Macro.inspect_atom(:literal, :"with spaces")
+ ":\"with spaces\""
+
+ ### As a key
+
+ iex> Macro.inspect_atom(:key, :foo)
+ "foo:"
+ iex> Macro.inspect_atom(:key, :<>)
+ "<>:"
+ iex> Macro.inspect_atom(:key, :Foo)
+ "Foo:"
+ iex> Macro.inspect_atom(:key, :"with spaces")
+ "\"with spaces\":"
+
+ ### As a remote call
+
+ iex> Macro.inspect_atom(:remote_call, :foo)
+ "foo"
+ iex> Macro.inspect_atom(:remote_call, :<>)
+ "<>"
+ iex> Macro.inspect_atom(:remote_call, :Foo)
+ "\"Foo\""
+ iex> Macro.inspect_atom(:remote_call, :"with spaces")
+ "\"with spaces\""
+
+ """
+ @doc since: "1.14.0"
+ @spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
+ def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
+ Atom.to_string(atom)
+ end
+
+ def inspect_atom(:literal, atom) when is_atom(atom) do
+ binary = Atom.to_string(atom)
+
+ case classify_atom(atom) do
+ :alias ->
+ case binary do
+ binary when binary in ["Elixir", "Elixir.Elixir"] -> binary
+ "Elixir.Elixir." <> _rest -> binary
+ "Elixir." <> rest -> rest
+ end
+
+ :quoted ->
+ {escaped, _} = Code.Identifier.escape(binary, ?")
+ IO.iodata_to_binary([?:, ?", escaped, ?"])
+
+ _ ->
+ ":" <> binary
+ end
+ end
+
+ def inspect_atom(:key, atom) 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, ?")
+ IO.iodata_to_binary([?", escaped, ?", ?:])
+
+ _ ->
+ IO.iodata_to_binary([binary, ?:])
+ end
+ end
+
+ def inspect_atom(:remote_call, atom) when is_atom(atom) do
+ binary = Atom.to_string(atom)
+
+ case inner_classify(atom) do
+ type when type in [:identifier, :unquoted_operator, :quoted_operator] ->
+ binary
+
+ type ->
+ escaped =
+ if type in [:not_callable, :alias] do
+ binary
+ else
+ elem(Code.Identifier.escape(binary, ?"), 0)
+ end
+
+ IO.iodata_to_binary([?", 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
+ #
+ # * `:identifier` - an atom that can be used as a variable/local call;
+ # this category includes identifiers like `:foo`
+ #
+ # * `:unquoted_operator` - all callable operators, such as `:<>`. Note
+ # operators such as `:..` are not callable because of ambiguity
+ #
+ # * `:quoted_operator` - callable operators that must be wrapped in quotes when
+ # defined as an atom. For example, `::` must be written as `:"::"` to avoid
+ # the ambiguity between the atom and the keyword identifier
+ #
+ # * `:not_callable` - an atom that cannot be used as a function call after the
+ # `.` operator. Those are typically AST nodes that are special forms (such as
+ # `:%{}` and `:<<>>>`) as well as nodes that are ambiguous in calls (such as
+ # `:..` and `:...`). This category also includes atoms like `:Foo`, since
+ # they are valid identifiers but they need quotes to be used in function
+ # calls (`Foo."Bar"`)
+ #
+ # * `:other` - any other atom (these are usually escaped when inspected, like
+ # `:"foo and bar"`)
+ #
+ defp inner_classify(atom) when is_atom(atom) do
+ cond do
+ atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
+ :not_callable
+
+ atom in [:"::"] ->
+ :quoted_operator
+
+ operator?(atom, 1) or operator?(atom, 2) ->
+ :unquoted_operator
+
+ true ->
+ charlist = Atom.to_charlist(atom)
+
+ if valid_alias?(charlist) do
+ :alias
+ else
+ case :elixir_config.identifier_tokenizer().tokenize(charlist) do
+ {kind, _acc, [], _, _, special} ->
+ if kind == :identifier and not :lists.member(?@, special) do
+ :identifier
+ else
+ :not_callable
+ end
+
+ _ ->
+ :other
+ end
+ end
+ end
+ end
+
+ defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest)
+ defp valid_alias?(_other), do: false
+
+ defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
+ do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
+
+ defp valid_alias_piece?([]), do: true
+ defp valid_alias_piece?(_other), do: false
+
+ defp trim_leading_while_valid_identifier([char | rest])
+ when char >= ?a and char <= ?z
+ when char >= ?A and char <= ?Z
+ when char >= ?0 and char <= ?9
+ when char == ?_ do
+ trim_leading_while_valid_identifier(rest)
+ end
+
+ defp trim_leading_while_valid_identifier(other) do
+ other
+ end
end
diff --git a/lib/elixir/src/elixir_aliases.erl b/lib/elixir/src/elixir_aliases.erl
index 6480606b0..fa4c2dc09 100644
--- a/lib/elixir/src/elixir_aliases.erl
+++ b/lib/elixir/src/elixir_aliases.erl
@@ -6,7 +6,7 @@
inspect(Atom) when is_atom(Atom) ->
case elixir_config:is_bootstrap() of
true -> atom_to_binary(Atom, utf8);
- false -> 'Elixir.Code.Identifier':inspect_as_atom(Atom)
+ false -> 'Elixir.Macro':inspect_atom(literal, Atom)
end.
%% Store an alias in the given scope
diff --git a/lib/elixir/src/elixir_overridable.erl b/lib/elixir/src/elixir_overridable.erl
index 0f04c123f..071dcf8b2 100644
--- a/lib/elixir/src/elixir_overridable.erl
+++ b/lib/elixir/src/elixir_overridable.erl
@@ -128,6 +128,6 @@ format_error({no_super, Module, {Name, Arity}}) ->
[Name, Arity, elixir_aliases:inspect(Module), Joined]).
format_fa({Name, Arity}) ->
- A = 'Elixir.Code.Identifier':inspect_as_function(Name),
+ A = 'Elixir.Macro':inspect_atom(remote_call, Name),
B = integer_to_binary(Arity),
<<A/binary, $/, B/binary>>.
diff --git a/lib/elixir/test/elixir/code_formatter/integration_test.exs b/lib/elixir/test/elixir/code_formatter/integration_test.exs
index 945ab76ef..69f6b4820 100644
--- a/lib/elixir/test/elixir/code_formatter/integration_test.exs
+++ b/lib/elixir/test/elixir/code_formatter/integration_test.exs
@@ -57,7 +57,7 @@ defmodule Code.Formatter.IntegrationTest do
assert_same """
defp module_attribute_read?({:@, _, [{var, _, var_context}]})
when is_atom(var) and is_atom(var_context) do
- Code.Identifier.classify(var) == :callable_local
+ Macro.classify_atom(var) == :identifier
end
"""
end
diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex
index 290ecaf3c..8e1b952f1 100644
--- a/lib/ex_unit/lib/ex_unit/diff.ex
+++ b/lib/ex_unit/lib/ex_unit/diff.ex
@@ -14,7 +14,6 @@ defmodule ExUnit.Diff do
# literal and doesn't contain meta, the `:diff` meta will be placed in a
# wrapping block.
- alias Code.Identifier
alias Inspect.Algebra
defstruct equivalent?: true,
@@ -975,7 +974,7 @@ defmodule ExUnit.Diff do
end
defp safe_key_to_algebra(key, _diff_wrapper) do
- Identifier.inspect_as_key(key)
+ Macro.inspect_atom(:key, key)
end
defp map_item_to_algebra(quoted, diff_wrapper) do
@@ -1016,7 +1015,7 @@ defmodule ExUnit.Diff do
end
defp safe_struct_to_algebra(name, _diff_wrapper) do
- Code.Identifier.inspect_as_atom(name)
+ Macro.inspect_atom(:literal, name)
end
defp select_list_item_algebra(list) do
diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex
index f5230d3f8..8bcff880c 100644
--- a/lib/iex/lib/iex/autocomplete.ex
+++ b/lib/iex/lib/iex/autocomplete.ex
@@ -431,7 +431,7 @@ defmodule IEx.Autocomplete do
defp usable_as_unquoted_module?(name) do
# Conversion to atom is not a problem because
# it is only called with existing modules names.
- Code.Identifier.classify(String.to_atom(name)) != :other
+ Macro.classify_atom(String.to_atom(name)) != :quoted
end
defp match_modules(hint, elixir_root?) do
diff --git a/lib/logger/lib/logger/translator.ex b/lib/logger/lib/logger/translator.ex
index 4a73124f6..1d99f1f5e 100644
--- a/lib/logger/lib/logger/translator.ex
+++ b/lib/logger/lib/logger/translator.ex
@@ -581,7 +581,7 @@ defmodule Logger.Translator do
defp registered_name(_name), do: []
defp format_mfa(mod, fun, :undefined),
- do: [inspect(mod), ?., Code.Identifier.inspect_as_function(fun) | "/?"]
+ do: [inspect(mod), ?., Macro.inspect_atom(:remote_call, fun) | "/?"]
defp format_mfa(mod, fun, args),
do: Exception.format_mfa(mod, fun, args)
diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex
index 957ae3c29..16bc00d03 100644
--- a/lib/mix/lib/mix/tasks/compile.protocols.ex
+++ b/lib/mix/lib/mix/tasks/compile.protocols.ex
@@ -163,7 +163,7 @@ defmodule Mix.Tasks.Compile.Protocols do
# We cannot use the inspect protocol while consolidating
# since inspect may not be available.
defp inspect_protocol(protocol) do
- Code.Identifier.inspect_as_atom(protocol)
+ Macro.inspect_atom(:literal, protocol)
end
defp reload(module) do