Credence.Pattern.NoCaseOnParamDispatch (credence v0.7.0)

Copy Markdown

Detects a single-parameter function clause whose entire body is a case that dispatches on that parameter, and rewrites it into multi-clause function heads — the idiomatic Elixir way to express this.

Bad

def pick_coins(coins) do
  case coins do
    [] -> 0
    [first] -> first
    [first, second] -> max(first, second)
    _ -> do_pick_coins(coins, 0, 0)
  end
end

Good

def pick_coins([]), do: 0
def pick_coins([first]), do: first
def pick_coins([first, second]), do: max(first, second)
def pick_coins(_ = coins), do: do_pick_coins(coins, 0, 0)

Scope — what it flags

This rule is deliberately narrowed to a single-parameter core where the rewrite is provably behaviour-preserving for every input. It fires only when:

  • The clause is a plain def/defp head (no when guard on the head) with exactly one parameter that is a bare variable v.
  • The clause body is only a do: body (no rescue/catch/after) and is a single case v do … end whose subject is exactly that parameter.
  • The case has at least 2 clauses, none using a ^ pin in its pattern.
  • At least one clause is an unguarded catch-all (a bare variable or _), so the case is total over the parameter.

Why these limits (safety)

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

  • Single bare-variable parameter / subject is that parameter. A bare variable is side-effect-free, so there is no double-evaluation when its pattern matching moves into the function head. Multi-parameter / tuple dispatch (case {x, y}) is not flagged — reconstructing reordered or partial parameter lists is a separate, riskier transformation.
  • Totality (an unguarded catch-all). A non-total case raises CaseClauseError, but the equivalent function heads raise FunctionClauseError — a different exception. We only fire when both constructs are total over the parameter, so neither ever raises.
  • No ^ pin. case v do ^v -> … end matches against the in-scope subject; as a function head def f(^v) the pinned variable is unbound.
  • No rescue/catch/after, body is exactly the case. Any extra statement or clause would be silently dropped by the rewrite.

Guards on individual case clauses are preserved verbatim as head guards — guard semantics (including error-swallowing) are identical in a case clause and a function head, so this is safe. Sibling clauses of the same function need no special handling: the flagged clause's bare-variable head is itself a function-level catch-all, so later siblings are already unreachable and earlier siblings shadow identically before and after the rewrite.

Reconstructing the head

Each case clause pattern -> body becomes def name(head) do body end, where head keeps the parameter bound when the body or a clause guard still refers to it: if pattern already binds the parameter name it is used as-is; otherwise, when the body/guard mention the parameter, the head becomes pattern = v so the original name stays in scope.