Credence.RuleHelpers
(credence v0.6.0)
Copy Markdown
Shared utilities used by all three Credence phases (Syntax, Semantic, Pattern).
Provides rule discovery, safe compilation with diagnostics capture, diff computation, and change logging so the phase modules don't duplicate this plumbing.
Summary
Functions
Invokes a Pattern rule's fix on source, dispatching to either the
new patch-based fix_patches/2 callback (if the rule has migrated)
or the legacy fix/2 callback.
Compiles source with Code.with_diagnostics/1 and returns
{:ok, diagnostics} or {:error, diagnostics}.
Returns true if source compiles without errors.
Computes a line-by-line diff between two strings.
Returns all modules implementing behaviour, sorted by priority
(lower first) with module name as tiebreaker for determinism.
Returns the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]).
Returns true if module declares behaviour in its @behaviour attribute.
Logs a before/after diff under a [credence_fix] prefix.
Strips Sourceror.parse_string!/1's {:__block__, meta, [value]}
wrappers around literals, atoms, 2-tuples, and lists — producing the
bare-literal shape that Macro.to_string/1 expects for rendering.
Applies an AST-to-AST transform_fn, then emits patches via
AST-diff between the original AST and a re-parsed version of the
rendered output.
Emits patches by AST-diffing original against a pre-computed
transformed AST. Use when the transformation can't be expressed
as a single Macro.postwalk/2 matcher — e.g. it prunes nodes,
reorders siblings, or needs cross-clause information.
Mechanical migration helper for rules that today do
source |> Sourceror.parse_string!() |> Macro.postwalk(matcher) |> Sourceror.to_string().
Renders a replacement subtree as source text suitable for patching
back into the original source at original_range's position.
Replaces the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]) with new_body. Returns the
list unchanged if no :do entry is found.
Re-wraps a list of elements in a Sourceror :__block__ wrapper, reusing
the original wrapper's meta so bracket-position metadata survives the
rewrite. Paired with unwrap_list/1 for list-literal AST surgery.
Returns the short name of a rule module for logging.
Unwraps a Sourceror list-literal node ({:__block__, meta, [list]}).
Functions
Invokes a Pattern rule's fix on source, dispatching to either the
new patch-based fix_patches/2 callback (if the rule has migrated)
or the legacy fix/2 callback.
Used by both the orchestrator (Credence.Pattern.run_fixable_rules/3)
and rule tests, so test assertions on the post-fix source string
continue working unchanged regardless of which side of the
migration a rule is on.
Compiles source with Code.with_diagnostics/1 and returns
{:ok, diagnostics} or {:error, diagnostics}.
Uses :code.soft_purge/1 for cleanup so that compiling source
which redefines a currently-executing module does not kill the BEAM
(see :code.purge/1 — it sends an unconditional kill signal to
any process still running the old version of the module).
Returns true if source compiles without errors.
Convenience wrapper around compile_and_capture/1 for callers
that only need a boolean (e.g., the Pattern phase compile gate).
@spec diff_lines(String.t(), String.t()) :: [ {:removed, pos_integer(), String.t()} | {:added, pos_integer(), String.t()} ]
Computes a line-by-line diff between two strings.
Returns a list of {:removed, line_no, text} and {:added, line_no, text}
tuples for every line that changed.
Returns all modules implementing behaviour, sorted by priority
(lower first) with module name as tiebreaker for determinism.
iex> Credence.RuleHelpers.discover_rules(Credence.Pattern.Rule)
[Credence.Pattern.SomeRule, ...]
Returns the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]).
Returns {:ok, body} when present, :error otherwise.
Returns true if module declares behaviour in its @behaviour attribute.
Logs a before/after diff under a [credence_fix] prefix.
Shows every changed line — the diff is never truncated so that the full extent of each fix is visible in the log output.
Strips Sourceror.parse_string!/1's {:__block__, meta, [value]}
wrappers around literals, atoms, 2-tuples, and lists — producing the
bare-literal shape that Macro.to_string/1 expects for rendering.
Used by test helpers to canonicalize ASTs for structural comparison
(parse → normalize → Macro.to_string → string compare). Not used
by production rules — all rules pattern-match Sourceror's wrapped
form directly.
Unwraps:
- Literals:
{:__block__, _, [1]}→1 - Atoms:
{:__block__, _, [:do]}→:do - 2-tuples:
{:__block__, _, [{a, b}]}→{a, b} - Lists:
{:__block__, _, [[a, b]]}→[a, b]
Applies an AST-to-AST transform_fn, then emits patches via
AST-diff between the original AST and a re-parsed version of the
rendered output.
The re-parse step gives the transformed AST clean Sourceror metadata (literal wrappers, ranges) so the diff lines up structurally with the original. This is needed when a rule's transformation produces fresh subtrees with bare-literal shape that wouldn't otherwise match the original Sourceror-wrapped shape.
Returns [] if the transformation produced an identical AST.
Falls back to a whole-source patch when re-parsing fails (rare —
typically signals the transform produced invalid code).
Emits patches by AST-diffing original against a pre-computed
transformed AST. Use when the transformation can't be expressed
as a single Macro.postwalk/2 matcher — e.g. it prunes nodes,
reorders siblings, or needs cross-clause information.
The transformed AST should be built by walking original (so that
unchanged subtrees retain their Sourceror metadata for range lookup);
replacement subtrees can be freshly synthesized without metadata —
the diff uses the original node's range.
Mechanical migration helper for rules that today do
source |> Sourceror.parse_string!() |> Macro.postwalk(matcher) |> Sourceror.to_string().
Applies the matcher via Macro.postwalk/2 to build a transformed
AST, then diffs the original against the transformed and emits one
patch per outermost changed subtree. Non-overlapping by construction
— nested matches are subsumed by the outer patch.
The rule's fix_patches/2 becomes a one-line delegation:
def fix_patches(ast, _opts) do
Credence.RuleHelpers.patches_from_postwalk(ast, fn
{target_pattern, _, _} = node -> build_replacement(node)
node -> node
end)
end
Renders a replacement subtree as source text suitable for patching
back into the original source at original_range's position.
Uses Sourceror's default line_length: 98. Pass :line_length to
override — e.g. when a rule wants to force a multi-line replacement
to mirror a multi-line original (the original Issue 4 trick).
original_range is currently unused but reserved as a hint for
future rule-specific budget heuristics.
Replaces the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]) with new_body. Returns the
list unchanged if no :do entry is found.
Re-wraps a list of elements in a Sourceror :__block__ wrapper, reusing
the original wrapper's meta so bracket-position metadata survives the
rewrite. Paired with unwrap_list/1 for list-literal AST surgery.
Returns the short name of a rule module for logging.
iex> Credence.RuleHelpers.rule_name(Credence.Pattern.NoSortThenAt)
"NoSortThenAt"
Unwraps a Sourceror list-literal node ({:__block__, meta, [list]}).
Returns {:ok, elements, original_node} — the second value is the
wrapper itself so the caller can pass it to rewrap_list/2 later to
preserve bracket-position metadata on a rewrite.