diff --git a/lib/gradient/ast_specifier.ex b/lib/gradient/ast_specifier.ex index f79e648a..8aa4a4f3 100644 --- a/lib/gradient/ast_specifier.ex +++ b/lib/gradient/ast_specifier.ex @@ -556,7 +556,7 @@ defmodule Gradient.AstSpecifier do end @doc """ - Iterate over the list in abstract code format and runs mapper on each element + Iterate over the list in abstract code and runs mapper on each element. """ @spec cons_mapper(form(), [token()], options()) :: {form(), tokens()} def cons_mapper({:cons, anno, value, tail}, tokens, opts) do diff --git a/lib/gradient/debug.ex b/lib/gradient/debug.ex index 208aeca4..6633ad16 100644 --- a/lib/gradient/debug.ex +++ b/lib/gradient/debug.ex @@ -5,15 +5,37 @@ defmodule Gradient.Debug do ## TODO: specify elixir_form @type elixir_form() :: any() - @type erlang_form() :: Gradient.SpecifyErlAst.form() + @type erlang_form() :: Gradient.Types.form() + + @doc ~S""" + Translate the Elixir code to the Erlang AST. + """ + defmacro elixir_to_ast(do: code) do + quoted_to_ast(code) + end + + defmacro elixir_to_ast(code) do + quoted_to_ast(code) + end + + @doc ~S""" + Translate the Elixir AST to the Erlang AST. + """ + @spec quoted_to_ast(elixir_form()) :: erlang_form() + def quoted_to_ast(qt) do + env = :elixir_env.new() + {ast, _, _} = :elixir.quoted_to_erl(qt, env) + Macro.escape(ast) + end @doc ~S""" Return the Elixir AST of an Elixir module. """ @spec elixir_ast(module()) :: {:ok, [elixir_form()]} def elixir_ast(mod) do - {:ok, {_, [{:debug_info, {:debug_info_v1, :elixir_erl, abstract_code}}]}} = + {:ok, {_, [{:debug_info, {:debug_info_v1, :elixir_erl, abstract_code}}]}} = :beam_lib.chunks(get_beam_path_as_charlist(mod), [:debug_info]) + {:ok, _forms} = :elixir_erl.debug_info(:elixir_v1, :module_name, abstract_code, []) end @@ -22,7 +44,8 @@ defmodule Gradient.Debug do """ @spec erlang_ast(module()) :: {:ok, [erlang_form()]} def erlang_ast(mod) do - {:ok, _forms} = get_beam_path_as_charlist(mod) |> Gradient.ElixirFileUtils.get_forms_from_beam() + {:ok, _forms} = + get_beam_path_as_charlist(mod) |> Gradient.ElixirFileUtils.get_forms_from_beam() end @doc ~S""" diff --git a/lib/gradient/elixir_expr.ex b/lib/gradient/elixir_expr.ex new file mode 100644 index 00000000..3d0a911e --- /dev/null +++ b/lib/gradient/elixir_expr.ex @@ -0,0 +1,489 @@ +defmodule Gradient.ElixirExpr do + @moduledoc """ + Convert the Erlang abstract expressions to the Elixir code. + """ + + alias Gradient.ElixirFmt + + @type expr :: :erl_parse.abstract_expr() + @type clause :: :erl_parse.abstract_clause() + + @doc """ + Convert abstract expressions to Elixir code and format output with formatter. + """ + @spec pp_expr_format([expr()], keyword()) :: iodata() + def pp_expr_format(exprs, fmt_opts \\ []) do + exprs + |> pp_expr() + |> Code.format_string!(fmt_opts) + end + + @doc """ + Convert abstract expressions to Elixir code. + """ + @spec pp_expr(expr() | [expr()]) :: String.t() + def pp_expr(exprs) when is_list(exprs) do + exprs + |> Enum.map(&pp_expr/1) + |> Enum.join("; ") + end + + def pp_expr({:atom, _, val}) when val in [nil, true, false] do + Atom.to_string(val) + end + + def pp_expr({:atom, _, val}) do + case Atom.to_string(val) do + "Elixir." <> mod -> mod + str -> ":" <> str + end + end + + def pp_expr({:char, _, l}) do + "?" <> List.to_string([l]) + end + + def pp_expr({:float, _, l}) do + Float.to_string(l) + end + + def pp_expr({:integer, _, l}) do + Integer.to_string(l) + end + + def pp_expr({:string, _, charlist}) do + "\'" <> List.to_string(charlist) <> "\'" + end + + def pp_expr({:cons, _, _, _} = cons) do + case cons_to_int_list(cons) do + {:ok, l} -> + inspect(l) + + :error -> + items = pp_cons(cons) + + "[" <> items <> "]" + end + end + + def pp_expr({:fun, _, {:function, name, arity}}) do + "&#{name}/#{arity}" + end + + def pp_expr({:fun, _, {:function, {:atom, _, module}, {:atom, _, name}, arity}}) do + module = ElixirFmt.parse_module(module) + name = Atom.to_string(name) + arity = pp_expr(arity) + "&#{module}#{name}/#{arity}" + end + + def pp_expr({:fun, _, {:clauses, clauses}}) do + # print all as a one line + clauses = pp_clauses(clauses) + "fn " <> clauses <> " end" + end + + def pp_expr({:call, _, {:remote, _, {:atom, _, :erlang}, {:atom, _, :throw}}, [arg]}) do + "throw " <> pp_expr(arg) + end + + def pp_expr( + {:call, _, {:remote, _, {:atom, _, :erlang}, {:atom, _, :error}}, + [ + {:call, _, {:remote, _, {:atom, _, :erlang}, {:atom, _, :raise}}, + [ + {:atom, _, :error}, + {:call, _, {:remote, _, {:atom, _, Kernel.Utils}, {:atom, _, :raise}}, [var]}, + var_stacktrace + ]} + ]} + ) do + "reraise " <> pp_expr(var) <> ", " <> pp_expr(var_stacktrace) + end + + def pp_expr({:call, _, {:remote, _, {:atom, _, :erlang}, {:atom, _, :error}}, [arg]}) do + "raise " <> pp_raise_arg(arg) + end + + def pp_expr({:call, _, name, args}) do + args = + Enum.map(args, &pp_expr/1) + |> Enum.join(", ") + + pp_name(name) <> "(" <> args <> ")" + end + + def pp_expr({:map, _, pairs}) do + case try_get_struct(pairs) do + {nil, pairs} -> + pairs = format_map_elements(pairs) + "%{" <> pairs <> "}" + + {struct_name, pairs} -> + pairs = format_map_elements(pairs) + name = pp_expr(struct_name) + "%" <> name <> "{" <> pairs <> "}" + end + end + + def pp_expr({:map, _, map, pairs}) do + pairs = format_map_elements(pairs) + map = pp_expr(map) + "%{" <> map <> " | " <> pairs <> "}" + end + + def pp_expr({:match, _, var, expr}) do + pp_expr(var) <> " = " <> pp_expr(expr) + end + + def pp_expr({nil, _}) do + "[]" + end + + def pp_expr({:op, _, op, type}) do + operator_to_string(op) <> " " <> pp_expr(type) + end + + def pp_expr({:op, _, op, left_type, right_type}) do + operator = " " <> operator_to_string(op) <> " " + pp_expr(left_type) <> operator <> pp_expr(right_type) + end + + def pp_expr({:tuple, _, elements}) do + elements_str = Enum.map(elements, &pp_expr(&1)) |> Enum.join(", ") + "{" <> elements_str <> "}" + end + + def pp_expr({:var, anno, t}) do + case Atom.to_string(t) |> String.split("@") |> List.first() do + "_" -> if :erl_anno.generated(anno), do: "_gen", else: "_" + "_" <> name -> name + name -> name + end + end + + def pp_expr({:bin, _, [{:bin_element, _, {:string, _, value}, :default, :default}]}) do + "\"" <> to_string(value) <> "\"" + end + + def pp_expr({:bin, _, elements}) do + bin = + elements + |> Enum.map(fn e -> pp_bin_element(e) end) + |> Enum.join(", ") + + "<<" <> bin <> ">>" + end + + def pp_expr({t, _, expr0, quantifiers}) when t in [:bc, :lc] do + expr0 = pp_expr(expr0) + pquantifiers = pp_expr(quantifiers) + "for #{pquantifiers}, do: #{expr0}" + end + + # Quantifiers + def pp_expr({:b_generate, _, pattern, expr}) do + # drop >> to insert quantifier before + ppatern = String.slice(pp_expr(pattern), 0..-3) + # add a space before >> for a case whan expr is a bin + ppatern <> " <- " <> pp_expr(expr) <> " >>" + end + + def pp_expr({:case, _, condition, clauses} = case_expr) do + case get_conditional_type(clauses) do + :if -> + clauses = pp_clauses(clauses, :if) + "if " <> pp_expr(condition) <> " do " <> clauses <> " end" + + :cond -> + "cond do " <> pp_cond_expr(case_expr) <> " end" + + :case -> + clauses = pp_clauses(clauses, :case) + "case " <> pp_expr(condition) <> " do " <> clauses <> " end" + end + end + + def pp_expr({:receive, _, clauses}) do + "receive do " <> pp_clauses(clauses) <> " end" + end + + def pp_expr({:receive, _, clauses, after_value, after_body}) do + pclauses = pp_clauses(clauses) + pvalue = pp_expr(after_value) + pafter_body = pp_expr(after_body) + "receive do " <> pclauses <> " after " <> pvalue <> " -> " <> pafter_body <> " end" + end + + def pp_expr({:try, _, body, else_block, catchers, after_block}) do + "try do " + |> append_try_body(body) + |> maybe_try_else(else_block) + |> maybe_try_catch(catchers) + |> maybe_try_after(after_block) + |> Kernel.<>(" end") + end + + def pp_expr({:block, _, body}) do + pp_expr(body) + end + + # def pp_expr(expr) do + # :erl_pp.expr(expr) + # |> :erlang.iolist_to_binary() + # end + + @doc """ + Convert abstract clauses to Elixir code + """ + @spec pp_clauses([clause()], :case | :if | :catch) :: String.t() + def pp_clauses(clauses, type \\ :case) + + def pp_clauses(clauses, :case) do + Enum.map(clauses, &pp_case_clause/1) |> Enum.join("; ") + end + + def pp_clauses(clauses, :if) do + clauses + |> Enum.sort_by(fn c -> elem(hd(elem(c, 2)), 2) end, &>=/2) + |> Enum.map(&pp_if_clause/1) + |> Enum.join(" else ") + end + + def pp_clauses(clauses, :catch) do + Enum.map(clauses, &pp_catch_clause/1) |> Enum.join("; ") + end + + def pp_guards([]) do + "" + end + + def pp_guards([[guard]]) do + " when " <> pp_expr(guard) + end + + # Private + + def operator_to_string(:andalso), do: operator_to_string(:and) + def operator_to_string(:orelse), do: operator_to_string(:or) + def operator_to_string(op), do: Atom.to_string(op) + + defp pp_catch_clause({:clause, _, [{:tuple, _, [type, var, _stacktrace]}], guards, body}) do + # rescue/catch clause + case {elem(type, 2), get_error_struct(guards)} do + {:error, {:ok, error_struct}} -> + # rescue when error is struct + {var2, body2} = get_error_var(var, body) + + pp_expr(type) <> + ", %" <> + pp_expr(error_struct) <> + "{} = " <> pp_expr(var2) <> " -> " <> pp_expr(body2) + + {:error, :not_found} -> + # rescue + {var2, body2} = get_error_var(var, body) + + pp_expr(type) <> + ", " <> pp_expr(var2) <> " -> " <> pp_expr(body2) + + {:throw, :not_found} -> + # throw + pp_expr(type) <> ", " <> pp_expr(var) <> " -> " <> pp_expr(body) + end + end + + defp pp_case_clause({:clause, _, patterns, guards, body}) do + patterns = + patterns + |> Enum.map(&pp_expr/1) + |> Enum.join(", ") + + patterns <> pp_guards(guards) <> " -> " <> pp_expr(body) + end + + defp pp_if_clause({:clause, _, _, [], body}) do + pp_expr(body) + end + + def pp_cond_expr({:case, _, condition, clauses}) do + clauses = Enum.map(clauses, &cond_clause_pp/1) |> Enum.filter(&(&1 != "")) |> Enum.join("; ") + pp_expr(condition) <> " -> " <> clauses + end + + def pp_cond_expr(_), do: "" + + def cond_clause_pp({:clause, _, [{:atom, _, true}], _, body}), do: pp_expr(body) + + def cond_clause_pp({:clause, _, [{:atom, _, false}], _, [case_expr]}), + do: pp_cond_expr(case_expr) + + def cond_clause_pp(_), do: "" + + def get_conditional_type(clauses) do + if length(clauses) == 2 and + Enum.all?(clauses, fn + {:clause, _, [{:atom, _, bool}], [], _} -> is_boolean(bool) + _ -> false + end) do + if is_cond?(clauses), do: :cond, else: :if + else + :case + end + end + + def is_cond?(clauses) do + case Enum.find(clauses, fn {:clause, _, [{:atom, _, bool}], [], _} -> bool == false end) do + {:clause, _, _, _, [expr]} -> elem(expr, 0) == :case + _ -> false + end + end + + defp append_try_body(res, body) do + res <> pp_expr(body) + end + + defp maybe_try_else(res, []) do + res + end + + defp maybe_try_else(res, else_block) do + res <> "; else " <> pp_clauses(else_block) + end + + defp maybe_try_catch(res, []), do: res + defp maybe_try_catch(res, clauses), do: res <> "; catch " <> pp_clauses(clauses, :catch) + + defp maybe_try_after(res, []) do + res + end + + defp maybe_try_after(res, else_block) do + res <> "; after " <> pp_expr(else_block) + end + + def get_error_struct([[{:op, _, :andalso, {:op, _, :==, _, error_struct}, _}]]) do + {:ok, error_struct} + end + + def get_error_struct(_) do + :not_found + end + + def get_error_var({:var, _, v}, [{:match, _, user_var, {:var, _, v}} | body_tail]) do + {user_var, body_tail} + end + + def get_error_var({:var, _, v}, [ + {:match, _, user_var, {:call, _, _, [_, {:var, _, v} | _]}} | body_tail + ]) do + # Extract variable from Exception.normalize (used in reraise) + {user_var, body_tail} + end + + def get_error_var(var, body) do + {var, body} + end + + defp pp_bin_element({:bin_element, _, value, size, tsl}) do + value = bin_pp_value(value) + + bin_set_tsl(tsl) + |> bin_set_size(size) + |> bin_set_value(value) + end + + defp bin_pp_value({:string, _, val}), do: "\"" <> List.to_string(val) <> "\"" + defp bin_pp_value(val), do: pp_expr(val) + + defp bin_set_value("", value), do: value + defp bin_set_value(sufix, value), do: value <> "::" <> sufix + + defp bin_set_size("", :default), do: "" + defp bin_set_size("", {:integer, _, size}), do: Integer.to_string(size) + defp bin_set_size(tsl, :default), do: tsl + defp bin_set_size(tsl, {:integer, _, size}), do: "#{tsl}-size(#{Integer.to_string(size)})" + + defp bin_set_tsl(:default), do: "" + defp bin_set_tsl([:integer]), do: "" + defp bin_set_tsl([tsl]), do: Atom.to_string(tsl) + + def format_map_elements(elems) do + atom_keys = all_keys_atoms?(elems) + Enum.map(elems, fn p -> format_map_element(p, atom_keys) end) |> Enum.join(", ") + end + + @spec format_map_element(tuple(), boolean()) :: String.t() + def format_map_element({_field, _, key, value}, shortand_syntax) do + value = pp_expr(value) + + if shortand_syntax do + {:atom, _, key} = key + Atom.to_string(key) <> ": " <> value + else + pp_expr(key) <> " => " <> value + end + end + + def all_keys_atoms?(pairs) do + Enum.all?(pairs, fn {_, _, key, _} -> :atom == elem(key, 0) end) + end + + @spec try_get_struct([tuple()]) :: {struct_name :: nil | expr(), pairs_left :: [tuple()]} + def try_get_struct(pairs) do + {n, ps} = + Enum.reduce(pairs, {nil, []}, fn p, {n, ps} -> + case get_struct_name(p) do + nil -> {n, [p | ps]} + name -> {name, ps} + end + end) + + {n, Enum.reverse(ps)} + end + + def get_struct_name({_, _, {:atom, _, :__struct__}, val}), do: val + def get_struct_name(_), do: nil + + @spec cons_to_int_list(tuple()) :: {:ok, [integer()]} | :error + def cons_to_int_list(cons) do + try do + {:ok, try_int_list_(cons)} + catch + nil -> + :error + end + end + + defp pp_raise_arg({:call, _, {:remote, _, error_type, {:atom, _, :exception}}, [{nil, _}]}) do + pp_expr(error_type) + end + + defp pp_raise_arg( + {:call, _, {:remote, _, {:atom, _, RuntimeError}, {:atom, _, :exception}}, [arg]} + ) do + pp_expr(arg) + end + + defp pp_raise_arg({:call, _, {:remote, _, error_type, {:atom, _, :exception}}, [arg]}) do + pp_expr(error_type) <> ", " <> pp_expr(arg) + end + + defp pp_raise_arg(arg) do + pp_expr(arg) + end + + defp try_int_list_({nil, _}), do: [] + defp try_int_list_({:cons, _, {:integer, _, val}, t}), do: [val | try_int_list_(t)] + defp try_int_list_(_), do: throw(nil) + + defp pp_cons({:cons, _, h, {nil, _}}), do: pp_expr(h) + defp pp_cons({:cons, _, h, {:var, _, _} = v}), do: pp_expr(h) <> " | " <> pp_expr(v) + defp pp_cons({:cons, _, h, t}), do: pp_expr(h) <> ", " <> pp_cons(t) + + defp pp_name({:remote, _, {:atom, _, m}, {:atom, _, n}}), + do: ElixirFmt.parse_module(m) <> to_string(n) + + defp pp_name({:atom, _, n}), do: to_string(n) +end diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 2616e6ea..b6a404ee 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -6,6 +6,7 @@ defmodule Gradient.ElixirFmt do alias :gradualizer_fmt, as: FmtLib alias Gradient.ElixirType + alias Gradient.ElixirExpr def print_errors(errors, opts) do for {file, e} <- errors do @@ -28,7 +29,8 @@ defmodule Gradient.ElixirFmt do end def format_error(error, opts) do - opts = Keyword.put(opts, :fmt_type_fun, &ElixirType.pretty_print/1) + opts = Keyword.put_new(opts, :fmt_type_fun, &ElixirType.pretty_print/1) + opts = Keyword.put_new(opts, :fmt_expr_fun, &ElixirExpr.pp_expr/1) format_type_error(error, opts) end @@ -40,10 +42,10 @@ defmodule Gradient.ElixirFmt do def format_type_error({:call_undef, anno, module, func, arity}, opts) do :io_lib.format( - "~sCall to undefined function ~p:~p/~p~s~n", + "~sCall to undefined function ~s~p/~p~s~n", [ format_location(anno, :brief, opts), - module, + parse_module(module), func, arity, format_location(anno, :verbose, opts) @@ -143,13 +145,24 @@ defmodule Gradient.ElixirFmt do end end - def pp_expr(expression, _opts) do - IO.ANSI.blue() <> "#{inspect(expression)}" <> IO.ANSI.reset() + def pp_expr(expression, opts) do + fmt = Keyword.get(opts, :fmt_expr_fun, &ElixirExpr.pp_expr/1) + + if Keyword.get(opts, :colors, true) do + IO.ANSI.blue() <> fmt.(expression) <> IO.ANSI.reset() + else + fmt.(expression) + end end - def pp_type(type, _opts) do - pp = ElixirType.pretty_print(type) - IO.ANSI.cyan() <> pp <> IO.ANSI.reset() + def pp_type(type, opts) do + fmt = Keyword.get(opts, :fmt_type_fun, &ElixirType.pretty_print/1) + + if Keyword.get(opts, :colors, true) do + IO.ANSI.cyan() <> fmt.(type) <> IO.ANSI.reset() + else + fmt.(type) + end end def try_highlight_in_context(expression, opts) do @@ -202,6 +215,16 @@ defmodule Gradient.ElixirFmt do def get_ex_file_path([{:attribute, 1, :file, {path, 1}} | _]), do: {:ok, path} def get_ex_file_path(_), do: {:error, :not_found} + @spec parse_module(atom()) :: String.t() + def parse_module(:elixir), do: "" + + def parse_module(mod) do + case Atom.to_string(mod) do + "Elixir." <> mod_str -> mod_str <> "." + mod -> ":" <> mod <> "." + end + end + # defp warning_error_not_handled(error) do # msg = "\nElixir formatter not exist for #{inspect(error, pretty: true)} using default \n" # String.to_charlist(IO.ANSI.light_yellow() <> msg <> IO.ANSI.reset()) diff --git a/lib/gradient/elixir_type.ex b/lib/gradient/elixir_type.ex index 36ddf27a..a6022f9d 100644 --- a/lib/gradient/elixir_type.ex +++ b/lib/gradient/elixir_type.ex @@ -8,6 +8,8 @@ defmodule Gradient.ElixirType do are not used by Elixir so the pp support has not been added. """ + alias Gradient.ElixirFmt + @type abstract_type() :: Gradient.Types.abstract_type() @doc """ @@ -17,7 +19,7 @@ defmodule Gradient.ElixirType do def pretty_print({:remote_type, _, [{:atom, _, mod}, {:atom, _, name}, args]}) do args_str = Enum.map(args, &pretty_print(&1)) |> Enum.join(", ") name_str = Atom.to_string(name) - mod_str = parse_module(mod) + mod_str = ElixirFmt.parse_module(mod) mod_str <> name_str <> "(#{args_str})" end @@ -123,16 +125,6 @@ defmodule Gradient.ElixirType do ### Private ###### - @spec parse_module(atom()) :: String.t() - defp parse_module(:elixir), do: "" - - defp parse_module(mod) do - case Atom.to_string(mod) do - "Elixir." <> mod_str -> mod_str <> "." - mod -> mod <> "." - end - end - @spec association_type(tuple()) :: String.t() defp association_type({:type, _, :map_field_assoc, [key, value]}) do key_str = pretty_print(key) diff --git a/test/examples/Elixir.ListComprehension.beam b/test/examples/Elixir.ListComprehension.beam index 855dcf11..233f3886 100644 Binary files a/test/examples/Elixir.ListComprehension.beam and b/test/examples/Elixir.ListComprehension.beam differ diff --git a/test/examples/Elixir.SimpleApp.beam b/test/examples/Elixir.SimpleApp.beam index 65efabc9..6c25549e 100644 Binary files a/test/examples/Elixir.SimpleApp.beam and b/test/examples/Elixir.SimpleApp.beam differ diff --git a/test/examples/list_comprehension.ex b/test/examples/list_comprehension.ex index 80858783..552a64df 100644 --- a/test/examples/list_comprehension.ex +++ b/test/examples/list_comprehension.ex @@ -1,5 +1,13 @@ defmodule ListComprehension do def lc do + for n <- [1, 2, 3], do: n + end + + def bc do + for <<(r::8 <- <<1, 2, 3, 4, 5>>)>>, into: <<>>, do: <> + end + + def lc_complex do for n <- 0..5, rem(n, 3) == 0, do: n * n end end diff --git a/test/examples/type/Elixir.RecordEx.beam b/test/examples/type/Elixir.RecordEx.beam index 411e991a..47ab9330 100644 Binary files a/test/examples/type/Elixir.RecordEx.beam and b/test/examples/type/Elixir.RecordEx.beam differ diff --git a/test/examples/type/Elixir.WrongRet.beam b/test/examples/type/Elixir.WrongRet.beam index 7b3c8491..7e45ef95 100644 Binary files a/test/examples/type/Elixir.WrongRet.beam and b/test/examples/type/Elixir.WrongRet.beam differ diff --git a/test/examples/type/record.ex b/test/examples/type/record.ex index 4c8c36c8..d0d311d5 100644 --- a/test/examples/type/record.ex +++ b/test/examples/type/record.ex @@ -9,4 +9,7 @@ defmodule RecordEx do @spec ret_wrong_record2() :: user() def ret_wrong_record2(), do: user(name: 12) + + @spec ret_wrong_atom() :: atom() + def ret_wrong_atom(), do: user(name: "Kate") end diff --git a/test/examples/type/wrong_ret.ex b/test/examples/type/wrong_ret.ex index e1deba34..67a1f19e 100644 --- a/test/examples/type/wrong_ret.ex +++ b/test/examples/type/wrong_ret.ex @@ -61,6 +61,9 @@ defmodule WrongRet do @spec ret_wrong_keyword() :: keyword() def ret_wrong_keyword, do: [1, 2, 3] + @spec ret_wrong_list() :: list() + def ret_wrong_list, do: ?c + @spec ret_wrong_tuple() :: tuple() def ret_wrong_tuple, do: %{a: 1, b: 2} @@ -69,4 +72,10 @@ defmodule WrongRet do @spec ret_wrong_fun() :: (... -> atom()) def ret_wrong_fun, do: fn -> 12 end + + @spec ret_wrong_call() :: integer() + def ret_wrong_call, do: ret_wrong_boolean() + + @spec ret_wrong_integer5() :: integer() + def ret_wrong_integer5, do: &ret_wrong_atom/0 end diff --git a/test/gradient/ast_specifier_test.exs b/test/gradient/ast_specifier_test.exs index 7a178b65..633daa7e 100644 --- a/test/gradient/ast_specifier_test.exs +++ b/test/gradient/ast_specifier_test.exs @@ -709,40 +709,42 @@ defmodule Gradient.AstSpecifierTest do [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - assert {:function, 2, :lc, 0, + assert {:function, 11, :lc_complex, 0, [ - {:clause, 2, [], [], + {:clause, 11, [], [], [ - {:call, 3, {:remote, 3, {:atom, 3, :lists}, {:atom, 3, :reverse}}, + {:call, 12, {:remote, 12, {:atom, 12, :lists}, {:atom, 12, :reverse}}, [ - {:call, 3, {:remote, 3, {:atom, 3, Enum}, {:atom, 3, :reduce}}, + {:call, 12, {:remote, 12, {:atom, 12, Enum}, {:atom, 12, :reduce}}, [ - {:map, 3, + {:map, 12, [ - {:map_field_assoc, 3, {:atom, 3, :__struct__}, {:atom, 3, Range}}, - {:map_field_assoc, 3, {:atom, 3, :first}, {:integer, 3, 0}}, - {:map_field_assoc, 3, {:atom, 3, :last}, {:integer, 3, 5}}, - {:map_field_assoc, 3, {:atom, 3, :step}, {:integer, 3, 1}} + {:map_field_assoc, 12, {:atom, 12, :__struct__}, {:atom, 12, Range}}, + {:map_field_assoc, 12, {:atom, 12, :first}, {:integer, 12, 0}}, + {:map_field_assoc, 12, {:atom, 12, :last}, {:integer, 12, 5}}, + {:map_field_assoc, 12, {:atom, 12, :step}, {:integer, 12, 1}} ]}, - {nil, 3}, - {:fun, 3, + {nil, 12}, + {:fun, 12, {:clauses, [ - {:clause, 3, [{:var, 3, :_n@1}, {:var, 3, :_@1}], [], + {:clause, 12, [{:var, 12, :_n@1}, {:var, 12, :_@1}], [], [ - {:case, [generated: true, location: 3], - {:op, 3, :==, {:op, 3, :rem, {:var, 3, :_n@1}, {:integer, 3, 3}}, - {:integer, 3, 0}}, + {:case, [generated: true, location: 12], + {:op, 12, :==, + {:op, 12, :rem, {:var, 12, :_n@1}, {:integer, 12, 3}}, + {:integer, 12, 0}}, [ - {:clause, [generated: true, location: 3], - [{:atom, [generated: true, location: 3], true}], [], + {:clause, [generated: true, location: 12], + [{:atom, [generated: true, location: 12], true}], [], [ - {:cons, 3, {:op, 3, :*, {:var, 3, :_n@1}, {:var, 3, :_n@1}}, - {:var, 3, :_@1}} + {:cons, 12, + {:op, 12, :*, {:var, 12, :_n@1}, {:var, 12, :_n@1}}, + {:var, 12, :_@1}} ]}, - {:clause, [generated: true, location: 3], - [{:atom, [generated: true, location: 3], false}], [], - [{:var, 3, :_@1}]} + {:clause, [generated: true, location: 12], + [{:atom, [generated: true, location: 12], false}], [], + [{:var, 12, :_@1}]} ]} ]} ]}} diff --git a/test/gradient/elixir_expr_test.exs b/test/gradient/elixir_expr_test.exs new file mode 100644 index 00000000..619acf7b --- /dev/null +++ b/test/gradient/elixir_expr_test.exs @@ -0,0 +1,329 @@ +defmodule Gradient.ElixirExprTest do + use ExUnit.Case + doctest Gradient.ElixirExpr + + alias Gradient.ElixirExpr + alias Gradient.ExprData + + require Gradient.Debug + import Gradient.Debug, only: [elixir_to_ast: 1] + + describe "simple pretty print" do + for {name, type, expected} <- ExprData.all_basic_pp_test_data() do + test "#{name}" do + type = unquote(Macro.escape(type)) + assert unquote(expected) == ElixirExpr.pp_expr(type) + end + end + end + + test "pretty print expr formatted" do + actual = + elixir_to_ast do + case {:ok, 13} do + {:ok, v} -> v + _err -> :error + end + end + |> ElixirExpr.pp_expr_format() + |> Enum.join("") + + assert "case {:ok, 13} do\n {:ok, v} -> v\n _err -> :error\nend" == actual + end + + describe "complex pretty print" do + test "lambda" do + actual = + elixir_to_ast do + fn + {:ok, v} -> + v + + {:error, _} -> + :error + end + end + |> ElixirExpr.pp_expr() + + assert "fn {:ok, v} -> v; {:error, _} -> :error end" == actual + end + + test "binary comprehension" do + actual = + elixir_to_ast do + pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> + for <>, do: {r, g, b} + end + |> ElixirExpr.pp_expr() + + assert "pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>; for <>, do: {r, g, b}" == + actual + end + + test "binary comprehension 2" do + actual = + elixir_to_ast do + for <>)>>, do: one + end + |> ElixirExpr.pp_expr() + + assert "for <> >>, do: one" == actual + end + + test "receive" do + actual = + elixir_to_ast do + receive do + {:hello, msg} -> msg + end + end + |> ElixirExpr.pp_expr() + + assert "receive do {:hello, msg} -> msg end" == actual + end + + test "receive after" do + actual = + elixir_to_ast do + receive do + {:hello, msg} -> msg + after + 1_000 -> "nothing happened" + end + end + |> ElixirExpr.pp_expr() + + assert ~s(receive do {:hello, msg} -> msg after 1000 -> "nothing happened" end) == actual + end + + test "call pipe" do + actual = + elixir_to_ast do + [1, 2, 3] + |> Enum.map(fn x -> x + 1 end) + |> Enum.map(&(&1 + 1)) + end + |> ElixirExpr.pp_expr() + + assert "Enum.map(Enum.map([1, 2, 3], fn x -> x + 1 end), fn _ -> _ + 1 end)" == actual + end + + test "with" do + actual = + elixir_to_ast do + map = %{a: 12, b: 0} + + with {:ok, a} <- Map.fetch(map, :a), + {:ok, b} <- Map.fetch(map, :b) do + a + b + else + :error -> + 0 + end + end + |> ElixirExpr.pp_expr() + + assert "map = %{a: 12, b: 0}; case :maps.find(:a, map) do {:ok, a} -> case :maps.find(:b, map) do {:ok, b} -> a + b; _gen -> case _gen do :error -> 0; _gen -> raise {:with_clause, _gen} end end; _gen -> case _gen do :error -> 0; _gen -> raise {:with_clause, _gen} end end" == + actual + end + + test "try reraise" do + actual = + elixir_to_ast do + try do + raise "ok" + rescue + e -> + IO.puts(Exception.format(:error, e, __STACKTRACE__)) + reraise e, __STACKTRACE__ + end + end + |> ElixirExpr.pp_expr() + + assert ~s(try do raise "ok"; catch :error, e -> IO.puts(Exception.format(:error, e, __STACKTRACE__\)\); reraise e, __STACKTRACE__ end) == + actual + end + + test "try rescue without error var" do + actual = + elixir_to_ast do + try do + raise "oops" + rescue + RuntimeError -> "Error!" + end + end + |> ElixirExpr.pp_expr() + + assert ~s(try do raise "oops"; catch :error, %RuntimeError{} = _ -> "Error!" end) == + actual + end + + test "simple rescue try" do + actual = + elixir_to_ast do + try do + :ok + rescue + _ -> :ok + end + end + |> ElixirExpr.pp_expr() + + assert "try do :ok; catch :error, _ -> :ok end" == actual + end + + test "simple after try" do + actual = + elixir_to_ast do + try do + :ok + after + :ok + end + end + |> ElixirExpr.pp_expr() + + assert "try do :ok; after :ok end" == actual + end + + test "try guard" do + actual = + elixir_to_ast do + try do + throw("good") + :ok + rescue + e in RuntimeError -> + 11 + e + else + v when v == :ok -> + :ok + + v -> + :nok + catch + val when is_integer(val) -> + val + + _ -> + 0 + after + IO.puts("Cleaning!") + end + end + |> ElixirExpr.pp_expr() + + assert ~s(try do throw "good"; :ok; else v when v == :ok -> :ok; v -> :nok; catch :error, %RuntimeError{} = e -> 11; e; :throw, val -> val; :throw, _ -> 0; after IO.puts("Cleaning!"\) end) == + actual + end + + test "case guard" do + actual = + elixir_to_ast do + case {:ok, 10} do + {:ok, v} when (v > 0 and v > 1) or v < -1 -> + :ok + + t when is_tuple(t) -> + :nok + + _ -> + :err + end + end + |> ElixirExpr.pp_expr() + + assert "case {:ok, 10} do {:ok, v} when v > 0 and v > 1 or v < - 1 -> :ok; t when :erlang.is_tuple(t) -> :nok; _ -> :err end" == + actual + end + + test "case" do + actual = + elixir_to_ast do + case {:ok, 13} do + {:ok, v} -> v + _err -> :error + end + end + |> ElixirExpr.pp_expr() + + assert "case {:ok, 13} do {:ok, v} -> v; _err -> :error end" == actual + end + + test "if" do + actual = + elixir_to_ast do + if :math.floor(1.9) == 1.0 do + :ok + else + :error + end + end + |> ElixirExpr.pp_expr() + + assert "if :math.floor(1.9) == 1.0 do :ok else :error end" == actual + end + + test "unless" do + actual = + elixir_to_ast do + unless :math.floor(1.9) == 1.0 do + :ok + else + :error + end + end + |> ElixirExpr.pp_expr() + + assert "if :math.floor(1.9) == 1.0 do :error else :ok end" == actual + end + + test "cond" do + actual = + elixir_to_ast do + cond do + true == false -> + :ok + + :math.floor(1.9) == 1.0 -> + :ok + + true -> + :error + end + end + |> ElixirExpr.pp_expr() + + assert "cond do true == false -> :ok; :math.floor(1.9) == 1.0 -> :ok; true -> :error end" == + actual + end + + test "try with rescue and catch" do + actual = + elixir_to_ast do + try do + if true do + throw("good") + else + raise "oops" + end + rescue + e in RuntimeError -> + 11 + e + catch + val -> + 12 + val + end + end + |> ElixirExpr.pp_expr() + + assert ~s(try do if true do throw "good" else raise "oops" end;) <> + ~s( catch :error, %RuntimeError{} = e -> 11; e; :throw, val -> 12; val end) == + actual + end + end +end diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index 063e0339..78eafb14 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -28,84 +28,84 @@ defmodule Gradient.ElixirFmtTest do describe "types format" do test "return integer() instead of atom()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_atom) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_atom) assert String.contains?(expected, "atom()") assert String.contains?(actual, "1") end test "return tuple() instead of atom()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_atom2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_atom2) assert String.contains?(expected, "atom()") assert String.contains?(actual, "{:ok, []}") end test "return map() instead of atom()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_atom3) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_atom3) assert String.contains?(expected, "atom()") assert String.contains?(actual, "%{required(:a) => 1}") end test "return float() instead of integer()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_integer) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_integer) assert String.contains?(expected, "integer()") assert String.contains?(actual, "float()") end test "return atom() instead of integer()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_integer2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_integer2) assert String.contains?(expected, "integer()") assert String.contains?(actual, ":ok") end test "return boolean() instead of integer()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_integer3) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_integer3) assert String.contains?(expected, "integer()") assert String.contains?(actual, "true") end test "return list() instead of integer()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_integer4) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_integer4) assert String.contains?(expected, "integer()") assert String.contains?(actual, "nonempty_list()") end test "return integer() out of the range()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_out_of_range_int) + [expected, actual] = type_format_error_to_binary(errors.ret_out_of_range_int) assert String.contains?(expected, "1..10") assert String.contains?(actual, "12") end test "return integer() instead of float()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_float) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_float) assert String.contains?(expected, "float()") assert String.contains?(actual, "1") end test "return nil() instead of float()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_float2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_float2) assert String.contains?(expected, "float()") assert String.contains?(actual, "nil") end test "return charlist() instead of char()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_char) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_char) assert String.contains?(expected, "char()") assert String.contains?(actual, "nonempty_list()") end test "return nil() instead of char()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_char2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_char2) # unfortunately char is represented as {:integer, 0, _} assert String.contains?(expected, "111") @@ -113,69 +113,132 @@ defmodule Gradient.ElixirFmtTest do end test "return atom() instead of boolean()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_boolean) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_boolean) assert String.contains?(expected, "boolean()") assert String.contains?(actual, ":ok") end test "return binary() instead of boolean()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_boolean2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_boolean2) assert String.contains?(expected, "boolean()") assert String.contains?(actual, "binary()") end test "return integer() instead of boolean()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_boolean3) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_boolean3) assert String.contains?(expected, "boolean()") assert String.contains?(actual, "1") end test "return keyword() instead of boolean()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_boolean4) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_boolean4) assert String.contains?(expected, "boolean()") assert String.contains?(actual, "nonempty_list()") end test "return list() instead of keyword()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_keyword) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_keyword) assert String.contains?(expected, "{atom(), any()}") assert String.contains?(actual, "1") end test "return tuple() instead of map()", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_map) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_map) assert String.contains?(expected, "map()") assert String.contains?(actual, "{:a, 1, 2}") end test "return lambda with wrong returned type", %{wrong_ret_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_fun) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_fun) assert String.contains?(expected, "atom()") assert String.contains?(actual, "12") end test "return atom() instead of record()", %{record_type_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_record) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_record) assert String.contains?(expected, "user()") assert String.contains?(actual, ":ok") end test "return wrong record value type", %{record_type_errors: errors} do - [expected, actual] = format_error_to_binary(errors.ret_wrong_record2) + [expected, actual] = type_format_error_to_binary(errors.ret_wrong_record2) assert String.contains?(expected, "String.t()") assert String.contains?(actual, "12") end end + describe "expression format" do + test "atom", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_integer2) + assert String.contains?(expr, ":ok") + end + + test "integer", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_atom) + assert String.contains?(expr, "1") + end + + test "float", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_integer) + assert String.contains?(expr, "1.0") + end + + test "list", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_integer4) + assert String.contains?(expr, "[1, 2, 3]") + end + + test "map", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_tuple) + assert String.contains?(expr, "%{a: 1, b: 2}") + end + + test "tuple", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_map) + assert String.contains?(expr, "{:a, 1, 2}") + end + + test "string", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_boolean2) + assert String.contains?(expr, "\"1234\"") + end + + test "char", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_list) + # I don't know if it is possible to detect that we want char here. + assert String.contains?(expr, "99") + end + + test "charlist", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_char) + assert String.contains?(expr, "'Ala ma kota'") + end + + test "record", %{record_type_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_atom) + assert String.contains?(expr, "{:user, \"Kate\", 25}") + end + + test "call", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_call) + assert String.contains?(expr, "ret_wrong_boolean()") + end + + test "fun reference", %{wrong_ret_errors: errors} do + expr = expr_format_error_to_binary(errors.ret_wrong_integer5) + assert String.contains?(expr, "&ret_wrong_atom/0") + end + end + @tag :skip test "format_expr_type_error/4" do opts = [forms: basic_erlang_forms()] @@ -206,7 +269,21 @@ defmodule Gradient.ElixirFmtTest do {errors, forms} end - defp format_error_to_binary(error, opts \\ []) do + defp expr_format_error_to_binary(error, opts \\ []) do + opts = Keyword.put_new(opts, :fmt_type_fun, &mock_fmt/1) + opts = Keyword.put_new(opts, :colors, false) + + error + |> ElixirFmt.format_error(opts) + |> :erlang.iolist_to_binary() + |> String.split("on line") + |> List.first() + end + + defp type_format_error_to_binary(error, opts \\ []) do + opts = Keyword.put_new(opts, :fmt_expr_fun, &mock_fmt/1) + opts = Keyword.put_new(opts, :colors, false) + error |> ElixirFmt.format_error(opts) |> :erlang.iolist_to_binary() @@ -255,4 +332,6 @@ defmodule Gradient.ElixirFmtTest do end) |> Enum.map(&elem(&1, 2)) end + + def mock_fmt(_), do: "" end diff --git a/test/support/expr_data.ex b/test/support/expr_data.ex new file mode 100644 index 00000000..7be58f73 --- /dev/null +++ b/test/support/expr_data.ex @@ -0,0 +1,209 @@ +defmodule Gradient.ExprData do + require Gradient.Debug + import Gradient.Debug, only: [elixir_to_ast: 1] + + def all_basic_pp_test_data() do + [ + value_test_data(), + list_test_data(), + call_test_data(), + variable_test_data(), + exception_test_data(), + block_test_data(), + binary_test_data(), + map_test_data(), + function_ref_test_data(), + sigil_test_data() + ] + |> List.flatten() + end + + def value_test_data() do + [ + {"geric atom", {:atom, 0, :fjdksaose}, ":fjdksaose"}, + {"module atom", {:atom, 0, Gradient.ElixirExpr}, "Gradient.ElixirExpr"}, + {"nil atom", {:atom, 0, nil}, "nil"}, + {"true atom", {:atom, 0, true}, "true"}, + {"false atom", {:atom, 0, false}, "false"}, + {"char", {:char, 0, ?c}, "?c"}, + {"float", {:float, 0, 12.0}, "12.0"}, + {"integer", {:integer, 0, 1}, "1"}, + {"erlang string", {:string, 0, 'ala ma kota'}, ~s('ala ma kota')} + ] + end + + def list_test_data() do + [ + {"charlist", + {:cons, 0, {:integer, 0, 97}, + {:cons, 0, {:integer, 0, 108}, {:cons, 0, {:integer, 0, 97}, {nil, 0}}}}, ~s('ala')}, + {"int list", + {:cons, 0, {:integer, 0, 0}, + {:cons, 0, {:integer, 0, 1}, {:cons, 0, {:integer, 0, 2}, {nil, 0}}}}, "[0, 1, 2]"}, + {"mixed list", + {:cons, 0, {:integer, 0, 0}, + {:cons, 0, {:atom, 0, :ok}, {:cons, 0, {:integer, 0, 2}, {nil, 0}}}}, "[0, :ok, 2]"}, + {"var in list", {:cons, 0, {:integer, 0, 0}, {:cons, 0, {:var, 0, :a}, {nil, 0}}}, + "[0, a]"}, + {"list tail pm", elixir_to_ast([a | t] = [12, 13, 14]), "[a | t] = [12, 13, 14]"}, + {"empty list", elixir_to_ast([] = []), "[] = []"} + ] + end + + def call_test_data() do + [ + {"call", {:call, 0, {:atom, 0, :my_func}, []}, "my_func()"}, + {"remote call", {:call, 0, {:remote, 0, {:atom, 0, MyModule}, {:atom, 0, :my_func}}, []}, + "MyModule.my_func()"}, + {"erl remote call", {:call, 0, {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :my_func}}, []}, + ":erlang.my_func()"} + ] + end + + def variable_test_data() do + [ + {"variable", {:var, 0, :abbc}, "abbc"}, + {"underscore variable", {:var, 0, :_}, "_"}, + {"ast underscore variable", {:var, 0, :_@1}, "_"}, + {"ast variable", {:var, 0, :_val@1}, "val"} + ] + end + + def exception_test_data() do + [ + {"throw", elixir_to_ast(throw({:ok, 12})), "throw {:ok, 12}"}, + {"raise/1", elixir_to_ast(raise "test error"), ~s(raise "test error")}, + {"raise/1 without msg", elixir_to_ast(raise RuntimeError), "raise RuntimeError"}, + {"raise/2", elixir_to_ast(raise RuntimeError, "test error"), ~s(raise "test error")}, + {"custom raise", elixir_to_ast(raise ArithmeticError, "only odd numbers"), + ~s(raise ArithmeticError, "only odd numbers")} + ] + end + + def block_test_data() do + simple_block = + elixir_to_ast do + a = 1 + a + 1 + end + + [ + {"block", simple_block, "a = 1; a + 1"} + ] + end + + def map_test_data do + [ + {"string map", elixir_to_ast(%{"a" => 12}), ~s(%{"a" => 12})}, + {"map pm", elixir_to_ast(%{a: a} = %{a: 12}), "%{a: a} = %{a: 12}"}, + {"update map", elixir_to_ast(%{%{} | a: 1}), "%{%{} | a: 1}"}, + {"struct expr", elixir_to_ast(%{__struct__: TestStruct, name: "John"}), + ~s(%TestStruct{name: "John"})} + ] + end + + def function_ref_test_data() do + [ + {"&fun/arity", {:fun, 0, {:function, :my_fun, 0}}, "&my_fun/0"}, + {"&Mod.fun/arity", elixir_to_ast(&MyMod.my_fun/1), "&MyMod.my_fun/1"} + ] + end + + def sigil_test_data() do + [ + {"regex", elixir_to_ast(~r/foo|bar/), regex_exp()}, + {"string ~s", elixir_to_ast(~s(this is a string with "double" quotes, not 'single' ones)), + "\"this is a string with \"double\" quotes, not 'single' ones\""}, + {"string ~S", elixir_to_ast(~S(String without escape codes \x26 without #{interpolation})), + "\"String without escape codes \\x26 without \#{interpolation}\""}, + {"char lists", elixir_to_ast(~c(this is a char list containing 'single quotes')), + "'this is a char list containing \\'single quotes\\''"}, + {"word list", elixir_to_ast(~w(foo bar bat)), "[\"foo\", \"bar\", \"bat\"]"}, + {"word list atom", elixir_to_ast(~w(foo bar bat)a), "[:foo, :bar, :bat]"}, + {"date", elixir_to_ast(~D[2019-10-31]), + "%Date{calendar: Calendar.ISO, year: 2019, month: 10, day: 31}"}, + {"time", elixir_to_ast(~T[23:00:07.0]), + "%Time{calendar: Calendar.ISO, hour: 23, minute: 0, second: 7, microsecond: {0, 1}}"}, + {"naive date time", elixir_to_ast(~N[2019-10-31 23:00:07]), + "%NaiveDateTime{calendar: Calendar.ISO, year: 2019, month: 10, day: 31, hour: 23, minute: 0, second: 7, microsecond: {0, 0}}"}, + {"date time", elixir_to_ast(~U[2019-10-31 19:59:03Z]), + "%DateTime{calendar: Calendar.ISO, year: 2019, month: 10, day: 31, hour: 19, minute: 59, second: 3, microsecond: {0, 0}, time_zone: \"Etc/UTC\", zone_abbr: \"UTC\", utc_offset: 0, std_offset: 0}"} + ] + end + + def binary_test_data do + [ + bin_pm_bin_var(), + bin_joining_syntax(), + bin_with_bin_var(), + bin_with_pp_int_size(), + bin_with_pp_and_bitstring_size(), + {"bin float", elixir_to_ast(<<4.3::float>>), "<<4.3::float>>"} + ] + end + + defp bin_pm_bin_var do + ast = + elixir_to_ast do + <> = <<1, 2, 3, 4>> + end + + {"bin pattern matching with bin var", ast, "<> = <<1, 2, 3, 4>>"} + end + + defp bin_joining_syntax do + ast = + elixir_to_ast do + x = "b" + "a" <> x + end + + {"binary <> joining", ast, ~s(x = "b"; <<"a", x::binary>>)} + end + + defp bin_with_bin_var do + ast = + elixir_to_ast do + x = "b" + <<"a", "b", x::binary>> + end + + {"binary with bin var", ast, ~s(x = "b"; <<"a", "b", x::binary>>)} + end + + defp bin_with_pp_int_size do + ast = + elixir_to_ast do + <> = <<"abcd">> + end + + {"binary with int size", ast, ~s(<> = "abcd")} + end + + defp bin_with_pp_and_bitstring_size do + ast = + elixir_to_ast do + <> = + <<1, 2, 3, 4, 5, 101, 114, 97, 115, 101, 32, 116, 104, 101, 32, 101, 118, 105, 100, 101, + 110, 99, 101>> + end + + expected = + "<> = <<1, 2, 3, 4, 5, 101, 114, 97, 115, 101, 32, 116, 104, 101, 32, 101, 118, 105, 100, 101, 110, 99, 101>>" + + {"binary with patter matching and bitstring-size", ast, expected} + end + + defp regex_exp() do + <<37, 82, 101, 103, 101, 120, 123, 111, 112, 116, 115, 58, 32, 60, 60, 62, 62, 44, 32, 114, + 101, 95, 112, 97, 116, 116, 101, 114, 110, 58, 32, 123, 58, 114, 101, 95, 112, 97, 116, 116, + 101, 114, 110, 44, 32, 48, 44, 32, 48, 44, 32, 48, 44, 32, 34, 69, 82, 67, 80, 86, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 195, 191, 195, 191, 195, 191, 195, 191, 195, 191, 195, 191, 195, + 191, 195, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 194, 131, 0, 9, 29, 102, 29, 111, 29, 111, 119, + 0, 9, 29, 98, 29, 97, 29, 114, 120, 0, 18, 0, 34, 125, 44, 32, 114, 101, 95, 118, 101, 114, + 115, 105, 111, 110, 58, 32, 123, 34, 56, 46, 52, 52, 32, 50, 48, 50, 48, 45, 48, 50, 45, 49, + 50, 34, 44, 32, 58, 108, 105, 116, 116, 108, 101, 125, 44, 32, 115, 111, 117, 114, 99, 101, + 58, 32, 34, 102, 111, 111, 124, 98, 97, 114, 34, 125>> + end +end