Current section
Files
Jump to
Current section
Files
lib/pattern/no_list_append_in_loop.ex
defmodule Credence.Pattern.NoListAppendInLoop do
@moduledoc """
Performance rule: Detects the use of `++` inside looping constructs
that cannot be auto-fixed.
Appending to a list with `++` is O(n) because it must copy the entire
left-hand list. Inside a loop or recursion this compounds to O(n²).
Prefer prepending with `[item | acc]` and calling `Enum.reverse/1`
after the loop completes.
Note: simple `acc ++ [expr]` patterns inside `Enum.reduce` with `[]`
initial are handled by `ListAppendInReduce`, and direct `acc ++ [expr]`
in recursive tail calls (with a matching base case) are handled by
`ListAppendInRecursion`. This rule covers the remaining unfixable cases:
`for` comprehensions, indirect appends in recursion, and complex reduce
patterns.
## Bad — inside a for comprehension
for item <- list do
acc = []
acc ++ [item]
end
## Bad — indirect append in recursion (assigned to variable first)
defp slide([next | rest], window, current, max) do
new_window = window ++ [next]
slide(rest, new_window, current, max)
end
## Good
Enum.reduce(list, [], fn item, acc ->
[item * 2 | acc]
end)
|> Enum.reverse()
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Enum.reduce/3 — skip if fixable by ListAppendInReduce
{{:., _, [{:__aliases__, _, [:Enum]}, :reduce]}, _, [_enumerable, acc_init, fun]} = node,
issues ->
if acc_init == [] and fixable_reduce_lambda?(fun) do
{node, issues}
else
{node, find_append(fun, issues)}
end
# for comprehensions
{:for, _, args} = node, issues when is_list(args) ->
do_block = Keyword.get(List.last(args) || [], :do)
{node, find_append(do_block, issues)}
# def/defp with guard — skip if fixable by ListAppendInRecursion
{kind, _, [{:when, _, [{name, _, params}, _guard]}, body_kw]} = node, issues
when kind in [:def, :defp] and is_atom(name) and is_list(params) ->
body = extract_body(body_kw)
{node, check_recursive(body, name, params, issues)}
# def/defp without guard
{kind, _, [{name, _, params}, body_kw]} = node, issues
when kind in [:def, :defp] and is_atom(name) and is_list(params) ->
body = extract_body(body_kw)
{node, check_recursive(body, name, params, issues)}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
# Only flag ++ in recursive functions when the pattern is NOT the simple
# fixable one (acc ++ [expr] directly in the recursive call args).
defp check_recursive(body, name, params, issues) do
if body_calls_self?(body, name) do
if direct_append_in_recursive_call?(body, name, params) do
# Fixable by ListAppendInRecursion — skip
issues
else
find_append(body, issues)
end
else
issues
end
end
# Checks if the last expression is name(..., acc ++ [expr], ...)
# where acc matches a parameter.
defp direct_append_in_recursive_call?(body, name, params) do
last = last_expression(body)
case last do
{^name, _, args} when is_list(args) ->
Enum.any?(args, fn
{:++, _, [lhs, [single]]} ->
not cons_cell?(single) and Enum.any?(params, &same_var?(&1, lhs))
_ ->
false
end)
_ ->
false
end
end
# --- Reduce fixability check (same as ListAppendInReduce) ---
defp fixable_reduce_lambda?({:fn, _, [{:->, _, [params, body]}]}) when length(params) == 2 do
acc_var = List.last(params)
if simple_var?(acc_var) do
last = last_expression(body)
case last do
{:++, _, [lhs, [single]]} ->
same_var?(lhs, acc_var) and not cons_cell?(single)
_ ->
false
end
else
false
end
end
defp fixable_reduce_lambda?(_), do: false
# --- Shared helpers ---
defp find_append(ast, acc) do
{_ast, issues} =
Macro.prewalk(ast, acc, fn
{:++, meta, _args} = node, issues ->
{node, [build_issue(meta) | issues]}
node, issues ->
{node, issues}
end)
issues
end
defp body_calls_self?(nil, _name), do: false
defp body_calls_self?(body, name) do
{_ast, found} =
Macro.prewalk(body, false, fn
{^name, _, args} = node, _acc when is_list(args) -> {node, true}
node, acc -> {node, acc}
end)
found
end
defp extract_body(body_kw) when is_list(body_kw), do: Keyword.get(body_kw, :do)
defp extract_body(body), do: body
defp last_expression({:__block__, _, exprs}) when is_list(exprs), do: List.last(exprs)
defp last_expression(expr), do: expr
defp simple_var?({name, _, ctx}) when is_atom(name) and (is_nil(ctx) or is_atom(ctx)), do: true
defp simple_var?(_), do: false
defp same_var?({name, _, _}, {name, _, _}) when is_atom(name), do: true
defp same_var?(_, _), do: false
defp cons_cell?({:|, _, _}), do: true
defp cons_cell?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :no_list_append_in_loop,
message:
"Avoid using '++' inside loops or recursive functions. Prefer prepending with '[item | acc]' and calling 'Enum.reverse/1' outside the loop.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end