Credence
A tool that reads Elixir code written by an AI and fixes the clumsy bits.
Three kinds of checker look at Elixir code. The compiler checks that the code is spelled right. Credo checks that it looks tidy. Credence checks what the code actually does — it finds code that runs fine and passes its tests but is written in a roundabout way, is slower than it needs to be, or was copied over from Python or JavaScript habits that don't fit Elixir. Then it rewrites it the normal Elixir way.
How it works: three rounds
Credence runs your code through three rounds, one after another. Each round fixes a different kind of problem:
Credence.Syntax → can the parser even read it? (fixes the raw text)
Credence.Semantic → does the compiler accept it? (fixes compiler warnings)
Credence.Pattern → is it written the Elixir way? (deeper idiomatic/performance rules)
Round 1 — Syntax fixes code that won't even parse — for example
n * (n + 1) div 2 (someone translated Python's // straight across) becomes
div(n * (n + 1), 2).
Round 2 — Semantic collects the warnings the compiler would print and fixes
them: an unused variable gets an _ in front of it, a call to a function that
doesn't exist gets corrected when we can tell what was meant.
Round 3 — Pattern is the big one: ~117 rules that spot clumsy-but-working
code and rewrite it. Enum.sort |> Enum.reverse becomes Enum.sort(:desc),
counting things by hand becomes Enum.frequencies/1, acc ++ [x] becomes
[x | acc].
Each round has its own kind of rule. Credence finds all the rules by itself and runs them in a set order.
One firm promise: every Pattern rule fixes what it finds. There is no "just warn me" mode. If a problem can only be pointed at but not safely fixed — because the fix would have to rearrange code in several places, because there's more than one reasonable fix, or because the fix would change the type of value the code returns — we leave that rule out of the program entirely.
Installation
def deps do
[
{:credence, "~> 0.7.1", only: [:dev, :test], runtime: false}
]
end
Usage
Analyze — point out problems without touching the code:
%{valid: true, issues: []} = Credence.analyze(code)
Fix — repair what can be repaired, and 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 also run just some of the rules:
Credence.analyze(code, rules: [
Credence.Pattern.NoListAppendInRecursion,
Credence.Pattern.NoSortForTopK,
Credence.Pattern.NoListFold
])
See which rules would run for a set of options, without running them:
# every rule across all three rounds, tagged by round, in execution order
Credence.rule_status(assumptions: :strict)
#=> [%{round: :pattern, name: "NoSortThenReverse", enabled: true, ...}, ...]
# just the on-names
Credence.enabled_rules(assumptions: :strict)
Only the Pattern round is filtered by options; Syntax and Semantic rules always
report enabled: true (whether they fire at runtime depends on the code — a
parse failure or a compiler warning — not the options).
Safety switches
A few cleanups are identical to your original code for almost every input, and
differ only on rare Unicode — a decomposed accent (an "e" plus a separate
accent mark), a joined emoji, a flag. Credence keeps those cleanups behind a
safety switch: a promise about the text your program handles while it
runs — names, messages, file contents — not the characters in your .ex
source files.
The first switch, single_codepoint_graphemes, is on by default: Credence
assumes the text your code processes is all plain, single-piece characters,
which is true for almost every app. If your code processes arbitrary Unicode and
you want the iron-clad "identical for every possible input" guarantee, turn the
promises off:
# play it safe everywhere — only always-correct rules run
Credence.fix(code, assumptions: :strict)
# or set it project-wide
config :credence, assumptions: :strict
Credence.Pattern.rule_status/1 shows which rules are on and which promises they
need (or Credence.rule_status/1 for the same view across all three rounds).
Full reference: the Credence.Assumptions moduledoc.
Writing your own rules
Start by scaffolding the rule and its tests:
mix credence.gen.rule MyRule # a Pattern rule (default)
mix credence.gen.rule MyRule --type syntax # or syntax / semantic
This writes a correctly-named rule plus its test files — heredoc fixtures, the
right module names, already passing every structural meta gate. The generated
tests start red (they carry real assertions against an empty stub), so
mix test shows you exactly what to fill in: check/fix (or analyze/match?)
and the example fixtures.
Each round has its own kind of rule.
Pattern rules
A Pattern rule works on the AST — the tree shape a parser turns your code
into, instead of plain text. A Pattern rule has two parts: check/2 finds the
problems, and the fix is written in one of two ways — pick whichever is
simpler for your change.
Way A — patches (preferred). Walk the tree, find the spots you want to
change with Sourceror.get_range/1, and hand back %{range, change} patches.
Only the bytes you point at move; everything around them stays exactly as the
person wrote it.
defmodule Credence.Pattern.MyRule do
use Credence.Pattern.Rule
@impl true
def priority, do: 500 # default; lower numbers run first
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
# match the node you care about; add a %Credence.Issue{} when it matches
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
{_ast, patches} =
Macro.prewalk(ast, [], fn node, acc ->
# for each matched node, hand back:
# %{range: Sourceror.get_range(node), change: replacement_source}
{node, acc}
end)
Enum.reverse(patches)
end
end
Way B — rewrite the whole text. When your change is really about the text
(a search-and-replace, an edit by line), write fix/2 instead. The built-in
fix_patches/2 you get from use Credence.Pattern.Rule wraps your text change
as one big patch.
defmodule Credence.Pattern.MyRule do
use Credence.Pattern.Rule
@impl true
def check(ast, _opts), do: [...]
@impl true
def fix(source, _opts) do
# hand back the changed source string; keeping the layout tidy is on you
source
end
end
Way A keeps the surrounding layout intact when several rules change the same file, so it's the better default. Way B is fine for a small, self-contained rewrite.
Syntax rules (for code that won't parse)
defmodule Credence.Syntax.MyFix do
use Credence.Syntax.Rule
@impl true
def analyze(source), do: [] # hand back [%Issue{}] for problems you spot
@impl true
def fix(source), do: source # hand back the repaired source string
end
Semantic rules (for compiler warnings)
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
end
What happens when you call fix
When you call Credence.fix(code), your code goes through the three rounds in
order. Each round handles a different kind of problem, and they run in this
order on purpose: the earlier rounds tidy up things that would otherwise confuse
the later ones.
The whole journey:
┌──────────┐
source string → │ Syntax │ → can it be parsed?
└────┬─────┘
│
┌────▼─────┐
│ Semantic │ → does it compile? fix warnings
└────┬─────┘
│
┌────▼─────┐
│ Pattern │ → is it the Elixir way? (only if it compiles)
└────┬─────┘
│
fixed source + leftover issues
Round 1: Syntax — fix the raw text
Syntax rules work on the plain text of your code, before Elixir's parser ever
sees it. They only run when Code.string_to_quoted/1 fails to read the code —
if it already parses, this round is skipped.
This is where we fix things like Python-style // division that the AI
translated word-for-word. These rules use text and pattern matching on the
string — no tree involved.
If the syntax rules get the code parsing again, we move on. If it still won't parse after every syntax rule has tried, the later rounds do the best they can with what's there.
Round 2: Semantic — fix compiler warnings
This round compiles the code with Code.compile_string/2, wrapped in
Code.with_diagnostics/1. That hands us the same warnings and errors you'd see
in your terminal — unused variables, functions that don't exist, and so on.
Each warning is checked against the semantic rules. When a rule knows how to fix
one, it changes the code. For example, the UnusedVariable rule turns count
into _count when nothing ever uses it.
If the code has real compile errors (not just warnings), the semantic rules try to fix those first, then compile again to catch any warnings the errors were hiding. This try-again loop runs up to three times.
Round 3: Pattern — fix clumsy-but-working code
These rules look at the parsed tree for code that compiles fine and passes its tests but isn't written the Elixir way. Think of it as a picky code reviewer who knows the mistakes AIs tend to make.
The compile check first. Before any Pattern rule runs, Credence compiles the code one more time to make sure it actually works. If it doesn't — say there's a variable that was never set and no semantic rule could fix it — the Pattern rules are skipped. This is on purpose. Pattern rules rewrite code based on its tree shape, and rewriting code that's already broken tends to make things worse, not better. Skipping is the safe choice.
When that check passes, each rule gets the tree and walks it looking for a
particular shape. For example, NoExplicitSumReduce looks for:
Enum.reduce(list, 0, fn x, acc -> acc + x end)
and replaces it with:
Enum.sum(list)
Rules run in order (lower priority number runs first), and each rule sees the code as the rules before it left it.
The after-the-fix check. After each rule makes its change, Credence compiles
the result. If the new code doesn't compile — a buggy rule, or a rule whose
change was fine on its own but clashed with an earlier one — Credence undoes
that rule's change and keeps going. The undone rule shows up as {Rule, :reverted} in applied_rules, and a [warning] line in the log names it. This
keeps one broken rule from wrecking everything after it.
What you see in the logs
Every step of the fix is written to the log at :debug level with a
[credence_fix] tag. Turn your Logger up to :debug and you'll see exactly
what happened:
[debug] [credence_fix] syntax fix pipeline: source already parses, skipping
[debug] [credence_fix] starting semantic fix pipeline (max 3 passes, 6 rules)
[debug] [credence_fix] semantic pass 1: compilation OK, 1 warning(s)
[debug] [credence_fix] UnusedVariable: matched diagnostic, running fix...
[debug] [credence_fix] UnusedVariable: source CHANGED:
L4 - unused = 1
L4 + _unused = 1
[debug] [credence_fix] semantic done. Applied: [UnusedVariable(1)]
[debug] [credence_fix] starting pattern fix pipeline (117 rules)
[debug] [credence_fix] NoExplicitSumReduce: check found 1 issue(s), running fix...
[debug] [credence_fix] NoExplicitSumReduce: source CHANGED:
L5 - Enum.reduce(list, 0, fn x, acc -> acc + x end)
L5 + Enum.sum(list)
[debug] [credence_fix] done. Applied: [NoExplicitSumReduce(1)]
Every line that changed is shown as a before/after. When something goes wrong, the log tells you which rule ran, what it changed, and where Credence stopped.
What you get back
Credence.fix/2 hands back a map with three keys:
%{
code: "...", # the fixed source string
issues: [...], # problems still found after all the fixing
applied_rules: [...] # {rule_module, issue_count | :reverted} for every rule that ran
}
issues is whatever check/2 still flags after all the fixing is done. Since
every Pattern rule fixes what it finds, this list is usually empty — but if a
rule's fix produced code that wouldn't compile, the after-the-fix check undoes
it, and that original problem stays on the list for you to look at.
applied_rules tells you exactly what each round did. Each entry is
{rule_module, issue_count} when a fix worked, or {rule_module, :reverted}
when the after-the-fix check had to undo a buggy rule. The list covers all three
rounds — syntax, semantic, and pattern — so you can see the full story of what
happened to your code.
License
MIT