# 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 a comprehension
| 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` |
### `with`-macro conventions
| Rule | Pattern flagged | Configurable |
|---|---|---|
| `ForgeCredoChecks.WithBareBinding` | `=` clauses inside a `with` chain (must be `<-`) | no |
| `ForgeCredoChecks.WithElseClauses` | `with` 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)
| Rule | Pattern flagged |
|---|---|
| `ForgeCredoChecks.InconsistentParamNames` | multi-clause function whose same positional argument has different base names across clauses |
| `ForgeCredoChecks.NoKernelShadowing` | variable 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.NoUnnecessaryCatchAllRaise` | `def`/`defp` clause whose every arg is a wildcard AND whose body is exactly `raise(...)` |
| `ForgeCredoChecks.NoCaseTrueFalse` | `case <bool_expr> do true -> ...; false -> ... end` (and `true`/`_`, `false`/`_` variants) |
| `ForgeCredoChecks.NoKernelOpInPipeline` | `pipeline \|> 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.
```elixir
# 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`:
```elixir
def deps do
[
{:forge_credo_checks, "~> 0.4", 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, []},
{ForgeCredoChecks.WithBareBinding, []},
{ForgeCredoChecks.WithElseClauses, []},
{ForgeCredoChecks.WithResultTag, []},
{ForgeCredoChecks.InconsistentParamNames, []},
{ForgeCredoChecks.NoKernelShadowing, []},
{ForgeCredoChecks.NoUnnecessaryCatchAllRaise, []},
{ForgeCredoChecks.NoCaseTrueFalse, []},
{ForgeCredoChecks.NoKernelOpInPipeline, []}
]
}
]
}
```
## License
MIT