defmodule Mix.Tasks.Recode.Rename do
@shortdoc "Renames the given function"
@moduledoc """
A mix task to rename functions.
```shell
mix recode.rename Module.name[/arity] new_name
```
This mix task overwrite the definition and all calls of the given function
with the `new_name`. If the `arity` is given to `Module.name` only functions
with the corresponding `arity` will be rewritten.
## Examples
```shell
mix recode.rename MyApp.hello say_hello
```
```shell
mix recode.rename MyApp.SomeMoudle.do_it/2 make
```
## Switchtes
* `--config` - specifies an alternative config file.
* `--dry`, `--no-dry` - Activates/deactivates the dry mode. No file is
overwritten in dry mode. Overwrites the corresponding value in the
configuration.
* `--verbose`, `--no-verbose` - Activate/deactivates the verbose mode.
Overwrites the corresponding value in the configuration.
"""
use Mix.Task
alias Recode.Config
alias Recode.Runner
alias Recode.Task.Rename
@opts strict: [config: :string, dry: :boolean, verbose: :boolean]
@impl Mix.Task
def run(opts) do
{opts, args} = OptionParser.parse!(opts, @opts)
args = args!(args)
config =
opts
|> config!()
|> Keyword.merge(opts)
|> update(:verbose)
:ok = prepare(config)
Runner.run({Rename, args}, config)
end
defp config!(opts) do
case Config.read(opts) do
{:ok, config} -> config
{:error, :not_found} -> Mix.raise("Config file not found")
end
end
defp prepare(opts) do
Mix.Task.run("compile")
inputs = opts[:inputs]
exclude_compilation =
case Keyword.fetch(opts, :exclude_compilation) do
{:ok, wildcard} ->
wildcard
|> List.wrap()
|> Enum.flat_map(fn wildcard -> Path.wildcard(wildcard) end)
:error ->
[]
end
ExUnit.start(autorun: false)
Code.put_compiler_option(:ignore_module_conflict, true)
config = Mix.Project.config()
elixirc_paths = Keyword.fetch!(config, :elixirc_paths)
compile_path = Mix.Project.compile_path()
{:ok, _modules, _warnings} =
inputs
|> List.wrap()
|> Enum.flat_map(&Path.wildcard/1)
|> Enum.reject(fn input ->
String.ends_with?(input, ".exs") or
Enum.any?(elixirc_paths, fn path -> String.starts_with?(input, path) end) or
input in exclude_compilation
end)
|> Kernel.ParallelCompiler.compile_to_path(compile_path)
:ok
end
defp args!([from, to]) do
case to_mfa(from) do
{:ok, from_mfa} ->
to = %{fun: String.to_atom(to)}
[from: from_mfa, to: to]
:error ->
Mix.raise("Can not parse arguments")
end
end
defp args!(_invalid) do
Mix.raise("""
Expected module.fun and the new function name.
Examples:
mix recode.rename MyApp.Some.make_it do_it\
""")
end
defp to_mfa(string) when is_binary(string) do
~r{[.|/]}
|> Regex.split(string)
|> Enum.group_by(fn part ->
cond do
part =~ ~r/^[A-Z]/ -> :module
part =~ ~r/^[a-z]/ -> :fun
part =~ ~r/^[0-9]/ -> :arity
true -> :error
end
end)
|> to_mfa()
end
defp to_mfa(%{error: _error}), do: :error
defp to_mfa(%{} = parts) do
with {:ok, fun} <- to_fun(parts),
{:ok, arity} <- to_arity(parts),
{:ok, module} <- to_module(parts) do
{:ok, {module, fun, arity}}
end
end
defp to_module(%{module: module}), do: {:ok, Module.concat(module)}
defp to_module(_parts), do: :error
defp to_fun(%{fun: [fun]}), do: {:ok, String.to_atom(fun)}
defp to_fun(_parts), do: :error
defp to_arity(%{arity: [arity]}), do: {:ok, String.to_integer(arity)}
defp to_arity(_parts), do: {:ok, nil}
defp update(opts, :verbose) do
case opts[:dry] do
true -> Keyword.put(opts, :verbose, true)
false -> opts
end
end
end