Skip to content

elixir-vibe/ex_slop

Repository files navigation

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

Recommended Credo Built-in Checks

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

About

Credo checks that catch AI-generated Elixir code slop

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages