Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor comment handling #128

Merged
merged 5 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 85 additions & 36 deletions lib/sourceror/comments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ defmodule Sourceror.Comments do
`:trailing_comments` field.
"""
@spec merge_comments(Macro.t(), list(map)) :: Macro.t()
def merge_comments({:__block__, _meta, []} = quoted, comments) do
trailing_block({quoted, comments})
end

def merge_comments(quoted, comments) do
{quoted, leftovers} =
Macro.traverse(quoted, comments, &do_merge_comments/2, &merge_leftovers/2)
Expand Down Expand Up @@ -134,27 +138,47 @@ defmodule Sourceror.Comments do
quoted
end

Macro.postwalk(quoted, [], fn
{_, _, _} = quoted, acc ->
do_extract_comments(quoted, acc, collapse_comments)
output =
Macro.prewalk(quoted, [], fn
{_, meta, _} = quoted, acc ->
traling_block? = meta[:__sourceror__][:trailing_block] || false
do_extract_comments(quoted, acc, collapse_comments, traling_block?)

other, acc ->
{other, acc}
end)
other, acc ->
{other, acc}
end)

output
end

defp do_extract_comments({_, meta, _} = quoted, acc, collapse_comments) do
leading_comments = Keyword.get(meta, :leading_comments, [])
defp do_extract_comments({_, meta, [quoted]}, [], collapse_comments, true) do
{quoted, comments} = do_extract_comments(quoted, [], collapse_comments, false)
quoted = update_empty_quoted(quoted)

{start, span} = span(quoted)

{leading_comments, end_leading_comments} =
trailing_block_comments(meta[:leading_comments], collapse_comments, start)

leading_comments_count = length(leading_comments)
end_line =
if span > 0 do
end_leading_comments + start + span
else
max(end_leading_comments, 1)
end

{trailing_comments, _} =
trailing_block_comments(meta[:trailing_comments], collapse_comments, end_line)

{quoted, Enum.concat([leading_comments, comments, trailing_comments])}
end

defp do_extract_comments({_, meta, _} = quoted, acc, collapse_comments, false) do
leading_comments = Keyword.get(meta, :leading_comments, [])

leading_comments =
if collapse_comments do
for {comment, i} <- Enum.with_index(leading_comments, 0) do
next_eol_correction = max(0, comment.next_eol_count - 1)
line = max(1, meta[:line] - (leading_comments_count - i + next_eol_correction))
%{comment | line: line}
end
collapse_comments(meta[:line], leading_comments)
else
leading_comments
end
Expand All @@ -163,7 +187,7 @@ defmodule Sourceror.Comments do

trailing_comments =
if collapse_comments do
collapse_trailing_comments(quoted, trailing_comments)
quoted |> Sourceror.get_end_line() |> collapse_comments(trailing_comments)
else
trailing_comments
end
Expand All @@ -190,34 +214,59 @@ defmodule Sourceror.Comments do
{quoted, acc}
end

defp collapse_trailing_comments(quoted, trailing_comments) do
meta = Sourceror.get_meta(quoted)
trailing_block? = meta[:__sourceror__][:trailing_block]
defp span({:__block__, meta, []}), do: {meta[:line], 0}

defp span(quoted) do
%{start: range_start, end: range_end} = Sourceror.get_range(quoted, include_comments: true)
{range_start[:line], range_end[:line] - range_start[:line] + 1}
end

defp update_empty_quoted({:__block__, meta, []}) do
{:__block__, Keyword.put(meta, :line, 1), []}
end

defp update_empty_quoted(quoted), do: quoted

defp trailing_block_comments([], _collapse_comments, line), do: {[], line - 1}

comments =
Enum.map(trailing_comments, fn comment ->
line = meta[:end_of_expression][:line] || meta[:line]
defp trailing_block_comments(comments, false, line), do: {comments, line}

%{comment | line: line - 1}
defp trailing_block_comments([comment | _] = comments, true, line) do
prev = min(comment.previous_eol_count, 2) - 1
line = line + prev

{comments, line} =
Enum.reduce(comments, {[], line}, fn comment, {acc, line} ->
comment = %{comment | line: line}
line = line + min(comment.next_eol_count, 2)
{[comment | acc], line}
end)

comments =
case comments do
[first | rest] ->
prev_eol_count = if trailing_block?, do: first.previous_eol_count, else: 0
{Enum.reverse(comments), line}
end

[%{first | previous_eol_count: prev_eol_count} | rest]
defp collapse_comments(_line, []), do: []

_ ->
comments
end
defp collapse_comments(line, comments) do
comments
|> Enum.reverse()
|> Enum.reduce({[], line}, fn comment, {acc, line} ->
line = line - comment.next_eol_count
comment = %{comment | line: line}
{[comment | acc], line}
end)
|> elem(0)
end

case List.pop_at(comments, -1) do
{last, rest} when is_map(last) ->
rest ++ [%{last | next_eol_count: 0}]
defp trailing_block({quoted, []}), do: quoted

_ ->
comments
end
defp trailing_block({{_form, meta, _args} = quoted, leftovers}) do
{:__block__,
[
__sourceror__: %{trailing_block: true},
trailing_comments: leftovers,
leading_comments: [],
line: meta[:line]
], [quoted]}
end
end
170 changes: 119 additions & 51 deletions lib/sourceror/lines_corrector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,76 +14,112 @@ defmodule Sourceror.LinesCorrector do
* If a node has a line number lower than the one before, its line number is
recursively incremented by the line number difference, if it's not a
pipeline operator.
* If a node has leading comments, it's line number is incremented by the
length of the comments list
* If a node has trailing comments, it's end_of_expression and end line
metadata are set to the line of their last child plus the trailing comments
list length
* If a node has leading comments, its line number is incremented by the
newlines for those comments.
* If a node has trailing comments, its end_of_expression and end line
metadata are set to the line of their last child plus the newlines of the
trailing comments list.
"""
def correct(quoted) do
{ast, _} =
Macro.traverse(quoted, %{last_line: 1, last_form: nil}, &pre_correct/2, &post_correct/2)
case trailing_block(quoted) do
{:ok, block, line} ->
block
|> do_correct(line)
|> into_trailing_block(quoted)

