Credo checks that catch AI-generated Elixir code slop
Find a file
2026-06-11 12:19:15 +03:00
.github/workflows Use shared Elixir CI workflow 2026-06-06 21:48:41 +03:00
lib Fix LengthComparison dogfood issue 2026-05-25 09:09:22 +03:00
test Add LengthComparison check (EXS4027) 2026-05-24 12:35:22 +02:00
.formatter.exs Initial: 14 Credo checks for AI-generated Elixir slop 2026-03-07 17:09:18 +03:00
.gitignore Initial: 14 Credo checks for AI-generated Elixir slop 2026-03-07 17:09:18 +03:00
.tool-versions Use Elixir 1.20 by default 2026-06-06 16:59:39 +03:00
AGENTS.md Move project to Elixir Vibe 2026-04-30 11:13:22 +03:00
CHANGELOG.md Release v0.4.2 2026-05-25 13:00:59 +03:00
LICENSE Fix author name in LICENSE 2026-03-07 22:47:23 +03:00
mix.exs Use Elixir 1.20 by default 2026-06-06 16:59:39 +03:00
mix.lock Add mix ci: compile, format, test, credo, dialyzer, ex_dna 2026-03-07 17:33:31 +03:00
README.md README: ecosystem footer linking org and Building Blocks standard 2026-06-11 12:19:15 +03:00

ExSlop

Hex.pm

Credo checks that catch AI-generated code slop in Elixir.

40 checks for patterns that LLMs produce but experienced Elixir developers don't — blanket rescues, narrator docs, obvious comments, anti-idiomatic Enum usage, try/rescue around non-raising functions, N+1 queries, and more.

Most checks avoid built-in Credo overlap: Credo never inspects doc/comment content, doesn't catch Ecto anti-patterns or identity passthrough, and its MapInto / CaseTrivialMatches checks are disabled or deprecated. A few semantic-performance checks intentionally overlap with useful Credo refactors so ExSlop can serve generated-code validation pipelines.

Installation

Add to mix.exs:

def deps do
  [
    {:ex_slop, "~> 0.1", only: [:dev, :test], runtime: false}
  ]
end

Register the plugin in your .credo.exs:

# .credo.exs
%{
  configs: [
    %{
      name: "default",
      plugins: [{ExSlop, []}]
    }
  ]
}

This enables 31 high-signal checks automatically and leaves noisier style/performance checks opt-in. Or cherry-pick individual checks — append them to the extra list:

# .credo.exs
{ExSlop.Check.Warning.BlanketRescue, []},
{ExSlop.Check.Warning.RescueWithoutReraise, []},
{ExSlop.Check.Warning.RepoAllThenFilter, []},
{ExSlop.Check.Warning.QueryInEnumMap, []},
{ExSlop.Check.Warning.GenserverAsKvStore, []},
{ExSlop.Check.Warning.PathExpandPriv, []},
{ExSlop.Check.Warning.DualKeyAccess, []},

{ExSlop.Check.Refactor.FilterNil, []},
{ExSlop.Check.Refactor.RejectNil, []},
{ExSlop.Check.Refactor.ReduceAsMap, []},
{ExSlop.Check.Refactor.MapIntoLiteral, []},
{ExSlop.Check.Refactor.IdentityPassthrough, []},
{ExSlop.Check.Refactor.IdentityMap, []},
{ExSlop.Check.Refactor.CaseTrueFalse, []},
{ExSlop.Check.Refactor.TryRescueWithSafeAlternative, []},
{ExSlop.Check.Refactor.WithIdentityElse, []},
{ExSlop.Check.Refactor.WithIdentityDo, []},
{ExSlop.Check.Refactor.SortThenReverse, []},
{ExSlop.Check.Refactor.StringConcatInReduce, []},
{ExSlop.Check.Refactor.ReduceMapPut, []},
{ExSlop.Check.Refactor.RedundantBooleanIf, []},
{ExSlop.Check.Refactor.FlatMapFilter, []},
{ExSlop.Check.Refactor.RedundantEnumJoinSeparator, []},
{ExSlop.Check.Refactor.UseMapJoin, []},
{ExSlop.Check.Refactor.PreferEnumSlice, []},
{ExSlop.Check.Refactor.GraphemesLength, []},
{ExSlop.Check.Refactor.ManualStringReverse, []},
{ExSlop.Check.Refactor.SortThenAt, []},
{ExSlop.Check.Refactor.SortForTopK, []},
{ExSlop.Check.Refactor.ListFold, []},
{ExSlop.Check.Refactor.ListLast, []},
{ExSlop.Check.Refactor.LengthInGuard, []},
{ExSlop.Check.Refactor.LengthComparison, []},
{ExSlop.Check.Refactor.ExplicitSumReduce, []},
{ExSlop.Check.Readability.NarratorDoc, []},
{ExSlop.Check.Readability.DocFalseOnPublicFunction, []},
{ExSlop.Check.Readability.BoilerplateDocParams, []},
{ExSlop.Check.Readability.ObviousComment, [additional_keywords: []]},
{ExSlop.Check.Readability.StepComment, []},
{ExSlop.Check.Readability.NarratorComment, []},
{ExSlop.Check.Readability.UnaliasedModuleUse, []}

Cherry-pick only the checks that make sense for your project.

Checks

Warnings

