Current section
Files
Jump to
Current section
Files
lib/rule/no_guard_equality_for_pattern_match.ex
defmodule Credence.Rule.NoGuardEqualityForPatternMatch do
@moduledoc """
Readability rule: Detects guard clauses that compare a parameter to a
literal value with `==` when pattern matching in the function head would
be clearer and more idiomatic.
This only flags simple `var == literal` comparisons where `var` is one of
the function's parameters and `literal` is an integer, atom, or string.
## Bad
defp do_count(n, _a, b) when n == 2, do: b
def process(action) when action == :stop, do: :halted
## Good
defp do_count(2, _a, b), do: b
def process(:stop), do: :halted
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{kind, _meta, [{:when, _, [call, guard]} | _rest]} = node, issues
when kind in [:def, :defp] ->
{_name, _, params} = call
param_names = extract_param_names(params)
new_issues = find_guard_equalities(guard, param_names, issues)
{node, new_issues}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
# Extract simple variable names from the parameter list.
# Handles plain vars like `n`, ignores patterns like `[h | t]` and `{a, b}`.
defp extract_param_names(params) when is_list(params) do
for {name, _, context} <- params, is_atom(name), is_atom(context), do: name
end
defp extract_param_names(_), do: []
# Recursively flatten `and`/`or` guards and check each part.
defp find_guard_equalities(guard, param_names, acc) do
parts = flatten_guard(guard)
Enum.reduce(parts, acc, fn
# var == literal
{:==, meta, [{var_name, _, nil}, literal]}, acc
when is_atom(var_name) and (is_integer(literal) or is_atom(literal) or is_binary(literal)) ->
if var_name in param_names do
[build_issue(var_name, literal, meta) | acc]
else
acc
end
# literal == var (reversed)
{:==, meta, [literal, {var_name, _, nil}]}, acc
when is_atom(var_name) and (is_integer(literal) or is_atom(literal) or is_binary(literal)) ->
if var_name in param_names do
[build_issue(var_name, literal, meta) | acc]
else
acc
end
_, acc ->
acc
end)
end
defp flatten_guard({:and, _, [left, right]}), do: flatten_guard(left) ++ flatten_guard(right)
defp flatten_guard({:or, _, [left, right]}), do: flatten_guard(left) ++ flatten_guard(right)
defp flatten_guard(other), do: [other]
defp build_issue(var_name, literal, meta) do
%Issue{
rule: :no_guard_equality_for_pattern_match,
severity: :info,
message:
"Guard `#{var_name} == #{inspect(literal)}` can be replaced by " <>
"pattern matching `#{inspect(literal)}` directly in the function head.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end