一键导入
add-lint-rule
// Add a new built-in lint rule to the Panache linter — wire it into the registry, gate it on the right extension/flavor, add a regression fixture with focused assertions, and document it.
// Add a new built-in lint rule to the Panache linter — wire it into the registry, gate it on the right extension/flavor, add a regression fixture with focused assertions, and document it.
Drive the staged in-tree YAML formatter rollout — implement the rule-based style spec, cross-validate against pretty_yaml, joint parser+formatter cutover, then hashpipe extension. Sibling to yaml-shadow-expand (parser-coverage); invoke when the work is formatter-side or the joint cutover gate.
Guard Panache's YAML shadow parser coverage — yaml-test-suite parity, allowlist nibbling, triage regen, and parser-side cluster fixes. Sibling to yaml-formatter-cutover (which owns the in-tree formatter rollout and joint cutover); invoke this one when the work is parser-coverage or test-suite triage.
Incrementally make Panache's CST shape for HTML-block /
Incrementally migrate Panache's Pandoc-dialect inline parsing onto the unified inline IR (currently CommonMark-only) one bounded sub-task at a time, verifying every CST divergence against pandoc-native before fixing or deferring.
Triage and fix panache smoke-test regressions (idempotency, losslessness, parse/format checks) from CI debug-format reports and linked issues.
Profile-driven performance work on the panache parser or formatter. Measure first with perf + the right harness; classify hotspots into one of a small set of buckets; apply the matching cheap fix; verify median wall-time moved before committing.
| name | add-lint-rule |
| description | Add a new built-in lint rule to the Panache linter — wire it into the registry, gate it on the right extension/flavor, add a regression fixture with focused assertions, and document it. |
Use this skill when asked to add a new built-in lint rule (warning, error, or info), regardless of whether it ships with an auto-fix.
src/linter/external_linters* and are out of scope here.crates/panache-parser/src/syntax/ rather than
re-parsing inside the rule.LintRunner. The rule must
not emit CLI-formatted strings; it produces Diagnostic values and the
shared rendering paths handle presentation.src/linter/rules.rs — Rule trait, RuleRegistry, pub mod list. Every
new rule module is declared here.src/linter/rules/<rule_name>.rs — one file per rule. Contains the
pub struct <Name>Rule plus its impl Rule and unit tests.src/linter.rs — default_registry() registers each rule, gated on
extension/flavor flags and config.lint.is_rule_enabled(...).src/linter/diagnostics.rs — Diagnostic, Severity, Location, Edit,
Fix, DiagnosticNoteKind. The full builder API for diagnostics.src/syntax.rs — re-exports SyntaxKind, SyntaxNode, and typed AST
wrappers from panache_parser::syntax.tests/linting.rs + tests/linting/<rule_name>.{md,qmd,Rmd} — integration
test fixtures. Pattern: a focused fixture file plus a #[test] that filters
diagnostics by code and asserts count, span, and (if present) fix shape.docs/guide/linting.qmd — user-facing reference. Every rule needs a section,
and auto-fix-capable rules also get a bullet under "Auto-Fix Capabilities".Pick the rule name (kebab-case) — this is the diagnostic code, the
config key under [lint.rules], and the slug used in URLs/help text. It
must be unique and stable: renaming it is a breaking config change. Match
tone of existing names (heading-hierarchy, duplicate-reference-labels,
adjacent-footnote-refs).
Decide gating before writing code:
Warning is the default; Error only for genuinely broken
output; Info is reserved.default_registry: e.g. ext.footnotes,
ext.citations, ext.emoji, or
matches!(config.flavor, Flavor::Quarto | Flavor::RMarkdown) for
chunk-related rules. Skip the gate only if the rule is universally
applicable.Fix when the replacement is unambiguous and
preserves intent. If multiple resolutions are valid (rename vs delete vs
merge), omit the fix and explain why in the docs.Write a failing test first (TDD per AGENTS.md). Either:
#[cfg(test)] mod tests, using
crate::parser::parse(input, Some(config.clone())) and calling
Rule::check directly, ortests/linting/<rule_name>.{md,qmd,Rmd} and
a #[test] in tests/linting.rs that calls lint_file(...) and filters
by d.code == "<rule-name>".
Cover the positive case, the negative ("should not flag") case, and any
edge case the rule explicitly handles.Implement the rule in src/linter/rules/<rule_name>.rs:
tree.descendants() and match on SyntaxKind for raw kinds, or
cast to typed wrappers (Heading::cast(node), FootnoteReference::cast,
etc.) when available — typed wrappers are preferred wherever they exist.Location with Location::from_range(range, input) or
Location::from_node(&node, input).TextRange::new(p, p))
and replacements over a precise span rather than rewriting whole
nodes. Multi-edit fixes are allowed but must be independent — they are
applied in source order.fn check(
&self,
tree: &SyntaxNode,
input: &str,
config: &Config,
metadata: Option<&crate::metadata::DocumentMetadata>,
) -> Vec<Diagnostic>
Even unused params should keep their names (_config, _metadata).Wire it up:
pub mod <rule_name>; to src/linter/rules.rs (alphabetical, with
the rest of the pub mod list).src/linter.rs::default_registry behind the right gate:
if ext.<flag> && config.lint.is_rule_enabled("<rule-name>") {
registry.register(Box::new(rules::<rule_name>::<Name>Rule));
}
Even default-enabled rules must call is_rule_enabled so users can opt
out via [lint.rules].Document in docs/guide/linting.qmd:
### \`subsection under "Lint Rules", placed near thematically related rules. Use the existing definition-list shape:Severity, Auto-fix, Requirements(if any),Description, then an Example violation:block, and (if auto-fixable) anAuto-fix output:` block.Validate in this order:
cargo test --lib <rule_name> and
cargo test --test linting <test_name>.cargo run --quiet -- lint /tmp/<fixture>.md and
cargo run --quiet -- lint --fix /tmp/<fixture>.md (verify the file
contents after --fix).cargo check --workspace, cargo test --workspace,
cargo clippy --workspace --all-targets --all-features -- -D warnings,
cargo fmt -- --check.src/linter/ (e.g. via crate::salsa::symbol_usage_index_from_tree),
not duplicated.LintRunner::run_with_metadata
already filters by ignored ranges, so the rule emits unconditionally.eprintln! from a rule. Return
Diagnostic values and let the renderer handle output.input. Walk the CST/AST.When done, report:
default_registry.rules.rs,
linter.rs, linting.rs, linting.qmd).cargo test --workspace, clippy, fmt).