Current section
Files
Jump to
Current section
Files
lib/rule/no_enum_slice_in_loop.ex
defmodule Credence.Rule.NoEnumSliceInLoop do
@moduledoc """
Performance rule: Flags usage of `Enum.slice/3` in iterative and recursive list-processing patterns.
Elixir lists are linked structures, so `Enum.slice/3` is O(n). When used repeatedly
inside loops, comprehensions, or recursion, this produces O(n²) behavior.
This is a common pitfall in:
- n-gram generation
- sliding window extraction
- manual recursive traversal of lists
Prefer `Enum.chunk_every/4` for sliding windows.
## Bad
for i <- 0..max_index do
Enum.slice(list, i, n)
end
def loop(i, list) do
Enum.slice(list, i, n)
loop(i + 1, list)
end
## Good
list
|> Enum.chunk_every(n, 1, :discard)
def loop(list) do
list
|> Enum.chunk_every(n, 1, :discard)
end
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# ------------------------------------------------------------
# Detect Enum.slice/3 anywhere (core performance issue)
# ------------------------------------------------------------
{{:., _, [{:__aliases__, _, [:Enum]}, :slice]}, meta, _args} = node, issues ->
issue = %Issue{
rule: :no_enum_slice_in_loop,
severity: :warning,
message:
"`Enum.slice/3` is O(n) on lists. Repeated use in loops or recursion leads to O(n²) behavior. " <>
"Use `Enum.chunk_every(list, size, 1, :discard)` for sliding windows instead.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
# ------------------------------------------------------------
# Detect recursive functions that likely re-slice lists
# (heuristic: function calls itself AND contains Enum.slice/3)
# ------------------------------------------------------------
{:def, _, [{name, _, _args}, [do: body]]} = node, issues when is_atom(name) ->
slice_used? =
Macro.prewalk(body, false, fn
{{:., _, [{:__aliases__, _, [:Enum]}, :slice]}, _, _} = slice_node, _ ->
{slice_node, true}
other, acc ->
{other, acc}
end)
|> elem(1)
recursive_call? =
Macro.prewalk(body, false, fn
{^name, _, _} = call, _ ->
{call, true}
other, acc ->
{other, acc}
end)
|> elem(1)
issues =
if slice_used? and recursive_call? do
issue = %Issue{
rule: :no_enum_slice_in_loop,
severity: :warning,
message:
"Recursive function uses `Enum.slice/3` while calling itself. " <>
"This commonly creates O(n²) behavior in manual sliding-window implementations. " <>
"Refactor using `Enum.chunk_every/4` or accumulator-based traversal.",
meta: %{function: name}
}
[issue | issues]
else
issues
end
{node, issues}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
end