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}]
end

Quick 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)
end

LLM 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
end

The 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
AvoidGraphemesEnumCountEnum.count/1 on String.graphemes/1 result — use String.length/1 instead
AvoidGraphemesLengthlength/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
NoEagerWithIndexInReduceEnum.with_index/1 passed directly into Enum.reduce — use Stream.with_index/1
NoEnumAtBinarySearchEnum.at/2 inside recursive binary search functions — use a tuple/array
NoEnumAtInLoopEnum.at/2 inside looping constructs — O(n) per iteration
NoEnumAtLoopAccessEnum.at/2 inside loops (heuristic)
NoEnumAtMidpointAccessEnum.at/2 with a midpoint index inside divide-and-conquer patterns
NoEnumAtNegativeIndexEnum.at/2 with negative index — grouped into reverse + pattern match, or List.last
NoEnumCountForLengthEnum.count/1 without a predicate on a plain list — use length/1
NoEnumDropNegativeEnum.drop(list, -n) — use Enum.take/2 instead
NoEnumTakeNegativeEnum.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
NoIdentityFunctionInEnumEnum._by with identity callback (fn x -> x end, & &1) — use non-_by variant
NoIntegerToStringDigitsInteger.to_string/1 |> String.graphemes/1 — use Integer.digits/1
NoIsPrefixForNonGuardis_ prefix on non-guard def/defp functions — use ? suffix
NoKernelOpInPipelineKernel.op/2 in pipeline — extract to infix operator
NoKernelShadowing Variables that shadow Kernel functions
NoLengthComparisonForEmptylength(list) compared to 0–5 — use == [], != [], or match?/2
NoLengthGuardToPatternlength/1 inside guard clauses — use pattern matching up to 5 elements
NoLengthInGuardlength/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²)
NoListDeleteAtInLoopList.delete_at/2 inside looping constructs
NoListFoldList.foldl/3 or List.foldr/3 — use Enum.reduce/3
NoListLastList.last/1 — use pattern matching or Enum.at(list, -1)
NoListToTupleForAccessList.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
NoManualMaxif expression reimplementing Kernel.max/2
NoManualMinif expression reimplementing Kernel.min/2
NoManualStringReverse Manual string reversal via graphemes — use String.reverse/1
NoMapAsSetMap with boolean values used as a set — use MapSet
NoMapKeysEnumLookupMap.keys/1 piped into an Enum lookup — use Map.has_key?/2
NoMapKeysOrValuesForIterationMap.values/1 or Map.keys/1 fed into Enum iteration — iterate the map directly
NoMapKeysOrValuesForRawIterationMap.values/1 or Map.keys/1 into Enum (unfixable variant)
NoMapThenAggregateEnum.map/2 immediately followed by a terminal aggregation — use map_ variant
NoMapUpdateThenFetchMap.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
NoMultiplyByOnePointZeroexpr * 1.0 Python float coercion — remove the no-op
NoNestedEnumOnSameEnumerableEnum.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
NoRedundantEnumJoinSeparatorEnum.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)
NoSortThenAtEnum.sort |> Enum.at(index) — use Enum.min/max directly
NoSortThenAtUnfixableEnum.sort |> Enum.at via intermediate variable (unfixable variant)
NoSortThenReverseEnum.sort/1 then Enum.reverse/1 — use Enum.sort(list, :desc)
NoSortThenReverseUnfixable Sort then reverse via intermediate variable (unfixable variant)
NoSplitToCountlength(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)
NoStringLengthForCharCheckString.length(x) == 1 to check for a single character — use pattern matching
NoTakeWhileLengthCheckEnum.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
PreferDescSortOverNegativeTakeEnum.sort |> Enum.take(-n) — use Enum.sort(list, :desc) |> Enum.take(n)
PreferEnumReverseTwoEnum.reverse(list) ++ other — use Enum.reverse(list, other)
PreferEnumSliceEnum.drop/2 |> Enum.take/2 — use Enum.slice/3
PreferHeredocForMultiLineDoc Multi-line @doc with \n escapes — convert to heredoc """
PreferMapFetchOverHasKeyMap.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)
UseMapJoinEnum.map/2 |> Enum.join/1,2 — use Enum.map_join/3

License

MIT