Current section

Files

Jump to
credence lib credence.ex
Raw

lib/credence.ex

defmodule Credence do
@moduledoc """
Credence — Semantic Linter for Elixir.
Routes analysis and fixing through three phases:
1. **Syntax** — string-level fixes for code that won't parse
2. **Semantic** — fixes for compiler warnings (unused vars, undefined fns)
3. **Pattern** — AST-level anti-pattern rules (the bulk of Credence)
Each phase has its own `Rule` behaviour and discovers rules automatically.
"""
alias Credence.Issue
alias Credence.RuleHelpers
@type rule_status_entry :: %{
round: :syntax | :semantic | :pattern,
rule: module(),
name: String.t(),
assumptions: [atom()],
enabled: boolean(),
missing: [atom()]
}
@spec analyze(String.t(), keyword()) :: %{valid: boolean(), issues: [Issue.t()]}
def analyze(code_string, opts \\ []) do
syntax_issues = Credence.Syntax.analyze(code_string, opts)
if Enum.any?(syntax_issues) do
%{valid: false, issues: syntax_issues}
else
semantic_issues = Credence.Semantic.analyze(code_string, opts)
pattern_issues = Credence.Pattern.analyze(code_string, opts)
all_issues = semantic_issues ++ pattern_issues
%{valid: Enum.empty?(all_issues), issues: all_issues}
end
end
@spec fix(String.t(), keyword()) :: %{
code: String.t(),
issues: [Issue.t()],
applied_rules: [{module(), non_neg_integer() | :reverted}]
}
def fix(code_string, opts \\ []) do
# Phase 1: Syntax (with trace)
{after_syntax, syntax_applied} = Credence.Syntax.fix_with_trace(code_string, opts)
# Phase 2: Semantic (with trace)
{after_semantic, semantic_applied} = Credence.Semantic.fix_with_trace(after_syntax, opts)
# Phase 3: Pattern (with trace)
{fixed, pattern_applied} = Credence.Pattern.fix_with_trace(after_semantic, opts)
all_applied = syntax_applied ++ semantic_applied ++ pattern_applied
%{issues: remaining} = analyze(fixed, Keyword.put(opts, :source, fixed))
%{code: fixed, issues: remaining, applied_rules: all_applied}
end
@doc """
Reports which rules would run for the given `opts`, across all three rounds,
**without running them** — the opts-only counterpart to `fix/2`.
Returns one entry per rule, in execution order (Syntax → Semantic → Pattern),
each a map with:
- `:round` — `:syntax`, `:semantic`, or `:pattern`
- `:rule` — the rule module
- `:name` — its short name
- `:assumptions` — the promises it needs (always `[]` for Syntax/Semantic)
- `:enabled` — whether it is eligible under `opts`
- `:missing` — the needed promises that are off
Only the Pattern round is opts-filtered — by `:assumptions` and an explicit
`:rules` list (see `Credence.Pattern.rule_status/1`). Syntax and Semantic
rules are never opts-filtered, so every discovered rule comes back
`enabled: true`. Whether a rule *actually fires* further depends on the code
itself — Syntax only runs when the source won't parse, Semantic only on the
compiler diagnostics it matches — which this opts-only view does not inspect.
"""
@spec rule_status(keyword()) :: [rule_status_entry()]
def rule_status(opts \\ []) do
unfiltered_entries(:syntax, Credence.Syntax.default_rules()) ++
unfiltered_entries(:semantic, Credence.Semantic.default_rules()) ++
Enum.map(Credence.Pattern.rule_status(opts), &Map.put(&1, :round, :pattern))
end
defp unfiltered_entries(round, rules) do
Enum.map(rules, fn rule ->
%{
round: round,
rule: rule,
name: RuleHelpers.rule_name(rule),
assumptions: [],
enabled: true,
missing: []
}
end)
end
@doc """
The short names of every rule that would run under `opts`, across all three
rounds, in execution order. Derived from `rule_status/1` so the two answers
never disagree.
"""
@spec enabled_rules(keyword()) :: [String.t()]
def enabled_rules(opts \\ []) do
opts |> rule_status() |> Enum.filter(& &1.enabled) |> Enum.map(& &1.name)
end
end