Current section

Files

Jump to
credence lib pattern no_list_last.ex
Raw

lib/pattern/no_list_last.ex

defmodule Credence.Pattern.NoListLast do
@moduledoc """
Performance rule: Flags usage of `List.last/1`.
Elixir lists are linked lists — accessing the last element requires
traversing the entire list, making `List.last/1` an O(n) operation.
This is often a sign that the algorithm should be restructured to
avoid needing the last element entirely.
## Common refactors
- If building a list with `Enum.reduce`, track the last value in the
accumulator instead of extracting it afterward
- If splitting a list, destructure the right half instead of taking
the last of the left half
- If the list was sorted or built in a known order, consider whether
`hd/1` on a reversed or desc-sorted list gives you the answer
## Bad
Enum.reduce(1..(rows - 1), initial_row, fn _, prev ->
Enum.scan(prev, &(&1 + &2))
end)
|> List.last()
## Good — track the answer in the accumulator
{_row, last} =
Enum.reduce(1..(rows - 1), {initial_row, 1}, fn _, {prev, _} ->
row = Enum.scan(prev, &(&1 + &2))
{row, List.last(row)} # or track running total differently
end)
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{{:., _, [{:__aliases__, _, [:List]}, :last]}, meta, _args} = node, issues ->
issue = %Issue{
rule: :no_list_last,
message:
"`List.last/1` traverses the entire list (O(n)). " <>
"Restructure to avoid needing the last element: " <>
"track the value in an accumulator, destructure from the other end, " <>
"or reverse before taking the head. " <>
"Do NOT reimplement List.last manually — that has the same O(n) cost.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
end