Skip to main content

lib/errata/env.ex

defmodule Errata.Env do
  @moduledoc """
  A struct that holds compile time environment information that is used for the `:env` field of
  Errata error types.

  This struct is a subset of `Macro.Env` and includes the following fields:

    * `context` - the context of the environment; it can be nil (default context), :guard
      (inside a guard) or :match (inside a match)
    * `context_modules` - a list of modules defined in the current context
    * `file` - the current absolute file name as a binary
    * `function` - a tuple as {atom, integer}, where the first element is the function name and
      the second its arity; returns nil if not inside a function
    * `line` - the current line as an integer
    * `module` - the current module name
    * `stacktrace` - the stacktrace for the current process at the time of creation
  """

  @typedoc """
  Type to represent the `:env` field of error structs.

  This struct is a subset of `Macro.Env`.
  """
  @type t :: %Errata.Env{
          context: Macro.Env.context(),
          context_modules: Macro.Env.context_modules(),
          file: Macro.Env.file(),
          function: Macro.Env.name_arity() | nil,
          line: Macro.Env.line(),
          module: module(),
          stacktrace: Exception.stacktrace() | nil
        }

  @typedoc """
  Type to represent an `Errata.Env` struct as a plain, JSON-encodable map.
  """
  @type env_map :: %{
          module: String.t(),
          function: String.t(),
          file: Macro.Env.file(),
          line: Macro.Env.line(),
          file_line: String.t()
        }

  defstruct [:context, :context_modules, :file, :function, :line, :module, :stacktrace]

  @doc """
  Creates a new `Errata.Env` struct from the given `Macro.Env` struct.
  """
  @spec new(Macro.Env.t()) :: Errata.Env.t()
  def new(%Macro.Env{} = env, stacktrace \\ nil) do
    stacktrace =
      if is_list(stacktrace) do
        stacktrace
      else
        {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
        stacktrace
      end

    params =
      env
      |> Map.from_struct()
      |> Map.put(:stacktrace, stacktrace)

    struct(__MODULE__, params)
  end

  @doc """
  Converts the given `Errata.Env` struct to a plain, JSON-encodable map.
  """
  @spec to_map(Errata.Env.t()) :: Errata.Env.env_map()
  def to_map(%__MODULE__{module: module, file: file, line: line} = env) do
    %{
      # Use `inspect/1` so module names serialize as "MyApp.Foo" rather than
      # the raw atom form "Elixir.MyApp.Foo".
      module: inspect(module),
      function: format_mfa(env),
      file: file,
      line: line,
      # `Exception.format_file_line/2` appends a trailing ":"; drop it.
      file_line: String.trim_trailing(Exception.format_file_line(file, line), ":")
    }
  end

  def to_map(_), do: %{}

  defp format_mfa(%{module: module, function: {function, arity}}),
    do: Exception.format_mfa(module, function, arity)

  defp format_mfa(_), do: nil
end