lib/rewrite/dot_formatter.ex

defmodule Rewrite.DotFormatter do
  @moduledoc """
  This module provides the information from `.formatter.exs`.
  """

  @presistent_term_key {:rewrite, :dot_formatter}

  @doc """
  Returns the options from the `.formatter.exs`.
  """
  @spec opts :: keyword()
  def opts do
    with nil <- get_opts() do
      ".formatter.exs"
      |> eval_file_with_keyword_list()
      |> eval_deps_opts()
      |> tap(&put_opts/1)
    end
  end

  defp get_opts do
    :persistent_term.get(@presistent_term_key, nil)
  end

  defp put_opts(opts) do
    :persistent_term.put(@presistent_term_key, opts)
  end

  @doc """
  Returns the option `inputs` from the `.formatter.exs`.
  """
  @spec inputs :: list()
  def inputs, do: Keyword.get(opts(), :inputs, [])

  @doc """
  Returns the option `locals_without_parens` from the `.formatter.exs`.
  """
  @spec locals_without_parens :: keyword()
  def locals_without_parens, do: Keyword.get(opts(), :locals_without_parens, [])

  defp eval_deps_opts(formatter_opts) do
    eval_deps_opts(formatter_opts, Keyword.get(formatter_opts, :import_deps, []))
  end

  defp eval_deps_opts(formatter_opts, []) do
    formatter_opts
  end

  defp eval_deps_opts(formatter_opts, deps) do
    deps_paths = Mix.Project.deps_paths()

    locals_without_parens =
      for dep <- deps,
          dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
          dep_dot_formatter = Path.join(dep_path, ".formatter.exs"),
          File.regular?(dep_dot_formatter),
          dep_opts = eval_file_with_keyword_list(dep_dot_formatter),
          parenless <- dep_opts[:export][:locals_without_parens] || [],
          uniq: true,
          do: parenless

    Keyword.update(
      formatter_opts,
      :locals_without_parens,
      locals_without_parens,
      fn list -> list ++ locals_without_parens end
    )
  end

  defp assert_valid_dep_and_fetch_path(dep, deps_paths) when is_atom(dep) do
    case Map.fetch(deps_paths, dep) do
      {:ok, path} ->
        if File.dir?(path) do
          path
        else
          Mix.raise("""
          Unavailable dependency #{inspect(dep)} given to :import_deps in the formatter \
          configuration. The dependency cannot be found in the file system, please run \
          "mix deps.get" and try again\
          """)
        end

      :error ->
        Mix.raise("""
        Unknown dependency #{inspect(dep)} given to :import_deps in the formatter \
        configuration. The dependency is not listed in your mix.exs for environment \
        #{inspect(Mix.env())}\
        """)
    end
  end

  defp assert_valid_dep_and_fetch_path(dep, _deps_paths) do
    Mix.raise("Dependencies in :import_deps should be atoms, got: #{inspect(dep)}")
  end

  defp eval_file_with_keyword_list(path) do
    {opts, _} = Code.eval_file(path)

    unless Keyword.keyword?(opts) do
      Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}")
    end

    opts
  end
end