README.md

# ForgeCredoChecks

Custom [Credo](https://github.com/rrrene/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.

```elixir
# 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`:

```elixir
def deps do
  [
    {:forge_credo_checks, "~> 0.2", only: [:dev, :test], runtime: false}
  ]
end
```

Then add to `.credo.exs`:

```elixir
%{
  configs: [
    %{
      name: "default",
      checks: [
        # ...
        {ForgeCredoChecks.FilterMap, []},
        {ForgeCredoChecks.RejectMap, []},
        {ForgeCredoChecks.MapReject, []},
        {ForgeCredoChecks.MapRejectNil, []},
        {ForgeCredoChecks.MapNewFromInto, []},
        {ForgeCredoChecks.MapNewFromReduce, []},
        {ForgeCredoChecks.ReverseListFirst, []},
        {ForgeCredoChecks.SortListFirst, []}
      ]
    }
  ]
}
```

## License

MIT