Credence.Pattern.NoListDeleteAtWithLength (credence v0.7.1)

Copy Markdown

Detects List.delete_at(list, length(list) - 1) where length/1 is computed inline only to build the last index for List.delete_at/2, and rewrites it to the native negative index List.delete_at(list, -1).

Why this matters

length/1 is O(n) just to discover the last index, which List.delete_at/2 already supports natively through negative indexing. The manual computation is redundant and obscures intent:

# Bad — computes the index by hand
List.delete_at(tail, length(tail) - 1)

# Good — let delete_at count from the end
List.delete_at(tail, -1)

Detection scope

Matches only List.delete_at(x, length(x) - 1) where x is the same variable in both positions. The offset must be exactly 1.

Larger offsets are deliberately not flagged: List.delete_at(x, length(x) - K) removes the single element K positions from the end, but List.delete_at(x, -K) is not equivalent — they diverge when the list is shorter than K. For [1] and K = 2, List.delete_at([1], length([1]) - 2) is List.delete_at([1], -1) == [], whereas List.delete_at([1], -2) == [1] (the index is out of range, so nothing is removed). Only the - 1 case has the same answer for every list length, so only it is rewritten. The length stored in a separate variable is also out of scope (that case may be handled by no_length_based_indexing).

The rewrite stays a List operation rather than Enum.drop/2 or Enum.split/2 on purpose: length/1 raises ArgumentError on non-lists (Range, Map, MapSet, ...), and List.delete_at/2 raises the same ArgumentError on those inputs, so the rewrite preserves the raise-on-non-list contract exactly. Enum.drop(x, -1) would silently succeed on those inputs and change the answer.

Bad

List.delete_at(tail, length(tail) - 1)

Good

List.delete_at(tail, -1)