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

apply_rule_fix(rule, source, opts \\ [])

@spec apply_rule_fix(module(), String.t(), keyword()) :: String.t()

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.

compile_and_capture(source)

@spec compile_and_capture(String.t()) :: {:ok, [map()]} | {:error, [map()]}

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).

compiles?(source)

@spec compiles?(String.t()) :: boolean()

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).

diff_lines(before, after_fix)

@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.

discover_rules(behaviour)

@spec discover_rules(module()) :: [module()]

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, ...]

effective_assumptions(opts)

@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.

extract_do_body(kw)

@spec extract_do_body(list()) :: {:ok, term()} | :error

Returns the value bound to :do in a Sourceror-shaped keyword list ([{{:__block__, _, [:do]}, body} | _]).

Returns {:ok, body} when present, :error otherwise.

filter_by_assumptions(rules, opts, explicit_list? \\ false)

@spec filter_by_assumptions([module()], keyword(), boolean()) :: [module()]

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".

implements?(module, behaviour)

@spec implements?(module(), module()) :: boolean()

Returns true if module declares behaviour in its @behaviour attribute.

log_diff(label, before, after_fix)

@spec log_diff(String.t(), String.t(), String.t()) :: :ok

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.

merge_assumptions(defaults, layers)

@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).

missing_assumptions(rule, effective)

@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.

patches_from_ast_transform(ast, source, transform_fn)

@spec patches_from_ast_transform(Macro.t(), String.t(), (Macro.t() -> Macro.t())) :: [
  map()
]

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).

patches_from_diff(original, transformed)

@spec patches_from_diff(Macro.t(), Macro.t()) :: [map()]

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.

patches_from_postwalk(ast, matcher)

@spec patches_from_postwalk(Macro.t(), (Macro.t() -> Macro.t())) :: [map()]

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

render_replacement(new_ast, original_range, opts \\ [])

@spec render_replacement(Macro.t(), map(), keyword()) :: String.t()

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.

replace_do_body(kw_list, new_body)

@spec replace_do_body(list(), term()) :: list()

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.

rewrap_list(arg, new_elements)

@spec rewrap_list(term(), list()) :: term()

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.

rule_enabled?(rule, opts)

@spec rule_enabled?(
  module(),
  keyword()
) :: boolean()

Whether rule's every needed promise is on under opts.

rule_name(module)

@spec rule_name(module()) :: String.t()

Returns the short name of a rule module for logging.

iex> Credence.RuleHelpers.rule_name(Credence.Pattern.NoSortThenAt)
"NoSortThenAt"

unwrap_list(node)

@spec unwrap_list(term()) :: {:ok, list(), term()} | :error

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.