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