Credence.Pattern.NoManualListLast (credence v0.7.0)

Copy Markdown

Detects hand-rolled reimplementations of List.last/1.

Why this matters

When NoListLast flags List.last/1, LLMs "fix" it by writing the exact same O(n) traversal under a different name:

# Flagged — this IS List.last, just hand-rolled
defp get_last_element([val]), do: val
defp get_last_element([_ | rest]), do: get_last_element(rest)

This has the same performance characteristics as List.last/1 but adds unnecessary code. The real fix is to restructure the algorithm to avoid needing the last element:

  • Track the value in an accumulator during a reduce
  • Reverse the list and take the head
  • Destructure from the other end

Detection scope

A two-clause defp (or def) function with arity 1 where:

  1. One clause matches [val] (single-element list) and returns val
  2. The other clause matches [_ | rest] and recurses with rest

Auto-fix

Replaces the hand-rolled function with hd(Enum.reverse(list)) and rewrites call sites within the same source file.

We deliberately avoid List.last/1: the hand-rolled form has no [] clause, so it raises on the empty list, whereas List.last([]) returns nil — a behaviour change. hd(Enum.reverse([])) raises (ArgumentError) like the original, so the fix is behaviour-preserving (the only difference is the raised error's type on the degenerate empty-list input).