Current section

Files

Jump to
credence lib pattern no_list_delete_at_in_loop.ex
Raw

lib/pattern/no_list_delete_at_in_loop.ex

defmodule Credence.Pattern.NoListDeleteAtInLoop do
@moduledoc """
Performance rule: Detects `List.delete_at/2` inside looping constructs
(`for`, `Enum.reduce`, `Enum.map`, `Enum.flat_map`) or recursive functions.
`List.delete_at/2` is O(n) because it must traverse the linked list to the
given index. Inside a loop this compounds to O(n²) per iteration.
## Bad
for {elem, idx} <- Enum.with_index(list) do
rest = List.delete_at(list, idx)
[elem | permutations(rest)]
end
## Good
# Use List.delete/2 or pass remaining elements via pattern matching
defp permutations([]), do: [[]]
defp permutations(list) do
for elem <- list do
rest = List.delete(list, elem)
for perm <- permutations(rest), do: [elem | perm]
end |> List.flatten()
end
"""
use Credence.Pattern.Rule
alias Credence.Issue
@enum_loops [:reduce, :map, :flat_map, :each, :filter]
@impl true
def check(ast, _opts) do
loop_issues = find_in_loops(ast)
recursive_issues = find_in_recursive(ast)
# Deduplicate by line — a List.delete_at inside a for inside a recursive
# function should only be reported once.
(loop_issues ++ recursive_issues)
|> Enum.uniq_by(fn issue -> issue.meta.line end)
end
# Pass 1: Find List.delete_at inside for/Enum loop constructs
defp find_in_loops(ast) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:for, _meta, _args} = node, issues ->
{node, find_delete_at(node, issues)}
{{:., _, [{:__aliases__, _, [:Enum]}, func]}, _meta, args} = node, issues
when func in @enum_loops and is_list(args) ->
{node, find_delete_at(node, issues)}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
# Pass 2: Find List.delete_at in recursive function bodies
defp find_in_recursive(ast) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{kind, _meta, [{:when, _, [{name, _, _params}, _guard]}, body_kw]} = node, issues
when kind in [:def, :defp] and is_atom(name) ->
body = extract_body(body_kw)
{node, check_recursive_body(body, name, issues)}
{kind, _meta, [{name, _, _params}, body_kw]} = node, issues
when kind in [:def, :defp] and is_atom(name) ->
body = extract_body(body_kw)
{node, check_recursive_body(body, name, issues)}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp check_recursive_body(nil, _name, acc), do: acc
defp check_recursive_body(body, name, acc) do
if body_calls_self?(body, name) do
find_delete_at(body, acc)
else
acc
end
end
defp body_calls_self?(body, name) do
{_ast, found} =
Macro.prewalk(body, false, fn
{^name, _, args} = n, _acc when is_list(args) -> {n, true}
n, a -> {n, a}
end)
found
end
defp find_delete_at(scope_ast, acc) do
{_ast, issues} =
Macro.prewalk(scope_ast, acc, fn
{{:., _, [{:__aliases__, _, [:List]}, :delete_at]}, meta, _} = node, issues ->
issue = %Issue{
rule: :no_list_delete_at_in_loop,
message:
"Avoid `List.delete_at/2` inside loops — it traverses the list to the index (O(n)), " <>
"creating O(n²) cost per iteration. Use pattern matching or `List.delete/2` instead.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
node, issues ->
{node, issues}
end)
issues
end
defp extract_body(body_kw) when is_list(body_kw), do: Keyword.get(body_kw, :do)
defp extract_body(body), do: body
end