lib/mix/tasks/ex_factor.ex

defmodule Mix.Tasks.ExFactor do
  @shortdoc """
  Refactor a module, function, and arity to a new module namespace. Find or create the new module as appropriate.
  Required command line args: --module, --function, --arity, --target.
  `mix help ex_factor` for additional options
  See additional explantion in: #{__MODULE__}
  """

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

  By identifying a Module, function name, and arity, it will identify all non-test usages
  and extract them to a new Module.

  If the Module exists, it adds the refactored function to the end of the file and change all calls to the
  new module's name. If the Module does not exist ExFactor will conditionally create the path and the module
  and the refactored function will be added to the new module.

  Required command line args: --module, --function, --arity, --target.
    - `:module` is the fully-qualified source module containing the function to move.
    - `:function` is the name of the function (as a string)
    - `:arity` the arity of function to move.
    - `:target` is the fully-qualified destination for the removed function. If the moduel does not exist, it will be created.

  Optional command line args: --source_path, --target_path, --dryrun, --verbose
    - `:target_path` Specify an alternate (non-standard) path for the source file.
    - `:source_path` Specify an alternate (non-standard) path for the destination file.
    - `:dryrun` Don't write any updates, only return the built results.
    - `:verbose` (Default false) include the :state and :file_contents key values.
    - `:format` (Default true) when false don't run the formatter

  Example Usage:
  ```
  mix ex_factor --module MyModule.ToChange --function fn_to_change
  --arity 2 --target YourModule.ChangeTo
  ```

  Example Usage:
  ```
  mix ex_factor --module MyModule.ToChange --function fn_to_change
    --arity 2 --target YourModule.ChangeTo
    --no-format --no-dryrun --no-verbose
  ```
  """

  use Mix.Task

  def run(argv) do
    {parsed_opts, _, _} =
      OptionParser.parse(argv,
        strict: [
          arity: :integer,
          dryrun: :boolean,
          verbose: :boolean,
          format: :boolean,
          function: :string,
          key: :string,
          module: :string,
          moduleonly: :boolean,
          source_path: :string,
          target: :string,
          target_path: :string
        ]
      )

    parsed_opts
    |> options()
    |> choose_your_path()
    |> cli_output(parsed_opts)
  end

  defp choose_your_path(opts) do
    if Keyword.get(opts, :moduleonly, false) do
      ExFactor.refactor_module(opts)
    else
      opts
      |> Keyword.put(:source_function, Keyword.fetch!(opts, :function))
      |> ExFactor.refactor()
    end
  end

  defp options(opts) do
    opts
    |> Keyword.put(:source_module, Keyword.fetch!(opts, :module))
    |> Keyword.put(:target_module, Keyword.fetch!(opts, :target))
    |> Keyword.put(:dry_run, Keyword.get(opts, :dryrun, false))
  end

  defp cli_output(map, opts) do
    verbose = Keyword.get(opts, :verbose, false)

    format_entry(Map.get(map, :additions), "Additions", :light_cyan_background, verbose)
    format_entry(Map.get(map, :changes), "Changes", :light_green_background, verbose)
    format_entry(Map.get(map, :removals), "Removals", :light_red_background, verbose)
    message(false)
  end

  defp format_entry(entry, title, color, verbose) do
    IO.puts(IO.ANSI.format([color, IO.ANSI.format([:black, title])]))

    IO.puts(
      IO.ANSI.format([
        color,
        IO.ANSI.format([
          :black,
          "================================================================================"
        ])
      ])
    )

    IO.puts(
      IO.ANSI.format([
        color,
        IO.ANSI.format([
          :black,
          "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
        ])
      ])
    )

    handle_entry(entry, color, verbose)

    IO.puts(
      IO.ANSI.format([
        color,
        IO.ANSI.format([
          :black,
          "================================================================================"
        ])
      ])
    )

    IO.puts("")
  end

  defp handle_entry(entries, color, verbose) when is_list(entries) do
    Enum.map(entries, &handle_entry(&1, color, verbose))
  end

  defp handle_entry(entry, color, true) do
    handle_entry(entry, color, false)
    IO.puts("  File contents: \n#{entry.file_contents}")
    IO.puts("  State: #{inspect(entry.state)}")
  end

  defp handle_entry(entry, color, false) do
    IO.puts(IO.ANSI.format([color, IO.ANSI.format([:black, "  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"])]))
    IO.puts(IO.ANSI.format([color, IO.ANSI.format([:black, "  Module: #{entry.module}"])]))
    IO.puts(IO.ANSI.format([color, IO.ANSI.format([:black, "  Path: #{entry.path}"])]))
    IO.puts(IO.ANSI.format([color, IO.ANSI.format([:black, "  Message: #{entry.message}"])]))
  end

  @message """
  `ExFactor` (by design), does not change test files.

  There are two important and practical reason for this.

  Reason1:
    The test failures provide a safety net to help ensure that the ExFactor-ed functions
    continue to behave as expected.

  Reason2:
    Because .exs files are evaluated at runtime the introspection provided by the compiler
    is not available.

  In future revisions, we hope to address this issue, but for now, refactoring test files
  remains your responsibility.
  """
  defp message(true), do: ""

  defp message(false) do
    IO.puts(
      IO.ANSI.format([
        :magenta_background,
        IO.ANSI.format([
          :bright,
          "⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠"
        ])
      ])
    )

    IO.puts(IO.ANSI.format([:magenta_background, IO.ANSI.format([:bright, "IMPORTANT NOTE:"])]))

    IO.puts(
      IO.ANSI.format([
        :magenta_background,
        IO.ANSI.format([
          :bright,
          "✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖✖"
        ])
      ])
    )

    IO.puts(
      IO.ANSI.format([
        :magenta_background,
        IO.ANSI.format([
          :bright,
          "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
        ])
      ])
    )

    IO.puts(IO.ANSI.format([:magenta_background, IO.ANSI.format([:bright, ""])]))
    IO.puts(IO.ANSI.format([:magenta_background, IO.ANSI.format([:bright, @message])]))
  end
end