Current section

Files

Jump to
credence lib rule no_enum_count_for_length.ex
Raw

lib/rule/no_enum_count_for_length.ex

defmodule Credence.Rule.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)
LLMs reach for `Enum.count` reflexively because they're in "Enum
pipeline" mode after using `Enum.map`, `Enum.reduce`, etc.
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.
## Severity
`:info`
"""
@behaviour Credence.Rule
alias Credence.Issue
@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
# ------------------------------------------------------------
# NODE MATCHING
# ------------------------------------------------------------
# Direct call: Enum.count(expr) — but NOT pipeline Enum.count(predicate)
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
# Pipeline form: expr |> Enum.count()
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
# ------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------
defp enum_module?({:__aliases__, _, [:Enum]}), do: true
defp enum_module?(_), do: false
# In pipeline form `x |> Enum.count(&pred)`, the single arg is a
# predicate (capture or anonymous function), not a collection.
defp predicate?({:&, _, _}), do: true
defp predicate?({:fn, _, _}), do: true
defp predicate?(_), do: false
# ------------------------------------------------------------
# MESSAGE GENERATION
# ------------------------------------------------------------
defp build_issue(meta) do
%Issue{
rule: :no_enum_count_for_length,
severity: :info,
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