Current section

Files

Jump to
credence lib pattern no_list_fold.ex
Raw

lib/pattern/no_list_fold.ex

defmodule Credence.Pattern.NoListFold do
@moduledoc """
Detects usage of `List.foldl/3` and `List.foldr/3` and suggests
`Enum.reduce/3` instead.
## Why this matters
`Enum.reduce/3` is the idiomatic Elixir way to fold over a collection.
While `List.foldl/3` and `List.foldr/3` exist in Elixir's standard
library, they are rarely used in practice and signal code ported from
Erlang (`:lists.foldl`) or Haskell.
LLMs frequently generate `List.foldl` because their training data
includes heavy Erlang and Haskell influence. The resulting code is
functionally correct but non-idiomatic, which reduces readability for
Elixir developers.
## Flagged patterns
| Pattern | Suggested replacement |
| -------------------- | ----------------------- |
| `List.foldl/3` | `Enum.reduce/3` |
| `List.foldr/3` | `Enum.reduce/3` (with `Enum.reverse/1`) |
"""
use Credence.Pattern.Rule
alias Credence.Issue
@flagged_fns [:foldl, :foldr]
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
case extract_list_fold(node) do
{:ok, fn_name, meta} ->
issue = %Issue{
rule: :no_list_fold,
message: build_message(fn_name),
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
:error ->
{node, issues}
end
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
Credence.RuleHelpers.patches_from_postwalk(ast, fn
# Piped: source |> List.foldl(acc, fun) / List.foldr(acc, fun)
{:|>, pipe_meta, [source, {{:., _, [mod, fn_name]}, call_meta, args}]} = node
when fn_name in @flagged_fns and is_list(args) ->
if list_module?(mod) do
fix_piped_fold(fn_name, source, call_meta, args, pipe_meta)
else
node
end
# Direct 3-arg: List.foldl(list, acc, fun) / List.foldr(list, acc, fun)
{{:., _, [mod, fn_name]}, meta, [_list, _acc, _fun] = args} = node
when fn_name in @flagged_fns ->
if list_module?(mod) do
fix_direct_fold(fn_name, args, meta)
else
node
end
node ->
node
end)
end
# foldl piped: source |> List.foldl(acc, fun) → source |> Enum.reduce(acc, fun)
defp fix_piped_fold(:foldl, source, _call_meta, args, pipe_meta) do
{:|>, pipe_meta, [source, enum_reduce_call(args)]}
end
# foldr piped: source |> List.foldr(acc, fun) → source |> Enum.reverse() |> Enum.reduce(acc, fun)
defp fix_piped_fold(:foldr, source, _call_meta, args, pipe_meta) do
reversed = {:|>, [], [source, enum_reverse_call()]}
{:|>, pipe_meta, [reversed, enum_reduce_call(args)]}
end
# foldl direct: List.foldl(list, acc, fun) → Enum.reduce(list, acc, fun)
defp fix_direct_fold(:foldl, args, _meta) do
enum_reduce_call(args)
end
# foldr direct: List.foldr(list, acc, fun) → Enum.reduce(Enum.reverse(list), acc, fun)
defp fix_direct_fold(:foldr, [list | rest_args], _meta) do
reversed_list = {{:., [], [{:__aliases__, [], [:Enum]}, :reverse]}, [], [list]}
enum_reduce_call([reversed_list | rest_args])
end
defp enum_reduce_call(args) do
{{:., [], [{:__aliases__, [], [:Enum]}, :reduce]}, [], args}
end
defp enum_reverse_call do
{{:., [], [{:__aliases__, [], [:Enum]}, :reverse]}, [], []}
end
defp extract_list_fold({{:., meta, [mod, fn_name]}, _, args})
when fn_name in @flagged_fns and is_list(args) do
if list_module?(mod), do: {:ok, fn_name, meta}, else: :error
end
defp extract_list_fold(_), do: :error
defp list_module?({:__aliases__, _, [:List]}), do: true
defp list_module?(_), do: false
defp build_message(:foldl) do
"""
`List.foldl/3` is not idiomatic Elixir.
Use `Enum.reduce/3` instead — it is the standard way to
fold over a collection:
Enum.reduce(list, acc, fn elem, acc -> ... end)
"""
end
defp build_message(:foldr) do
"""
`List.foldr/3` is not idiomatic Elixir.
Use `Enum.reduce/3` instead. If right-fold order is needed,
reverse the list first:
list |> Enum.reverse() |> Enum.reduce(acc, fun)
"""
end
end