Current section
Files
Jump to
Current section
Files
lib/pattern/no_enum_count_for_length.ex
defmodule Credence.Pattern.NoEnumCountForLength do
@moduledoc """
Detects `Enum.count/1` (without a predicate) and suggests `length/1`
or a more specific size function.
## Why this matters
`Enum.count/1` goes through the `Enumerable` protocol, adding dispatch
overhead. When the argument is a list — which it almost always is in
LLM-generated code — `length/1` is a BIF that does the same traversal
without protocol dispatch:
# Flagged — protocol dispatch overhead
total = Enum.count(chars)
# Idiomatic — BIF, no protocol overhead
total = length(chars)
For other collection types, more specific functions exist:
`map_size/1` for maps, `MapSet.size/1` for sets, `tuple_size/1`
for tuples.
## Flagged patterns
Only the **single-argument** form `Enum.count(x)` is flagged.
The two-argument form `Enum.count(x, predicate)` is not flagged
because it filters and counts in one pass — there is no simpler
replacement.
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def fixable?, do: true
@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(source, _opts) do
source
|> Sourceror.parse_string!()
|> Macro.postwalk(fn
# Direct: Enum.count(expr) → length(expr)
# Must not match the predicate-only piped form
{{:., _, [{:__aliases__, _, [:Enum]}, :count]}, _, [arg]} = node ->
if predicate?(arg) do
node
else
{:length, [], [arg]}
end
# Pipeline: ... |> Enum.count() → ... |> length()
{{:., _, [{:__aliases__, _, [:Enum]}, :count]}, _, []} ->
{:length, [], []}
node ->
node
end)
|> Sourceror.to_string()
end
defp check_node({{:., meta, [mod, :count]}, _, [arg]}) do
if enum_module?(mod) and not predicate?(arg) do
{:ok, build_issue(meta)}
else
:error
end
end
defp check_node({{:., meta, [mod, :count]}, _, []}) do
if enum_module?(mod) do
{:ok, build_issue(meta)}
else
:error
end
end
defp check_node(_), do: :error
defp enum_module?({:__aliases__, _, [:Enum]}), do: true
defp enum_module?(_), do: false
defp predicate?({:&, _, _}), do: true
defp predicate?({:fn, _, _}), do: true
defp predicate?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :no_enum_count_for_length,
message: """
`Enum.count/1` used without a predicate.
If the argument is a list, use `length/1` — it's a BIF with \
no protocol dispatch overhead.
For other collections, prefer the specific size function:
`map_size/1` for maps, `MapSet.size/1` for sets, `tuple_size/1` \
for tuples.
""",
meta: %{line: Keyword.get(meta, :line)}
}
end
end