Skip to content

Commit

Permalink
Pipify: d(a |> b |> c) => a |> b() |> c() |> d()(#198)
Browse files Browse the repository at this point in the history
Closes #133
  • Loading branch information
novaugust authored Nov 7, 2024
1 parent fe32a84 commit 60f79c7
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 71 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/style.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
3 changes: 1 addition & 2 deletions lib/style/blocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions lib/style/pipes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
181 changes: 112 additions & 69 deletions test/style/pipes_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ defmodule Styler.Style.PipesTest do
y
end
a(foo(if_result), b)
if_result |> foo() |> a(b)
"""
)
end
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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

0 comments on commit 60f79c7

Please sign in to comment.