:error ->
do_correct(quoted, 1)
end
end

ast
defp into_trailing_block(quoted, {:__block__, meta, [_]}) do
{:__block__, meta, [quoted]}
end

defp pre_correct({form, meta, args} = quoted, state) do
{quoted, state} =
cond do
is_nil(meta[:line]) ->
meta = Keyword.put(meta, :line, state.last_line)
{{form, meta, args}, %{state | last_form: form}}
defp trailing_block({:__block__, meta, [quoted]}) do
if meta[:__sourceror__][:trailing_block] || false do
{:ok, quoted, comments_eol_count(meta[:leading_comments])}
else
:error
end
end

get_line(quoted) <= state.last_line and not is_binary_op(form) ->
correction = state.last_line - get_line(quoted)
quoted = recursive_correct_lines(quoted, correction)
{quoted, %{state | last_line: get_line(quoted), last_form: form}}
defp trailing_block(_quoted), do: :error

true ->
{quoted, %{state | last_form: form}}
end
defp do_correct(quoted, line) do
quoted
|> Macro.traverse(%{last_line: line}, &pre_correct/2, &post_correct/2)
|> elem(0)
end

if has_leading_comments?(quoted) do
leading_comments = length(meta[:leading_comments])
quoted = recursive_correct_lines(quoted, leading_comments + 1)
{quoted, %{state | last_line: meta[:line]}}
else
{quoted, state}
end
defp pre_correct({_form, _meta, _args} = quoted, state) do
do_pre_correct(quoted, state)
end

defp pre_correct(quoted, state) do
{quoted, state}
end

