lib/recode/task/single_pipe.ex

defmodule Recode.Task.SinglePipe do
  @moduledoc """
  Pipes (`|>`) should only be used when piping data through multiple calls.

      # preferred
      some_string |> String.downcase() |> String.trim()
      Enum.reverse(some_enum)

      # not preferred
      some_enum |> Enum.reverse()

  `SinglePipe` does not change a single `|>` that starts with a none zero arity
  function.

      # will not be changed
      one(:a) |> two()

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

  use Recode.Task, correct: true, check: true

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

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

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

    case opts[:autocorrect] do
      true ->
        Source.update(source, SinglePipe, ast: Zipper.root(zipper))

      false ->
        Source.add_issues(source, issues)
    end
  end

  defp single_pipe({{def, _meta, _args}, _zipper_mea} = zipper, issues, _autocorrect)
       when def in @defs do
    {Zipper.next(zipper), issues}
  end

  defp single_pipe(
         {{:|>, _meta1, [{:|>, _meta2, _args}, _ast]}, _zipper_meta} = zipper,
         issues,
         _autocorrect
       ) do
    {skip(zipper), issues}
  end

  defp single_pipe({{:|>, _meta, _ast}, _zipper_meta} = zipper, issues, true) do
    zipper = zipper |> Zipper.update(&update/1) |> skip()

    {zipper, issues}
  end

  defp single_pipe({{:|>, meta, _ast}, _zipper_meta} = zipper, issues, false) do
    issue =
      Issue.new(
        SinglePipe,
        "Use a function call when a pipeline is only one function long.",
        meta
      )

    {zipper, [issue | issues]}
  end

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

  defp skip({{:|>, _meta, _ast}, _zipper_meta} = zipper) do
    zipper |> Zipper.next() |> skip()
  end

  defp skip(zipper), do: zipper

  defp update({:|>, _meta1, [{_name, _meta2, nil} = arg, {fun, meta, args}]}) do
    {fun, meta, [arg | args]}
  end

  defp update({:|>, _meta1, [{_name, _meta2, []} = arg, {fun, meta, args}]}) do
    {fun, meta, [arg | args]}
  end

  defp update({:|>, _meta1, [{:__block__, _meta2, [_arg]} = block, {fun, meta, args}]}) do
    {fun, meta, [block | args]}
  end

  defp update({:|>, _meta1, [{:%{}, _meta2, _args} = map, {fun, meta, args}]}) do
    {fun, meta, [map | args]}
  end

  # Single pipes with two function calls are not changed.
  # e.g. `foo(1) |> bar(2)`
  # Because we do not want: `bar(2, foo(1))`. Some other check should expand
  # this to `1 |> foo() |> bar(2)`.
  defp update(ast), do: ast
end