ruff/crates/ruff_dev/src/generate_rules_table.rs
Charlie Marsh 465f835cf9
Escape trailing placeholders in rule documentation (#9301)
## Summary

If a rule ends with a trailing placeholder (like "Use {target}"), that
gets interpreted as an HTML attribute adding, `target="target"` to the
node. This PR escapes such cases. In reality, they're rare, since we
almost always wrap placeholders in backticks, which avoids this problem
-- but in some cases, they are in fact correct to be un-backticked.

Closes https://github.com/astral-sh/ruff/issues/9288.

## Test Plan

<img width="673" alt="Screen Shot 2023-12-28 at 9 33 40 AM"
src="14aaa168-c802-4258-b82d-217a85b42ebf">
2023-12-28 09:44:51 -05:00

152 lines
5.4 KiB
Rust

//! Generate a Markdown-compatible table of supported lint rules.
//!
//! Used for <https://docs.astral.sh/ruff/rules/>.
use itertools::Itertools;
use std::borrow::Cow;
use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
const FIX_SYMBOL: &str = "🛠️";
const PREVIEW_SYMBOL: &str = "🧪";
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>, linter: &Linter) {
table_out.push_str("| Code | Name | Message | |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | ------: |");
table_out.push('\n');
for rule in rules {
let fix_token = match rule.fixable() {
FixAvailability::Always | FixAvailability::Sometimes => {
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
}
FixAvailability::None => {
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
}
};
let preview_token = if rule.is_preview() || rule.is_nursery() {
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
} else {
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
};
let status_token = format!("{fix_token} {preview_token}");
let rule_name = rule.as_ref();
// If the message ends in a bracketed expression (like: "Use {replacement}"), escape the
// brackets. Otherwise, it'll be interpreted as an HTML attribute via the `attr_list`
// plugin. (Above, we'd convert to "Use {replacement\}".)
let message = rule.message_formats()[0];
let message = if let Some(prefix) = message.strip_suffix('}') {
Cow::Owned(format!("{prefix}\\}}"))
} else {
Cow::Borrowed(message)
};
#[allow(clippy::or_fun_call)]
table_out.push_str(&format!(
"| {0}{1} {{ #{0}{1} }} | {2} | {3} | {4} |",
linter.common_prefix(),
linter.code_for_rule(rule).unwrap(),
rule.explanation()
.is_some()
.then_some(format_args!("[{rule_name}](rules/{rule_name}.md)"))
.unwrap_or(format_args!("{rule_name}")),
message,
status_token,
));
table_out.push('\n');
}
table_out.push('\n');
}
pub(crate) fn generate() -> String {
// Generate the table string.
let mut table_out = String::new();
table_out.push_str(&format!(
"The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option."));
table_out.push('\n');
table_out.push('\n');
table_out.push_str(&format!(
"The {PREVIEW_SYMBOL} emoji indicates that a rule is in [\"preview\"](faq.md#what-is-preview)."
));
table_out.push('\n');
table_out.push('\n');
for linter in Linter::iter() {
let codes_csv: String = match linter.common_prefix() {
"" => linter
.upstream_categories()
.unwrap()
.iter()
.map(|c| c.prefix)
.join(", "),
prefix => prefix.to_string(),
};
table_out.push_str(&format!("### {} ({codes_csv})", linter.name()));
table_out.push('\n');
table_out.push('\n');
if let Some(url) = linter.url() {
let host = url
.trim_start_matches("https://")
.split('/')
.next()
.unwrap();
table_out.push_str(&format!(
"For more, see [{}]({}) on {}.",
linter.name(),
url,
match host {
"pypi.org" => "PyPI",
"github.com" => "GitHub",
host => panic!(
"unexpected host in URL of {}, expected pypi.org or github.com but found \
{host}",
linter.name()
),
}
));
table_out.push('\n');
table_out.push('\n');
}
if Options::metadata().has(linter.name()) {
table_out.push_str(&format!(
"For related settings, see [{}](settings.md#{}).",
linter.name(),
linter.name(),
));
table_out.push('\n');
table_out.push('\n');
}
let rules_by_upstream_category = linter
.all_rules()
.map(|rule| (rule.upstream_category(&linter), rule))
.into_group_map();
if rules_by_upstream_category.len() > 1 {
for (opt, rules) in &rules_by_upstream_category {
if opt.is_some() {
let UpstreamCategoryAndPrefix { category, prefix } = opt.unwrap();
table_out.push_str(&format!("#### {category} ({prefix})"));
}
table_out.push('\n');
table_out.push('\n');
generate_table(&mut table_out, rules.clone(), &linter);
}
} else {
generate_table(&mut table_out, linter.all_rules(), &linter);
}
}
table_out
}