Current section

Files

Jump to
credence lib rule_scaffold.ex
Raw

lib/rule_scaffold.ex

defmodule Credence.RuleScaffold do
@moduledoc """
Pure templates for a new rule and its test files. `files/2` returns
`[{path, content}]` for a type — no disk I/O — so it is testable in memory and
reused by both the Mix task (`mix credence.gen.rule`) that writes the files and
the pin (`Credence.GeneratorMetaTest`) that proves the output passes every
structural gate.
The emitted tests are **intentionally red**: each carries a real positive /
transform assertion that fails against the inert stub, alongside the negative /
fixpoint / attribution *shapes* the structural gates require — so the gates pass
while the rule is unmistakably unfinished. Fixtures are heredocs and use
parseable placeholders (`foo(bar)` / `baz(qux)`), so the parser never crashes on
them at runtime.
Templates carry `__TQ__` for a `\"""` heredoc delimiter (so the template strings
themselves don't nest heredocs) and `__RULE_MODULE__` / `__TEST_MODULE__` /
`__RULE__` / `__SNAKE__` name slots, all filled by `render/3` from
`Credence.RuleName`.
"""
alias Credence.RuleName
@doc "File plans (`[{path, content}]`) for a new `name` rule of `type`."
@spec files(String.t(), RuleName.rule_type()) :: [{String.t(), String.t()}]
def files(name, type) do
d = RuleName.derive(name, type)
case type do
:pattern -> pattern_files(d)
:syntax -> syntax_files(d)
:semantic -> semantic_files(d)
end
end
# ---------------------------------------------------------------- pattern ----
defp pattern_files(d) do
[
{d.rule_path, render(pattern_rule(), d, nil)},
{RuleName.test_path(d, "check"), render(pattern_check(), d, "check")},
{RuleName.test_path(d, "fix"), render(pattern_fix(), d, "fix")},
{RuleName.test_path(d, "equivalence"), render(pattern_equivalence(), d, "equivalence")}
]
end
defp pattern_rule do
~S"""
defmodule __RULE_MODULE__ do
@moduledoc __TQ__
TODO: describe the anti-pattern this rule detects and fixes.
## Bad
# TODO: example of the flagged code
## Good
# TODO: example of the rewritten code
__TQ__
use Credence.Pattern.Rule
@impl true
def check(_ast, _opts) do
# TODO: walk the AST and return issues, e.g.:
# [%Issue{rule: :__SNAKE__, message: "...", meta: %{line: line}}]
[]
end
@impl true
def fix_patches(_ast, _opts) do
# TODO: emit patches, e.g. via Credence.RuleHelpers.patches_from_postwalk/2
[]
end
end
"""
end
defp pattern_check do
~S"""
defmodule __TEST_MODULE__ do
use Credence.RuleCase, async: true
alias __RULE_MODULE__
test "flags the anti-pattern" do
# TODO: replace with real code the rule should flag
assert flagged?(__RULE__, __TQ__
foo(bar)
__TQ__)
end
test "leaves good code alone" do
# TODO: replace with real code the rule should ignore
assert clean?(__RULE__, __TQ__
baz(qux)
__TQ__)
end
end
"""
end
defp pattern_fix do
~S"""
defmodule __TEST_MODULE__ do
use Credence.RuleCase, async: true
alias __RULE_MODULE__
test "rewrites the anti-pattern" do
# TODO: replace input/expected with a real before/after pair
input = __TQ__
foo(bar)
__TQ__
expected = __TQ__
baz(qux)
__TQ__
assert fix(__RULE__, input) == expected
end
end
"""
end
defp pattern_equivalence do
~S"""
defmodule __TEST_MODULE__ do
use Credence.RuleCase, async: true
alias __RULE_MODULE__
test "fix preserves behaviour" do
# TODO: replace with a real before-expression the rule fires on, its free
# variables, and inputs (see Credence.EquivalenceInputs, e.g. B.term_lists()).
assert_equivalent(
__TQ__
foo(bar)
__TQ__,
rule: __RULE__,
vars: [:bar],
inputs: [1, 2, 3]
)
end
end
"""
end
# ----------------------------------------------------------------- syntax ----
defp syntax_files(d) do
[
{d.rule_path, render(syntax_rule(), d, nil)},
{RuleName.test_path(d, "analyze"), render(syntax_analyze(), d, "analyze")},
{RuleName.test_path(d, "fix"), render(syntax_fix(), d, "fix")}
]
end
defp syntax_rule do
~S"""
defmodule __RULE_MODULE__ do
@moduledoc __TQ__
TODO: describe the unparseable syntax this rule detects and fixes.
__TQ__
use Credence.Syntax.Rule
@impl true
def analyze(_source) do
# TODO: scan the source and return issues, e.g.:
# [%Credence.Issue{rule: :__SNAKE__, message: "...", meta: %{line: line}}]
[]
end
@impl true
def fix(source) do
# TODO: return the repaired source
source
end
end
"""
end
defp syntax_analyze do
~S"""
defmodule __TEST_MODULE__ do
use ExUnit.Case
alias Credence.Issue
alias __RULE_MODULE__
defp analyze(code), do: __RULE__.analyze(code)
test "flags the unparseable code" do
# TODO: replace with real code the rule should flag
assert [%Issue{rule: :__SNAKE__}] =
analyze(__TQ__
foo(bar)
__TQ__)
end
test "leaves good code alone" do
# TODO: replace with real code the rule should ignore
assert analyze(__TQ__
baz(qux)
__TQ__) == []
end
end
"""
end
defp syntax_fix do
~S"""
defmodule __TEST_MODULE__ do
use ExUnit.Case
import Credence.RuleCase, only: [valid_syntax?: 1]
alias __RULE_MODULE__
defp analyze(code), do: __RULE__.analyze(code)
defp fix(code), do: __RULE__.fix(code)
test "fixes the syntax error" do
# TODO: replace input/expected with a real before/after pair
input = __TQ__
foo(bar)
__TQ__
expected = __TQ__
baz(qux)
__TQ__
assert fix(input) == expected
end
test "fixed output no longer flags" do
# TODO: replace with real code the rule should flag, then fix to clean
assert analyze(fix(__TQ__
foo(bar)
__TQ__)) == []
end
test "fixed output is well-formed (parses)" do
# the repaired source must be valid Elixir
assert valid_syntax?(fix(__TQ__
foo(bar)
__TQ__))
end
end
"""
end
# --------------------------------------------------------------- semantic ----
defp semantic_files(d) do
[
{d.rule_path, render(semantic_rule(), d, nil)},
{RuleName.test_path(d, "check"), render(semantic_check(), d, "check")},
{RuleName.test_path(d, "fix"), render(semantic_fix(), d, "fix")}
]
end
defp semantic_rule do
~S"""
defmodule __RULE_MODULE__ do
@moduledoc __TQ__
TODO: describe the compiler diagnostic this rule matches and fixes.
__TQ__
use Credence.Semantic.Rule
alias Credence.Issue
@impl true
def match?(_diagnostic) do
# TODO: return true for the diagnostics this rule handles
false
end
@impl true
def to_issue(diagnostic) do
# TODO: build the issue this rule reports
%Issue{rule: :__SNAKE__, message: "TODO", meta: %{line: line(diagnostic)}}
end
@impl true
def fix(source, _diagnostic) do
# TODO: return the repaired source
source
end
defp line(%{position: {line, _col}}), do: line
defp line(%{position: line}) when is_integer(line), do: line
end
"""
end
defp semantic_check do
~S"""
defmodule __TEST_MODULE__ do
use ExUnit.Case
alias __RULE_MODULE__
test "matches the diagnostic" do
# TODO: replace with a diagnostic this rule should handle
diag = %{severity: :warning, message: "TODO matching message", position: {1, 1}}
assert __RULE__.match?(diag)
end
test "ignores unrelated diagnostics" do
diag = %{severity: :warning, message: "unrelated", position: {1, 1}}
refute __RULE__.match?(diag)
end
test "attributes the issue to this rule" do
diag = %{severity: :warning, message: "TODO matching message", position: {1, 1}}
assert __RULE__.to_issue(diag).rule == :__SNAKE__
end
end
"""
end
defp semantic_fix do
~S"""
defmodule __TEST_MODULE__ do
use ExUnit.Case
import Credence.RuleCase, only: [valid_syntax?: 1]
alias __RULE_MODULE__
defp fix(source, message, line \\ 1) do
__RULE__.fix(source, %{severity: :warning, message: message, position: {line, 1}})
end
test "fixes the source" do
# TODO: replace input/expected/message with a real case
input = __TQ__
foo(bar)
__TQ__
expected = __TQ__
baz(qux)
__TQ__
message = "TODO matching message"
assert fix(input, message) == expected
end
test "fixed output is well-formed (parses)" do
# the repaired source must be valid Elixir
message = "TODO matching message"
assert valid_syntax?(fix(__TQ__
foo(bar)
__TQ__, message))
end
end
"""
end
# ------------------------------------------------------------------ render ----
defp render(template, d, kind) do
test_module = if kind, do: inspect(RuleName.test_module(d, kind)), else: ""
template
|> String.replace("__TQ__", ~s["""])
|> String.replace("__RULE_MODULE__", inspect(d.rule_module))
|> String.replace("__TEST_MODULE__", test_module)
|> String.replace("__RULE__", d.pascal)
|> String.replace("__SNAKE__", d.snake)
end
end