Current section

Files

Jump to
credence lib semantic.ex
Raw

lib/semantic.ex

defmodule Credence.Semantic do
@moduledoc """
Semantic phase — fixes compiler warnings and errors.
Uses `Code.with_diagnostics/1` to compile the source and capture
diagnostics without permanently loading modules. Delegates to rules
implementing `Credence.Semantic.Rule` behaviour.
When compilation succeeds, warning-level diagnostics are matched
against rules and fixed. When compilation fails, error-level
diagnostics are matched first; if any fix is applied, the phase
retries (up to `max_passes`) to catch warnings that only appear
once the error is resolved.
"""
alias Credence.RuleHelpers
@default_max_passes 3
@spec analyze(String.t(), keyword()) :: [Credence.Issue.t()]
def analyze(source, _opts \\ []) do
case compile_and_capture(source) do
{:ok, diagnostics} ->
diagnostics
|> Enum.filter(&(&1.severity == :warning))
|> Enum.flat_map(&match_rules/1)
{:error, diagnostics} ->
diagnostics
|> Enum.filter(&(&1.severity == :error))
|> Enum.flat_map(&match_rules/1)
end
end
@spec fix(String.t(), keyword()) :: String.t()
def fix(source, opts \\ []) do
max_passes = Keyword.get(opts, :max_passes, @default_max_passes)
do_fix(source, max_passes)
end
defp do_fix(source, 0), do: source
defp do_fix(source, passes_remaining) do
case compile_and_capture(source) do
{:ok, diagnostics} ->
# Compilation succeeded — fix warnings (terminal pass, no retry needed)
apply_fixes(source, diagnostics, :warning)
{:error, diagnostics} ->
# Compilation failed — fix errors, then retry
fixed = apply_fixes(source, diagnostics, :error)
if fixed != source do
do_fix(fixed, passes_remaining - 1)
else
# No rule could fix the error — return as-is
fixed
end
end
end
defp apply_fixes(source, diagnostics, severity) do
diagnostics
|> Enum.filter(&(&1.severity == severity))
|> Enum.reduce(source, fn diagnostic, src ->
case find_matching_rule(diagnostic) do
nil -> src
rule -> rule.fix(src, diagnostic)
end
end)
end
defp compile_and_capture(source) do
{result, diagnostics} =
Code.with_diagnostics(fn ->
try do
Code.compile_string(source, "credence_check.ex")
rescue
_ -> :error
end
end)
case result do
:error ->
{:error, diagnostics}
modules when is_list(modules) ->
cleanup_modules(modules)
{:ok, diagnostics}
end
end
defp cleanup_modules(modules) do
for {mod, _binary} <- modules do
:code.purge(mod)
:code.delete(mod)
end
end
defp match_rules(diagnostic) do
case find_matching_rule(diagnostic) do
nil -> []
rule -> [rule.to_issue(diagnostic)]
end
end
defp find_matching_rule(diagnostic) do
Enum.find(rules(), fn rule -> rule.match?(diagnostic) end)
end
defp rules do
RuleHelpers.discover_rules(Credence.Semantic.Rule)
end
end