Skip to content

Commit 131b540

Browse files
committed
humanize if_ast bounds nonsense
1 parent 006f622 commit 131b540

3 files changed

Lines changed: 120 additions & 61 deletions

File tree

lib/style.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ defmodule Styler.Style do
170170
{directive, updated_meta, children}
171171
end
172172

173+
def first_line({:__block__, m, [{_, cm, _} | _]}), do: m[:line] || cm[:line]
174+
def first_line(node), do: meta(node)[:line]
175+
173176
def max_line([_ | _] = list), do: list |> List.last() |> max_line()
174177

175178
def max_line(ast) do
@@ -198,7 +201,7 @@ defmodule Styler.Style do
198201
{nodes, shifted_comments, comments, _line} =
199202
Enum.reduce(nodes, {[], [], comments, first_line}, fn node, {n_acc, c_acc, comments, move_to_line} ->
200203
meta = meta(node)
201-
line = meta[:line]
204+
line = first_line(node)
202205
last_line = max_line(node)
203206
{mine, comments} = comments_for_lines(comments, line, last_line)
204207

@@ -223,7 +226,7 @@ defmodule Styler.Style do
223226
@doc """
224227
Returns all comments "for" a node, including on the line before it. see `comments_for_lines` for more
225228
"""
226-
def comments_for_node({_, m, _} = node, comments), do: comments_for_lines(comments, m[:line], max_line(node))
229+
def comments_for_node(node, comments), do: comments_for_lines(comments, first_line(node), max_line(node))
227230

228231
@doc """
229232
Gets all comments in range start_line..last_line, and any comments immediately before start_line.s
@@ -239,6 +242,7 @@ defmodule Styler.Style do
239242
here, comments_for_lines(comments, 4, 6) is "a", "b", "c", "d"
240243
"""
241244
def comments_for_lines(comments, start_line, last_line) do
245+
if !start_line, do: raise "nil"
242246
comments |> Enum.reverse() |> comments_for_lines(start_line, last_line, [], [])
243247
end
244248

lib/style/blocks.ex

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,15 @@ defmodule Styler.Style.Blocks do
3535
# case statement with exactly 2 `->` cases
3636
# rewrite to `if` if it's any of 3 trivial cases
3737
def run({{:case, m, [head, [{_, [{:->, am, [[lhs_a], a]}, {:->, bm, [[lhs_b], b]}]}]]}, _} = zipper, ctx) do
38-
end_line = m[:end][:line]
39-
40-
ctx =
41-
ctx
42-
|> Map.update!(:comments, &pull_leading_comment(&1, am[:line], body_start_line(a)))
43-
|> Map.update!(:comments, &pull_leading_comment(&1, bm[:line], body_start_line(b)))
38+
# @TODO shouldn't be shifting if we aren't doing if_ast rewrites.
39+
# try to put shift into the if_ast header that matches transformations?
40+
ctx = shift_arrow_comments_into_body(ctx, {am, a}, {bm, b})
41+
b = Macro.update_meta(b, &Keyword.put(&1, :end_of_expression, [line: m[:end][:line], newlines: 1]))
4442

4543
case {lhs_a, lhs_b} do
46-
{{_, _, [true]}, {_, _, [false]}} -> if_ast(zipper, head, a, b, ctx, else: end_line)
47-
{{_, _, [true]}, {:_, _, _}} -> if_ast(zipper, head, a, b, ctx, else: end_line)
48-
{{_, _, [false]}, {_, _, [true]}} -> if_ast(zipper, head, b, a, ctx, do: end_line, else: bm[:line])
44+
{{_, _, [true]}, {_, _, [false]}} -> if_ast(zipper, head, a, b, ctx)
45+
{{_, _, [true]}, {:_, _, _}} -> if_ast(zipper, head, a, b, ctx)
46+
{{_, _, [false]}, {_, _, [true]}} -> if_ast(zipper, head, b, a, ctx)
4947
_ -> {:cont, zipper, ctx}
5048
end
5149
end
@@ -85,7 +83,7 @@ defmodule Styler.Style.Blocks do
8583
{:cont, zipper, ctx}
8684
end
8785

88-
def run({{:cond, m, [[{do_, clauses}]]}, _} = zipper, ctx) do
86+
def run({{:cond, _, [[{do_, clauses}]]}, _} = zipper, ctx) do
8987
# ensure all final `atom -> final_clause` use `true` for consistency.
9088
# `:else` is cute but consistency is all.
9189
rewrite_literal_to_true = fn
@@ -106,12 +104,10 @@ defmodule Styler.Style.Blocks do
106104
# `b` (the final clause, going into `else`) trails right up to this `cond`'s own `end` - use that
107105
# real boundary so a dangling/trailing comment moves along with its content instead of getting stranded.
108106
[{:->, am, [[head], a]}, {:->, bm, [[{:__block__, _, [true]}], b]}] ->
109-
comments =
110-
ctx.comments
111-
|> pull_leading_comment(am[:line], body_start_line(a))
112-
|> pull_leading_comment(bm[:line], body_start_line(b))
113-
114-
if_ast(zipper, head, a, b, %{ctx | comments: comments}, else: m[:end][:line])
107+
ctx = shift_arrow_comments_into_body(ctx, {am, a}, {bm, b})
108+
# TODO needs a test for danglers in cond do
109+
# b = Macro.update_meta(b, &Keyword.put(&1, :end_of_expression, [line: m[:end][:line], newlines: 1]))
110+
if_ast(zipper, head, a, b, ctx)
115111

116112
clauses ->
117113
{:cont, Zipper.replace_children(zipper, [[{do_, clauses}]]), ctx}
@@ -207,6 +203,11 @@ defmodule Styler.Style.Blocks do
207203
# Credo.Check.Refactor.NegatedConditionsWithElse
208204
# if !x, do: y, else: z => if x, do: z, else: y
209205
[negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) ->
206+
# end of expression hacks ensure that these bodies keep dangling comments in their blocks.
207+
# someday we might find a better way!
208+
# ohhhhhhh probably i need to fix the line numbers on the do and else to match the lines.... HMM
209+
do_body = Macro.update_meta(do_body, &Keyword.put(&1, :end_of_expression, [line: Style.meta(else_)[:line], newlines: 1]))
210+
else_body = Macro.update_meta(else_body, &Keyword.put(&1, :end_of_expression, [line: m[:end][:line], newlines: 1]))
210211
zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx)
211212

