lib/ex_factor.ex

defmodule ExFactor do
  @moduledoc """
  `ExFactor` is a refactoring helper.

  By identifying a source module, function name, and arity, it will identify all non-test usages
  and extract them to the target module.

  If the target module exists, it adds the function to the end of the file and changes all calls to the
  new module namespace. Otherwise ExFactor will create the target module at the target_path or
  at a (resonably) expected location by the module namespace
  """

  _docp = "results struct"
  defstruct [:module, :path, :message, :file_contents, :state]

  alias ExFactor.Changer
  alias ExFactor.Extractor
  alias ExFactor.Formatter
  alias ExFactor.Remover

  @doc """
  Call Extractor, Remover, and Formatter modules
  """
  def refactor(opts) do
    source_module = Keyword.fetch!(opts, :source_module)
    target_module = Keyword.fetch!(opts, :target_module)
    dry_run = Keyword.get(opts, :dry_run, false)

    opts =
      opts
      |> Keyword.put_new(:target_path, path(target_module))
      |> Keyword.put_new(:source_path, path(source_module))

    emplace = Extractor.emplace(opts)
    changes = Changer.change(opts)
    # remove should be last (before format)
    removals = Remover.remove(opts)

    format(%{additions: emplace, changes: changes, removals: removals}, dry_run, opts)
  end

  def refactor_module(opts) do
    source_module = Keyword.fetch!(opts, :source_module)
    target_module = Keyword.fetch!(opts, :target_module)
    dry_run = Keyword.get(opts, :dry_run, false)

    opts =
      opts
      |> Keyword.put_new(:target_path, path(target_module))
      |> Keyword.put_new(:source_path, path(source_module))

    changes = Changer.change(opts)

    format(%{additions: %ExFactor{}, changes: changes, removals: %ExFactor{}}, dry_run, opts)
  end

  def path(module) do
    Path.join(["lib", Macro.underscore(module) <> ".ex"])
  end

  defp format(%{path: nil} = struct, _dry_run, _format), do: struct

  defp format(output, true, _format), do: output

  defp format(%{additions: adds, changes: changes, removals: removals} = output, false, opts) do
    %{
      additions: format(adds, opts),
      changes: format(changes, opts),
      removals: format(removals, opts)
    }

    output
  end

  defp format(list, opts) when is_list(list) do
    Enum.map(list, fn elem ->
      format(elem, opts)
      Map.get_and_update(elem, :state, fn val -> {val, [:formatted | val]} end)
    end)
  end

  defp format(%{state: [:unchanged]} = struct, _opts), do: struct

  defp format(struct, opts) do
    Formatter.format([struct.path], opts)
    Map.get_and_update(struct, :state, fn val -> {val, [:formatted | val]} end)
  end
end