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 fixable?, do: true
# ── Check ─────────────────────────────────────────────────────────
@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
# ── Fix ───────────────────────────────────────────────────────────
@impl true
def fix(source, _opts) do
case Sourceror.parse_string(source) do
{:ok, ast} ->
if has_fixable?(ast) do
ast
|> Macro.postwalk(&maybe_rewrite/1)
|> Sourceror.to_string()
else
source
end
{:error, _} ->
source
end
end
# ── Detection ────────────────────────────────────────────────────
# 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.
# Handles both Code.string_to_quoted and Sourceror forms.
defp extract_do_clauses(kw) do
Enum.find_value(kw, fn
{:do, clauses} when is_list(clauses) -> clauses
{{:__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
# ── Pre-scan ─────────────────────────────────────────────────────
defp has_fixable?(ast) do
{_, found} =
Macro.prewalk(ast, false, fn
_node, true ->
{nil, true}
{:cond, _, _} = node, false ->
{node, two_clause_cond?(node)}
node, acc ->
{node, acc}
end)
found
end
# ── Fix: node rewriting ──────────────────────────────────────────
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, preserving the
# keyword format (bare or __block__-wrapped) from the original cond.
defp build_if_clauses(original_kw, do_body, else_body) do
case hd(original_kw) do
{{:__block__, do_meta, [:do]}, _} ->
[
{{:__block__, do_meta, [:do]}, do_body},
{{:__block__, do_meta, [:else]}, else_body}
]
{:do, _} ->
[do: do_body, else: else_body]
end
end
# ── Issue construction ───────────────────────────────────────────
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