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 a comprehension

RulePattern 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

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

Wasteful list-extremum patterns

RulePattern flaggedReplacement
ForgeCredoChecks.ReverseListFirstxs |> Enum.reverse() |> List.first()List.last(xs)
ForgeCredoChecks.SortListFirstEnum.sort \| List.firstEnum.min/Enum.max/*_by

with-macro conventions

RulePattern flaggedConfigurable
ForgeCredoChecks.WithBareBinding= clauses inside a with chain (must be <-)no
ForgeCredoChecks.WithElseClauseswith blocks whose else exceeds :max_clauses:max_clauses (default 1)
ForgeCredoChecks.WithResultTag<- clauses with atom-tagged LHS outside the allowlist:allowed_atoms (default [:ok, :error])

LLM tells (function shape and idiomatic Elixir)

RulePattern flagged
ForgeCredoChecks.InconsistentParamNamesmulti-clause function whose same positional argument has different base names across clauses
ForgeCredoChecks.NoKernelShadowingvariable binding (=, fn, def/defp param) named max/min/length/elem/hd/tl/abs/round/trunc/div/rem/tuple_size/map_size/byte_size/bit_size
ForgeCredoChecks.NoUnnecessaryCatchAllRaisedef/defp clause whose every arg is a wildcard AND whose body is exactly raise(...)
ForgeCredoChecks.NoCaseTrueFalsecase <bool_expr> do true -> ...; false -> ... end (and true/_, false/_ variants)
ForgeCredoChecks.NoKernelOpInPipelinepipeline |> Kernel.<op>(arg) for comparison/boolean operators (==/!=/</>/<=/>=/===/!==/and/or)

The two-pass Enum chains walk the input twice and allocate intermediate lists; a comprehension does both in one pass and preserves order naturally. The map-building forms are pure equivalences with cleaner intent. The sort-then-pick patterns are O(N log N) when O(N) suffices. The with checks codify the convention that every clause uses <-, that step return shapes get normalized in helpers (so non-matches fall through), and that result tags stay within a project's intended vocabulary.

# Flagged by FilterMap
things
|> Enum.filter(&keep?/1)
|> Enum.map(&transform/1)

# Preferred replacement: comprehension (one pass, in-order, no reverse)
for x <- things, keep?(x), do: transform(x)

For the Enum-chain checks the suggested fix order is:

  1. Comprehension (preferred). Single pass, preserves order, no intermediate list, no reverse step.
  2. Enum.flat_map/2 when the transform is naturally 0-or-more (e.g. parse(x) returning nil-or-value).
  3. Enum.reduce/3 only as a last resort, and only when the consumer does not care about order. Do not tack on |> Enum.reverse/1 to restore order: that second pass is exactly the tax the comprehension exists to avoid.

All Enum-chain 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.4", 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, []},
        {ForgeCredoChecks.WithBareBinding, []},
        {ForgeCredoChecks.WithElseClauses, []},
        {ForgeCredoChecks.WithResultTag, []},
        {ForgeCredoChecks.InconsistentParamNames, []},
        {ForgeCredoChecks.NoKernelShadowing, []},
        {ForgeCredoChecks.NoUnnecessaryCatchAllRaise, []},
        {ForgeCredoChecks.NoCaseTrueFalse, []},
        {ForgeCredoChecks.NoKernelOpInPipeline, []}
      ]
    }
  ]
}

License

MIT