Current section

Files

Jump to
styler lib style blocks.ex
Raw

lib/style/blocks.ex

# Copyright 2023 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
defmodule Styler.Style.Blocks do
@moduledoc """
Simple 1-1 rewrites all crammed into one module to make for more efficient traversals
Credo Rules addressed:
* Credo.Check.Consistency.ParameterPatternMatching
* Credo.Check.Readability.LargeNumbers
* Credo.Check.Readability.ParenthesesOnZeroArityDefs
* Credo.Check.Readability.PreferImplicitTry
* Credo.Check.Readability.WithSingleClause
* Credo.Check.Refactor.CaseTrivialMatches
* Credo.Check.Refactor.CondStatements
* Credo.Check.Refactor.RedundantWithClauseResult
* Credo.Check.Refactor.WithClauses
"""
@behaviour Styler.Style
alias Styler.Style
# @TODO handle comments https://github.com/adobe/elixir-styler/issues/79
def run({node, meta}, ctx), do: {:cont, {style(node), meta}, ctx}
defmacrop trivial_case(head, a, a_body, b, b_body) do
quote do
{:case, _,
[
unquote(head),
[
{_,
[
{:->, _, [[unquote(a)], unquote(a_body)]},
{:->, _, [[unquote(b)], unquote(b_body)]}
]}
]
]}
end
end
defp style(trivial_case(head, {_, _, [true]}, do_, {_, _, [false]}, else_)), do: styled_if(head, do_, else_)
defp style(trivial_case(head, {_, _, [false]}, else_, {_, _, [true]}, do_)), do: styled_if(head, do_, else_)
defp style(trivial_case(head, {_, _, [true]}, do_, {:_, _, _}, else_)), do: styled_if(head, do_, else_)
# `Credo.Check.Refactor.CondStatements`
# This also detects strings and lists...
defp style({:cond, _, [[{_, [{:->, _, [[expr], do_body]}, {:->, _, [[{:__block__, _, [truthy]}], else_body]}]}]]})
when is_atom(truthy) and truthy not in [nil, false] do
styled_if(expr, do_body, else_body)
end
# Credo.Check.Readability.WithSingleClause
# rewrite `with success <- single_statement do body else ...elses end`
# to `case single_statement do success -> body; ...elses end`
defp style({:with, m, [{:<-, am, [success, single_statement]}, [body, elses]]}) do
{{:__block__, do_meta, [:do]}, body} = body
{{:__block__, _else_meta, [:else]}, elses} = elses
clauses = [{{:__block__, am, [:do]}, [{:->, do_meta, [[success], body]} | elses]}]
style({:case, m, [single_statement, clauses]})
end
# Credo.Check.Refactor.WithClauses
# Credo.Check.Refactor.RedundantWithClauseResult
defp style({:with, m, children} = with) when is_list(children) do
if Enum.any?(children, &left_arrow?/1) do
{preroll, children} = Enum.split_while(children, &(not left_arrow?(&1)))
# the do/else keyword macro of the with statement is the last element of the list
[[{do_block, do_body} | elses] | reversed_clauses] = Enum.reverse(children)
{postroll, reversed_clauses} = Enum.split_while(reversed_clauses, &(not left_arrow?(&1)))
[{:<-, _, [lhs, rhs]} = _final_clause | rest] = reversed_clauses
# drop singleton identity else clauses like `else foo -> foo end`
elses =
case elses do
[{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses
_ -> elses
end
{reversed_clauses, do_body} =
cond do
# Put the postroll into the body
Enum.any?(postroll) ->
{_, do_body_meta, _} = do_body
do_body = {:__block__, do_body_meta, Enum.reverse(postroll, [do_body])}
{reversed_clauses, do_body}
# Credo.Check.Refactor.RedundantWithClauseResult
Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) ->
{rest, rhs}
# no change
true ->
{reversed_clauses, do_body}
end
children = Enum.reverse(reversed_clauses, [[{do_block, do_body} | elses]])
if Enum.any?(preroll),
do: {:__block__, m, preroll ++ [{:with, m, children}]},
else: {:with, m, children}
else
# maybe this isn't a with statement - could be a function named `with`
# or it's just a with statement with no arrows, but that's too saddening to imagine
with
end
end
# Credo.Check.Refactor.UnlessWithElse
defp style({:unless, m, [{_, hm, _} = head, [_, _] = do_else]}), do: style({:if, m, [{:!, hm, [head]}, do_else]})
# Credo.Check.Refactor.NegatedConditionsInUnless
defp style({:unless, m, [{negator, _, [expr]}, [{do_, do_body}]]}) when negator in [:!, :not],
do: style({:if, m, [expr, [{do_, do_body}]]})
# Credo.Check.Refactor.NegatedConditionsWithElse
defp style({:if, m, [{negator, _, [expr]}, [{do_, do_body}, {else_, else_body}]]}) when negator in [:!, :not],
do: style({:if, m, [expr, [{do_, else_body}, {else_, do_body}]]})
defp style({:if, m, [head, [do_block, {_, {:__block__, _, [nil]}}]]}), do: {:if, m, [head, [do_block]]}
defp style(node), do: node
defp left_arrow?({:<-, _, _}), do: true
defp left_arrow?(_), do: false
defp nodes_equivalent?(a, b) do
# compare nodes without metadata
Style.update_all_meta(a, fn _ -> nil end) == Style.update_all_meta(b, fn _ -> nil end)
end
defp styled_if(head, do_body, else_body) do
{_, meta, _} = head
line = meta[:line]
# @TODO figure out appropriate line meta for `else` and `if->end->line`
children = [head, [{{:__block__, [line: line], [:do]}, do_body}, {{:__block__, [], [:else]}, else_body}]]
style({:if, [line: line, do: [line: line], end: []], children})
end
end