mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 13:30:49 -05:00
Previously the rule documentation referenced configuration options
via full https:// URLs, which was bad for several reasons:
* changing the website would mean you'd have to change all URLs
* the links didn't work when building mkdocs locally
* the URLs showed up in the `ruff rule` output
* broken references weren't detected by our CI
This commit solves all of these problems by post-processing the
Markdown, recognizing sections such as:
## Options
* `flake8-tidy-imports.ban-relative-imports`
`cargo dev generate-all` will automatically linkify such references
and panic if the referenced option doesn't exist.
Note that the option can also be linked in the other Markdown sections
via e.g. [`flake8-tidy-imports.ban-relative-imports`] since
the post-processing code generates a CommonMark link definition.
Resolves #2766.
86 lines
2.7 KiB
Rust
86 lines
2.7 KiB
Rust
//! Generate Markdown documentation for applicable rules.
|
|
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
|
|
|
use std::fs;
|
|
|
|
use anyhow::Result;
|
|
use ruff::registry::{Linter, Rule, RuleNamespace};
|
|
use ruff::settings::options::Options;
|
|
use ruff::settings::options_base::ConfigurationOptions;
|
|
use ruff::AutofixAvailability;
|
|
use strum::IntoEnumIterator;
|
|
|
|
#[derive(clap::Args)]
|
|
pub struct Args {
|
|
/// Write the generated docs to stdout (rather than to the filesystem).
|
|
#[arg(long)]
|
|
pub(crate) dry_run: bool,
|
|
}
|
|
|
|
pub fn main(args: &Args) -> Result<()> {
|
|
for rule in Rule::iter() {
|
|
if let Some(explanation) = rule.explanation() {
|
|
let mut output = String::new();
|
|
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.code()));
|
|
output.push('\n');
|
|
output.push('\n');
|
|
|
|
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
|
output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
|
|
output.push('\n');
|
|
output.push('\n');
|
|
|
|
if let Some(autofix) = rule.autofixable() {
|
|
output.push_str(match autofix.available {
|
|
AutofixAvailability::Sometimes => "Autofix is sometimes available.",
|
|
AutofixAvailability::Always => "Autofix is always available.",
|
|
});
|
|
output.push('\n');
|
|
output.push('\n');
|
|
}
|
|
|
|
process_documentation(explanation.trim(), &mut output);
|
|
|
|
if args.dry_run {
|
|
println!("{output}");
|
|
} else {
|
|
fs::create_dir_all("docs/rules")?;
|
|
fs::write(format!("docs/rules/{}.md", rule.as_ref()), output)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn process_documentation(documentation: &str, out: &mut String) {
|
|
let mut in_options = false;
|
|
let mut after = String::new();
|
|
|
|
for line in documentation.split_inclusive('\n') {
|
|
if line.starts_with("## ") {
|
|
in_options = line == "## Options\n";
|
|
} else if in_options {
|
|
if let Some(rest) = line.strip_prefix("* `") {
|
|
let option = rest.trim_end().trim_end_matches('`');
|
|
|
|
assert!(
|
|
Options::get(Some(option)).is_some(),
|
|
"unknown option {option}"
|
|
);
|
|
|
|
let anchor = option.rsplit('.').next().unwrap();
|
|
out.push_str(&format!("* [`{option}`]\n"));
|
|
after.push_str(&format!("[`{option}`]: ../../settings#{anchor}"));
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
out.push_str(line);
|
|
}
|
|
if !after.is_empty() {
|
|
out.push_str("\n\n");
|
|
out.push_str(&after);
|
|
}
|
|
}
|