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.FilterMapEnum.filter |> Enum.map
ForgeCredoChecks.RejectMapEnum.reject |> Enum.map
ForgeCredoChecks.MapRejectEnum.map |> Enum.reject
ForgeCredoChecks.MapRejectNilEnum.map |> Enum.reject(&is_nil/1)

Hand-rolled map building: use Map.new/2

Rule Pattern flagged
ForgeCredoChecks.MapNewFromIntoEnum.into(%{}, fn ...)
ForgeCredoChecks.MapNewFromReduceEnum.reduce(_, %{}, &Map.put(acc, k, v))

Wasteful list-extremum patterns

Rule Pattern flagged Replacement
ForgeCredoChecks.ReverseListFirstxs |> Enum.reverse() |> List.first()List.last(xs)
ForgeCredoChecks.SortListFirstEnum.sort \| List.firstEnum.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