Credence
A semantic linter for LLM-generated Elixir code.
Elixir's compiler checks syntax. Credo checks style. Credence checks semantics — it catches patterns that compile and pass tests but are non-idiomatic, inefficient, or ported from Python/JavaScript conventions that don't belong in Elixir.
Built for LLM code pipelines. LLMs make the same mistakes every time: List.foldl instead of Enum.reduce, Enum.sort |> Enum.take(1) instead of Enum.min, Python-style _private function names, defensive catch-all clauses that degrade Elixir's built-in error reporting. Credence catches these at scale and feeds violations back as retry context.
Installation
def deps do
[{:credence, github: "Cinderella-Man/credence", only: [:dev, :test], runtime: false}]
endQuick start
result = Credence.analyze(File.read!("lib/my_module.ex"))
unless result.valid do
Enum.each(result.issues, fn issue ->
IO.puts("#{issue.rule}: #{issue.message}")
end)
endLLM pipeline integration
Credence fits as a validation step after mix compile, mix format, and mix test. Feed violations back to the LLM as error context for retry:
defmodule Pipeline.SemanticCheck do
def validate(code) do
case Credence.analyze(code) do
%{valid: true} ->
:ok
%{issues: issues} ->
feedback =
Enum.map_join(issues, "\n", fn issue ->
"Line #{issue.meta.line}: #{issue.message}"
end)
{:error, feedback}
end
end
endThe feedback string goes straight into your LLM retry prompt. Credence messages include the fix — the LLM gets actionable instructions, not just complaints.
You can also run a subset of rules:
Credence.analyze(code, rules: [
Credence.Pattern.NoListAppendInLoop,
Credence.Pattern.NoSortForTopK,
Credence.Pattern.NoListFold
])Writing custom rules
Every rule implements Credence.Rule:
defmodule Credence.Pattern.MyRule do
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
# pattern match on node, return {node, [issue | issues]} or {node, issues}
end)
Enum.reverse(issues)
end
end
Pass custom rules via the :rules option or add them to @default_rules in Credence.
Rules
| Rule | Description | Auto-fixable |
|---|---|---|
AvoidGraphemesEnumCount | Enum.count/1 on String.graphemes/1 result — use String.length/1 instead | ✅ |
AvoidGraphemesLength | length/1 on String.graphemes/1 result — use String.length/1 instead | ✅ |
InconsistentParamNames | Same positional parameter uses different names across function clauses | ✅ |
NoAnonFnApplicationInPipe |
Anonymous functions applied with .() inside a pipe chain | ✅ |
NoDestructureReconstruct | List destructured into variables only to reconstruct the same list | ✅ |
NoDocFalseOnPrivate | @doc false on private functions (defp) — redundant | ✅ |
NoDoubleSortSameList |
Same list sorted twice (ascending then descending) — use Enum.sort/2 once | ✅ |
NoEagerWithIndexInReduce | Enum.with_index/1 passed directly into Enum.reduce — use Stream.with_index/1 | ✅ |
NoEnumAtBinarySearch | Enum.at/2 inside recursive binary search functions — use a tuple/array | ❌ |
NoEnumAtInLoop | Enum.at/2 inside looping constructs — O(n) per iteration | ❌ |
NoEnumAtLoopAccess | Enum.at/2 inside loops (heuristic) | ❌ |
NoEnumAtMidpointAccess | Enum.at/2 with a midpoint index inside divide-and-conquer patterns | ✅ |
NoEnumAtNegativeIndex | Enum.at/2 with negative index — grouped into reverse + pattern match, or List.last | ✅ |
NoEnumCountForLength | Enum.count/1 without a predicate on a plain list — use length/1 | ✅ |
NoEnumDropNegative | Enum.drop(list, -n) — use Enum.take/2 instead | ✅ |
NoEnumTakeNegative | Enum.take(list, -n) — use Enum.drop/2 and reverse instead | ✅ |
NoExplicitMaxReduce |
Explicit max-reduction pattern inside Enum.reduce/3 — use Enum.max/1 | ✅ |
NoExplicitMinReduce |
Explicit min-reduction pattern inside Enum.reduce/3 — use Enum.min/1 | ✅ |
NoExplicitSumReduce |
Explicit sum-reduction pattern inside Enum.reduce/3 — use Enum.sum/1 | ✅ |
NoGraphemePalindromeCheck |
String palindrome check via String.graphemes — use String.reverse/1 | ✅ |
NoGuardEqualityForPatternMatch | Guard equality check on a parameter that could be a pattern match clause | ✅ |
NoIdentityFunctionInEnum | Enum._by with identity callback (fn x -> x end, & &1) — use non-_by variant | ✅ |
NoIntegerToStringDigits | Integer.to_string/1 |> String.graphemes/1 — use Integer.digits/1 | ✅ |
NoIsPrefixForNonGuard | is_ prefix on non-guard def/defp functions — use ? suffix | ✅ |
NoKernelOpInPipeline | Kernel.op/2 in pipeline — extract to infix operator | ✅ |
NoKernelShadowing |
Variables that shadow Kernel functions | ❌ |
NoLengthComparisonForEmpty | length(list) compared to 0–5 — use == [], != [], or match?/2 | ✅ |
NoLengthGuardToPattern | length/1 inside guard clauses — use pattern matching up to 5 elements | ✅ |
NoLengthInGuard | length/1 inside guard clauses — nest logic instead | ❌ |
NoListAppendInLoop | ++ inside non-fixable looping constructs — O(n²) | ❌ |
NoListAppendInRecursion | ++ inside recursion — O(n²) | ✅ |
NoListAppendInReduce | ++ inside reduce — O(n²) | ✅ |
NoListDeleteAtInLoop | List.delete_at/2 inside looping constructs | ❌ |
NoListFold | List.foldl/3 or List.foldr/3 — use Enum.reduce/3 | ✅ |
NoListLast | List.last/1 — use pattern matching or Enum.at(list, -1) | ❌ |
NoListToTupleForAccess | List.to_tuple(list) only for index access — use Enum.at/2 | ✅ |
NoManualEnumUniq |
Manual uniqueness filtering reimplementing Enum.uniq/1 | ✅ |
NoManualFrequencies |
Manual frequency counting reimplementing Enum.frequencies/1 | ✅ |
NoManualListLast |
Hand-rolled reimplementation of List.last/1 | ✅ |
NoManualMax | if expression reimplementing Kernel.max/2 | ✅ |
NoManualMin | if expression reimplementing Kernel.min/2 | ✅ |
NoManualStringReverse |
Manual string reversal via graphemes — use String.reverse/1 | ✅ |
NoMapAsSet | Map with boolean values used as a set — use MapSet | ❌ |
NoMapKeysEnumLookup | Map.keys/1 piped into an Enum lookup — use Map.has_key?/2 | ✅ |
NoMapKeysOrValuesForIteration | Map.values/1 or Map.keys/1 fed into Enum iteration — iterate the map directly | ✅ |
NoMapKeysOrValuesForRawIteration | Map.values/1 or Map.keys/1 into Enum (unfixable variant) | ❌ |
NoMapThenAggregate | Enum.map/2 immediately followed by a terminal aggregation — use map_ variant | ✅ |
NoMapUpdateThenFetch | Map.update/4 or Map.update!/3 followed by Map.fetch/get on the same key | ✅ |
NoMultipleEnumAt |
Multiple Enum.at/2 calls on the same list — convert to tuple | ✅ |
NoMultiplyByOnePointZero | expr * 1.0 Python float coercion — remove the no-op | ✅ |
NoNestedEnumOnSameEnumerable | Enum.member?/2 nested inside another Enum.* traversal on the same enumerable | ✅ |
NoNestedEnumOnSameEnumerableUnfixable |
Nested Enum.* calls on the same enumerable (unfixable variant) | ❌ |
NoParamRebinding | Rebinding parameter names inside a function body | ✅ |
NoRedundantEnumJoinSeparator | Enum.join(list, "") — the empty string is the default; omit it | ✅ |
NoRedundantNegatedGuard | Guard clause logically redundant because a preceding clause already handles the case | ✅ |
NoRepeatedEnumTraversal |
Same variable traversed multiple times in separate Enum calls | ❌ |
NoSortForTopK |
Full sort just to take the top-k elements — use Enum.min_max_by / Enum.take | ✅ |
NoSortForTopKReduce | Full sort for top-k inside a reduce (unfixable variant) | ❌ |
NoSortThenAt | Enum.sort |> Enum.at(index) — use Enum.min/max directly | ✅ |
NoSortThenAtUnfixable | Enum.sort |> Enum.at via intermediate variable (unfixable variant) | ❌ |
NoSortThenReverse | Enum.sort/1 then Enum.reverse/1 — use Enum.sort(list, :desc) | ✅ |
NoSortThenReverseUnfixable | Sort then reverse via intermediate variable (unfixable variant) | ❌ |
NoSplitToCount | length(String.split(str, sep)) - 1 — Python str.count() translation | ❌ |
NoStringConcatInLoop | <> string concatenation inside loops — use IO.iodata_to_binary / iodata | ✅ |
NoStringConcatInLoopUnfixable | <> string concatenation in complex loops (unfixable variant) | ❌ |
NoStringLengthForCharCheck | String.length(x) == 1 to check for a single character — use pattern matching | ✅ |
NoTakeWhileLengthCheck | Enum.take_while/2 |> length/1 — use Enum.count/2 with a predicate | ✅ |
NoTrailingNewlineInDoc |
Trailing \n in @doc/@moduledoc strings — strip it | ✅ |
NoUnderscoreFunctionName |
Leading _ in function names to indicate privacy — use defp instead | ✅ |
NoUnnecessaryCatchAllRaise | Catch-all clause where every argument is a wildcard and the body just raises | ✅ |
PreferDescSortOverNegativeTake | Enum.sort |> Enum.take(-n) — use Enum.sort(list, :desc) |> Enum.take(n) | ✅ |
PreferEnumReverseTwo | Enum.reverse(list) ++ other — use Enum.reverse(list, other) | ✅ |
PreferEnumSlice | Enum.drop/2 |> Enum.take/2 — use Enum.slice/3 | ✅ |
PreferHeredocForMultiLineDoc |
Multi-line @doc with \n escapes — convert to heredoc """ | ✅ |
PreferMapFetchOverHasKey | Map.has_key?/2 in if/cond conditions — use Map.fetch/2 instead | ❌ |
RedundantListGuard |
Redundant is_list/1 guard on a variable already matched as a list | ✅ |
UnnecessaryGraphemeChunking | N-gram pipeline that converts string to graphemes unnecessarily | ✅ |
UnnecessaryGraphemeChunkingUnfixable | Inefficient grapheme-based string transformation (unfixable variant) | ❌ |
UseMapJoin | Enum.map/2 |> Enum.join/1,2 — use Enum.map_join/3 | ✅ |
License
MIT