Credence.Pattern.NoManualCountWithPredicate
(credence v0.7.1)
Copy Markdown
Detects hand-rolled recursive counting functions that should use Enum.count/2.
Why this matters
When a function counts list elements matching a predicate using manual
tail-recursion with an accumulator, it is reimplementing Enum.count/2:
# Flagged — 3-clause guard pattern
defp do_count([], _target, acc), do: acc
defp do_count([h | t], target, acc) when h == target,
do: do_count(t, target, acc + 1)
defp do_count([_h | t], target, acc),
do: do_count(t, target, acc)
# Flagged — 2-clause if pattern
defp do_count([], acc), do: acc
defp do_count([h | t], acc) do
new_acc = if h > 0, do: acc + 1, else: acc
do_count(t, new_acc)
endThe fix
Each flagged group is collapsed to a single clause that delegates to
Enum.count/2, threading the accumulator through unchanged:
defp do_count(list, target, acc) when is_list(list),
do: acc + Enum.count(list, fn h -> h == target end)This is a behaviour-preserving rewrite for every input:
- The collapsed clause keeps the original name/arity, so callers (which
seed the accumulator, e.g.
do_count(list, target, 0)) are unchanged. - The
when is_list(list)guard preserves the original domain: the multi-clause original only ever matched lists (and raisedFunctionClauseErrorotherwise), and so does the collapsed clause. acc + Enum.count(list, pred)reproducesinitial_acc + matches.
Detection scope (only the safely-fixable cases)
3-clause guard pattern — exactly 3 clauses, all arity 3, same name, where:
([], _bound, acc)returnsacc,([h | t], bound, acc) when <guard>recursesfn(t, bound, acc + 1),([_h | t], bound, acc)recursesfn(t, bound, acc),
and the <guard> is a non-raising boolean guard (comparisons,
and/or/not, unary is_* type checks over variables/literals). A
guard that can raise (e.g. rem(h, 2) == 0) is not flagged: a guard
silently skips an element whose guard raises, but the same expression
used as an Enum.count/2 predicate would raise — a different answer.
The bound argument must be passed through the recursion unchanged.
2-clause if pattern — exactly 2 clauses, same name, where:
- one clause matches
[](in some position) and returns the trailing accumulator, the other matches
[h | t]in the same position, bindsnew_acc = if(<cond>, do: acc + 1, else: acc)and recurses with the list tail andnew_acc, passing every other argument through unchanged.
The <cond> is an ordinary expression (not a guard), so it carries no
error-swallowing semantics — moving it into Enum.count/2 preserves its
truthiness, side effects, and any exception it raises. No narrowing of
<cond> is needed.