Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/basedpython/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 3 additions & 48 deletions crates/ruff/src/commands/rule.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::fmt::Write as _;
use std::io::{self, BufWriter, Write};

use anyhow::Result;
Expand All @@ -9,6 +8,7 @@ use strum::IntoEnumIterator;
use ruff_linter::FixAvailability;
use ruff_linter::codes::RuleGroup;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::rule_documentation;

use crate::args::HelpFormat;

Expand Down Expand Up @@ -52,57 +52,12 @@ impl<'a> Explanation<'a> {
}
}

fn format_rule_text(rule: Rule) -> String {
let mut output = String::new();
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
output.push('\n');
output.push('\n');

let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
let _ = write!(
&mut output,
"Derived from the **{}** linter.",
linter.name()
);
output.push('\n');
output.push('\n');

let fix_availability = rule.fixable();
if matches!(
fix_availability,
FixAvailability::Always | FixAvailability::Sometimes
) {
output.push_str(&fix_availability.to_string());
output.push('\n');
output.push('\n');
}

if rule.is_preview() {
output.push_str(
r"This rule is in preview and is not stable. The `--preview` flag is required for use.",
);
output.push('\n');
output.push('\n');
}

if let Some(explanation) = rule.explanation() {
output.push_str(explanation.trim());
} else {
output.push_str("Message formats:");
for format in rule.message_formats() {
output.push('\n');
let _ = write!(&mut output, "* {format}");
}
}
output
}

/// Explain a `Rule` to the user.
pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> {
let mut stdout = BufWriter::new(io::stdout().lock());
match format {
HelpFormat::Text => {
writeln!(stdout, "{}", format_rule_text(rule))?;
writeln!(stdout, "{}", rule_documentation(rule))?;
}
HelpFormat::Json => {
serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?;
Expand All @@ -117,7 +72,7 @@ pub(crate) fn rules(format: HelpFormat) -> Result<()> {
match format {
HelpFormat::Text => {
for rule in Rule::iter() {
writeln!(stdout, "{}", format_rule_text(rule))?;
writeln!(stdout, "{}", rule_documentation(rule))?;
writeln!(stdout)?;
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use locator::Locator;
pub use noqa::{SuppressionKind, generate_suppression_edits};
#[cfg(feature = "clap")]
pub use registry::clap_completion::RuleParser;
pub use rule_documentation::rule_documentation;
#[cfg(feature = "clap")]
pub use rule_selector::clap_completion::UnresolvedRuleSelectorParser;
pub use rule_selector::{RuleSelector, UnresolvedRuleSelector};
Expand Down Expand Up @@ -40,6 +41,7 @@ pub mod packaging;
pub mod preview;
pub mod registry;
mod renamer;
pub mod rule_documentation;
mod rule_redirects;
pub mod rule_selector;
pub mod rules;
Expand Down
95 changes: 95 additions & 0 deletions crates/ruff_linter/src/rule_documentation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! The prose a rule is explained with, rendered once for everyone who asks.
//!
//! `buff rule F401` and the editor's *Explain Rule* are the same question, so they are the same
//! answer: one renderer here, in the crate that owns the rules, rather than one in the CLI and a
//! second wherever else the text was wanted.

use std::fmt::Write as _;

use crate::FixAvailability;
use crate::registry::{Linter, Rule, RuleNamespace};

/// The markdown explaining `rule`: what it is, where it came from, and what it does about it.
///
/// Markdown because a rule's own `explanation` is written in it — the rest is built to match, so a
/// rule with documentation and one without read as the same kind of document.
pub fn rule_documentation(rule: Rule) -> String {
let mut output = String::new();
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
output.push('\n');
output.push('\n');

let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string())
.expect("a rule's own noqa code is one its linter parses");
let _ = write!(
&mut output,
"Derived from the **{}** linter.",
linter.name()
);
output.push('\n');
output.push('\n');

let fix_availability = rule.fixable();
if matches!(
fix_availability,
FixAvailability::Always | FixAvailability::Sometimes
) {
output.push_str(&fix_availability.to_string());
output.push('\n');
output.push('\n');
}

if rule.is_preview() {
output.push_str(
r"This rule is in preview and is not stable. The `--preview` flag is required for use.",
);
output.push('\n');
output.push('\n');
}

if let Some(explanation) = rule.explanation() {
output.push_str(explanation.trim());
} else {
// Not every rule carries prose. The formats it reports under say what it looks for, which
// is more use than an empty document.
output.push_str("Message formats:");
for format in rule.message_formats() {
output.push('\n');
let _ = write!(&mut output, "* {format}");
}
}
output
}

#[cfg(test)]
mod tests {
use super::*;
use strum::IntoEnumIterator;

#[test]
fn every_rule_renders_a_document_naming_itself() {
for rule in Rule::iter() {
let doc = rule_documentation(rule);
assert!(
doc.starts_with(&format!("# {} ({})", rule.name(), rule.noqa_code())),
"{} did not lead with its own name and code",
rule.name()
);
assert!(
doc.contains("linter."),
"{} did not say which linter it came from",
rule.name()
);
}
}

/// A rule with no prose still has to say what it looks for, rather than trailing off.
#[test]
fn a_rule_without_an_explanation_lists_its_message_formats() {
let undocumented = Rule::iter().find(|rule| rule.explanation().is_none());
let Some(rule) = undocumented else {
return; // every rule is documented, which is the better problem to have
};
assert!(rule_documentation(rule).contains("Message formats:"));
}
}
3 changes: 3 additions & 0 deletions crates/ruff_server/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pub(super) fn request(req: server::Request) -> Task {
background_request_task::<request::DocumentDiagnostic>(req, BackgroundSchedule::Worker)
}
request::ExecuteCommand::METHOD => sync_request_task::<request::ExecuteCommand>(req),
request::ExplainRule::METHOD => {
background_request_task::<request::ExplainRule>(req, BackgroundSchedule::Worker)
}
request::Format::METHOD => {
background_request_task::<request::Format>(req, BackgroundSchedule::Fmt)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_server/src/server/api/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod code_action;
mod code_action_resolve;
mod diagnostic;
mod execute_command;
mod explain_rule;
mod format;
mod format_range;
mod hover;
Expand All @@ -18,6 +19,7 @@ pub(super) use code_action::CodeActions;
pub(super) use code_action_resolve::CodeActionResolve;
pub(super) use diagnostic::DocumentDiagnostic;
pub(super) use execute_command::ExecuteCommand;
pub(super) use explain_rule::ExplainRule;
pub(super) use format::Format;
pub(super) use format_range::FormatRange;
pub(super) use hover::Hover;
Expand Down
Loading
Loading