Current section

Files

Jump to
erlex lib erlex.ex
Raw

lib/erlex.ex

defmodule Erlex do
@moduledoc """
Convert Erlang style structs and error messages to equivalent Elixir.
Lexes and parses the Erlang output, then runs through pretty
printer.
## Usage
Invoke `Erlex.pretty_print/1` wuth the input string.
```elixir
iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map()) -> 'Elixir.Plug.Conn':t()"
iex> Erlex.pretty_print(str)
(Plug.Conn.t(), binary() | atom(), Keyword.t() | map()) :: Plug.Conn.t()
```
While the lion's share of the work is done via invoking
`Erlex.pretty_print/1`, other higher order functions exist for further
formatting certain messages by running through the Elixir formatter.
Because we know the previous example is a type, we can invoke the
`Erlex.pretty_print_contract/1` function, which would format that
appropriately for very long lines.
```elixir
iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map(), map() | atom(), non_neg_integer(), binary(), binary(), binary(), binary(), binary()) -> 'Elixir.Plug.Conn':t()"
iex> Erlex.pretty_print_contract(str)
(
Plug.Conn.t(),
binary() | atom(),
Keyword.t() | map(),
map() | atom(),
non_neg_integer(),
binary(),
binary(),
binary(),
binary(),
binary()
) :: Plug.Conn.t()
```
"""
defp lex(str) do
try do
{:ok, tokens, _} = :lexer.string(str)
tokens
rescue
_ ->
throw({:error, :lexing, str})
end
end
defp parse(tokens) do
try do
{:ok, [first | _]} = :parser.parse(tokens)
first
rescue
_ ->
throw({:error, :parsing, tokens})
end
end
defp format(code) do
try do
Code.format_string!(code)
rescue
_ ->
throw({:error, :formatting, code})
end
end
@spec pretty_print_infix(infix :: String.t()) :: String.t()
def pretty_print_infix('=:='), do: "==="
def pretty_print_infix('=/='), do: "!=="
def pretty_print_infix('/='), do: "!="
def pretty_print_infix('=<'), do: "<="
def pretty_print_infix(infix), do: to_string(infix)
@spec pretty_print(str :: String.t()) :: String.t()
def pretty_print(str) do
parsed =
str
|> to_charlist()
|> lex()
|> parse()
try do
do_pretty_print(parsed)
rescue
_ ->
throw({:error, :pretty_printing, parsed})
end
end
@spec pretty_print_pattern(pattern :: String.t()) :: String.t()
def pretty_print_pattern('pattern ' ++ rest) do
pretty_print_type(rest)
end
def pretty_print_pattern(pattern) do
pretty_print_type(pattern)
end
@spec pretty_print_contract(
contract :: String.t(),
module :: String.t(),
function :: String.t()
) :: String.t()
def pretty_print_contract(contract, module, function) do
[head | tail] =
contract
|> to_string()
|> String.split(";")
head =
head
|> String.trim_leading(to_string(module))
|> String.trim_leading(":")
|> String.trim_leading(to_string(function))
[head | tail]
|> Enum.join(";")
|> pretty_print_contract()
end
@spec pretty_print_contract(contract :: String.t()) :: String.t()
def pretty_print_contract(contract) do
[head | tail] =
contract
|> to_string()
|> String.split(";")
if Enum.empty?(tail) do
do_pretty_print_contract(head)
else
joiner = "Contract head:\n"
pretty =
Enum.map_join([head | tail], "\n\n" <> joiner, fn contract ->
contract
|> to_charlist()
|> do_pretty_print_contract()
end)
joiner <> pretty
end
end
defp do_pretty_print_contract(contract) do
prefix = "@spec a"
suffix = "\ndef a() do\n :ok\nend"
pretty = pretty_print(contract)
"""
@spec a#{pretty}
def a() do
:ok
end
"""
|> format()
|> Enum.join("")
|> String.trim_leading(prefix)
|> String.trim_trailing(suffix)
|> String.replace("\n ", "\n")
end
@spec pretty_print_type(type :: String.t()) :: String.t()
def pretty_print_type(type) do
prefix = "@spec a("
suffix = ") :: :ok\ndef a() do\n :ok\nend"
indented_suffix = ") ::\n :ok\ndef a() do\n :ok\nend"
pretty = pretty_print(type)
"""
@spec a(#{pretty}) :: :ok
def a() do
:ok
end
"""
|> format()
|> Enum.join("")
|> String.trim_leading(prefix)
|> String.trim_trailing(suffix)
|> String.trim_trailing(indented_suffix)
|> String.replace("\n ", "\n")
end
@spec pretty_print_args(args :: String.t()) :: String.t()
def pretty_print_args(args) do
prefix = "@spec a"
suffix = " :: :ok\ndef a() do\n :ok\nend"
pretty = pretty_print(args)
"""
@spec a#{pretty} :: :ok
def a() do
:ok
end
"""
|> format()
|> Enum.join("")
|> String.trim_leading(prefix)
|> String.trim_trailing(suffix)
|> String.replace("\n ", "\n")
end
defp do_pretty_print({:any}) do
"_"
end
defp do_pretty_print({:inner_any_function}) do
"(...)"
end
defp do_pretty_print({:any_function}) do
"(... -> any)"
end
defp do_pretty_print({:assignment, {:atom, atom}, value}) do
name =
atom
|> deatomize()
|> to_string()
|> strip_var_version()
"#{name} = #{do_pretty_print(value)}"
end
defp do_pretty_print({:atom, [:_]}) do
"_"
end
defp do_pretty_print({:atom, ['_']}) do
"_"
end
defp do_pretty_print({:atom, atom}) do
atomize(atom)
end
defp do_pretty_print({:binary_part, value, _, size}) do
"#{do_pretty_print(value)} :: #{do_pretty_print(size)}"
end
defp do_pretty_print({:binary_part, value, size}) do
"#{do_pretty_print(value)} :: #{do_pretty_print(size)}"
end
defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 8}}}]}) do
"binary()"
end
defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 1}}}]}) do
"bitstring()"
end
defp do_pretty_print({:binary, binary_parts}) do
binary_parts = Enum.map_join(binary_parts, ", ", &do_pretty_print/1)
"<<#{binary_parts}>>"
end
defp do_pretty_print({:binary, value, size}) do
"<<#{do_pretty_print(value)} :: #{do_pretty_print(size)}>>"
end
defp do_pretty_print({:byte_list, byte_list}) do
byte_list
|> Enum.into(<<>>, fn byte ->
<<byte::8>>
end)
|> inspect()
end
defp do_pretty_print({:contract, {:args, args}, {:return, return}, {:whens, whens}}) do
printed_whens =
Enum.map_join(whens, ", ", fn {_, name, type} ->
do_pretty_print({:named_type_with_appended_colon, name, type})
end)
"#{do_pretty_print(args)} :: #{do_pretty_print(return)} when #{printed_whens}"
end
defp do_pretty_print({:contract, {:args, {:inner_any_function}}, {:return, return}}) do
"((...) -> #{do_pretty_print(return)})"
end
defp do_pretty_print({:contract, {:args, args}, {:return, return}}) do
"#{do_pretty_print(args)} :: #{do_pretty_print(return)}"
end
defp do_pretty_print({:function, {:contract, {:args, args}, {:return, return}}}) do
"(#{do_pretty_print(args)} -> #{do_pretty_print(return)})"
end
defp do_pretty_print({:int, int}) do
"#{to_string(int)}"
end
defp do_pretty_print({:list, :paren, items}) do
"(#{Enum.map_join(items, ", ", &do_pretty_print/1)})"
end
defp do_pretty_print(
{:list, :square,
[
tuple: [
{:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}},
{:atom, [:_]}
]
]}
) do
"Keyword.t()"
end
defp do_pretty_print(
{:list, :square,
[
tuple: [
{:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}},
t
]
]}
) do
"Keyword.t(#{do_pretty_print(t)})"
end
defp do_pretty_print({:list, :square, items}) do
"[#{Enum.map_join(items, ", ", &do_pretty_print/1)}]"
end
defp do_pretty_print({:map_entry, key, value}) do
"#{do_pretty_print(key)} => #{do_pretty_print(value)}"
end
defp do_pretty_print(
{:map,
[
{:map_entry, {:atom, '\'__struct__\''},
{:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}},
{:map_entry, {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, {:atom, [:_]}}
]}
) do
"struct()"
end
defp do_pretty_print(
{:map,
[
{:map_entry, {:atom, '\'__struct__\''},
{:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}},
{:map_entry, {:atom, [:_]}, {:atom, [:_]}}
]}
) do
"struct()"
end
defp do_pretty_print(
{:map,
[
{:map_entry, {:atom, '\'__exception__\''}, {:atom, '\'true\''}},
{:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}},
{:map_entry, {:atom, [:_]}, {:atom, [:_]}}
]}
) do
"Exception.t()"
end
defp do_pretty_print({:map, map_keys}) do
%{struct_name: struct_name, entries: entries} = struct_parts(map_keys)
if struct_name do
"%#{struct_name}{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}"
else
"%{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}"
end
end
defp do_pretty_print({:named_type_with_appended_colon, named_type, type})
when is_tuple(named_type) and is_tuple(type) do
case named_type do
{:atom, name} ->
name =
name
|> deatomize()
|> to_string()
|> strip_var_version()
"#{name}: #{do_pretty_print(type)}"
other ->
"#{do_pretty_print(other)}: #{do_pretty_print(type)}"
end
end
defp do_pretty_print({:named_type, named_type, type})
when is_tuple(named_type) and is_tuple(type) do
case named_type do
{:atom, name} ->
name =
name
|> deatomize()
|> to_string()
|> strip_var_version()
"#{name} :: #{do_pretty_print(type)}"
other ->
"#{do_pretty_print(other)} :: #{do_pretty_print(type)}"
end
end
defp do_pretty_print({:named_type, named_type, type}) when is_tuple(named_type) do
case named_type do
{:atom, name = '\'Elixir' ++ _} ->
"#{atomize(name)}.#{deatomize(type)}()"
{:atom, name} ->
name =
name
|> deatomize()
|> to_string()
|> strip_var_version()
"#{name} :: #{deatomize(type)}()"
other ->
name = do_pretty_print(other)
"#{name} :: #{deatomize(type)}()"
end
end
defp do_pretty_print({nil}) do
"nil"
end
defp do_pretty_print({:pattern, pattern_items}) do
"#{Enum.map_join(pattern_items, ", ", &do_pretty_print/1)}"
end
defp do_pretty_print(
{:pipe_list, {:atom, ['f', 'a', 'l', 's', 'e']}, {:atom, ['t', 'r', 'u', 'e']}}
) do
"boolean()"
end
defp do_pretty_print(
{:pipe_list, {:atom, '\'infinity\''},
{:type_list, ['n', 'o', 'n', :_, 'n', 'e', 'g', :_, 'i', 'n', 't', 'e', 'g', 'e', 'r'],
{:list, :paren, []}}}
) do
"timeout()"
end
defp do_pretty_print({:pipe_list, head, tail}) do
"#{do_pretty_print(head)} | #{do_pretty_print(tail)}"
end
defp do_pretty_print({:range, from, to}) do
"#{do_pretty_print(from)}..#{do_pretty_print(to)}"
end
defp do_pretty_print({:rest}) do
"..."
end
defp do_pretty_print({:size, size}) do
"size(#{do_pretty_print(size)})"
end
defp do_pretty_print({:tuple, tuple_items}) do
"{#{Enum.map_join(tuple_items, ", ", &do_pretty_print/1)}}"
end
defp do_pretty_print({:type, type}) do
"#{deatomize(type)}()"
end
defp do_pretty_print({:type, module, type}) do
module = do_pretty_print(module)
type =
if is_tuple(type) do
do_pretty_print(type)
else
deatomize(type) <> "()"
end
"#{module}.#{type}"
end
defp do_pretty_print({:type, module, type, inner_type}) do
"#{atomize(module)}.#{deatomize(type)}(#{do_pretty_print(inner_type)})"
end
defp do_pretty_print({:type_list, type, types}) do
"#{deatomize(type)}#{do_pretty_print(types)}"
end
defp atomize("Elixir." <> module_name) do
"#{String.trim(module_name, "'")}"
end
defp atomize(atom) when is_list(atom) do
atom_string =
atom
|> deatomize()
|> to_string()
stripped = strip_var_version(atom_string)
if stripped == atom_string do
atomize(stripped)
else
stripped
end
end
defp atomize(<<atom>>) when is_number(atom) do
"#{atom}"
end
defp atomize(atom) do
atom = to_string(atom)
if String.starts_with?(atom, "_") do
atom
else
inspect(:"#{String.trim(atom, "'")}")
end
end
defp atom_part_to_string({:int, atom_part}), do: Integer.to_charlist(atom_part)
defp atom_part_to_string(atom_part), do: atom_part
defp strip_var_version(var_name) do
var_name
|> String.replace(~r/^V(.+)@\d+$/, "\\1")
|> String.replace(~r/^(.+)@\d+$/, "\\1")
end
defp struct_parts(map_keys) do
%{struct_name: struct_name, entries: entries} =
Enum.reduce(map_keys, %{struct_name: nil, entries: []}, &struct_part/2)
%{struct_name: struct_name, entries: Enum.reverse(entries)}
end
defp struct_part({:map_entry, {:atom, '\'__struct__\''}, {:atom, struct_name}}, struct_parts) do
struct_name =
struct_name
|> atomize()
|> String.trim("\"")
Map.put(struct_parts, :struct_name, struct_name)
end
defp struct_part(entry, struct_parts = %{entries: entries}) do
Map.put(struct_parts, :entries, [entry | entries])
end
defp deatomize([:_, :_, '@', {:int, _}]) do
"_"
end
defp deatomize(chars) when is_list(chars) do
Enum.map(chars, fn char ->
char
|> deatomize_char()
|> atom_part_to_string()
end)
end
defp deatomize_char(char) when is_atom(char) do
Atom.to_string(char)
end
defp deatomize_char(char), do: char
end