Credence.Pattern.NoHdTlWhenConsBound (credence v0.7.1)

Copy Markdown

Detects hd(var) / tl(var) calls in a function body where var is already bound to a non-empty list by an anonymous cons pattern (var = [_ | _] or [_ | _] = var) in the function clause head, and rewrites them to destructure [head | tail] in the head, using head / tail directly instead of Kernel.hd/1 / Kernel.tl/1.

Because the head pattern already guarantees var matched [_ | _] (a non-empty, possibly improper list), hd(var) is exactly the head and tl(var) is exactly the tail of a [head | tail] = var destructure — for every admitted value, including improper lists like [1 | 2]. The rewrite is therefore answer-preserving.

Narrowing — the safe core

The fix only fires when it is mechanically guaranteed safe:

  • Anonymous cons only. Only [_ | _] (both sides the bare _) is handled. A named cons ([h | t], [_head | _tail]) already binds the parts and is left alone — promoting those would mean reusing or renaming existing (possibly underscore-suppressed) bindings.

  • No rebinding in the body. The body must contain no binding/scoping form (=, fn, for, with, case, cond, receive, try, quote). This rules out any inner scope that could shadow var, so a matched hd(var) / tl(var) unambiguously refers to the head binding.
  • Keep the variable when it is still needed. If var is referenced anywhere outside the rewritten hd / tl calls (a guard, another parameter, or elsewhere in the body), the var = ... binding is kept (var = [head | tail]); otherwise the whole binding collapses to the cons pattern ([head | tail]).

Bad

def first(list = [_ | _]), do: hd(list)
def split(list = [_ | _]), do: {hd(list), tl(list)}

Good

def first([head | _]), do: head
def split([head | tail]), do: {head, tail}