zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

erlex.ex (15622B)


      1 defmodule Erlex do
      2   @moduledoc """
      3   Convert Erlang style structs and error messages to equivalent Elixir.
      4 
      5   Lexes and parses the Erlang output, then runs through pretty
      6   printer.
      7 
      8   ## Usage
      9 
     10   Invoke `Erlex.pretty_print/1` wuth the input string.
     11 
     12   ```elixir
     13   iex> str = ~S"('Elixir.Plug.Conn':t(),binary() | atom(),'Elixir.Keyword':t() | map()) -> 'Elixir.Plug.Conn':t()"
     14   iex> Erlex.pretty_print(str)
     15   (Plug.Conn.t(), binary() | atom(), Keyword.t() | map()) :: Plug.Conn.t()
     16   ```
     17 
     18   While the lion's share of the work is done via invoking
     19   `Erlex.pretty_print/1`, other higher order functions exist for further
     20   formatting certain messages by running through the Elixir formatter.
     21   Because we know the previous example is a type, we can invoke the
     22   `Erlex.pretty_print_contract/1` function, which would format that
     23   appropriately for very long lines.
     24 
     25   ```elixir
     26   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()"
     27   iex> Erlex.pretty_print_contract(str)
     28   (
     29     Plug.Conn.t(),
     30     binary() | atom(),
     31     Keyword.t() | map(),
     32     map() | atom(),
     33     non_neg_integer(),
     34     binary(),
     35     binary(),
     36     binary(),
     37     binary(),
     38     binary()
     39   ) :: Plug.Conn.t()
     40   ```
     41   """
     42 
     43   defp lex(str) do
     44     try do
     45       {:ok, tokens, _} = :lexer.string(str)
     46       tokens
     47     rescue
     48       _ ->
     49         throw({:error, :lexing, str})
     50     end
     51   end
     52 
     53   defp parse(tokens) do
     54     try do
     55       {:ok, [first | _]} = :parser.parse(tokens)
     56       first
     57     rescue
     58       _ ->
     59         throw({:error, :parsing, tokens})
     60     end
     61   end
     62 
     63   defp format(code) do
     64     try do
     65       Code.format_string!(code)
     66     rescue
     67       _ ->
     68         throw({:error, :formatting, code})
     69     end
     70   end
     71 
     72   @spec pretty_print_infix(infix :: String.t()) :: String.t()
     73   def pretty_print_infix('=:='), do: "==="
     74   def pretty_print_infix('=/='), do: "!=="
     75   def pretty_print_infix('/='), do: "!="
     76   def pretty_print_infix('=<'), do: "<="
     77   def pretty_print_infix(infix), do: to_string(infix)
     78 
     79   @spec pretty_print(str :: String.t()) :: String.t()
     80   def pretty_print(str) do
     81     parsed =
     82       str
     83       |> to_charlist()
     84       |> lex()
     85       |> parse()
     86 
     87     try do
     88       do_pretty_print(parsed)
     89     rescue
     90       _ ->
     91         throw({:error, :pretty_printing, parsed})
     92     end
     93   end
     94 
     95   @spec pretty_print_pattern(pattern :: String.t()) :: String.t()
     96   def pretty_print_pattern('pattern ' ++ rest) do
     97     pretty_print_type(rest)
     98   end
     99 
    100   def pretty_print_pattern(pattern) do
    101     pretty_print_type(pattern)
    102   end
    103 
    104   @spec pretty_print_contract(
    105           contract :: String.t(),
    106           module :: String.t(),
    107           function :: String.t()
    108         ) :: String.t()
    109   def pretty_print_contract(contract, module, function) do
    110     [head | tail] =
    111       contract
    112       |> to_string()
    113       |> String.split(";")
    114 
    115     head =
    116       head
    117       |> String.trim_leading(to_string(module))
    118       |> String.trim_leading(":")
    119       |> String.trim_leading(to_string(function))
    120 
    121     [head | tail]
    122     |> Enum.join(";")
    123     |> pretty_print_contract()
    124   end
    125 
    126   @spec pretty_print_contract(contract :: String.t()) :: String.t()
    127   def pretty_print_contract(contract) do
    128     [head | tail] =
    129       contract
    130       |> to_string()
    131       |> String.split(";")
    132 
    133     if Enum.empty?(tail) do
    134       do_pretty_print_contract(head)
    135     else
    136       joiner = "Contract head:\n"
    137 
    138       pretty =
    139         Enum.map_join([head | tail], "\n\n" <> joiner, fn contract ->
    140           contract
    141           |> to_charlist()
    142           |> do_pretty_print_contract()
    143         end)
    144 
    145       joiner <> pretty
    146     end
    147   end
    148 
    149   defp do_pretty_print_contract(contract) do
    150     prefix = "@spec a"
    151     suffix = "\ndef a() do\n  :ok\nend"
    152     pretty = pretty_print(contract)
    153 
    154     """
    155     @spec a#{pretty}
    156     def a() do
    157       :ok
    158     end
    159     """
    160     |> format()
    161     |> Enum.join("")
    162     |> String.trim_leading(prefix)
    163     |> String.trim_trailing(suffix)
    164     |> String.replace("\n      ", "\n")
    165   end
    166 
    167   @spec pretty_print_type(type :: String.t()) :: String.t()
    168   def pretty_print_type(type) do
    169     prefix = "@spec a("
    170     suffix = ") :: :ok\ndef a() do\n  :ok\nend"
    171     indented_suffix = ") ::\n        :ok\ndef a() do\n  :ok\nend"
    172     pretty = pretty_print(type)
    173 
    174     """
    175     @spec a(#{pretty}) :: :ok
    176     def a() do
    177       :ok
    178     end
    179     """
    180     |> format()
    181     |> Enum.join("")
    182     |> String.trim_leading(prefix)
    183     |> String.trim_trailing(suffix)
    184     |> String.trim_trailing(indented_suffix)
    185     |> String.replace("\n      ", "\n")
    186   end
    187 
    188   @spec pretty_print_args(args :: String.t()) :: String.t()
    189   def pretty_print_args(args) do
    190     prefix = "@spec a"
    191     suffix = " :: :ok\ndef a() do\n  :ok\nend"
    192     pretty = pretty_print(args)
    193 
    194     """
    195     @spec a#{pretty} :: :ok
    196     def a() do
    197       :ok
    198     end
    199     """
    200     |> format()
    201     |> Enum.join("")
    202     |> String.trim_leading(prefix)
    203     |> String.trim_trailing(suffix)
    204     |> String.replace("\n      ", "\n")
    205   end
    206 
    207   defp do_pretty_print({:any}) do
    208     "_"
    209   end
    210 
    211   defp do_pretty_print({:inner_any_function}) do
    212     "(...)"
    213   end
    214 
    215   defp do_pretty_print({:any_function}) do
    216     "(... -> any)"
    217   end
    218 
    219   defp do_pretty_print({:assignment, {:atom, atom}, value}) do
    220     name =
    221       atom
    222       |> deatomize()
    223       |> to_string()
    224       |> strip_var_version()
    225 
    226     "#{name} = #{do_pretty_print(value)}"
    227   end
    228 
    229   defp do_pretty_print({:atom, [:_]}) do
    230     "_"
    231   end
    232 
    233   defp do_pretty_print({:atom, ['_']}) do
    234     "_"
    235   end
    236 
    237   defp do_pretty_print({:atom, atom}) do
    238     atomize(atom)
    239   end
    240 
    241   defp do_pretty_print({:binary_part, value, _, size}) do
    242     "#{do_pretty_print(value)} :: #{do_pretty_print(size)}"
    243   end
    244 
    245   defp do_pretty_print({:binary_part, value, size}) do
    246     "#{do_pretty_print(value)} :: #{do_pretty_print(size)}"
    247   end
    248 
    249   defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 8}}}]}) do
    250     "binary()"
    251   end
    252 
    253   defp do_pretty_print({:binary, [{:binary_part, {:any}, {:any}, {:size, {:int, 1}}}]}) do
    254     "bitstring()"
    255   end
    256 
    257   defp do_pretty_print({:binary, binary_parts}) do
    258     binary_parts = Enum.map_join(binary_parts, ", ", &do_pretty_print/1)
    259     "<<#{binary_parts}>>"
    260   end
    261 
    262   defp do_pretty_print({:binary, value, size}) do
    263     "<<#{do_pretty_print(value)} :: #{do_pretty_print(size)}>>"
    264   end
    265 
    266   defp do_pretty_print({:byte_list, byte_list}) do
    267     byte_list
    268     |> Enum.into(<<>>, fn byte ->
    269       <<byte::8>>
    270     end)
    271     |> inspect()
    272   end
    273 
    274   defp do_pretty_print({:contract, {:args, args}, {:return, return}, {:whens, whens}}) do
    275     {printed_whens, when_names} = collect_and_print_whens(whens)
    276 
    277     args = {:when_names, when_names, args}
    278     return = {:when_names, when_names, return}
    279 
    280     "(#{do_pretty_print(args)}) :: #{do_pretty_print(return)} when #{printed_whens}"
    281   end
    282 
    283   defp do_pretty_print({:contract, {:args, {:inner_any_function}}, {:return, return}}) do
    284     "((...) -> #{do_pretty_print(return)})"
    285   end
    286 
    287   defp do_pretty_print({:contract, {:args, args}, {:return, return}}) do
    288     "#{do_pretty_print(args)} :: #{do_pretty_print(return)}"
    289   end
    290 
    291   defp do_pretty_print({:function, {:contract, {:args, args}, {:return, return}}}) do
    292     "(#{do_pretty_print(args)} -> #{do_pretty_print(return)})"
    293   end
    294 
    295   defp do_pretty_print({:int, int}) do
    296     "#{to_string(int)}"
    297   end
    298 
    299   defp do_pretty_print({:list, :paren, items}) do
    300     "(#{Enum.map_join(items, ", ", &do_pretty_print/1)})"
    301   end
    302 
    303   defp do_pretty_print(
    304          {:list, :square,
    305           [
    306             tuple: [
    307               {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}},
    308               {:atom, [:_]}
    309             ]
    310           ]}
    311        ) do
    312     "Keyword.t()"
    313   end
    314 
    315   defp do_pretty_print(
    316          {:list, :square,
    317           [
    318             tuple: [
    319               {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}},
    320               t
    321             ]
    322           ]}
    323        ) do
    324     "Keyword.t(#{do_pretty_print(t)})"
    325   end
    326 
    327   defp do_pretty_print({:list, :square, items}) do
    328     "[#{Enum.map_join(items, ", ", &do_pretty_print/1)}]"
    329   end
    330 
    331   defp do_pretty_print({:map_entry, key, value}) do
    332     "#{do_pretty_print(key)} => #{do_pretty_print(value)}"
    333   end
    334 
    335   defp do_pretty_print(
    336          {:map,
    337           [
    338             {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}},
    339             {:map_entry, {:atom, [:_]}, {:atom, [:_]}}
    340           ]}
    341        ) do
    342     "struct()"
    343   end
    344 
    345   defp do_pretty_print(
    346          {:map,
    347           [
    348             {:map_entry, {:atom, '\'__struct__\''},
    349              {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}},
    350             {:map_entry, {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}, {:atom, [:_]}}
    351           ]}
    352        ) do
    353     "struct()"
    354   end
    355 
    356   defp do_pretty_print(
    357          {:map,
    358           [
    359             {:map_entry, {:atom, '\'__struct__\''},
    360              {:type_list, ['a', 't', 'o', 'm'], {:list, :paren, []}}},
    361             {:map_entry, {:atom, [:_]}, {:atom, [:_]}}
    362           ]}
    363        ) do
    364     "struct()"
    365   end
    366 
    367   defp do_pretty_print(
    368          {:map,
    369           [
    370             {:map_entry, {:atom, '\'__exception__\''}, {:atom, '\'true\''}},
    371             {:map_entry, {:atom, '\'__struct__\''}, {:atom, [:_]}},
    372             {:map_entry, {:atom, [:_]}, {:atom, [:_]}}
    373           ]}
    374        ) do
    375     "Exception.t()"
    376   end
    377 
    378   defp do_pretty_print({:map, map_keys}) do
    379     %{struct_name: struct_name, entries: entries} = struct_parts(map_keys)
    380 
    381     if struct_name do
    382       "%#{struct_name}{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}"
    383     else
    384       "%{#{Enum.map_join(entries, ", ", &do_pretty_print/1)}}"
    385     end
    386   end
    387 
    388   defp do_pretty_print({:named_type_with_appended_colon, named_type, type})
    389        when is_tuple(named_type) and is_tuple(type) do
    390     case named_type do
    391       {:atom, name} ->
    392         name =
    393           name
    394           |> deatomize()
    395           |> to_string()
    396           |> strip_var_version()
    397 
    398         "#{name}: #{do_pretty_print(type)}"
    399 
    400       other ->
    401         "#{do_pretty_print(other)}: #{do_pretty_print(type)}"
    402     end
    403   end
    404 
    405   defp do_pretty_print({:named_type, named_type, type})
    406        when is_tuple(named_type) and is_tuple(type) do
    407     case named_type do
    408       {:atom, name} ->
    409         name =
    410           name
    411           |> deatomize()
    412           |> to_string()
    413           |> strip_var_version()
    414 
    415         "#{name} :: #{do_pretty_print(type)}"
    416 
    417       other ->
    418         "#{do_pretty_print(other)} :: #{do_pretty_print(type)}"
    419     end
    420   end
    421 
    422   defp do_pretty_print({:named_type, named_type, type}) when is_tuple(named_type) do
    423     case named_type do
    424       {:atom, name = '\'Elixir' ++ _} ->
    425         "#{atomize(name)}.#{deatomize(type)}()"
    426 
    427       {:atom, name} ->
    428         name =
    429           name
    430           |> deatomize()
    431           |> to_string()
    432           |> strip_var_version()
    433 
    434         "#{name} :: #{deatomize(type)}()"
    435 
    436       other ->
    437         name = do_pretty_print(other)
    438         "#{name} :: #{deatomize(type)}()"
    439     end
    440   end
    441 
    442   defp do_pretty_print({nil}) do
    443     "nil"
    444   end
    445 
    446   defp do_pretty_print({:pattern, pattern_items}) do
    447     "#{Enum.map_join(pattern_items, ", ", &do_pretty_print/1)}"
    448   end
    449 
    450   defp do_pretty_print(
    451          {:pipe_list, {:atom, ['f', 'a', 'l', 's', 'e']}, {:atom, ['t', 'r', 'u', 'e']}}
    452        ) do
    453     "boolean()"
    454   end
    455 
    456   defp do_pretty_print(
    457          {:pipe_list, {:atom, '\'infinity\''},
    458           {:type_list, ['n', 'o', 'n', :_, 'n', 'e', 'g', :_, 'i', 'n', 't', 'e', 'g', 'e', 'r'],
    459            {:list, :paren, []}}}
    460        ) do
    461     "timeout()"
    462   end
    463 
    464   defp do_pretty_print({:pipe_list, head, tail}) do
    465     "#{do_pretty_print(head)} | #{do_pretty_print(tail)}"
    466   end
    467 
    468   defp do_pretty_print({:range, from, to}) do
    469     "#{do_pretty_print(from)}..#{do_pretty_print(to)}"
    470   end
    471 
    472   defp do_pretty_print({:rest}) do
    473     "..."
    474   end
    475 
    476   defp do_pretty_print({:size, size}) do
    477     "size(#{do_pretty_print(size)})"
    478   end
    479 
    480   defp do_pretty_print({:tuple, tuple_items}) do
    481     "{#{Enum.map_join(tuple_items, ", ", &do_pretty_print/1)}}"
    482   end
    483 
    484   defp do_pretty_print({:type, type}) do
    485     "#{deatomize(type)}()"
    486   end
    487 
    488   defp do_pretty_print({:type, module, type}) do
    489     module = do_pretty_print(module)
    490 
    491     type =
    492       if is_tuple(type) do
    493         do_pretty_print(type)
    494       else
    495         deatomize(type) <> "()"
    496       end
    497 
    498     "#{module}.#{type}"
    499   end
    500 
    501   defp do_pretty_print({:type, module, type, inner_type}) do
    502     "#{atomize(module)}.#{deatomize(type)}(#{do_pretty_print(inner_type)})"
    503   end
    504 
    505   defp do_pretty_print({:type_list, type, types}) do
    506     "#{deatomize(type)}#{do_pretty_print(types)}"
    507   end
    508 
    509   defp do_pretty_print({:when_names, when_names, {:list, :paren, items}}) do
    510     Enum.map_join(items, ", ", &format_when_names(do_pretty_print(&1), when_names))
    511   end
    512 
    513   defp do_pretty_print({:when_names, when_names, item}) do
    514     format_when_names(do_pretty_print(item), when_names)
    515   end
    516 
    517   defp format_when_names(item, when_names) do
    518     trimmed = String.trim_leading(item, ":")
    519 
    520     if trimmed in when_names do
    521       downcase_first(trimmed)
    522     else
    523       item
    524     end
    525   end
    526 
    527   defp collect_and_print_whens(whens) do
    528     {pretty_names, when_names} =
    529       Enum.reduce(whens, {[], []}, fn {_, when_name, type}, {prettys, whens} ->
    530         pretty_name =
    531           {:named_type_with_appended_colon, when_name, type}
    532           |> do_pretty_print()
    533           |> downcase_first()
    534 
    535         {[pretty_name | prettys], [when_name | whens]}
    536       end)
    537 
    538     when_names =
    539       when_names
    540       |> Enum.map(fn {_, v} -> v |> atomize() |> String.trim_leading(":") end)
    541 
    542     printed_whens = pretty_names |> Enum.reverse() |> Enum.join(", ")
    543 
    544     {printed_whens, when_names}
    545   end
    546 
    547   defp downcase_first(string) do
    548     {first, rest} = String.split_at(string, 1)
    549     String.downcase(first) <> rest
    550   end
    551 
    552   defp atomize("Elixir." <> module_name) do
    553     String.trim(module_name, "'")
    554   end
    555 
    556   defp atomize([char]) do
    557     to_string(char)
    558   end
    559 
    560   defp atomize(atom) when is_list(atom) do
    561     atom_string =
    562       atom
    563       |> deatomize()
    564       |> to_string()
    565 
    566     stripped = strip_var_version(atom_string)
    567 
    568     if stripped == atom_string do
    569       atomize(stripped)
    570     else
    571       stripped
    572     end
    573   end
    574 
    575   defp atomize(<<atom>>) when is_number(atom) do
    576     "#{atom}"
    577   end
    578 
    579   defp atomize(atom) do
    580     atom = to_string(atom)
    581 
    582     if String.starts_with?(atom, "_") do
    583       atom
    584     else
    585       inspect(:"#{String.trim(atom, "'")}")
    586     end
    587   end
    588 
    589   defp atom_part_to_string({:int, atom_part}), do: Integer.to_charlist(atom_part)
    590   defp atom_part_to_string(atom_part), do: atom_part
    591 
    592   defp strip_var_version(var_name) do
    593     var_name
    594     |> String.replace(~r/^V(.+)@\d+$/, "\\1")
    595     |> String.replace(~r/^(.+)@\d+$/, "\\1")
    596   end
    597 
    598   defp struct_parts(map_keys) do
    599     %{struct_name: struct_name, entries: entries} =
    600       Enum.reduce(map_keys, %{struct_name: nil, entries: []}, &struct_part/2)
    601 
    602     %{struct_name: struct_name, entries: Enum.reverse(entries)}
    603   end
    604 
    605   defp struct_part({:map_entry, {:atom, '\'__struct__\''}, {:atom, struct_name}}, struct_parts) do
    606     struct_name =
    607       struct_name
    608       |> atomize()
    609       |> String.trim("\"")
    610 
    611     Map.put(struct_parts, :struct_name, struct_name)
    612   end
    613 
    614   defp struct_part(entry, struct_parts = %{entries: entries}) do
    615     Map.put(struct_parts, :entries, [entry | entries])
    616   end
    617 
    618   defp deatomize([:_, :_, '@', {:int, _}]) do
    619     "_"
    620   end
    621 
    622   defp deatomize(chars) when is_list(chars) do
    623     Enum.map(chars, fn char ->
    624       char
    625       |> deatomize_char()
    626       |> atom_part_to_string()
    627     end)
    628   end
    629 
    630   defp deatomize_char(char) when is_atom(char) do
    631     Atom.to_string(char)
    632   end
    633 
    634   defp deatomize_char(char), do: char
    635 end