lib/recode/task/format.ex

defmodule Recode.Task.Format do
  @moduledoc """
  This task runs the Elixir formatter.

  This task runs as first task by any `mix recode` call.
  """

  use Recode.Task, correct: true, check: true

  alias Recode.Issue
  alias Recode.Task.Format
  alias Rewrite.Source

  @impl Recode.Task
  def run(source, opts) do
    format(source, opts[:autocorrect], Map.get(source.private, :dot_formatter_opts))
  end

  defp format(source, true, formatter_opts) do
    code = format(source, formatter_opts)
    Source.update(source, Format, code: code)
  end

  defp format(source, false, formatter_opts) do
    code = format(source, formatter_opts)

    case Source.code(source) == code do
      true ->
        source

      false ->
        Source.add_issue(source, Issue.new(Format, "The file is not formatted."))
    end
  end

  defp format(source, formatter_opts) do
    formatter = formatter(source, formatter_opts)

    source
    |> Source.code()
    |> formatter.()
  end

  defp formatter(source, formatter_opts) do
    file = Source.path(source) || "elixir.ex"
    ext = Path.extname(file)

    formatter_opts = if is_nil(formatter_opts), do: formatter_opts(file), else: formatter_opts

    case plugins_for_ext(ext, formatter_opts) do
      [] ->
        fn content -> elixir_format(content, formatter_opts) end

      plugins ->
        formatter_opts = [extension: ext, file: file] ++ formatter_opts
        fn content -> plugins_format(plugins, content, formatter_opts) end
    end
  end

  defp formatter_opts(file) do
    {_formatter, formatter_opts} = Mix.Tasks.Format.formatter_for_file(file)
    formatter_opts
  end

  defp elixir_format(content, formatter_opts) do
    case Code.format_string!(content, formatter_opts) do
      [] -> ""
      formatted_content -> IO.iodata_to_binary([formatted_content, ?\n])
    end
  end

  defp plugins_format(plugins, content, formatter_opts) do
    Enum.reduce(plugins, content, fn plugin, content ->
      plugin.format(content, formatter_opts)
    end)
  end

  defp plugins_for_ext(ext, formatter_opts) do
    formatter_opts
    |> Keyword.get(:plugins, [])
    |> Enum.filter(fn
      Recode.FormatterPlugin ->
        false

      plugin ->
        Code.ensure_loaded?(plugin) and function_exported?(plugin, :features, 1) and
          ext in List.wrap(plugin.features(formatter_opts)[:extensions])
    end)
  end
end