use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use crate::config::LintConfig;
use crate::linter::check::{check_description_document, check_document, check_paths_with_config};
use crate::linter::diagnostic::{Diagnostic, Fix};
use crate::linter::fix::apply_fixes;
use crate::linter::render::{OutputMode, render_findings};
use crate::linter::rules::{AnyRule, Example, rules_by_category};
const PAGE_PREAMBLE: &str = "\
<!-- Generated by `cargo run --example docgen`. Do not edit by hand: edit each rule's \
`description()`/`examples()` in `src/linter/rules/` and regenerate. -->
# Lint rules
`arity lint` runs a set of built-in rules over each file and reports a finding
for every match. This page is the catalogue: one section per rule, keyed by its
stable **rule ID**. That ID is what a finding reports, what `select`/`ignore`
target in the [`[lint]` table](configuration.md#lint), and what an
`# arity-lint skip` comment names (see [Directives](directives.md)).
Where a rewrite is unambiguous a rule carries an autofix. A *safe* fix (shown
below as \"After applying the fix\") is applied by `arity lint --fix`; an
*unsafe* one is applied only with `--unsafe-fixes` or as an editor code action,
so it has no \"after\" block here. A fix is a textual edit and never lays code
out, so the intended pipeline is fix-then-format.
Every example below is linted live to produce its diagnostic and fixed output,
so this page never drifts from the rules' actual behavior.
";
fn example_path(rule: &AnyRule) -> PathBuf {
match rule {
AnyRule::R(_) => PathBuf::from("example.R"),
AnyRule::Dcf(_) => PathBuf::from("DESCRIPTION"),
}
}
fn example_language(rule: &AnyRule) -> &'static str {
match rule {
AnyRule::R(_) => "r",
AnyRule::Dcf(_) => "text",
}
}
pub fn example_lint_config(rule: &AnyRule) -> LintConfig {
let mut select = vec![rule.id().to_string()];
select.extend(rule.doc_select().iter().map(|id| id.to_string()));
LintConfig {
select: Some(select),
compat: rule.doc_compat(),
..Default::default()
}
}
pub fn lint_example(rule: &AnyRule, example: &Example) -> Vec<Diagnostic> {
let config = example_lint_config(rule);
let path = example_path(rule);
if !rule.doc_package().is_empty() {
return lint_example_in_package(rule, example, &config, &path);
}
match rule {
AnyRule::R(_) => check_document(&path, example.source, &config),
AnyRule::Dcf(_) => check_description_document(&path, example.source, &config),
}
.unwrap_or_default()
}
fn lint_example_in_package(
rule: &AnyRule,
example: &Example,
config: &LintConfig,
path: &Path,
) -> Vec<Diagnostic> {
let Ok(dir) = tempfile::tempdir() else {
return Vec::new();
};
let root = dir.path();
let example_at = match rule {
AnyRule::R(_) => root.join("R").join("example.R"),
AnyRule::Dcf(_) => root.join("DESCRIPTION"),
};
let files = rule
.doc_package()
.iter()
.map(|(relative, contents)| (root.join(relative), *contents))
.chain(std::iter::once((example_at.clone(), example.source)));
for (at, contents) in files {
if at
.parent()
.is_some_and(|parent| std::fs::create_dir_all(parent).is_err())
|| std::fs::write(&at, contents).is_err()
{
return Vec::new();
}
}
let Ok(result) = check_paths_with_config(&[root.to_path_buf()], config) else {
return Vec::new();
};
result
.reports
.into_iter()
.filter(|report| report.path == example_at)
.flat_map(|report| report.diagnostics)
.map(|mut d| {
d.path = path.to_path_buf();
d
})
.collect()
}
pub fn render_rules_page() -> String {
let mut out = String::from(PAGE_PREAMBLE);
let categories: Vec<_> = rules_by_category()
.into_iter()
.map(|(category, rules)| {
let documented: Vec<_> = rules
.into_iter()
.filter(|rule| !rule.examples().is_empty())
.collect();
(category, documented)
})
.filter(|(_, rules)| !rules.is_empty())
.collect();
for (category, rules) in &categories {
let _ = writeln!(out);
let _ = writeln!(out, "**{}**", category.title());
let _ = writeln!(out);
for rule in rules {
let id = rule.id();
let _ = writeln!(out, "- [`{id}`](#{id})");
}
}
for (category, rules) in &categories {
let _ = writeln!(out);
let _ = writeln!(out, "## {}", category.title());
for rule in rules {
let _ = writeln!(out);
let _ = out.write_str(&render_rule_doc(rule));
}
}
out
}
pub fn render_rule_doc(rule: &AnyRule) -> String {
let mut out = String::new();
let id = rule.id();
let _ = writeln!(out, "### `{id}`");
let description = rule.description().trim();
if !description.is_empty() {
let _ = writeln!(out);
let _ = writeln!(out, "{description}");
}
let _ = writeln!(out);
let status = if rule.default_enabled() {
"This rule is **enabled by default**."
} else {
"This rule is **disabled by default**; enable it with `select`."
};
let _ = writeln!(out, "{status}");
let language = example_language(rule);
let path = example_path(rule);
for example in rule.examples() {
let _ = writeln!(out);
if !example.caption.is_empty() {
let _ = writeln!(out, "{}", example.caption);
let _ = writeln!(out);
}
fenced(&mut out, language, example.source);
let diagnostics = lint_example(rule, example);
let source = example.source.to_string();
let rendered = render_findings(&diagnostics, OutputMode::Pretty, false, &|candidate| {
(candidate == &path).then(|| source.clone())
});
let _ = writeln!(out);
fenced(&mut out, "text", &rendered);
let fixes: Vec<Fix> = diagnostics.iter().filter_map(|d| d.fix.clone()).collect();
let after = apply_fixes(example.source, &fixes, false);
if after.applied > 0 {
let _ = writeln!(out);
let _ = writeln!(out, "After applying the fix:");
let _ = writeln!(out);
fenced(&mut out, language, &after.output);
}
}
out
}
fn fenced(out: &mut String, lang: &str, body: &str) {
let _ = writeln!(out, "```{lang}");
let _ = out.write_str(body);
if !body.ends_with('\n') {
let _ = out.write_str("\n");
}
let _ = writeln!(out, "```");
}