Current section

Files

Jump to
credence lib pattern no_list_foldl.ex
Raw

lib/pattern/no_list_foldl.ex

defmodule Credence.Pattern.NoListFoldl do
@moduledoc """
Detects `List.foldl/3` on a provably-list argument and suggests `Enum.reduce/3`.
`List.foldl(list, acc, fun)` and `Enum.reduce(list, acc, fun)` fold a **list**
identically — same left-to-right order, same `fun.(elem, acc)` argument order.
`Enum.reduce/3` is the idiomatic Elixir fold; `List.foldl/3` usually signals
code ported from Erlang.
## Safe core — provably-list argument only
The two are **not** interchangeable on a non-list enumerable: `List.foldl`
raises `FunctionClauseError` on a map/range/stream, whereas `Enum.reduce`
succeeds — a value-vs-crash divergence. So the rule fires only when the folded
argument is **provably a list**: a list literal, a `++`, or a (possibly piped)
call to a list-returning function (`Enum.map/filter/…`, `Map.keys`,
`String.split`, `List.*`, …). A bare variable is left alone — we can't prove
it is a list, and an expression rewrite can't add an `is_list` guard.
`List.foldr/3` is intentionally **not** covered: it is a *right* fold, needing
`Enum.reduce(Enum.reverse(list), acc, fun)` — more verbose than `List.foldr`.
## Bad
List.foldl([1, 2, 3], 0, fun)
nums |> Enum.filter(&pos?/1) |> List.foldl(0, fun)
## Good
Enum.reduce([1, 2, 3], 0, fun)
nums |> Enum.filter(&pos?/1) |> Enum.reduce(0, fun)
"""
use Credence.Pattern.Rule
alias Credence.Issue
# Functions whose result is always a list (mirrors no_enum_count_for_length).
@list_returning %{
Enum: ~w(map filter reject to_list sort sort_by uniq uniq_by reverse take drop
take_while drop_while flat_map with_index dedup dedup_by concat
intersperse chunk_every chunk_by slice map_every scan)a,
String: ~w(graphemes codepoints split to_charlist)a,
Map: ~w(keys values to_list)a,
Keyword: ~w(keys values to_list delete drop take merge put put_new)a,
List: ~w(wrap flatten delete delete_at insert_at replace_at update_at duplicate)a,
Tuple: ~w(to_list)a
}
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
case check_node(node) do
{:ok, issue} -> {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) → source |> Enum.reduce(acc, fun)
{:|>, pipe_meta, [source, {{:., _, [mod, :foldl]}, _, [_acc, _fun] = args}]} = node ->
if list_module?(mod) and provably_list?(source) do
{:|>, pipe_meta, [source, enum_reduce_call(args)]}
else
node
end
# Direct: List.foldl(list, acc, fun) → Enum.reduce(list, acc, fun)
{{:., _, [mod, :foldl]}, _meta, [list, _acc, _fun] = args} = node ->
if list_module?(mod) and provably_list?(list), do: enum_reduce_call(args), else: node
node ->
node
end)
end
# Piped: source |> List.foldl(acc, fun) — the folded value is the pipe source.
defp check_node({:|>, _, [source, {{:., meta, [mod, :foldl]}, _, [_acc, _fun]}]}) do
if list_module?(mod) and provably_list?(source), do: {:ok, build_issue(meta)}, else: :error
end
# Direct: List.foldl(list, acc, fun).
defp check_node({{:., meta, [mod, :foldl]}, _, [list, _acc, _fun]}) do
if list_module?(mod) and provably_list?(list), do: {:ok, build_issue(meta)}, else: :error
end
defp check_node(_), do: :error
defp enum_reduce_call(args) do
{{:., [], [{:__aliases__, [], [:Enum]}, :reduce]}, [], args}
end
defp list_module?({:__aliases__, _, [:List]}), do: true
defp list_module?(_), do: false
# The argument is known to be a list when it is a list literal, a `++`, a
# list-returning call, or a pipe whose last step returns a list.
defp provably_list?(list) when is_list(list), do: true
# Sourceror wraps a list literal as {:__block__, meta, [[...]]}.
defp provably_list?({:__block__, _, [inner]}), do: provably_list?(inner)
defp provably_list?({:++, _, [_, _]}), do: true
defp provably_list?({:|>, _, [_lhs, step]}), do: list_returning_call?(step)
defp provably_list?({{:., _, [{:__aliases__, _, [_mod]}, _fun]}, _, _args} = call),
do: list_returning_call?(call)
defp provably_list?(_), do: false
defp list_returning_call?({{:., _, [{:__aliases__, _, [mod]}, fun]}, _, _args}) do
case Map.fetch(@list_returning, mod) do
{:ok, funs} -> fun in funs
:error -> false
end
end
defp list_returning_call?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :no_list_foldl,
message:
"`List.foldl/3` on a list is non-idiomatic; use `Enum.reduce/3` instead. " <>
"(Only flagged when the argument is provably a list — the two differ on non-list enumerables.)",
meta: %{line: Keyword.get(meta, :line)}
}
end
end