Current section
Files
Jump to
Current section
Files
lib/pattern/rule.ex
defmodule Credence.Pattern.Rule do
@moduledoc """
Behaviour for pattern-level rules that detect and auto-fix anti-patterns.
Every Pattern rule fixes the issue it detects — there is no "warn-only"
mode. Rules that could only detect but not fix were archived to
`docs/unfixable_rules/` and removed from compilation.
## Interface
Three callbacks, all mandatory (only `priority/0` has a default):
- **`priority() :: integer()`** — fire order, lower runs first. Default 500.
- **`check(ast, opts) :: [Issue.t()]`** — detect issues in the AST.
- **`fix_patches(ast, opts) :: [patch]`** — emit byte-range patches that,
when applied, resolve the issues `check/2` reported. Empty list = no
change.
Both callbacks receive Sourceror AST (`Sourceror.parse_string!/1`) and
`opts` containing `:source` for rules that need raw source bytes.
## Implementation choices for `fix_patches/2`
All rules return `[patch]`, but the way they compute those patches
varies with what the transformation needs:
- **AST-walking** — `Credence.RuleHelpers.patches_from_postwalk/2`
handles rules whose fix is a single `Macro.postwalk/2` matcher.
- **AST-with-restructuring** —
`Credence.RuleHelpers.patches_from_ast_transform/3` for fixes that
prune, reorder, or insert siblings (the rendered result is re-parsed
for clean range diffing).
- **Direct patch emission** — a rule walks the AST itself and builds
`[%{range: Sourceror.Range, change: String.t()}]` manually. Useful
when the kept subtree's source bytes must be preserved verbatim
(e.g. parens metadata Sourceror's renderer would drop) — slice the
original source bytes for the kept range instead of re-rendering.
See each helper's docstring for the rule-side calling convention.
"""
@typedoc """
A byte-range patch against the source string.
- `range` carries Sourceror-style start/end positions (`[line: L, column: C]`).
- `change` is the replacement text. Apply via `Sourceror.patch_string/2`.
"""
@type patch :: %{
required(:range) => map(),
required(:change) => String.t()
}
@callback priority() :: integer()
@doc "Detect issues in the AST. Returns list of issues."
@callback check(ast :: Macro.t(), opts :: keyword()) :: [Credence.Issue.t()]
@doc "Auto-fix via byte-range patches. Returns a list of patches; `[]` means no change."
@callback fix_patches(ast :: Macro.t(), opts :: keyword()) :: [patch()]
defmacro __using__(_opts) do
quote do
@behaviour Credence.Pattern.Rule
alias Credence.Issue
@impl true
def priority, do: 500
defoverridable priority: 0
end
end
end