212213
# drop `else end`
@@ -217,15 +218,9 @@ defmodule Styler.Style.Blocks do
217218
[head, [do_block, {_, {:__block__, _, [nil]}}]] ->
218219
{:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx}
219220

220-
[head, [do_, {else_kw, _} = else_]] ->
221+
[head, [do_, else_]] ->
221222
if Style.max_line(do_) > Style.max_line(else_) do
222-
# we inverted the if/else blocks of this `if` statement in a previous pass (due to negators or unless)
223-
# shift comments etc to make it happy now. `do_`'s content used to be paired with `else_kw` and always
224-
# trailed right up to this `if`'s own `end`; `else_`'s content used to be paired with `do_kw`, and
225-
# `else_kw`'s real (unmodified) line still marks exactly where that content used to trail off to -
226-
# use those real boundaries so a dangling/trailing comment moves along with its content instead of
227-
# getting stranded.
228-
if_ast(zipper, head, do_, else_, ctx, do: m[:end][:line], else: Style.meta(else_kw)[:line])
223+
if_ast(zipper, head, do_, else_, ctx)
229224
else
230225
{:cont, zipper, ctx}
231226
end
@@ -387,52 +382,45 @@ defmodule Styler.Style.Blocks do
387382

388383
defp nodes_equivalent?(a, b), do: Style.without_meta(a) == Style.without_meta(b)
389384

390-
defp body_start_line({:__block__, meta, [child | _]}), do: meta[:line] || Style.meta(child)[:line]
391-
defp body_start_line({_, meta, _}), do: meta[:line]
392-
393-
# A leading comment on a `case`/`cond` clause's own `->` line (eg `# a` directly above `false ->`) can sit
394-
# a line or more above the clause body's own content once that body is multi-line - too far for the
395-
# (unwidened, on purpose - see `bound_trailing`) adjacency check in `order_line_meta_and_comments` to find.
396-
# Pull any such leading comment down to sit directly adjacent to the body's real first line instead, so
397-
# normal adjacency finds it. Leaves the body's own `:line` (and thus its rendered position) untouched.
398-
defp pull_leading_comment(comments, header_line, body_line) do
399-
{mine, rest} = Style.comments_for_lines(comments, header_line, header_line)
400-
delta = body_line - header_line
401-
if delta == 0, do: comments, else: Enum.sort_by(rest ++ Enum.map(mine, &%{&1 | line: &1.line + delta}), & &1.line)
385+
# shifts comments sitting directly on arrows into the body.
386+
# ideally this gets rolled into comment management, but because we're removing the arrows, we can't see the gaps
387+
# in the bodies of these things.
388+
# maybe an alternative is to hack the bodies to have a start line equal to the arrow's start line, thereby
389+
# leaving all comment manip to our comment manip function
390+
# yeah, the main problem seems to be that the body of an arrow can have a line number much higher than the arrow itself, and so we lose comments modifying the arrow.
391+
# we need to encode that arrow line number somehow. essentially, that's the line number of our `do` keyword or whatever
392+
defp shift_arrow_comments_into_body(ctx, {am, a}, {bm, b}) do
393+
ctx
394+
|> Map.update!(:comments, &do_shift_arrow_comments_into_body(&1, am, a))
395+
|> Map.update!(:comments, &do_shift_arrow_comments_into_body(&1, bm, b))
396+
end
397+
398+
defp do_shift_arrow_comments_into_body(comments, arrow_meta, body) do
399+
arrow_line = arrow_meta[:line]
400+
body_line = Style.first_line(body)
401+
402+
if body_line == arrow_line do
403+
comments
404+
else
405+
{mine, rest} = Style.comments_for_lines(comments, arrow_line, arrow_line)
406+
mine = Enum.map(mine, &%{&1 | line: &1.line + 1})
407+
Enum.sort_by(rest ++ mine, & &1.line)
408+
end
402409
end
403410

