Repository: adobe/elixir-styler (elixir)

Issue:
styler doesn't pipe-ify pipes wrapped in a function call
## Versions

* Elixir: use `elixir --version`
* Styler: use `mix deps | grep locked | grep styler`

## Example Input

```elixir
# should get piped but doesn't, because the root node is not a pipe
a(b |> c |> d)

# correctly gets piped since the root node is a pipe
a(b(c)) |> d()
```

## Stacktrace / Current Behaviour


Pull request:
Pipify
Closes #133 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11460ae..1a1e535 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ they can and will change without that change being reflected in Styler's semanti
 
 ### Improvements
 
+* `pipes`: pipe-ifies when first arg to a function is a pipe. reach out if this happens in unstylish places in your code (Closes #133)
 * `pipes`: unpiping assignments will make the assignment one-line when possible (Closes #181)
 
 ### Fixes
diff --git a/lib/style.ex b/lib/style.ex
index 6368293..da93416 100644
--- a/lib/style.ex
+++ b/lib/style.ex
@@ -83,6 +83,9 @@ defmodule Styler.Style do
     end
   end
 
+  def do_block?([{{:__block__, _, [:do]}, _body} | _]), do: true
+  def do_block?(_), do: false
+
   @doc """
   Returns a zipper focused on the nearest node where additional nodes can be inserted (a "block").
 
diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex
index ce3a849..96e10ce 100644
--- a/lib/style/blocks.ex
+++ b/lib/style/blocks.ex
@@ -79,9 +79,8 @@ defmodule Styler.Style.Blocks do
   def run({{:with, with_meta, children}, _} = zipper, ctx) when is_list(children) do
     # a std lib `with` block will have at least one left arrow and a `do` body. anything else we skip ¯\_(ツ)_/¯
     arrow_or_match? = &(left_arrow?(&1) || match?({:=, _, _}, &1))
-    do_block? = &match?([{{:__block__, _, [:do]}, _body} | _], &1)
 
-    if Enum.any?(children, arrow_or_match?) and Enum.any?(children, do_block?) do
+    if Enum.any?(children, arrow_or_match?) and Enum.any?(children, &Style.do_block?/1) do
       {preroll, children} =
         children
         |> Enum.map(fn
diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex
index b6204ca..76a97b4 100644
--- a/lib/style/pipes.ex
+++ b/lib/style/pipes.ex
@@ -130,6 +130,48 @@ defmodule Styler.Style.Pipes do
     end
   end
 
+  # a(b |> c[, ...args])
+  # The first argument to a function-looking node is a pipe.
+  # Maybe pipe the whole thing?
+  def run({{f, m, [{:|>, _, _} = pipe | args]}, _} = zipper, ctx) do
+    parent =
+      case Zipper.up(zipper) do
+        {{parent, _, _}, _} -> parent
+        _ -> nil
+      end
+
+    stringified = is_atom(f) && to_string(f)
+
+    cond do
+      # this is likely a macro
+      # assert a |> b() |> c()
+      !m[:closing] ->
+        {:cont, zipper, ctx}
+
+      # leave bools alone as they often read better coming first, like when prepended with `not`
+      # [not ]is_nil(a |> b() |> c())
+      stringified && (String.starts_with?(stringified, "is_") or String.ends_with?(stringified, "?")) ->
+        {:cont, zipper, ctx}
+
+      # string interpolation, module attribute assignment, or prettier bools with not
+      parent in [:"::", :@, :not] ->
+        {:cont, zipper, ctx}
+
+      # double down on being good to exunit macros, and any other special ops
+      # ..., do: assert(a |> b |> c)
+      # not (a |> b() |> c())
+      f in [:assert, :refute | @special_ops] ->
+        {:cont, zipper, ctx}
+
+      # if a |> b() |> c(), do: ...
+      Enum.any?(args, &Style.do_block?/1) ->
+        {:cont, zipper, ctx}
+
+      true ->
+        {:cont, Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]}), ctx}
+    end
+  end
+
   def run(zipper, ctx), do: {:cont, zipper, ctx}
 
   defp fix_pipe_start({pipe, zmeta} = zipper) do


Test patch:
diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs
index 93ba461..0da3eb5 100644
--- a/test/style/pipes_test.exs
+++ b/test/style/pipes_test.exs
@@ -90,7 +90,7 @@ defmodule Styler.Style.PipesTest do
             y
           end
 
-        a(foo(if_result), b)
+        if_result |> foo() |> a(b)
         """
       )
     end
@@ -767,8 +767,8 @@ defmodule Styler.Style.PipesTest do
     end
   end
 
-  describe "comments" do
-    test "unpiping doesn't move comment in anonymous function" do
+  describe "comments and..." do
+    test "unpiping" do
       assert_style(
         """
         aliased =
@@ -850,73 +850,116 @@ defmodule Styler.Style.PipesTest do
         """
       )
     end
+
+    test "optimizing" do
+      assert_style(
+        """
+        a
+        |> Enum.map(fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Enum.join(x)
+        |> Enum.each(...)
+        """,
+        """
+        a
+        |> Enum.map_join(x, fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Enum.each(...)
+        """
+      )
+
+      assert_style(
+        """
+        a
+        |> Enum.map(fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Enum.into(x)
+        |> Enum.each(...)
+        """,
+        """
+        a
+        |> Enum.into(x, fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Enum.each(...)
+        """
+      )
+
+      assert_style(
+        """
+        a
+        |> Enum.map(fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Keyword.new()
+        |> Enum.each(...)
+        """,
+        """
+        a
+        |> Keyword.new(fn b ->
+          c
+          # a comment
+          d
+        end)
+        |> Enum.each(...)
+        """
+      )
+    end
   end
 
-  test "optimizing with comments" do
-    assert_style(
-      """
-      a
-      |> Enum.map(fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Enum.join(x)
-      |> Enum.each(...)
-      """,
-      """
-      a
-      |> Enum.map_join(x, fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Enum.each(...)
-      """
-    )
-
-    assert_style(
-      """
-      a
-      |> Enum.map(fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Enum.into(x)
-      |> Enum.each(...)
-      """,
-      """
-      a
-      |> Enum.into(x, fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Enum.each(...)
-      """
-    )
-
-    assert_style(
-      """
-      a
-      |> Enum.map(fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Keyword.new()
-      |> Enum.each(...)
-      """,
-      """
-      a
-      |> Keyword.new(fn b ->
-        c
-        # a comment
-        d
-      end)
-      |> Enum.each(...)
-      """
-    )
+  describe "pipifying" do
+    test "no false positives" do
+      pipe = "a() |> b() |> c()"
+      assert_style pipe
+      assert_style String.replace(pipe, " |>", "\n|>")
+      assert_style "fn -> #{pipe} end"
+      assert_style "if #{pipe}, do: ..."
+      assert_style "x\n\n#{pipe}"
+      assert_style "@moduledoc #{pipe}"
+      assert_style "!(#{pipe})"
+      assert_style "not foo(#{pipe})"
+      assert_style ~s<"\#{#{pipe}}">
+    end
+
+    test "pipifying" do
+      assert_style "d(a |> b |> c)", "a |> b() |> c() |> d()"
+
+      assert_style(
+        """
+        # d
+        d(
+        # a
+          a
+          # b
+          |> b
+          # c
+          |> c
+        )
+        """,
+        """
+        # d
+        # a
+        a
+        # b
+        |> b()
+        # c
+        |> c()
+        |> d()
+        """
+      )
+    end
   end
 end