lib/recode/task/rename.ex

defmodule Recode.Task.Rename do
  @moduledoc """
  A refactoring task to rename functions.

  For usage, see `mix recode.rename`.
  """

  use Recode.Task, refactor: true

  import Kernel, except: [match?: 2]

  alias Recode.AST
  alias Recode.Context
  alias Recode.DebugInfo
  alias Recode.Source
  alias Recode.Task.Rename
  alias Sourceror.Zipper

  @impl Recode.Task
  def run(source, opts) do
    zipper =
      source
      |> Source.zipper()
      |> Context.traverse(fn zipper, context ->
        opts = Keyword.put(opts, :source, source)
        rename(zipper, context, opts)
      end)

    Source.update(source, Rename, code: zipper)
  end

  defp rename({{fun, _meta, [call, _expr]}, _zipper_meta} = zipper, context, opts)
       when fun in [
              :def,
              :defp,
              :defmacro,
              :defmacrop
            ] do
    case rename?(:definition, call, context, opts[:from]) do
      true ->
        zipper = zipper |> update_definition(opts[:to]) |> Zipper.next()
        {zipper, context}

      false ->
        zipper = Zipper.next(zipper)
        {zipper, context}
    end
  end

  defp rename({{fun, _meta, _args}, _zipper_meta} = zipper, context, _opts)
       when fun in [
              :alias,
              :defmodule,
              :defdelegate,
              :__aliases__,
              :__block__
            ] do
    {zipper, context}
  end

  defp rename(
         {{:@, _meta1, [{:spec, _meta2, [arg]}]}, _zipper_meta} = zipper,
         context,
         opts
       ) do
    case rename?(:spec, arg, context, opts[:from]) do
      true ->
        zipper = update_spec(zipper, opts[:to])
        {zipper, context}

      false ->
        {zipper, context}
    end
  end

  defp rename({{:@, _meta1, _args}, _zipper_meta} = zipper, context, _opts) do
    {zipper, context}
  end

  defp rename({{:import, _meta, [alias, _import_opts]}, _zipper_meta} = zipper, context, opts) do
    case AST.aliases_concat(alias) == elem(opts[:from], 0) do
      true ->
        zipper = update_import_only(zipper, opts)
        {zipper, context}

      false ->
        {zipper, context}
    end
  end

  defp rename({{:&, _meta, _args} = ast, _zipper_meta} = zipper, context, opts) do
    case rename?(:capture, ast, context, opts) do
      false ->
        {zipper, context}

      true ->
        zipper = update_capture(zipper, opts[:to])
        {zipper, context}
    end
  end

  defp rename({{fun, _meta, _args} = ast, _zipper_meta} = zipper, context, opts)
       when is_atom(fun) do
    case rename?(:call, ast, context, opts) do
      false ->
        {zipper, context}

      true ->
        zipper = update_call(zipper, opts[:to])

        {zipper, context}
    end
  end

  defp rename(
         {{{:., _meta1, [{:__aliases__, _meta2, _args2}, _fun]}, _meta3, _args3} = ast,
          _zipper_meta} = zipper,
         context,
         opts
       ) do
    case rename?(:dot_call, ast, context, opts) do
      false ->
        {zipper, context}

      true ->
        zipper = update_dot_call(zipper, opts[:to])

        {zipper, context}
    end
  end

  defp rename(zipper, context, _opts) do
    {zipper, context}
  end

  defp rename?(
         :spec,
         {:"::", _meta1, [{fun, _meta2, args}, _result]},
         %Context{module: {module, _meta3}},
         {module, fun, arity}
       ) do
    arity?(args, arity)
  end

  defp rename?(:spec, {:when, _meta, [spec, _when]}, context, from) do
    rename?(:spec, spec, context, from)
  end

  defp rename?(:spec, _ast, _context, _from), do: false

  defp rename?(
         :definition,
         {fun, _meta1, args},
         %Context{module: {module, _meta2}},
         {module, fun, arity}
       ) do
    arity?(args, arity)
  end

  defp rename?(
         :definition,
         {:when, _meta1, [{fun, _meta2, args}, _when]},
         %Context{module: {module, _meta3}},
         {module, fun, arity}
       ) do
    arity?(args, arity)
  end

  defp rename?(:definition, _ast, _context, _from) do
    false
  end

  defp rename?(:call, {fun, _meta, args}, context, opts) when not is_nil(args) do
    from = opts[:from]

    with true <- fun == elem(from, 1) do
      mfa = {nil, fun, length(args)}
      rename?(:debug, mfa, context, opts)
    end
  end

  defp rename?(:call, _ast, _context, _from) do
    false
  end

  defp rename?(
         :capture,
         {:&, _meta, [{:/, _meta2, [{fun, _meta3, nil}, {:__block__, _meta4, [arity]}]}]},
         context,
         opts
       ) do
    from = opts[:from]
    mfa = {nil, fun, arity}

    match?(mfa, from) && rename?(:debug, mfa, context, opts)
  end

  defp rename?(
         :capture,
         {:&, _meta, [{:/, _meta2, [dot_fun, {:__block__, _meta4, [arity]}]}]},
         context,
         opts
       ) do
    {{:., _meta1, [{:__aliases__, _meta2, alias}, fun]}, _meta3, []} = dot_fun
    module = Module.concat(alias)
    from = opts[:from]
    mfa = {module, fun, arity}

    match?(mfa, from) && rename?(:debug, mfa, context, opts)
  end

  defp rename?(:capture, _ast, _context, _from) do
    false
  end

  defp rename?(:dot_call, ast, context, opts) do
    mfa = AST.mfa(ast)
    from = opts[:from]

    with true <- fun?(mfa, from) do
      case Context.expand_mfa(context, mfa) do
        {:ok, mfa} ->
          match?(mfa, from)

        :error ->
          rename?(:debug, mfa, context, opts)
      end
    end
  end

  defp rename?(:debug, mfa, context, opts) do
    source = opts[:source]

    case Source.debug_info(source, Context.module(context)) do
      {:error, _reason} ->
        false

      {:ok, debug_info} ->
        case DebugInfo.expand_mfa(debug_info, context, mfa) do
          {:ok, mfa} -> match?(mfa, opts[:from])
          :error -> false
        end
    end
  end

  defp arity?(args, arity) do
    cond do
      is_nil(arity) -> true
      is_nil(args) -> arity == 0
      true -> length(args) == arity
    end
  end

  defp fun?({_module1, fun, _arity1}, {_module2, fun, _arity2}), do: true

  defp fun?(_mfa1, _mfa2), do: false

  defp match?({module, fun, arity}, {module, fun, arity}), do: true

  defp match?({module, fun, _arity}, {module, fun, nil}), do: true

  defp match?({nil, fun, _arity}, {_module, fun, nil}), do: true

  defp match?({nil, fun, arity}, {_module, fun, arity}), do: true

  defp match?(_mfa1, _mfa2), do: false

  defp update_definition({ast, _meta} = zipper, %{fun: name}) do
    ast = AST.update_definition(ast, name: name)
    Zipper.replace(zipper, ast)
  end

  defp update_spec({ast, _meta} = zipper, %{fun: name}) do
    ast = AST.update_spec(ast, name: name)
    Zipper.replace(zipper, ast)
  end

  defp update_call({ast, _meta} = zipper, %{fun: name}) do
    ast = AST.update_call(ast, name: name)
    Zipper.replace(zipper, ast)
  end

  defp update_dot_call({ast, _meta} = zipper, %{fun: name}) do
    ast = AST.update_dot_call(ast, name: name)
    Zipper.replace(zipper, ast)
  end

  defp update_import_only({{:import, meta, [alias, import_opts]}, _zipper_meta} = zipper, opts) do
    updated_opts =
      Enum.map(import_opts, fn {key, value} ->
        case AST.get_value(key) do
          :only -> {key, update_import_only(value, opts)}
          _else -> {key, value}
        end
      end)

    Zipper.replace(zipper, {:import, meta, [alias, updated_opts]})
  end

  defp update_import_only(ast, opts) do
    {_module, fun, arity} = opts[:from]

    updated =
      ast
      |> AST.get_value()
      |> Enum.map(fn {key_ast, value_ast} ->
        key = AST.get_value(key_ast)
        value = AST.get_value(value_ast)

        case {fun, arity, key, value} do
          {fun, nil, fun, arity} ->
            {AST.put_value(key_ast, opts[:to].fun), AST.put_value(value_ast, arity)}

          {fun, arity, fun, arity} ->
            {AST.put_value(key_ast, opts[:to].fun), AST.put_value(value_ast, arity)}

          _else ->
            {key_ast, value_ast}
        end
      end)

    AST.put_value(ast, updated)
  end

  defp update_capture({{:&, _meta, _args} = ast, _zipper_meta} = zipper, to) do
    ast = AST.update_capture(ast, name: to.fun)
    Zipper.replace(zipper, ast)
  end
end