404411
# When we're coming in from here, we know we're coming in for a transformation from a different block
405-
defp if_ast(zipper, head, {_, _, _} = do_body, {_, _, _} = else_body, ctx, bounds) do
412+
defp if_ast(zipper, head, {_, _, _} = do_body, {_, _, _} = else_body, ctx) do
406413
do_ = {{:__block__, [line: nil], [:do]}, do_body}
407414
else_ = {{:__block__, [line: nil], [:else]}, else_body}
408-
if_ast(zipper, head, do_, else_, ctx, bounds)
415+
if_ast(zipper, head, do_, else_, ctx)
409416
end
410417

411-
defp if_ast(zipper, {_, meta, _} = head, {do_kw, do_body}, {else_kw, else_body}, ctx, bounds) do
418+
defp if_ast(zipper, {_, meta, _} = head, {do_kw, do_body}, {else_kw, else_body}, ctx) do
412419
line = meta[:line]
413420

414-
# When converting different blocks to if statements, the comment algo can miss dangling comments for different shapes.
415-
# Setting a fake end_of_expression helps clue it in to grab those comments.
416-
# this is a dirty hack - the end_of_expression gets nixed before this function returns
417-
[do_body, else_body] =
418-
for {kw, {node, meta, children}} <- [do: do_body, else: else_body] do
419-
meta =
420-
if line = bounds[kw],
421-
do: Keyword.put(meta, :end_of_expression, [newlines: 1, line: line - 1]),
422-
else: Keyword.delete(meta, :end_of_expression)
423-
424-
meta =
425-
if meta[:line],
426-
do: meta,
427-
else: Keyword.put(meta, :line, Style.meta(hd(children))[:line])
428-
429-
{node, meta, children}
430-
end
431-
432421
{[do_body, else_body], comments} = Style.order_line_meta_and_comments([do_body, else_body], ctx.comments, line)
433422

434-
# the lines for the else and end keywords. not sure why the bounds...
435-
else_line = Style.max_line(do_body) + if(bounds[:do], do: 1, else: 0)
423+
else_line = Style.max_line(do_body)
436424
end_line = Style.max_line(else_body) + 1
437425
# clean up the dangling comments hack
438426
do_body = Macro.update_meta(do_body, &Keyword.delete(&1, :end_of_expression))

test/style/blocks_test.exs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,26 +1844,93 @@ defmodule Styler.Style.BlocksTest do
18441844
case foo do
18451845
# a
18461846
false ->
1847+
# foo
18471848
d1
18481849
d2
18491850
# b
18501851
true ->
1852+
# bar
18511853
e
18521854
# dangling
18531855
end
18541856
""",
18551857
"""
18561858
if foo do
18571859
# b
1860+
# bar
18581861
e
18591862
# dangling
18601863
else
18611864
# a
1865+
# foo
18621866
d1
18631867
d2
18641868
end
18651869
"""
18661870
)
18671871
end
1872+
test "another" do
1873+
assert_style(
1874+
"""
1875+
case foo do
1876+
# a
1877+
false ->
1878+
# foo
1879+
d1
1880+
# b
1881+
true ->
1882+
# bar
1883+
e
1884+
f
1885+
# dangling
1886+
end
1887+
""",
1888+
"""
1889+
if foo do
1890+
# b
1891+
# bar
1892+
e
1893+
f
1894+
# dangling
1895+
else
1896+
# a
1897+
# foo
1898+
d1
1899+
end
1900+
"""
1901+
)
1902+
end
1903+
1904+
test "yet another" do
1905+
assert_style(
1906+
"""
1907+
case foo do
1908+
# a
1909+
true ->
1910+
# foo
1911+
d1
1912+
# b
1913+
false ->
1914+
# bar
1915+
e
1916+
f
1917+
# dangling
1918+
end
1919+
""",
1920+
"""
1921+
if foo do
1922+
# a
1923+
# foo
1924+
d1
1925+
else
1926+
# b
1927+
# bar
1928+
e
1929+
f
1930+
# dangling
1931+
end
1932+
"""
1933+
)
1934+
end
18681935
end
18691936
end

0 commit comments

Comments
 (0)