Credence
A semantic linter for LLM-generated Elixir code.
Elixir's compiler checks syntax. Credo checks style. Credence checks semantics — it mainly catches patterns that compile and pass tests but are non-idiomatic, inefficient, or ported from Python/JavaScript conventions that don't belong in Elixir.
Three-phase pipeline
Credence runs code through three escalating phases:
Credence.Syntax → can the parser read it? (string-level fixes)
Credence.Semantic → does the compiler accept it? (compiler warning fixes)
Credence.Pattern → is it idiomatic Elixir? (80+ AST-level rules)Syntax repairs code that won't parse — e.g. n * (n + 1) div 2 (Python's // translated as infix) becomes div(n * (n + 1), 2).
Semantic captures compiler warnings via Code.with_diagnostics/1 and fixes them — unused variables get _ prefixed, undefined function calls get corrected(if possible).
Pattern detects and auto-fixes 80+ anti-patterns using AST analysis — Enum.sort |> Enum.reverse becomes Enum.sort(:desc), manual frequency counting becomes Enum.frequencies/1, acc ++ [x] becomes [x | acc].
Each phase has its own Rule behaviour. Rules are discovered automatically and run in priority order.
Installation
def deps do
[
{:credence, "~> 0.4.3", only: [:dev, :test], runtime: false}
]
endUsage
Analyze — detect issues without modifying code:
%{valid: true, issues: []} = Credence.analyze(code)Fix — auto-fix what's fixable, report the rest:
%{code: fixed, issues: remaining} = Credence.fix(code)Example
code = ~S"""
defmodule StudentAnalyzer do
@doc "Analyzes scores.\nReturns statistics.\n"
def analyze(scores) do
if length(scores) == 0 do
%{error: "no scores"}
else
total = Enum.map(scores, fn s -> s end) |> Enum.sum()
avg = total / Enum.count(scores) * 1.0
freq = Enum.reduce(scores, %{}, fn s, acc ->
Map.update(acc, s, 1, &(&1 + 1))
end)
ranked = Enum.sort(scores) |> Enum.reverse()
top_3 = Enum.sort(scores) |> Enum.take(-3)
unique = scores |> Enum.uniq_by(fn s -> s end)
csv = Enum.map(unique, fn s -> Integer.to_string(s) end) |> Enum.join(",")
%{average: avg, frequencies: freq, top_3: top_3,
csv: csv, passing: is_passing(avg)}
end
end
def is_passing(avg), do: avg |> Kernel.>=(60.0)
end
"""
%{code: fixed, issues: remaining} = Credence.fix(code)You can run a subset of rules:
Credence.analyze(code, rules: [
Credence.Pattern.NoListAppendInRecursion,
Credence.Pattern.NoSortForTopK,
Credence.Pattern.NoListFold
])Writing custom rules
Each phase has its own Rule behaviour:
Pattern rules (AST-level)
defmodule Credence.Pattern.MyRule do
use Credence.Pattern.Rule
@impl true
def priority, do: 500 # default; lower runs first
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
# pattern match on node
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fixable?, do: true
@impl true
def fix(source, _opts) do
# return modified source string
source
end
endSyntax rules (string-level, for unparseable code)
defmodule Credence.Syntax.MyFix do
use Credence.Syntax.Rule
@impl true
def analyze(source), do: [] # return [%Issue{}] for detected problems
@impl true
def fix(source), do: source # return repaired source string
endSemantic rules (compiler warning fixes)
defmodule Credence.Semantic.MyFix do
use Credence.Semantic.Rule
@impl true
def match?(%{severity: :warning, message: msg}), do: false
@impl true
def to_issue(diagnostic), do: %Credence.Issue{rule: :my_fix, message: diagnostic.message, meta: %{}}
@impl true
def fix(source, diagnostic), do: source
endLicense
MIT