Check What it catches
BlanketRescue rescue _ -> nil or rescue _e -> {:error, "..."}
RescueWithoutReraise rescue e -> Logger.error(...); :error — logs but swallows
RepoAllThenFilter Repo.all(User) |> Enum.filter(& &1.active) — filter in SQL
QueryInEnumMap Enum.map(users, fn u -> Repo.get(...) end) — N+1 query
GenserverAsKvStore GenServer that's just Map.get/Map.put on state — use ETS or Agent
PathExpandPriv Path.expand("...priv...", __DIR__) — use Application.app_dir/2
DualKeyAccess Map.get(m, :key) || Map.get(m, "key"), get_in(m, [:key]) || get_in(m, ["key"]), or m[:key] || m["key"] — normalize once instead

Refactoring

Check Bad Good
FilterNil Enum.filter(fn x -> x != nil end) Enum.reject(&is_nil/1)
RejectNil Enum.reject(fn x -> x == nil end) Enum.reject(&is_nil/1)
ReduceAsMap Enum.reduce([], fn x, acc -> [f(x) | acc] end) Enum.map(&f/1)
MapIntoLiteral Enum.map(...) |> Enum.into(%{}) Map.new(...)
IdentityPassthrough case r do {:ok, v} -> {:ok, v}; {:error, e} -> {:error, e} end r
IdentityMap Enum.map(fn x -> x end) remove the call
CaseTrueFalse case flag do true -> a; false -> b end if flag, do: a, else: b
TryRescueWithSafeAlternative try do String.to_integer(x) rescue _ -> nil end Integer.parse(x)
WithIdentityElse with {:ok, v} <- f() do v else {:error, r} -> {:error, r} end drop the else
WithIdentityDo with {:ok, v} <- f() do {:ok, v} end f()
SortThenReverse Enum.sort() |> Enum.reverse() Enum.sort(:desc)
StringConcatInReduce Enum.reduce("", fn x, acc -> acc <> x end) Enum.join/1 or IO data
ReduceMapPut Enum.reduce(%{}, fn x, acc -> Map.put(acc, k, v) end) Map.new/2
RedundantBooleanIf if cond, do: true, else: false use the condition directly
FlatMapFilter Enum.flat_map(fn x -> if cond, do: [x], else: [] end) Enum.filter/2
RedundantEnumJoinSeparator Enum.join(parts, "") Enum.join(parts)
UseMapJoin `Enum.map(...) > Enum.join(...)`
PreferEnumSlice `Enum.drop(n) > Enum.take(k)`
GraphemesLength `String.graphemes(s) > length()`
ManualStringReverse `String.graphemes(s) > Enum.reverse()
SortThenAt `Enum.sort() > Enum.at(0)`
SortForTopK `Enum.sort() > Enum.take(1)`
ListFold List.foldl(list, acc, fun) Enum.reduce(list, acc, fun)
ListLast List.last(list) avoid needing the last element after traversal
LengthInGuard def f(xs) when length(xs) == 0 pattern match on [] / `[_
LengthComparison if length(xs) == 0, length(xs) <= 5 (any context) pattern match, or Enum.count_until/2 for a threshold
ExplicitSumReduce Enum.reduce(nums, 0, fn n, acc -> n + acc end) Enum.sum(nums)

Readability

Check What it catches
NarratorDoc @moduledoc "This module provides functionality for..."
DocFalseOnPublicFunction Multiple @doc false on def in one module — cargo-culted
BoilerplateDocParams ## Parameters\n- conn: The connection struct
ObviousComment # Fetch the user above Repo.get(User, id)
StepComment # Step 1: Validate input
NarratorComment # Here we fetch the user / # Now we validate / # Let's create
UnaliasedModuleUse Credo.Code.prewalk used 2+ times without alias Credo.Code

These Credo built-in checks are especially good at catching AI slop. Enable them in your .credo.exs if you haven't already:

# Catches length(list) == 0 (traverses entire list) → use list == [] or Enum.empty?/1
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},

# Catches acc ++ [item] (O(n²) append) → use [item | acc] then Enum.reverse
{Credo.Check.Refactor.AppendSingleItem, []},

# Catches !!var (double negation) — LLMs use this to "cast to boolean"
{Credo.Check.Refactor.DoubleBooleanNegation, []},

# Catches case x do true -> a; false -> b end → if/else
{Credo.Check.Refactor.CondStatements, []},

# Catches Enum.map |> Enum.map → single Enum.map
{Credo.Check.Refactor.MapMap, []},

# Catches Enum.filter |> Enum.filter → single Enum.filter
{Credo.Check.Refactor.FilterFilter, []},

# Catches Enum.reject |> Enum.reject → single Enum.reject
{Credo.Check.Refactor.RejectReject, []},

# Catches Enum.count(enum) > 0 → Enum.any?/1
{Credo.Check.Refactor.FilterCount, []},

# Catches negated conditions in unless → rewrite with positive condition
{Credo.Check.Refactor.NegatedConditionsInUnless, []},

# Catches unless x do .. else .. end → if/else (clearer)
{Credo.Check.Refactor.UnlessWithElse, []}

Credits

Several semantic-performance checks are inspired by Credence, an MIT-licensed standalone semantic linter for generated Elixir code.

Part of Elixir Vibe

ExSlop catches patterns that AI code generation over-produces — it lints the generator, not the language.

It is one building block of a larger stack — tools that make AI-generated software checkable: structural search, dependence analysis, duplication and slop detection, session replay, and ecosystem-wide code search. See the Elixir Vibe organization for the rest, and Building Blocks for the Future Web for the thesis, architecture, and roadmap that tie them together.

License

MIT