Current section

Files

Jump to
credence lib pattern no_cond_two_clauses.ex
Raw

lib/pattern/no_cond_two_clauses.ex

defmodule Credence.Pattern.NoCondTwoClauses do
@moduledoc """
Detects `cond` with exactly two clauses where the second guard is
`true` — a pattern that is just an `if/else` in disguise.
## Bad
cond do
low > high -> false
true ->
mid = div(low + high, 2)
search(mid, target)
end
## Good
if low > high do
false
else
mid = div(low + high, 2)
search(mid, target)
end
## Auto-fix
Rewrites as `if/else` using the first clause's guard as the
condition. The condition is never modified.
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:cond, meta, _} = node, acc ->
if two_clause_cond?(node) do
{node, [build_issue(meta) | acc]}
else
{node, acc}
end
node, acc ->
{node, acc}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
Credence.RuleHelpers.patches_from_postwalk(ast, &maybe_rewrite/1)
end
# Checks if a cond node has exactly 2 clauses with `true` as
# the second guard.
defp two_clause_cond?({:cond, _, [kw]}) when is_list(kw) do
case extract_do_clauses(kw) do
[_first, second] -> guard_is_true?(second)
_ -> false
end
end
defp two_clause_cond?(_), do: false
# Extracts the list of arrow clauses from the cond's keyword args.
defp extract_do_clauses(kw) do
Enum.find_value(kw, fn
{{:__block__, _, [:do]}, clauses} when is_list(clauses) -> clauses
_ -> nil
end)
end
# Checks if an arrow clause has `true` as its guard.
defp guard_is_true?({:->, _, [[guard], _body]}) do
match_true?(guard)
end
defp guard_is_true?(_), do: false
defp match_true?(true), do: true
defp match_true?({:__block__, _, [true]}), do: true
defp match_true?(_), do: false
defp maybe_rewrite({:cond, meta, [kw]} = node) when is_list(kw) do
case extract_do_clauses(kw) do
[first, second] ->
if guard_is_true?(second) do
rewrite_to_if(meta, first, second, kw)
else
node
end
_ ->
node
end
end
defp maybe_rewrite(node), do: node
# Builds an if/else node from the two cond clauses.
defp rewrite_to_if(meta, first_clause, second_clause, original_kw) do
{:->, _, [[condition], do_body]} = first_clause
{:->, _, [[_true], else_body]} = second_clause
if_clauses = build_if_clauses(original_kw, do_body, else_body)
{:if, meta, [condition, if_clauses]}
end
# Builds the keyword list for the if node, reusing the original cond's
# `:do` meta so the rendered output sits on the same source line.
defp build_if_clauses(original_kw, do_body, else_body) do
{{:__block__, do_meta, [:do]}, _} = hd(original_kw)
[
{{:__block__, do_meta, [:do]}, do_body},
{{:__block__, do_meta, [:else]}, else_body}
]
end
defp build_issue(meta) do
%Issue{
rule: :no_cond_two_clauses,
message:
"`cond` with two clauses where the second guard is `true` " <>
"is an `if/else` in disguise. Use `if/else` instead.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end