ForgeCredoChecks
Custom Credo checks targeting Enum
anti-patterns LLMs (and some humans) commonly produce in Elixir code.
Stock Credo ships rules for filter |> filter, reject |> reject,
map |> join, etc. (same operation chained, or map terminating in a
collector). It does not catch chains where one operation composes
with the complementary one. These checks fill that gap.
Rules
Two-pass Enum chains: use Enum.reduce/3
| Rule | Pattern flagged |
|---|---|
ForgeCredoChecks.FilterMap | Enum.filter |> Enum.map |
ForgeCredoChecks.RejectMap | Enum.reject |> Enum.map |
ForgeCredoChecks.MapReject | Enum.map |> Enum.reject |
ForgeCredoChecks.MapRejectNil | Enum.map |> Enum.reject(&is_nil/1) |
Hand-rolled map building: use Map.new/2
| Rule | Pattern flagged |
|---|---|
ForgeCredoChecks.MapNewFromInto | Enum.into(%{}, fn ...) |
ForgeCredoChecks.MapNewFromReduce | Enum.reduce(_, %{}, &Map.put(acc, k, v)) |
Wasteful list-extremum patterns
| Rule | Pattern flagged | Replacement |
|---|---|---|
ForgeCredoChecks.ReverseListFirst | xs |> Enum.reverse() |> List.first() | List.last(xs) |
ForgeCredoChecks.SortListFirst | Enum.sort \| List.first | Enum.min/Enum.max/*_by |
The two-pass chains walk the input twice and allocate intermediate lists;
Enum.reduce/3 does neither. The map-building forms are pure equivalences
with cleaner intent. The sort-then-pick patterns are O(N log N) when
O(N) suffices.
# Flagged by FilterMap
things
|> Enum.filter(&keep?/1)
|> Enum.map(&transform/1)
# Suggested replacement
Enum.reduce(things, [], fn x, acc ->
if keep?(x), do: [transform(x) | acc], else: acc
end)
Append |> Enum.reverse() only if the output order matters. For
most callers (set membership, Map.new, sort, sum, count, etc.) it
does not.
All four rules detect the four AST shapes Elixir parses for any two-call chain: direct nested call, two-step pipe, partial pipe + call, and longer pipe chains.
Installation
Add to mix.exs:
def deps do
[
{:forge_credo_checks, "~> 0.2", only: [:dev, :test], runtime: false}
]
end
Then add to .credo.exs:
%{
configs: [
%{
name: "default",
checks: [
# ...
{ForgeCredoChecks.FilterMap, []},
{ForgeCredoChecks.RejectMap, []},
{ForgeCredoChecks.MapReject, []},
{ForgeCredoChecks.MapRejectNil, []},
{ForgeCredoChecks.MapNewFromInto, []},
{ForgeCredoChecks.MapNewFromReduce, []},
{ForgeCredoChecks.ReverseListFirst, []},
{ForgeCredoChecks.SortListFirst, []}
]
}
]
}License
MIT