Credence.RuleHelpers
(credence v0.7.1)
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: parses to a Sourceror tree,
calls the rule's fix_patches/2, applies the returned patches with
Sourceror.patch_string/2, and strips per-line trailing whitespace.
An empty patch list returns source unchanged.
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.
Resolves the effective assumption settings from opts, folding the three
layers (built-in defaults → config :credence, :assumptions → call opts),
later wins. See Credence.Assumptions for the merge algebra. User-supplied
layers are validated; an unknown switch name raises.
Returns the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]).
Keeps only the rules whose every needed promise is on under opts.
Returns true if module declares behaviour in its @behaviour attribute.
Logs a before/after diff under a [credence_fix] prefix.
Pure merge algebra for assumption layers, independent of the registry.
The assumptions a rule needs that are not on under effective settings.
An empty list means every needed promise holds. A switch the rule names that
is unknown (not in effective) counts as off — an unsatisfiable promise.
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.
Whether rule's every needed promise is on under opts.
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: parses to a Sourceror tree,
calls the rule's fix_patches/2, applies the returned patches with
Sourceror.patch_string/2, and strips per-line trailing whitespace.
An empty patch list returns source unchanged.
Used by both the orchestrator (Credence.Pattern.run_fixable_rules/3)
and rule tests, so test assertions on the post-fix source string run
through the exact bytes the pipeline ships.
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, ...]
@spec effective_assumptions(keyword()) :: Credence.Assumptions.settings()
Resolves the effective assumption settings from opts, folding the three
layers (built-in defaults → config :credence, :assumptions → call opts),
later wins. See Credence.Assumptions for the merge algebra. User-supplied
layers are validated; an unknown switch name raises.
Returns the value bound to :do in a Sourceror-shaped keyword list
([{{:__block__, _, [:do]}, body} | _]).
Returns {:ok, body} when present, :error otherwise.
Keeps only the rules whose every needed promise is on under opts.
A rule that names an unknown switch is treated as relying on a promise that
can never be satisfied, so it is filtered out and a warning is logged. When
explicit_list? is true (the caller passed an explicit rules: list), a
rule filtered out for an off (but known) promise also logs a warning, since
naming a rule and getting silence reads as "this rule is broken".
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.
@spec merge_assumptions(Credence.Assumptions.settings(), [map() | :strict | :default]) :: Credence.Assumptions.settings()
Pure merge algebra for assumption layers, independent of the registry.
Starts from defaults and folds each layer on top: :strict forces every
key off, :default resets every key to defaults, and a map patches only
the keys it names (unmentioned keys keep the value from the layer below).
@spec missing_assumptions(module(), Credence.Assumptions.settings()) :: [atom()]
The assumptions a rule needs that are not on under effective settings.
An empty list means every needed promise holds. A switch the rule names that
is unknown (not in effective) counts as off — an unsatisfiable promise.
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.
Whether rule's every needed promise is on under opts.
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.