Packages
styler
0.9.5
1.11.0
1.10.1
1.10.0
1.9.1
1.9.0
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
1.0.0-alpha.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.5
0.10.4
retired
0.10.3
0.10.2
0.10.1
0.10.0
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
retired
0.9.1
retired
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.1
0.1.0
A code-style enforcer that will just FIFY instead of complaining
Current section
Files
Jump to
Current section
Files
lib/style/single_node.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.SingleNode 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
alias Styler.Zipper
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
# Our use of the `literal_encoder` option of `Code.string_to_quoted_with_comments!/2` creates
# invalid charlists literal AST nodes from `'foo'`. this rewrites them to use the `~c` sigil
# 'foo' => ~c"foo".
defp style({:__block__, meta, [[int | _] = chars]} = node) when is_integer(int) do
if meta[:delimiter] == "'" do
new_meta = Keyword.put(meta, :delimiter, "\"")
{:sigil_c, new_meta, [{:<<>>, [line: meta[:line]], [List.to_string(chars)]}, []]}
else
node
end
end
# Add / Correct `_` location in large numbers. Formatter handles large number (>5 digits) rewrites,
# but doesn't rewrite typos like `100_000_0`, so it's worthwhile to have Styler do this
#
# `?-` isn't part of the number node - it's its parent - so all numbers are positive at this point
defp style({:__block__, meta, [number]}) when is_number(number) and number >= 10_000 do
# Checking here rather than in the anon function due to compiler bug https://github.com/elixir-lang/elixir/issues/10485
integer? = is_integer(number)
meta =
Keyword.update!(meta, :token, fn
"0x" <> _ = token ->
token
"0b" <> _ = token ->
token
"0o" <> _ = token ->
token
token when integer? ->
delimit(token)
# is float
token ->
[int_token, decimals] = String.split(token, ".")
"#{delimit(int_token)}.#{decimals}"
end)
{:__block__, meta, [number]}
end
## DEPRECATED & INEFFICIENT FUNCTION REWRITES
# Keep in mind when rewriting a `/n::pos_integer` arity function here that it should also be added
# to the pipes rewriting rules, where it will appear as `/n-1`
# Enum.into(enum, empty_map[, ...]) => Map.new(enum[, ...])
defp style({{:., dm, [{:__aliases__, am, [:Enum]}, :into]}, funm, [enum, collectable | rest]} = node) do
if Style.empty_map?(collectable), do: {{:., dm, [{:__aliases__, am, [:Map]}, :new]}, funm, [enum | rest]}, else: node
end
# Logger.warn => Logger.warning
defp style({{:., dm, [{:__aliases__, am, [:Logger]}, :warn]}, funm, args}),
do: {{:., dm, [{:__aliases__, am, [:Logger]}, :warning]}, funm, args}
# Timex.now() => DateTime.utc_now()
defp style({{:., dm, [{:__aliases__, am, [:Timex]}, :now]}, funm, []}),
do: {{:., dm, [{:__aliases__, am, [:DateTime]}, :utc_now]}, funm, []}
# Timex.now(tz) => DateTime.now!(tz)
defp style({{:., dm, [{:__aliases__, am, [:Timex]}, :now]}, funm, [tz]}),
do: {{:., dm, [{:__aliases__, am, [:DateTime]}, :now!]}, funm, [tz]}
if Version.match?(System.version(), ">= 1.15.0-dev") do
# {DateTime,NaiveDateTime,Time,Date}.compare(a, b) == :lt => {DateTime,NaiveDateTime,Time,Date}.before?(a, b)
# {DateTime,NaiveDateTime,Time,Date}.compare(a, b) == :gt => {DateTime,NaiveDateTime,Time,Date}.after?(a, b)
defp style({:==, _, [{{:., dm, [{:__aliases__, am, [mod]}, :compare]}, funm, args}, {:__block__, _, [result]}]})
when mod in ~w[DateTime NaiveDateTime Time Date]a and result in [:lt, :gt] do
fun = if result == :lt, do: :before?, else: :after?
{{:., dm, [{:__aliases__, am, [mod]}, fun]}, funm, args}
end
end
# Remove parens from 0 arity funs (Credo.Check.Readability.ParenthesesOnZeroArityDefs)
defp style({def, dm, [{fun, funm, []} | rest]}) when def in ~w(def defp)a and is_atom(fun),
do: style({def, dm, [{fun, Keyword.delete(funm, :closing), nil} | rest]})
# `Credo.Check.Readability.PreferImplicitTry`
defp style({def, dm, [head, [{_, {:try, _, [try_children]}}]]}) when def in ~w(def defp)a,
do: style({def, dm, [head, try_children]})
defp style({def, dm, [{fun, funm, params} | rest]}) when def in ~w(def defp)a,
do: {def, dm, [{fun, funm, put_matches_on_right(params)} | rest]}
# `Enum.reverse(foo) ++ bar` => `Enum.reverse(foo, bar)`
defp style({:++, _, [{{:., _, [{_, _, [:Enum]}, :reverse]} = reverse, r_meta, [lhs]}, rhs]}),
do: {reverse, r_meta, [lhs, rhs]}
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
# Credo.Check.Refactor.RedundantWithClauseResult
rewrite_body? = Enum.empty?(postroll) and Enum.empty?(elses) and nodes_equivalent?(lhs, do_body)
{_, do_body_meta, _} = do_body
{reversed_clauses, do_body} =
if rewrite_body?,
do: {rest, [rhs]},
else: {reversed_clauses, Enum.reverse(postroll, [do_body])}
do_else = [{do_block, {:__block__, do_body_meta, do_body}} | elses]
children = Enum.reverse(reversed_clauses, [do_else])
# only rewrite if it needs rewriting!
cond do
Enum.any?(preroll) ->
{:__block__, m, preroll ++ [{:with, m, children}]}
rewrite_body? or Enum.any?(postroll) ->
{:with, m, children}
true ->
with
end
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
# ARROW REWRITES
# `with`, `for` left arrow - if only we could write something this trivial for `->`!
defp style({:<-, cm, [lhs, rhs]}), do: {:<-, cm, [put_matches_on_right(lhs), rhs]}
# there's complexity to `:->` due to `cond` also utilizing the symbol but with different semantics.
# thus, we have to have a clause for each place that `:->` can show up
# `with` elses
defp style({{:__block__, _, [:else]} = else_, arrows}), do: {else_, rewrite_arrows(arrows)}
defp style({:case, cm, [head, [{do_, arrows}]]}), do: {:case, cm, [head, [{do_, rewrite_arrows(arrows)}]]}
defp style({:fn, m, arrows}), do: {:fn, m, rewrite_arrows(arrows)}
# IF / UNLESS REWRITES
# 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 rewrite_arrows(arrows) when is_list(arrows),
do: Enum.map(arrows, fn {:->, m, [lhs, rhs]} -> {:->, m, [put_matches_on_right(lhs), rhs]} end)
defp rewrite_arrows(macros_or_something_crazy_oh_no_abooort), do: macros_or_something_crazy_oh_no_abooort
defp put_matches_on_right(ast) do
ast
|> Zipper.zip()
|> Zipper.traverse(fn
# `_ = var ->` => `var ->`
{{:=, _, [{:_, _, nil}, var]}, _} = zipper -> Zipper.replace(zipper, var)
# `var = _ ->` => `var ->`
{{:=, _, [var, {:_, _, nil}]}, _} = zipper -> Zipper.replace(zipper, var)
# `var = *match*` -> `*match -> var`
{{:=, m, [{_, _, nil} = var, match]}, _} = zipper -> Zipper.replace(zipper, {:=, m, [match, var]})
zipper -> zipper
end)
|> Zipper.node()
end
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
defp delimit(token), do: token |> String.to_charlist() |> remove_underscores([]) |> add_underscores([])
defp remove_underscores([?_ | rest], acc), do: remove_underscores(rest, acc)
defp remove_underscores([digit | rest], acc), do: remove_underscores(rest, [digit | acc])
defp remove_underscores([], reversed_list), do: reversed_list
defp add_underscores([a, b, c, d | rest], acc), do: add_underscores([d | rest], [?_, c, b, a | acc])
defp add_underscores(reversed_list, acc), do: reversed_list |> Enum.reverse(acc) |> to_string()
end