Skip to content

Commit

Permalink
Add ruff rule --all subcommand (with JSON output) (#5059)
Browse files Browse the repository at this point in the history
## 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](#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?
  • Loading branch information
akx committed Jul 4, 2023
1 parent 952c623 commit d7214e7
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 55 deletions.
12 changes: 9 additions & 3 deletions crates/ruff_cli/src/args.rs
Expand Up @@ -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")]
Expand Down
136 changes: 86 additions & 50 deletions 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;
Expand All @@ -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,
}

/// 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();
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(),
}
}
}

match format {
HelpFormat::Text => {
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code()));
output.push('\n');
output.push('\n');
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');

output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
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');
}
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
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');
}
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}"));
}
}
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 mut stdout = BufWriter::new(io::stdout().lock());
match format {
HelpFormat::Text => {
writeln!(stdout, "{}", format_rule_text(rule))?;
}
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(),
})?);
serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?;
}
};
Ok(())
}

writeln!(stdout, "{output}")?;

/// 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 => {
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()?;
}
}
Ok(())
}
9 changes: 8 additions & 1 deletion crates/ruff_cli/src/lib.rs
Expand Up @@ -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)?,
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Expand Up @@ -161,7 +161,7 @@ Usage: ruff [OPTIONS] <COMMAND>
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
Expand Down

0 comments on commit d7214e7

Please sign in to comment.