lib/recode/task/pipe_fun_one.ex

defmodule Recode.Task.PipeFunOne do
  @shortdoc "Add parentheses to one-arity functions."

  @moduledoc """
  Add parentheses to one-arity functions.

      # preferred
      some_string |> String.downcase() |> String.trim()

      # not preferred
      some_string |> String.downcase |> String.trim

  This task rewrites the code when `mix recode` runs with `autocorrect: true`.
  """

  use Recode.Task, corrector: true, category: :readability

  alias Recode.Issue
  alias Recode.Task.PipeFunOne
  alias Rewrite.Source
  alias Sourceror.Zipper

  @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate]

  @impl Recode.Task
  def run(source, opts) do
    {zipper, issues} =
      source
      |> Source.get(:quoted)
      |> Zipper.zip()
      |> Zipper.traverse([], fn zipper, issues ->
        pipe_fun_one(zipper, issues, opts[:autocorrect])
      end)

    case opts[:autocorrect] do
      true -> Source.update(source, PipeFunOne, :quoted, Zipper.root(zipper))
      false -> Source.add_issues(source, issues)
    end
  end

  defp pipe_fun_one(%Zipper{node: {def, _meta, _args}} = zipper, issues, _autocorrect)
       when def in @defs do
    {Zipper.next(zipper), issues}
  end

  defp pipe_fun_one(%Zipper{node: {:|>, _meta, _tree}} = zipper, issues, true) do
    {Zipper.update(zipper, &update/1), issues}
  end

  defp pipe_fun_one(%Zipper{node: {:|>, meta, _tree} = ast} = zipper, issues, false) do
    case issue?(ast) do
      true ->
        issue = Issue.new(PipeFunOne, "Use parentheses for one-arity functions in pipes.", meta)

        {zipper, [issue | issues]}

      false ->
        {zipper, issues}
    end
  end

  defp pipe_fun_one(zipper, issues, _autocorrect), do: {zipper, issues}

  defp issue?({:|>, _meta1, [_a, {_name, _meta2, nil}]}), do: true

  defp issue?({:|>, _meta1, [_a, {_name, meta, []}]}), do: Keyword.get(meta, :no_parens, false)

  defp issue?(_ast), do: false

  defp update({:|>, meta, [a, b]}) do
    {:|>, meta, [a, update(b)]}
  end

  defp update({name, meta, nil}) do
    {name, meta, []}
  end

  defp update({name, meta, []}) do
    {name, Keyword.delete(meta, :no_parens), []}
  end

  defp update(tree), do: tree
end