diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 0199d2f552..1050aafcbf 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -35,11 +35,17 @@ pub struct Args { pub enum Command { /// Run Ruff on the given files or directories (default). Check(CheckArgs), - /// Explain a rule. + /// Explain a rule (or all rules). #[clap(alias = "--explain")] + #[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))] Rule { - #[arg(value_parser=Rule::from_code)] - rule: Rule, + /// Rule to explain + #[arg(value_parser=Rule::from_code, group = "selector")] + rule: Option, + + /// Explain all rules + #[arg(long, conflicts_with = "rule", group = "selector")] + all: bool, /// Output format #[arg(long, value_enum, default_value = "text")] diff --git a/crates/ruff_cli/src/commands/rule.rs b/crates/ruff_cli/src/commands/rule.rs index 9cd41e7bb2..a16ec4622b 100644 --- a/crates/ruff_cli/src/commands/rule.rs +++ b/crates/ruff_cli/src/commands/rule.rs @@ -1,7 +1,9 @@ use std::io::{self, BufWriter, Write}; use anyhow::Result; -use serde::Serialize; +use serde::ser::SerializeSeq; +use serde::{Serialize, Serializer}; +use strum::IntoEnumIterator; use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff_diagnostics::AutofixKind; @@ -11,72 +13,106 @@ use crate::args::HelpFormat; #[derive(Serialize)] struct Explanation<'a> { name: &'a str, - code: &'a str, + code: String, linter: &'a str, summary: &'a str, message_formats: &'a [&'a str], - autofix: &'a str, + autofix: String, explanation: Option<&'a str>, + nursery: bool, +} + +impl<'a> Explanation<'a> { + fn from_rule(rule: &'a Rule) -> Self { + let code = rule.noqa_code().to_string(); + let (linter, _) = Linter::parse_code(&code).unwrap(); + let autofix = rule.autofixable().to_string(); + Self { + name: rule.as_ref(), + code, + linter: linter.name(), + summary: rule.message_formats()[0], + message_formats: rule.message_formats(), + autofix, + explanation: rule.explanation(), + nursery: rule.is_nursery(), + } + } +} + +fn format_rule_text(rule: Rule) -> String { + let mut output = String::new(); + output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); + output.push('\n'); + output.push('\n'); + + let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); + output.push_str(&format!("Derived from the **{}** linter.", linter.name())); + output.push('\n'); + output.push('\n'); + + let autofix = rule.autofixable(); + if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { + output.push_str(&autofix.to_string()); + output.push('\n'); + output.push('\n'); + } + + if rule.is_nursery() { + output.push_str(&format!( + r#"This rule is part of the **nursery**, a collection of newer lints that are +still under development. As such, it must be enabled by explicitly selecting +{}."#, + rule.noqa_code() + )); + 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'); + output.push_str(&format!("* {format}")); + } + } + output } /// Explain a `Rule` to the user. pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> { - let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); let mut stdout = BufWriter::new(io::stdout().lock()); - let mut output = String::new(); - match format { HelpFormat::Text => { - output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); - output.push('\n'); - output.push('\n'); + writeln!(stdout, "{}", format_rule_text(rule))?; + } + HelpFormat::Json => { + serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?; + } + }; + Ok(()) +} - output.push_str(&format!("Derived from the **{}** linter.", linter.name())); - output.push('\n'); - output.push('\n'); - - let autofix = rule.autofixable(); - if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { - output.push_str(&autofix.to_string()); - output.push('\n'); - output.push('\n'); - } - - if rule.is_nursery() { - output.push_str(&format!( - r#"This rule is part of the **nursery**, a collection of newer lints that are -still under development. As such, it must be enabled by explicitly selecting -{}."#, - rule.noqa_code() - )); - 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'); - output.push_str(&format!("* {format}")); - } +/// Explain all rules to the user. +pub(crate) fn rules(format: HelpFormat) -> Result<()> { + let mut stdout = BufWriter::new(io::stdout().lock()); + match format { + HelpFormat::Text => { + for rule in Rule::iter() { + writeln!(stdout, "{}", format_rule_text(rule))?; + writeln!(stdout)?; } } HelpFormat::Json => { - output.push_str(&serde_json::to_string_pretty(&Explanation { - name: rule.as_ref(), - code: &rule.noqa_code().to_string(), - linter: linter.name(), - summary: rule.message_formats()[0], - message_formats: rule.message_formats(), - autofix: &rule.autofixable().to_string(), - explanation: rule.explanation(), - })?); + let mut serializer = serde_json::Serializer::pretty(stdout); + let mut seq = serializer.serialize_seq(None)?; + for rule in Rule::iter() { + seq.serialize_element(&Explanation::from_rule(&rule))?; + } + seq.end()?; } - }; - - writeln!(stdout, "{output}")?; - + } Ok(()) } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 90d705b286..edfb43f5de 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -134,7 +134,14 @@ quoting the executed command, along with the relevant file contents and `pyproje set_up_logging(&log_level)?; match command { - Command::Rule { rule, format } => commands::rule::rule(rule, format)?, + Command::Rule { rule, all, format } => { + if all { + commands::rule::rules(format)?; + } + if let Some(rule) = rule { + commands::rule::rule(rule, format)?; + } + } Command::Config { option } => return Ok(commands::config::config(option.as_deref())), Command::Linter { format } => commands::linter::linter(format)?, Command::Clean => commands::clean::clean(log_level)?, diff --git a/docs/configuration.md b/docs/configuration.md index a64bef7a50..3575253ed7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -161,7 +161,7 @@ Usage: ruff [OPTIONS] Commands: check Run Ruff on the given files or directories (default) - rule Explain a rule + rule Explain a rule (or all rules) config List or describe the available configuration options linter List all supported upstream linters clean Clear any caches in the current directory and any subdirectories