defmodule Rexbug.Translator do @moduledoc """ Utility module for translating Elixir syntax to the one expected by `:redbug`. You probably don't need to use it directly. """ @valid_guard_functions [ :is_atom, :is_binary, :is_bitstring, :is_boolean, :is_float, :is_function, :is_integer, :is_list, :is_map, :is_nil, :is_number, :is_pid, :is_port, :is_reference, :is_tuple, :abs, :bit_size, :byte_size, :hd, :length, :map_size, :round, :tl, :trunc, :tuple_size, # erlang guard :size ] @infix_guards_mapping %{ # comparison :== => :==, :!= => :"/=", :=== => :"=:=", :!== => :"=/=", :> => :>, :>= => :>=, :< => :<, :<= => :"=<" } @valid_infix_guards Map.keys(@infix_guards_mapping) @infix_guard_combinators_mapping %{ :and => :andalso, :or => :orelse } @valid_infix_guard_combinators Map.keys(@infix_guard_combinators_mapping) # =========================================================================== # Public functions # =========================================================================== # --------------------------------------------------------------------------- # Translating trace pattern # --------------------------------------------------------------------------- @spec translate(Rexbug.trace_pattern()) :: {:ok, charlist | atom} | {:ok, [charlist | atom]} | {:error, term} @doc """ Translates the Elixir trace pattern(s) (understood by Rexbug) to the Erlang trace pattern charlist(s) understood by `:redbug`. The translated version is not necessarily the cleanest possible, but should be correct and functionally equivalent. ## Example iex> import Rexbug.Translator iex> translate(":cowboy.start_clear/3") {:ok, '\\'cowboy\\':\\'start_clear\\'/3'} iex> translate("MyModule.do_sth(_, [pretty: true])") {:ok, '\\'Elixir.MyModule\\':\\'do_sth\\'(_, [{\\'pretty\\', true}])'} """ def translate(s) when s in [:send, "send"], do: {:ok, :send} def translate(r) when r in [:receive, "receive"], do: {:ok, :receive} def translate(patterns) when is_list(patterns) do patterns |> Enum.map(&translate/1) |> collapse_errors() end def translate(trace_pattern) when is_binary(trace_pattern) do with {mfag, actions} = split_to_mfag_and_actions!(trace_pattern), {:ok, quoted} <- Code.string_to_quoted(mfag), {:ok, {mfa, guards}} = split_quoted_into_mfa_and_guards(quoted), {:ok, {mfa, arity}} = split_mfa_into_mfa_and_arity(mfa), {:ok, {module, function, args}} = split_mfa_into_module_function_and_args(mfa), :ok <- validate_mfaa(module, function, args, arity), {:ok, translated_module} <- translate_module(module), {:ok, translated_function} <- translate_function(function), {:ok, translated_args} <- translate_args(args), {:ok, translated_arity} <- translate_arity(arity), {:ok, translated_guards} <- translate_guards(guards), translated_actions = translate_actions!(actions) do translated = case translated_arity do :any -> # no args, no arity "#{translated_module}#{translated_function}#{translated_actions}" arity when is_integer(arity) -> # no args, arity present "#{translated_module}#{translated_function}/#{arity}#{translated_actions}" nil -> # args present, no arity "#{translated_module}#{translated_function}#{translated_args}#{translated_guards}#{ translated_actions }" end {:ok, String.to_charlist(translated)} end end def translate(_), do: {:error, :invalid_trace_pattern_type} @doc false def split_to_mfag_and_actions!(trace_pattern) do {mfag, actions} = case String.split(trace_pattern, " ::", parts: 2) do [mfag, actions] -> {mfag, actions} [mfag] -> {mfag, ""} end {String.trim(mfag), String.trim(actions)} end @spec translate_guards(term) :: {:ok, String.t()} | {:error, term} defp translate_guards(nil), do: {:ok, ""} defp translate_guards(els) do _translate_guards(els) |> map_success(fn guards -> " when #{guards}" end) end # --------------------------------------------------------------------------- # Translating options # --------------------------------------------------------------------------- @spec translate_options(Keyword.t()) :: {:ok, Keyword.t()} | {:error, term} @doc """ Translates the options to be passed to `Rexbug.start/2` to the format expected by `:redbug` Relevant values passed as strings will be converted to charlists. """ def translate_options(options) when is_list(options) do options |> Enum.map(&translate_option/1) |> collapse_errors() end def translate_options(_), do: {:error, :invalid_options} # =========================================================================== # Private functions # =========================================================================== @binary_to_charlist_options [:file, :print_file] defp translate_option({file_option, filename}) when file_option in @binary_to_charlist_options and is_binary(filename) do {:ok, {file_option, String.to_charlist(filename)}} end defp translate_option({k, v}) do {:ok, {k, v}} end defp translate_option(_), do: {:error, :invalid_options} @spec collapse_errors([{:ok, term} | {:error, term}]) :: {:ok, [term]} | {:error, term} defp collapse_errors(tuples) do # we could probably play around with some monads for this first_error = Enum.find(tuples, :no_error_to_collapse, fn res -> !match?({:ok, _}, res) end) case first_error do :no_error_to_collapse -> results = Enum.map(tuples, fn {:ok, res} -> res end) {:ok, results} err -> err end end defp split_quoted_into_mfa_and_guards({:when, _line, [mfa, guards]}) do {:ok, {mfa, guards}} end defp split_quoted_into_mfa_and_guards(els) do {:ok, {els, nil}} end defp split_mfa_into_mfa_and_arity({:/, _line, [mfa, arity]}) do {:ok, {mfa, arity}} end defp split_mfa_into_mfa_and_arity(els) do {:ok, {els, nil}} end defp split_mfa_into_module_function_and_args({{:., _l1, [module, function]}, _l2, args}) do {:ok, {module, function, args}} end defp split_mfa_into_module_function_and_args(els) do {:ok, {els, nil, nil}} end # handling fringe cases that shouldn't happen defp validate_mfaa(module, function, args, arity) defp validate_mfaa(nil, _, _, _), do: {:error, :missing_module} defp validate_mfaa(_, nil, args, _) when not (args in [nil, []]), do: {:error, :missing_function} defp validate_mfaa(_, nil, _, arity) when arity != nil, do: {:error, :missing_function} defp validate_mfaa(_, _, args, arity) when not (args in [nil, []]) and arity != nil do {:error, :both_args_and_arity_provided} end defp validate_mfaa(_, _, _, _), do: :ok defp translate_module({:__aliases__, _line, elixir_module}) when is_list(elixir_module) do joined = [:"Elixir" | elixir_module] |> Enum.map(&Atom.to_string/1) |> Enum.join(".") {:ok, "'#{joined}'"} end defp translate_module(erlang_mod) when is_atom(erlang_mod) do {:ok, "\'#{Atom.to_string(erlang_mod)}\'"} end defp translate_module(module), do: {:error, {:invalid_module, module}} defp translate_function(nil) do {:ok, ""} end defp translate_function(f) when is_atom(f) do {:ok, ":'#{Atom.to_string(f)}'"} end defp translate_function(els) do {:error, {:invalid_function, els}} end defp translate_args(nil), do: {:ok, ""} defp translate_args(args) when is_list(args) do args |> Enum.map(&translate_arg/1) |> collapse_errors() |> map_success(&Enum.join(&1, ", ")) |> map_success(fn res -> "(#{res})" end) end defp translate_args(els) do {:error, {:invalid_args, els}} end defp translate_arg(nil), do: {:ok, "nil"} defp translate_arg(boolean) when is_boolean(boolean) do {:ok, "#{boolean}"} end defp translate_arg(arg) when is_atom(arg) do {:ok, "'#{Atom.to_string(arg)}'"} end defp translate_arg(string) when is_binary(string) do # TODO: more strict ASCII checking here if String.printable?(string) && byte_size(string) == String.length(string) do {:ok, "<<\"#{string}\">>"} else translate_arg({:<<>>, [line: 1], [string]}) end end defp translate_arg({:<<>>, _line, contents}) when is_list(contents) do contents |> Enum.map(&translate_binary_element/1) |> collapse_errors() |> map_success(&Enum.join(&1, ", ")) |> map_success(fn res -> "<<#{res}>>" end) end # defp translate_arg(bs) when is_bitstring(bs) do # :error # end defp translate_arg(ls) when is_list(ls) do ls |> Enum.map(&translate_arg/1) |> collapse_errors() |> map_success(fn elements -> "[#{Enum.join(elements, ", ")}]" end) end defp translate_arg({:-, _line, [num]}) when is_integer(num) do with {:ok, translated_num} = translate_arg(num), do: {:ok, "-#{translated_num}"} end defp translate_arg(num) when is_integer(num) do {:ok, "#{num}"} end defp translate_arg(f) when is_float(f) do {:error, {:bad_type, :float}} end defp translate_arg({:%{}, _line, kvs}) when is_list(kvs) do {ks, vs} = Enum.unzip(kvs) if Enum.any?(ks, &is_variable/1) do {:error, :variable_in_map_key} else key_args = ks |> Enum.map(&translate_arg/1) |> collapse_errors() value_args = vs |> Enum.map(&translate_arg/1) |> collapse_errors() [key_args, value_args] |> collapse_errors |> map_success(fn [keys, values] -> middle = keys |> Enum.zip(values) |> Enum.map(fn {k, v} -> "#{k} => #{v}" end) |> Enum.join(", ") "\#{#{middle}}" end) end end # there's a catch here: # iex(12)> Code.string_to_quoted!("{1,2,3}") # {:{}, [line: 1], [1, 2, 3]} # iex(13)> Code.string_to_quoted!("{1,2}") # {1, 2} defp translate_arg({:{}, _line, tuple_elements}) do tuple_elements |> Enum.map(&translate_arg/1) |> collapse_errors() |> map_success(fn elements -> "{#{Enum.join(elements, ", ")}}" end) end # the literally represented 2-tuples defp translate_arg({x, y}), do: translate_arg({:{}, [line: 1], [x, y]}) # other atoms are just variable names defp translate_arg({var, _line, nil}) when is_atom(var) do var |> Atom.to_string() |> String.capitalize() |> wrap_in_ok() end defp translate_arg(arg) do {:error, {:invalid_arg, arg}} end defp is_variable({var, _line, nil}) when is_atom(var), do: true defp is_variable(_), do: false defp translate_binary_element(i) when is_integer(i) do {:ok, "#{i}"} end defp translate_binary_element(s) when is_binary(s) do res = s |> :binary.bin_to_list() |> Enum.join(", ") {:ok, res} end defp translate_binary_element(els), do: {:error, {:invalid_binary_element, els}} defp translate_arity({var, [line: 1], nil}) when is_atom(var) do {:ok, :any} end defp translate_arity(i) when is_integer(i) do {:ok, i} end defp translate_arity(none) when none in [nil, ""] do {:ok, nil} end defp translate_arity(els) do {:error, {:invalid_arity, els}} end defp translate_actions!(empty) when empty in [nil, ""] do "" end defp translate_actions!(actions) when is_binary(actions) do " -> #{actions}" end # --------------------------------------------------------------------------- # Guards # --------------------------------------------------------------------------- @spec _translate_guards(term) :: {:ok, String.t()} | {:error, term} defp _translate_guards({:not, _line, [arg]}) do _translate_guards(arg) |> map_success(fn guard -> "not #{guard}" end) end defp _translate_guards({combinator, _line, [a, b]}) when combinator in @valid_infix_guard_combinators do erlang_combinator = @infix_guard_combinators_mapping[combinator] |> Atom.to_string() with {:ok, a_guards} <- _translate_guards(a), {:ok, b_guards} <- _translate_guards(b), do: {:ok, "(#{a_guards} #{erlang_combinator} #{b_guards})"} end defp _translate_guards(els), do: translate_guard(els) @spec translate_guard(term) :: {:ok, String.t()} | {:error, term} defp translate_guard({guard_fun, _line, args}) when guard_fun in @valid_guard_functions do with translated_fun = Atom.to_string(guard_fun), {:ok, translated_args} <- translate_args(args), do: {:ok, "#{translated_fun}#{translated_args}"} end defp translate_guard({infix_guard_fun, _line, [a, b]}) when infix_guard_fun in @valid_infix_guards do translated_infix_function = @infix_guards_mapping[infix_guard_fun] |> Atom.to_string() with {:ok, a_guard} <- translate_guard(a), {:ok, b_guard} <- translate_guard(b), do: {:ok, "#{a_guard} #{translated_infix_function} #{b_guard}"} end defp translate_guard(els) do translate_arg(els) end # --------------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------------- defp map_success({:ok, var}, fun) do {:ok, fun.(var)} end defp map_success(els, _), do: els defp wrap_in_ok(x), do: {:ok, x} end