Credence.Pattern.NoListDeleteAtWithLength
(credence v0.7.0)
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)