Add ruff rule --all subcommand (with JSON output) (#5059)

## Summary

This adds a `ruff rule --all` switch that prints out a human-readable
Markdown or a machine-readable JSON document of the lint rules known to
Ruff.

I needed a machine-readable document of the rules [for a
project](https://github.com/astral-sh/ruff/discussions/5078), and
figured it could be useful for other people – or tooling! – to be able
to interrogate Ruff about its arcane knowledge.

The JSON output is an array of the same objects printed by `ruff rule
--format=json`.

## Test Plan

I ran `ruff rule --all --format=json`. I think more might be needed, but
maybe a snapshot test is overkill?
This commit is contained in:
Aarni Koskela 2023-07-04 22:45:38 +03:00 committed by GitHub
parent 952c623102
commit d7214e77e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 57 deletions

View file

@ -35,11 +35,17 @@ pub struct Args {
pub enum Command { pub enum Command {
/// Run Ruff on the given files or directories (default). /// Run Ruff on the given files or directories (default).
Check(CheckArgs), Check(CheckArgs),
/// Explain a rule. /// Explain a rule (or all rules).
#[clap(alias = "--explain")] #[clap(alias = "--explain")]
#[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))]
Rule { Rule {
#[arg(value_parser=Rule::from_code)] /// Rule to explain
rule: Rule, #[arg(value_parser=Rule::from_code, group = "selector")]
rule: Option<Rule>,
/// Explain all rules
#[arg(long, conflicts_with = "rule", group = "selector")]
all: bool,
/// Output format /// Output format
#[arg(long, value_enum, default_value = "text")] #[arg(long, value_enum, default_value = "text")]

View file

@ -1,7 +1,9 @@
use std::io::{self, BufWriter, Write}; use std::io::{self, BufWriter, Write};
use anyhow::Result; 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::registry::{Linter, Rule, RuleNamespace};
use ruff_diagnostics::AutofixKind; use ruff_diagnostics::AutofixKind;
@ -11,72 +13,106 @@ use crate::args::HelpFormat;
#[derive(Serialize)] #[derive(Serialize)]
struct Explanation<'a> { struct Explanation<'a> {
name: &'a str, name: &'a str,
code: &'a str, code: String,
linter: &'a str, linter: &'a str,
summary: &'a str, summary: &'a str,
message_formats: &'a [&'a str], message_formats: &'a [&'a str],
autofix: &'a str, autofix: String,
explanation: Option<&'a str>, 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. /// Explain a `Rule` to the user.
pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> { 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 stdout = BufWriter::new(io::stdout().lock());
let mut output = String::new();
match format { match format {
HelpFormat::Text => { HelpFormat::Text => {
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); writeln!(stdout, "{}", format_rule_text(rule))?;
output.push('\n'); }
output.push('\n'); HelpFormat::Json => {
serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?;
}
};
Ok(())
}
output.push_str(&format!("Derived from the **{}** linter.", linter.name())); /// Explain all rules to the user.
output.push('\n'); pub(crate) fn rules(format: HelpFormat) -> Result<()> {
output.push('\n'); let mut stdout = BufWriter::new(io::stdout().lock());
match format {
let autofix = rule.autofixable(); HelpFormat::Text => {
if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { for rule in Rule::iter() {
output.push_str(&autofix.to_string()); writeln!(stdout, "{}", format_rule_text(rule))?;
output.push('\n'); writeln!(stdout)?;
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}"));
}
} }
} }
HelpFormat::Json => { HelpFormat::Json => {
output.push_str(&serde_json::to_string_pretty(&Explanation { let mut serializer = serde_json::Serializer::pretty(stdout);
name: rule.as_ref(), let mut seq = serializer.serialize_seq(None)?;
code: &rule.noqa_code().to_string(), for rule in Rule::iter() {
linter: linter.name(), seq.serialize_element(&Explanation::from_rule(&rule))?;
summary: rule.message_formats()[0], }
message_formats: rule.message_formats(), seq.end()?;
autofix: &rule.autofixable().to_string(),
explanation: rule.explanation(),
})?);
} }
}; }
writeln!(stdout, "{output}")?;
Ok(()) Ok(())
} }

View file

@ -134,7 +134,14 @@ quoting the executed command, along with the relevant file contents and `pyproje
set_up_logging(&log_level)?; set_up_logging(&log_level)?;
match command { 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::Config { option } => return Ok(commands::config::config(option.as_deref())),
Command::Linter { format } => commands::linter::linter(format)?, Command::Linter { format } => commands::linter::linter(format)?,
Command::Clean => commands::clean::clean(log_level)?, Command::Clean => commands::clean::clean(log_level)?,

View file

@ -161,7 +161,7 @@ Usage: ruff [OPTIONS] <COMMAND>
Commands: Commands:
check Run Ruff on the given files or directories (default) 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 config List or describe the available configuration options
linter List all supported upstream linters linter List all supported upstream linters
clean Clear any caches in the current directory and any subdirectories clean Clear any caches in the current directory and any subdirectories