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