Current section

Files

Jump to
credence lib syntax.ex
Raw

lib/syntax.ex

defmodule Credence.Syntax do
@moduledoc """
Syntax phase — fixes code that won't parse.
Only runs when `Sourceror.parse_string/1` fails. Delegates to rules
implementing `Credence.Syntax.Rule` behaviour.
Rules are discovered automatically and run in priority order (lower first),
with module name as tiebreaker for determinism.
"""
require Logger
alias Credence.RuleHelpers
@spec analyze(String.t(), keyword()) :: [Credence.Issue.t()]
def analyze(source, _opts \\ []) do
case Sourceror.parse_string(source) do
{:ok, _ast} -> []
{:error, _} -> Enum.flat_map(rules(), & &1.analyze(source))
end
end
@spec fix(String.t(), keyword()) :: String.t()
def fix(source, opts \\ []) do
{code, _applied} = fix_with_trace(source, opts)
code
end
@doc """
Like `fix/2`, but also returns a list of `{rule_module, issue_count}` tuples
for every rule that actually fired and was applied.
Every step is logged via `Logger.debug` with `[credence_fix]` prefix:
rule name, whether the source changed, and a before/after diff of the
lines that were modified. If the source already parses, the pipeline
is skipped entirely.
"""
@spec fix_with_trace(String.t(), keyword()) ::
{String.t(), [{module(), non_neg_integer()}]}
def fix_with_trace(source, _opts \\ []) do
all_rules = rules()
case Sourceror.parse_string(source) do
{:ok, _ast} ->
Logger.debug("[credence_fix] syntax fix pipeline: source already parses, skipping")
{source, []}
{:error, {meta, error_msg, token}} ->
line = Keyword.get(meta, :line)
Logger.debug(
"[credence_fix] starting syntax fix pipeline (#{length(all_rules)} rules), " <>
"parse error at line #{line}: #{error_msg} near #{inspect(token)}"
)
{fixed, applied} =
Enum.reduce(all_rules, {source, []}, fn rule, {src, applied} ->
name = RuleHelpers.rule_name(rule)
result = rule.fix(src)
if result == src do
{src, applied}
else
Logger.debug("[credence_fix] #{name}: fix produced a change")
RuleHelpers.log_diff(name, src, result)
{result, [{rule, 1} | applied]}
end
end)
applied = Enum.reverse(applied)
# Verify fix actually helped
case Sourceror.parse_string(fixed) do
{:ok, _} ->
Logger.debug("[credence_fix] syntax fix pipeline: source now parses successfully")
{:error, {meta, error_msg, token}} ->
line = Keyword.get(meta, :line)
Logger.debug(
"[credence_fix] syntax fix pipeline: source still does not parse " <>
"(line #{line}: #{error_msg} near #{inspect(token)})"
)
end
summary =
Enum.map_join(applied, ", ", fn {mod, count} ->
"#{RuleHelpers.rule_name(mod)}(#{count})"
end)
Logger.debug("[credence_fix] syntax done. Applied: [#{summary}]")
{fixed, applied}
end
end
defp rules do
RuleHelpers.discover_rules(Credence.Syntax.Rule)
end
end