defp post_correct({_, meta, _} = quoted, state) do
quoted =
with {form, meta, [{_, _, _} = left, right]} when is_binary_op(form) <- quoted do
# We must ensure that, for binary operators, the operator line number is
# not greater than the left operand. Otherwise the comment eol counts
# will be ignored by the formatter
left_line = get_line(left)

if left_line > get_line(quoted) do
{form, Keyword.put(meta, :line, left_line), [left, right]}
else
quoted
end
end
defp do_pre_correct({form, meta, args} = quoted, state) do
case correction(form, meta[:line], meta[:leading_comments], state) do
nil ->
meta = Keyword.put(meta, :line, state.last_line)
{{form, meta, args}, state}

0 ->
{quoted, state}

correction ->
quoted = recursive_correct_lines(quoted, correction)
state = %{state | last_line: get_line(quoted)}
{quoted, state}
end
end

defp correction(_form, nil, _comments, _state), do: nil

defp correction(form, _line, _comments, _state) when is_binary_op(form), do: 0

defp correction(_form, line, nil, state) do
max(state.last_line - line, 0)
end

defp correction(_form, line, [], state) do
max(state.last_line - line, 0)
end

defp correction(_form, line, comments, state) do
comments_last_line = comments_last_line(comments)
extra_lines = if state.last_line == line, do: 1, else: 0

extra_lines =
if line <= comments_last_line,
do: extra_lines + (comments_last_line - line) + 2,
else: extra_lines

last_line = Sourceror.get_end_line(quoted, state.last_line)
max(state.last_line + extra_lines + comments_eol_count(comments) - line, 0)
end

defp post_correct({_form, _meta, _args} = quoted, state) do
do_post_correct(quoted, state)
end

defp post_correct(quoted, state) do
{quoted, state}
end

defp do_post_correct({_, meta, _} = quoted, state) do
quoted = maybe_correct_binary_op(quoted)

last_line =
if has_trailing_comments?(quoted) do
if Sourceror.Identifier.do_block?(quoted) do
last_line + length(meta[:trailing_comments] || []) + 2
else
last_line + length(meta[:trailing_comments] || []) + 1
end
state.last_line + comments_eol_count(meta[:trailing_comments]) + 1
else
last_line
state.last_line
end

last_line = Sourceror.get_end_line(quoted, last_line)

quoted =
quoted
|> maybe_correct_end_of_expression(last_line)
Expand All @@ -93,10 +129,22 @@ defmodule Sourceror.LinesCorrector do
{quoted, %{state | last_line: last_line}}
end

defp post_correct(quoted, state) do
{quoted, state}
defp maybe_correct_binary_op({form, meta, [{_, _, _} = left, right]} = quoted)
when is_binary_op(form) do
# We must ensure that, for binary operators, the operator line number is
# not greater than the left operand. Otherwise the comment eol counts
# will be ignored by the formatter
left_line = get_line(left)

if left_line > get_line(quoted) do
{form, Keyword.put(meta, :line, left_line), [left, right]}
else
quoted
end
end

defp maybe_correct_binary_op(quoted), do: quoted

defp maybe_correct_end_of_expression({form, meta, args} = quoted, last_line) do
meta =
if meta[:end_of_expression] || has_trailing_comments?(quoted) do
Expand Down Expand Up @@ -159,4 +207,24 @@ defmodule Sourceror.LinesCorrector do
ast
end)
end

defp comments_last_line(comments) do
last = List.last(comments)
last.line
end

defp comments_eol_count(comments, count \\ nil)

defp comments_eol_count(nil, _count), do: 0

defp comments_eol_count([], count), do: count || 0

defp comments_eol_count([comment | _] = comments, nil) do
line = comment.previous_eol_count - 1
comments_eol_count(comments, line)
end

defp comments_eol_count([comment | comments], lines) do
comments_eol_count(comments, lines + comment.next_eol_count)
end
end
Loading
Loading