Current section

Files

Jump to
credence lib rule no_list_fold.ex
Raw

lib/rule/no_list_fold.ex

defmodule Credence.Rule.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 note about reversal) |
## Severity
`:warning`
"""
@behaviour Credence.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,
severity: :warning,
message: build_message(fn_name),
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
:error ->
{node, issues}
end
end)
Enum.reverse(issues)
end
# ------------------------------------------------------------
# DETECTION
#
# Matches fully-qualified calls: List.foldl(...) / List.foldr(...)
# in the AST form produced by Code.string_to_quoted/1.
# ------------------------------------------------------------
# Dot-call form: List.foldl(list, acc, fun)
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
# ------------------------------------------------------------
# MESSAGE GENERATION
# ------------------------------------------------------------
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
# ------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------
defp list_module?({:__aliases__, _, [:List]}), do: true
defp list_module?(_), do: false
end