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 {
/// 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<Rule>,
/// Explain all rules
#[arg(long, conflicts_with = "rule", group = "selector")]
all: bool,
/// Output format
#[arg(long, value_enum, default_value = "text")]

View file

@ -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(())
}

View file

@ -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)?,