Credence.Pattern.PreferFunctionClausesForListPatterns (credence v0.8.0)

Copy Markdown

Detects a redundant case inside a guarded function clause that dispatches on the same list parameter checked by is_list/1 in the guard, and promotes the case clauses to pattern-matching function heads.

Bad

def my_fun([], _k), do: 0
def my_fun(list, k) when is_list(list) and is_integer(k) and k >= 0 do
  case list do
    [] -> 0
    [_single] -> 0
    [h | t] ->
      # ... complex body
  end
end

Good

def my_fun([], _k), do: 0
def my_fun([_single], k) when is_integer(k) and k >= 0, do: 0
def my_fun([h | t], k) when is_integer(k) and k >= 0 do
  # ... complex body (without wrapping case)
end

Scope — what it flags

This rule is the guarded counterpart of no_case_on_param_dispatch (which only handles an unguarded, single bare-variable head). It fires only when ALL of these hold:

  • The clause is def/defp with a when guard containing is_list(var).
  • The function body is exactly case var do … end — no pre/post statements, no rescue/catch/after.
  • The case has at least 2 clauses, all matching list patterns, none using a ^ pin.
  • The case is total over lists: its guardless, fully-irrefutable list clauses cover every possible length (e.g. [] + [h | t], or [] + [_] + [a, b | _]).

Why these limits (safety)

Each limit closes a way the rewrite could change the answer:

  • is_list guard + case on that variable. is_list guarantees a list, so the case only discriminates list shape; promoting the patterns to heads is the same dispatch.
  • Totality over lists. A non-total case raises CaseClauseError on an unmatched list, but the equivalent function heads raise FunctionClauseError (or fall through to a sibling clause) — a different answer. We only fire when the list patterns are exhaustive, so neither construct ever fails to match a list.
  • Body is exactly the case / no rescue/catch/after. Any extra statement would be silently dropped.
  • No ^ pin. A pinned ^v matches the in-scope subject; as a function head pattern the variable would be unbound.

Clause guards and the non-is_list part of the head guard are preserved verbatim (guard semantics are identical in a case clause and a function head). The original parameter name is rebound with pattern = var whenever the body or a guard still refers to it, so nothing is left unbound.

Sibling clauses

Promoted heads are emitted in place. A promoted head is dropped only when an earlier, guardless sibling clause of the same function has a pattern that already subsumes it — in that case both the promoted head and the original case branch were already dead, so removing it changes nothing. Guarded siblings never trigger a drop (their guard may fail, leaving the branch live).