Skip to content

Commit

Permalink
Add --output-format to ruff config CLI (#11438)
Browse files Browse the repository at this point in the history
This is useful for extracting the defaults in order to construct
equivalent configs by external scripts. This is my first non-hello-world
rust code, comments and suggested tests appreciated.

## Summary

We already have `ruff linter --output-format json`, this provides `ruff
config x --output-format json` as well. I plan to use this to construct
an equivalent config snippet to include in some managed repos, so when
we update their version of ruff and it adds new lints, they get a PR
that includes the commented-out new lints.

Note that the no-args form of `ruff config` ignores output-format
currently, but probably should obey it (although array-of-strings
doesn't seem that useful, looking for input on format).

## Test Plan

I could use a hand coming up with a typical way to write automated tests
for this.

```sh-session
(.venv) [timhatch:ruff ]$ ./target/debug/ruff config lint.select
A list of rule codes or prefixes to enable. Prefixes can specify exact
rules (like `F841`), entire categories (like `F`), or anything in
between.

When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.

Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
Example usage:
``toml
# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E4", "E7", "E9", "F", "B", "Q"]
``
(.venv) [timhatch:ruff ]$ ./target/debug/ruff config lint.select --output-format json
{
  "Field": {
    "doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
    "default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
    "value_type": "list[RuleSelector]",
    "scope": null,
    "example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
    "deprecated": null
  }
}
```
  • Loading branch information
thatch committed May 16, 2024
1 parent b3e4d39 commit 27da223
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 12 deletions.
8 changes: 7 additions & 1 deletion crates/ruff/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ pub enum Command {
output_format: HelpFormat,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
Config {
/// Config key to show
option: Option<String>,
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
},
/// List all supported upstream linters.
Linter {
/// Output format
Expand Down
29 changes: 24 additions & 5 deletions crates/ruff/src/commands/config.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
use anyhow::{anyhow, Result};

use crate::args::HelpFormat;

use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;

#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>) -> Result<()> {
pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> {
match key {
None => print!("{}", Options::metadata()),
None => {
let metadata = Options::metadata();
match format {
HelpFormat::Text => {
println!("{metadata}");
}

HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&metadata)?);
}
}
}
Some(key) => match Options::metadata().find(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}
Some(entry) => {
print!("{entry}");
}
Some(entry) => match format {
HelpFormat::Text => {
print!("{entry}");
}

HelpFormat::Json => {
println!("{}", &serde_json::to_string_pretty(&entry)?);
}
},
},
}
Ok(())
Expand Down
7 changes: 5 additions & 2 deletions crates/ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,11 @@ pub fn run(
}
Ok(ExitStatus::Success)
}
Command::Config { option } => {
commands::config::config(option.as_deref())?;
Command::Config {
option,
output_format,
} => {
commands::config::config(option.as_deref(), output_format)?;
Ok(ExitStatus::Success)
}
Command::Linter { output_format } => {
Expand Down
55 changes: 55 additions & 0 deletions crates/ruff/tests/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! Tests for the `ruff config` subcommand.
use std::process::Command;

use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};

const BIN_NAME: &str = "ruff";

#[test]
fn lint_select() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select"), @r###"
success: true
exit_code: 0
----- stdout -----
A list of rule codes or prefixes to enable. Prefixes can specify exact
rules (like `F841`), entire categories (like `F`), or anything in
between.
When breaking ties between enabled and disabled rules (via `select` and
`ignore`, respectively), more specific prefixes override less
specific prefixes.
Default value: ["E4", "E7", "E9", "F"]
Type: list[RuleSelector]
Example usage:
```toml
# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E4", "E7", "E9", "F", "B", "Q"]
```
----- stderr -----
"###
);
}

#[test]
fn lint_select_json() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("config").arg("lint.select").arg("--output-format").arg("json"), @r###"
success: true
exit_code: 0
----- stdout -----
{
"doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes.",
"default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
"value_type": "list[RuleSelector]",
"scope": null,
"example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
"deprecated": null
}
----- stderr -----
"###
);
}
50 changes: 46 additions & 4 deletions crates/ruff_workspace/src/options_base.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use serde::{Serialize, Serializer};
use std::collections::BTreeMap;

use std::fmt::{Debug, Display, Formatter};

/// Visits [`OptionsMetadata`].
Expand Down Expand Up @@ -39,12 +42,13 @@ where
}

/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
#[serde(untagged)]
pub enum OptionEntry {
/// A single option.
Field(OptionField),

/// A set of options
/// A set of options.
Set(OptionSet),
}

Expand Down Expand Up @@ -325,13 +329,51 @@ impl Display for OptionSet {
}
}

struct SerializeVisitor<'a> {
entries: &'a mut BTreeMap<String, OptionField>,
}

impl<'a> Visit for SerializeVisitor<'a> {
fn record_set(&mut self, name: &str, set: OptionSet) {
// Collect the entries of the set.
let mut entries = BTreeMap::new();
let mut visitor = SerializeVisitor {
entries: &mut entries,
};
set.record(&mut visitor);

// Insert the set into the entries.
for (key, value) in entries {
self.entries.insert(format!("{name}.{key}"), value);
}
}

fn record_field(&mut self, name: &str, field: OptionField) {
self.entries.insert(name.to_string(), field);
}
}

impl Serialize for OptionSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut entries = BTreeMap::new();
let mut visitor = SerializeVisitor {
entries: &mut entries,
};
self.record(&mut visitor);
entries.serialize(serializer)
}
}

impl Debug for OptionSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}

#[derive(Debug, Eq, PartialEq, Clone)]
#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
pub struct OptionField {
pub doc: &'static str,
/// Ex) `"false"`
Expand All @@ -344,7 +386,7 @@ pub struct OptionField {
pub deprecated: Option<Deprecated>,
}

#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
pub struct Deprecated {
pub since: Option<&'static str>,
pub message: Option<&'static str>,
Expand Down

0 comments on commit 27da223

Please sign in to comment.