diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 3d2e42bf16..264b8bfde3 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -111,7 +111,13 @@ pub enum Command { output_format: HelpFormat, }, /// List or describe the available configuration options. - Config { option: Option }, + Config { + /// Config key to show + option: Option, + /// Output format + #[arg(long, value_enum, default_value = "text")] + output_format: HelpFormat, + }, /// List all supported upstream linters. Linter { /// Output format diff --git a/crates/ruff/src/commands/config.rs b/crates/ruff/src/commands/config.rs index 56facacdab..4f83d7525a 100644 --- a/crates/ruff/src/commands/config.rs +++ b/crates/ruff/src/commands/config.rs @@ -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(()) diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 3f797900be..1f31005389 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -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 } => { diff --git a/crates/ruff/tests/config.rs b/crates/ruff/tests/config.rs new file mode 100644 index 0000000000..ff0bf59184 --- /dev/null +++ b/crates/ruff/tests/config.rs @@ -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 ----- + "### + ); +} diff --git a/crates/ruff_workspace/src/options_base.rs b/crates/ruff_workspace/src/options_base.rs index e766c66db1..7b2c2d405e 100644 --- a/crates/ruff_workspace/src/options_base.rs +++ b/crates/ruff_workspace/src/options_base.rs @@ -1,3 +1,6 @@ +use serde::{Serialize, Serializer}; +use std::collections::BTreeMap; + use std::fmt::{Debug, Display, Formatter}; /// Visits [`OptionsMetadata`]. @@ -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), } @@ -325,13 +329,51 @@ impl Display for OptionSet { } } +struct SerializeVisitor<'a> { + entries: &'a mut BTreeMap, +} + +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(&self, serializer: S) -> Result + 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"` @@ -344,7 +386,7 @@ pub struct OptionField { pub deprecated: Option, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub struct Deprecated { pub since: Option<&'static str>, pub message: Option<&'static str>,