Skip to content

Commit d7214e7

Browse files
authored
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](#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?
1 parent 952c623 commit d7214e7

File tree

4 files changed

+104
-55
lines changed

4 files changed

+104
-55
lines changed

crates/ruff_cli/src/args.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ pub struct Args {
3535
pub enum Command {
3636
/// Run Ruff on the given files or directories (default).
3737
Check(CheckArgs),
38-
/// Explain a rule.
38+
/// Explain a rule (or all rules).
3939
#[clap(alias = "--explain")]
40+
#[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))]
4041
Rule {
41-
#[arg(value_parser=Rule::from_code)]
42-
rule: Rule,
42+
/// Rule to explain
43+
#[arg(value_parser=Rule::from_code, group = "selector")]
44+
rule: Option<Rule>,
45+
46+
/// Explain all rules
47+
#[arg(long, conflicts_with = "rule", group = "selector")]
48+
all: bool,
4349

4450
/// Output format
4551
#[arg(long, value_enum, default_value = "text")]

crates/ruff_cli/src/commands/rule.rs

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::io::{self, BufWriter, Write};
22

33
use anyhow::Result;
4-
use serde::Serialize;
4+
use serde::ser::SerializeSeq;
5+
use serde::{Serialize, Serializer};
6+
use strum::IntoEnumIterator;
57

68
use ruff::registry::{Linter, Rule, RuleNamespace};
79
use ruff_diagnostics::AutofixKind;
@@ -11,72 +13,106 @@ use crate::args::HelpFormat;
1113
#[derive(Serialize)]
1214
struct Explanation<'a> {
1315
name: &'a str,
14-
code: &'a str,
16+
code: String,
1517
linter: &'a str,
1618
summary: &'a str,
1719
message_formats: &'a [&'a str],
18-
autofix: &'a str,
20+
autofix: String,
1921
explanation: Option<&'a str>,
22+
nursery: bool,
2023
}
2124

22-
/// Explain a `Rule` to the user.
23-
pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> {
24-
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
25-
let mut stdout = BufWriter::new(io::stdout().lock());
26-
let mut output = String::new();
25+
impl<'a> Explanation<'a> {
26+
fn from_rule(rule: &'a Rule) -> Self {
27+
let code = rule.noqa_code().to_string();
28+
let (linter, _) = Linter::parse_code(&code).unwrap();
29+
let autofix = rule.autofixable().to_string();
30+
Self {
31+
name: rule.as_ref(),
32+
code,
33+
linter: linter.name(),
34+
summary: rule.message_formats()[0],
35+
message_formats: rule.message_formats(),
36+
autofix,
37+
explanation: rule.explanation(),
38+
nursery: rule.is_nursery(),
39+
}
40+
}
41+
}
2742

28-
match format {
29-
HelpFormat::Text => {
30-
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code()));
31-
output.push('\n');
32-
output.push('\n');
43+
fn format_rule_text(rule: Rule) -> String {
44+
let mut output = String::new();
45+
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code()));
46+
output.push('\n');
47+
output.push('\n');
3348

34-
output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
35-
output.push('\n');
36-
output.push('\n');
49+
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
50+
output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
51+
output.push('\n');
52+
output.push('\n');
3753

38-
let autofix = rule.autofixable();
39-
if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) {
40-
output.push_str(&autofix.to_string());
41-
output.push('\n');
42-
output.push('\n');
43-
}
54+
let autofix = rule.autofixable();
55+
if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) {
56+
output.push_str(&autofix.to_string());
57+
output.push('\n');
58+
output.push('\n');
59+
}
4460

45-
if rule.is_nursery() {
46-
output.push_str(&format!(
47-
r#"This rule is part of the **nursery**, a collection of newer lints that are
61+
if rule.is_nursery() {
62+
output.push_str(&format!(
63+
r#"This rule is part of the **nursery**, a collection of newer lints that are
4864
still under development. As such, it must be enabled by explicitly selecting
4965
{}."#,
50-
rule.noqa_code()
51-
));
52-
output.push('\n');
53-
output.push('\n');
54-
}
66+
rule.noqa_code()
67+
));
68+
output.push('\n');
69+
output.push('\n');
70+
}
5571

56-
if let Some(explanation) = rule.explanation() {
57-
output.push_str(explanation.trim());
58-
} else {
59-
output.push_str("Message formats:");
60-
for format in rule.message_formats() {
61-
output.push('\n');
62-
output.push_str(&format!("* {format}"));
63-
}
64-
}
72+
if let Some(explanation) = rule.explanation() {
73+
output.push_str(explanation.trim());
74+
} else {
75+
output.push_str("Message formats:");
76+
for format in rule.message_formats() {
77+
output.push('\n');
78+
output.push_str(&format!("* {format}"));
79+
}
80+
}
81+
output
82+
}
83+
84+
/// Explain a `Rule` to the user.
85+
pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> {
86+
let mut stdout = BufWriter::new(io::stdout().lock());
87+
match format {
88+
HelpFormat::Text => {
89+
writeln!(stdout, "{}", format_rule_text(rule))?;
6590
}
6691
HelpFormat::Json => {
67-
output.push_str(&serde_json::to_string_pretty(&Explanation {
68-
name: rule.as_ref(),
69-
code: &rule.noqa_code().to_string(),
70-
linter: linter.name(),
71-
summary: rule.message_formats()[0],
72-
message_formats: rule.message_formats(),
73-
autofix: &rule.autofixable().to_string(),
74-
explanation: rule.explanation(),
75-
})?);
92+
serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?;
7693
}
7794
};
95+
Ok(())
96+
}
7897

79-
writeln!(stdout, "{output}")?;
80-
98+
/// Explain all rules to the user.
99+
pub(crate) fn rules(format: HelpFormat) -> Result<()> {
100+
let mut stdout = BufWriter::new(io::stdout().lock());
101+
match format {
102+
HelpFormat::Text => {
103+
for rule in Rule::iter() {
104+
writeln!(stdout, "{}", format_rule_text(rule))?;
105+
writeln!(stdout)?;
106+
}
107+
}
108+
HelpFormat::Json => {
109+
let mut serializer = serde_json::Serializer::pretty(stdout);
110+
let mut seq = serializer.serialize_seq(None)?;
111+
for rule in Rule::iter() {
112+
seq.serialize_element(&Explanation::from_rule(&rule))?;
113+
}
114+
seq.end()?;
115+
}
116+
}
81117
Ok(())
82118
}

crates/ruff_cli/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,14 @@ quoting the executed command, along with the relevant file contents and `pyproje
134134
set_up_logging(&log_level)?;
135135

136136
match command {
137-
Command::Rule { rule, format } => commands::rule::rule(rule, format)?,
137+
Command::Rule { rule, all, format } => {
138+
if all {
139+
commands::rule::rules(format)?;
140+
}
141+
if let Some(rule) = rule {
142+
commands::rule::rule(rule, format)?;
143+
}
144+
}
138145
Command::Config { option } => return Ok(commands::config::config(option.as_deref())),
139146
Command::Linter { format } => commands::linter::linter(format)?,
140147
Command::Clean => commands::clean::clean(log_level)?,

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Usage: ruff [OPTIONS] <COMMAND>
161161
162162
Commands:
163163
check Run Ruff on the given files or directories (default)
164-
rule Explain a rule
164+
rule Explain a rule (or all rules)
165165
config List or describe the available configuration options
166166
linter List all supported upstream linters
167167
clean Clear any caches in the current directory and any subdirectories

0 commit comments

Comments
 (0)