lib/recode/formatter_plugin.ex

defmodule Recode.FormatterPlugin.Config do
  @moduledoc false
end

defmodule Recode.FormatterPlugin do
  @moduledoc """
  Defines Recode formatter plugin for `mix format`.

  Since Elixir 1.13, it is possible to define custom formatter plugins. This
  plugin allows you to run Recode autocorrecting tasks together when executing
  `mix format`.

  To use this formatter, simply add `Recode.FormatterPlugin` to your
  `.formatter.exs` plugins:

  ```
    [
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
      plugins: [Recode.FormatterPlugin]
    ]
  ```

  By default it uses the `.recode.exs` configuration file.

  If your project does not have a `.recode.exs` configuration file or if you
  want to overwrite the configuration, you can pass the configuration using the
  `recode` option:

  ```
    [
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
      plugins: [Recode.FormatterPlugin],
      recode: [
        tasks: [
          {Recode.Task.AliasExpansion, []},
          {Recode.Task.EnforceLineLength, []},
          {Recode.Task.SinglePipe, []}
        ]
      ]
    ]
  ```
  """

  @behaviour Mix.Tasks.Format

  alias Recode.Config
  alias Recode.Runner

  @persistent_term_key {__MODULE__, :config}

  @impl true
  def features(_opts) do
    [extensions: [".ex", ".exs"]]
  end

  @impl true
  def format(content, formatter_opts) do
    file = formatter_opts |> Keyword.get(:file, "source.ex") |> Path.relative_to_cwd()

    formatter_opts =
      Keyword.update(formatter_opts, :plugins, [], fn plugins ->
        Enum.reject(plugins, fn plugin -> plugin == Recode.FormatterPlugin end)
      end)

    config =
      formatter_opts
      |> config()
      |> Keyword.put(:dot_formatter_opts, Keyword.delete(formatter_opts, :recode))

    Runner.run(content, config, file)
  end

  defp config(opts) do
    with nil <- :persistent_term.get(@persistent_term_key, nil) do
      config = init_config(opts[:recode])
      :ok = :persistent_term.put(@persistent_term_key, config)
      config
    end
  end

  @config_error """
  No configuration for `Recode.FormatterPlugin` found. Run \
  `mix recode.gen.config` to create a config file or add config in \
  `.formatter.exs` under the key `:recode`.
  """
  defp init_config(nil) do
    case Config.read() do
      {:error, :not_found} -> Mix.raise(@config_error)
      {:ok, config} -> init_config(config)
    end
  end

  defp init_config(recode) do
    recode
    |> Keyword.merge(
      dry: false,
      verbose: false,
      autocorrect: true,
      check: false
    )
    |> validate_config!()
  end

  defp validate_config!(config) do
    case Config.validate(config) do
      :ok ->
        config

      {:error, :out_of_date} ->
        Mix.raise("The config is out of date. Run `mix recode.update.config` to update.")

      {:error, :no_tasks} ->
        Mix.raise("No `:tasks` key found in configuration.")
    end
  end
end