547 lines
14 KiB
Elixir
547 lines
14 KiB
Elixir
import Kernel, except: [inspect: 1]
|
|
import Inspect.Algebra
|
|
|
|
defprotocol Inspect do
|
|
@moduledoc """
|
|
The `Inspect` protocol is responsible for converting any Elixir
|
|
data structure into an algebra document. This document is then
|
|
formatted, either in pretty printing format or a regular one.
|
|
|
|
The `inspect/2` function receives the entity to be inspected
|
|
followed by the inspecting options, represented by the struct
|
|
`Inspect.Opts`.
|
|
|
|
Inspection is done using the functions available in `Inspect.Algebra`.
|
|
|
|
## Examples
|
|
|
|
Many times, inspecting a structure can be implemented in function
|
|
of existing entities. For example, here is `MapSet`'s `inspect`
|
|
implementation:
|
|
|
|
defimpl Inspect, for: MapSet do
|
|
import Inspect.Algebra
|
|
|
|
def inspect(dict, opts) do
|
|
concat ["#MapSet<", to_doc(MapSet.to_list(dict), opts), ">"]
|
|
end
|
|
end
|
|
|
|
The `concat` function comes from `Inspect.Algebra` and it
|
|
concatenates algebra documents together. In the example above,
|
|
it is concatenating the string `"MapSet<"` (all strings are
|
|
valid algebra documents that keep their formatting when pretty
|
|
printed), the document returned by `Inspect.Algebra.to_doc/2` and the
|
|
other string `">"`.
|
|
|
|
Since regular strings are valid entities in an algebra document,
|
|
an implementation of inspect may simply return a string,
|
|
although that will devoid it of any pretty-printing.
|
|
|
|
## Error handling
|
|
|
|
In case there is an error while your structure is being inspected,
|
|
Elixir will raise an `ArgumentError` error and will automatically fall back
|
|
to a raw representation for printing the structure.
|
|
|
|
You can however access the underlying error by invoking the Inspect
|
|
implementation directly. For example, to test Inspect.MapSet above,
|
|
you can invoke it as:
|
|
|
|
Inspect.MapSet.inspect(MapSet.new, %Inspect.Opts{})
|
|
|
|
"""
|
|
|
|
# Handle structs in Any
|
|
@fallback_to_any true
|
|
|
|
def inspect(term, opts)
|
|
end
|
|
|
|
defimpl Inspect, for: Atom do
|
|
require Macro
|
|
|
|
def inspect(atom, _opts) do
|
|
inspect(atom)
|
|
end
|
|
|
|
def inspect(false), do: "false"
|
|
def inspect(true), do: "true"
|
|
def inspect(nil), do: "nil"
|
|
def inspect(:""), do: ":\"\""
|
|
|
|
def inspect(atom) do
|
|
binary = Atom.to_string(atom)
|
|
|
|
cond do
|
|
valid_ref_identifier?(binary) ->
|
|
if only_elixir?(binary) do
|
|
binary
|
|
else
|
|
"Elixir." <> rest = binary
|
|
rest
|
|
end
|
|
valid_atom_identifier?(binary) ->
|
|
":" <> binary
|
|
atom in [:%{}, :{}, :<<>>, :..., :%] ->
|
|
":" <> binary
|
|
atom in Macro.binary_ops or atom in Macro.unary_ops ->
|
|
":" <> binary
|
|
true ->
|
|
<<?:, ?", Inspect.BitString.escape(binary, ?")::binary, ?">>
|
|
end
|
|
end
|
|
|
|
defp only_elixir?("Elixir." <> rest), do: only_elixir?(rest)
|
|
defp only_elixir?("Elixir"), do: true
|
|
defp only_elixir?(_), do: false
|
|
|
|
# Detect if atom is an atom alias (Elixir.Foo.Bar.Baz)
|
|
|
|
defp valid_ref_identifier?("Elixir" <> rest) do
|
|
valid_ref_piece?(rest)
|
|
end
|
|
|
|
defp valid_ref_identifier?(_), do: false
|
|
|
|
defp valid_ref_piece?(<<?., h, t::binary>>) when h in ?A..?Z do
|
|
valid_ref_piece? valid_identifier?(t)
|
|
end
|
|
|
|
defp valid_ref_piece?(<<>>), do: true
|
|
defp valid_ref_piece?(_), do: false
|
|
|
|
# Detect if atom
|
|
|
|
defp valid_atom_identifier?(<<h, t::binary>>) when h in ?a..?z or h in ?A..?Z or h == ?_ do
|
|
valid_atom_piece?(t)
|
|
end
|
|
|
|
defp valid_atom_identifier?(_), do: false
|
|
|
|
defp valid_atom_piece?(t) do
|
|
case valid_identifier?(t) do
|
|
<<>> -> true
|
|
<<??>> -> true
|
|
<<?!>> -> true
|
|
<<?@, t::binary>> -> valid_atom_piece?(t)
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
defp valid_identifier?(<<h, t::binary>>)
|
|
when h in ?a..?z
|
|
when h in ?A..?Z
|
|
when h in ?0..?9
|
|
when h == ?_ do
|
|
valid_identifier? t
|
|
end
|
|
|
|
defp valid_identifier?(other), do: other
|
|
end
|
|
|
|
defimpl Inspect, for: BitString do
|
|
def inspect(term, %Inspect.Opts{binaries: bins, base: base} = opts) when is_binary(term) do
|
|
if base == :decimal and (bins == :as_strings or (bins == :infer and String.printable?(term))) do
|
|
<<?", escape(term, ?")::binary, ?">>
|
|
else
|
|
inspect_bitstring(term, opts)
|
|
end
|
|
end
|
|
|
|
def inspect(term, opts) do
|
|
inspect_bitstring(term, opts)
|
|
end
|
|
|
|
## Escaping
|
|
|
|
@doc false
|
|
def escape(other, char) do
|
|
escape(other, char, <<>>)
|
|
end
|
|
|
|
defp escape(<<char, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, char>>)
|
|
end
|
|
defp escape(<<?#, ?{, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?#, ?{>>)
|
|
end
|
|
defp escape(<<?\a, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?a>>)
|
|
end
|
|
defp escape(<<?\b, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?b>>)
|
|
end
|
|
defp escape(<<?\d, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?d>>)
|
|
end
|
|
defp escape(<<?\e, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?e>>)
|
|
end
|
|
defp escape(<<?\f, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?f>>)
|
|
end
|
|
defp escape(<<?\n, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?n>>)
|
|
end
|
|
defp escape(<<?\r, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?r>>)
|
|
end
|
|
defp escape(<<?\\, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?\\>>)
|
|
end
|
|
defp escape(<<?\t, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?t>>)
|
|
end
|
|
defp escape(<<?\v, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, ?\\, ?v>>)
|
|
end
|
|
defp escape(<<h::utf8, t::binary>>, char, binary) do
|
|
head = <<h::utf8>>
|
|
if String.printable?(head) do
|
|
escape(t, char, append(head, binary))
|
|
else
|
|
<<byte::8, h::binary>> = head
|
|
t = <<h::binary, t::binary>>
|
|
escape(t, char, <<binary::binary, escape_char(byte)::binary>>)
|
|
end
|
|
end
|
|
defp escape(<<h, t::binary>>, char, binary) do
|
|
escape(t, char, <<binary::binary, escape_char(h)::binary>>)
|
|
end
|
|
defp escape(<<>>, _char, binary), do: binary
|
|
|
|
@doc false
|
|
# Also used by Regex
|
|
def escape_char(0) do
|
|
<<?\\, ?0>>
|
|
end
|
|
|
|
def escape_char(char) when char < 0x100 do
|
|
<<a::4, b::4>> = <<char::8>>
|
|
<<?\\, ?x, to_hex(a), to_hex(b)>>
|
|
end
|
|
|
|
def escape_char(char) when char < 0x10000 do
|
|
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
|
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
|
end
|
|
|
|
def escape_char(char) when char < 0x1000000 do
|
|
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
|
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c),
|
|
to_hex(d), to_hex(e), to_hex(f), ?}>>
|
|
end
|
|
|
|
defp to_hex(c) when c in 0..9, do: ?0+c
|
|
defp to_hex(c) when c in 10..15, do: ?A+c-10
|
|
|
|
defp append(<<h, t::binary>>, binary), do: append(t, <<binary::binary, h>>)
|
|
defp append(<<>>, binary), do: binary
|
|
|
|
## Bitstrings
|
|
|
|
defp inspect_bitstring("", _opts) do
|
|
"<<>>"
|
|
end
|
|
|
|
defp inspect_bitstring(bitstring, opts) do
|
|
nest surround("<<", each_bit(bitstring, opts.limit, opts), ">>"), 1
|
|
end
|
|
|
|
defp each_bit(_, 0, _) do
|
|
"..."
|
|
end
|
|
|
|
defp each_bit(<<>>, _counter, _opts) do
|
|
:doc_nil
|
|
end
|
|
|
|
defp each_bit(<<h::8>>, _counter, opts) do
|
|
Inspect.Integer.inspect(h, opts)
|
|
end
|
|
|
|
defp each_bit(<<h, t::bitstring>>, counter, opts) do
|
|
glue(concat(Inspect.Integer.inspect(h, opts), ","),
|
|
each_bit(t, decrement(counter), opts))
|
|
end
|
|
|
|
defp each_bit(bitstring, _counter, opts) do
|
|
size = bit_size(bitstring)
|
|
<<h::size(size)>> = bitstring
|
|
Inspect.Integer.inspect(h, opts) <> "::size(" <> Integer.to_string(size) <> ")"
|
|
end
|
|
|
|
defp decrement(:infinity), do: :infinity
|
|
defp decrement(counter), do: counter - 1
|
|
end
|
|
|
|
defimpl Inspect, for: List do
|
|
def inspect([], _opts), do: "[]"
|
|
|
|
# TODO: Deprecate :char_lists and :as_char_lists keys in v1.5
|
|
def inspect(term, %Inspect.Opts{charlists: lists, char_lists: lists_deprecated} = opts) do
|
|
lists =
|
|
if lists == :infer and lists_deprecated != :infer do
|
|
case lists_deprecated do
|
|
:as_char_lists ->
|
|
:as_charlists
|
|
_ ->
|
|
lists_deprecated
|
|
end
|
|
else
|
|
lists
|
|
end
|
|
|
|
cond do
|
|
lists == :as_charlists or (lists == :infer and printable?(term)) ->
|
|
<<?', Inspect.BitString.escape(IO.chardata_to_string(term), ?')::binary, ?'>>
|
|
keyword?(term) ->
|
|
surround_many("[", term, "]", opts, &keyword/2)
|
|
true ->
|
|
surround_many("[", term, "]", opts, &to_doc/2)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def keyword({key, value}, opts) do
|
|
concat(
|
|
key_to_binary(key) <> ": ",
|
|
to_doc(value, opts)
|
|
)
|
|
end
|
|
|
|
@doc false
|
|
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
|
case Atom.to_charlist(key) do
|
|
'Elixir.' ++ _ -> false
|
|
_ -> keyword?(rest)
|
|
end
|
|
end
|
|
|
|
def keyword?([]), do: true
|
|
def keyword?(_other), do: false
|
|
|
|
@doc false
|
|
def printable?([c | cs]) when c in 32..126, do: printable?(cs)
|
|
def printable?([?\n | cs]), do: printable?(cs)
|
|
def printable?([?\r | cs]), do: printable?(cs)
|
|
def printable?([?\t | cs]), do: printable?(cs)
|
|
def printable?([?\v | cs]), do: printable?(cs)
|
|
def printable?([?\b | cs]), do: printable?(cs)
|
|
def printable?([?\f | cs]), do: printable?(cs)
|
|
def printable?([?\e | cs]), do: printable?(cs)
|
|
def printable?([?\a | cs]), do: printable?(cs)
|
|
def printable?([]), do: true
|
|
def printable?(_), do: false
|
|
|
|
## Private
|
|
|
|
defp key_to_binary(key) do
|
|
case Inspect.Atom.inspect(key) do
|
|
":" <> right -> right
|
|
other -> other
|
|
end
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Tuple do
|
|
def inspect({}, _opts), do: "{}"
|
|
|
|
def inspect(tuple, opts) do
|
|
surround_many("{", Tuple.to_list(tuple), "}", opts, &to_doc/2)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Map do
|
|
def inspect(map, opts) do
|
|
nest inspect(map, "", opts), 1
|
|
end
|
|
|
|
def inspect(map, name, opts) do
|
|
map = :maps.to_list(map)
|
|
surround_many("%" <> name <> "{", map, "}", opts, traverse_fun(map))
|
|
end
|
|
|
|
defp traverse_fun(list) do
|
|
if Inspect.List.keyword?(list) do
|
|
&Inspect.List.keyword/2
|
|
else
|
|
&to_map/2
|
|
end
|
|
end
|
|
|
|
defp to_map({key, value}, opts) do
|
|
concat(
|
|
concat(to_doc(key, opts), " => "),
|
|
to_doc(value, opts)
|
|
)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Integer do
|
|
def inspect(term, %Inspect.Opts{base: base}) do
|
|
Integer.to_string(term, base_to_value(base))
|
|
|> prepend_prefix(base)
|
|
end
|
|
|
|
defp base_to_value(base) do
|
|
case base do
|
|
:binary -> 2
|
|
:decimal -> 10
|
|
:octal -> 8
|
|
:hex -> 16
|
|
end
|
|
end
|
|
|
|
defp prepend_prefix(value, :decimal), do: value
|
|
defp prepend_prefix(value, base) do
|
|
prefix = case base do
|
|
:binary -> "0b"
|
|
:octal -> "0o"
|
|
:hex -> "0x"
|
|
end
|
|
prefix <> value
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Float do
|
|
def inspect(term, _opts) do
|
|
IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Regex do
|
|
def inspect(regex, _opts) do
|
|
delim = ?/
|
|
concat ["~r",
|
|
<<delim, escape(regex.source, delim)::binary, delim>>,
|
|
regex.opts]
|
|
end
|
|
|
|
defp escape(bin, term),
|
|
do: escape(bin, <<>>, term)
|
|
|
|
defp escape(<<?\\, term>> <> rest, buf, term),
|
|
do: escape(rest, buf <> <<?\\, term>>, term)
|
|
|
|
defp escape(<<term>> <> rest, buf, term),
|
|
do: escape(rest, buf <> <<?\\, term>>, term)
|
|
|
|
# the list of characters is from "String.printable?" impl
|
|
# minus characters treated specially by regex: \s, \d, \b, \e
|
|
|
|
defp escape(<<?\n>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?n>>, term)
|
|
|
|
defp escape(<<?\r>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?r>>, term)
|
|
|
|
defp escape(<<?\t>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?t>>, term)
|
|
|
|
defp escape(<<?\v>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?v>>, term)
|
|
|
|
defp escape(<<?\f>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?f>>, term)
|
|
|
|
defp escape(<<?\a>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, ?\\, ?a>>, term)
|
|
|
|
defp escape(<<c::utf8>> <> rest, buf, term) do
|
|
charstr = <<c::utf8>>
|
|
if String.printable?(charstr) and not c in [?\d, ?\b, ?\e] do
|
|
escape(rest, buf <> charstr, term)
|
|
else
|
|
escape(rest, buf <> Inspect.BitString.escape_char(c), term)
|
|
end
|
|
end
|
|
|
|
defp escape(<<c>> <> rest, buf, term),
|
|
do: escape(rest, <<buf::binary, Inspect.BitString.escape_char(c)>>, term)
|
|
|
|
defp escape(<<>>, buf, _), do: buf
|
|
end
|
|
|
|
defimpl Inspect, for: Function do
|
|
def inspect(function, _opts) do
|
|
fun_info = :erlang.fun_info(function)
|
|
mod = fun_info[:module]
|
|
|
|
if fun_info[:type] == :external and fun_info[:env] == [] do
|
|
"&#{Inspect.Atom.inspect(mod)}.#{fun_info[:name]}/#{fun_info[:arity]}"
|
|
else
|
|
case Atom.to_charlist(mod) do
|
|
'elixir_compiler_' ++ _ ->
|
|
if function_exported?(mod, :__RELATIVE__, 0) do
|
|
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
|
|
else
|
|
default_inspect(mod, fun_info)
|
|
end
|
|
_ ->
|
|
default_inspect(mod, fun_info)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp default_inspect(mod, fun_info) do
|
|
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in " <>
|
|
"#{Inspect.Atom.inspect(mod)}#{extract_name(fun_info[:name])}>"
|
|
end
|
|
|
|
defp extract_name([]) do
|
|
""
|
|
end
|
|
|
|
defp extract_name(name) do
|
|
name = Atom.to_string(name)
|
|
case :binary.split(name, "-", [:global]) do
|
|
["", name | _] -> "." <> name
|
|
_ -> "." <> name
|
|
end
|
|
end
|
|
|
|
defp uniq(fun_info) do
|
|
Integer.to_string(fun_info[:new_index]) <> "." <>
|
|
Integer.to_string(fun_info[:uniq])
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: PID do
|
|
def inspect(pid, _opts) do
|
|
"#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Port do
|
|
def inspect(port, _opts) do
|
|
IO.iodata_to_binary :erlang.port_to_list(port)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Reference do
|
|
def inspect(ref, _opts) do
|
|
'#Ref' ++ rest = :erlang.ref_to_list(ref)
|
|
"#Reference" <> IO.iodata_to_binary(rest)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect, for: Any do
|
|
def inspect(%{__struct__: struct} = map, opts) do
|
|
try do
|
|
struct.__struct__
|
|
rescue
|
|
_ -> Inspect.Map.inspect(map, opts)
|
|
else
|
|
dunder ->
|
|
if :maps.keys(dunder) == :maps.keys(map) do
|
|
pruned = :maps.remove(:__exception__, :maps.remove(:__struct__, map))
|
|
Inspect.Map.inspect(pruned, Inspect.Atom.inspect(struct, opts), opts)
|
|
else
|
|
Inspect.Map.inspect(map, opts)
|
|
end
|
|
end
|
|
end
|
|
end
|