diff --git a/crates/ruff_cli/src/commands/config.rs b/crates/ruff_cli/src/commands/config.rs index 042ea066f949b..56facacdab454 100644 --- a/crates/ruff_cli/src/commands/config.rs +++ b/crates/ruff_cli/src/commands/config.rs @@ -1,12 +1,13 @@ use anyhow::{anyhow, Result}; use ruff_workspace::options::Options; +use ruff_workspace::options_base::OptionsMetadata; #[allow(clippy::print_stdout)] pub(crate) fn config(key: Option<&str>) -> Result<()> { match key { None => print!("{}", Options::metadata()), - Some(key) => match Options::metadata().get(key) { + Some(key) => match Options::metadata().find(key) { None => { return Err(anyhow!("Unknown option: {key}")); } diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs index 3cbb295509513..b6b70c7f4e324 100644 --- a/crates/ruff_dev/src/generate_docs.rs +++ b/crates/ruff_dev/src/generate_docs.rs @@ -11,6 +11,7 @@ use strum::IntoEnumIterator; use ruff_diagnostics::AutofixKind; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use ruff_workspace::options::Options; +use ruff_workspace::options_base::OptionsMetadata; use crate::ROOT_DIR; @@ -96,10 +97,7 @@ fn process_documentation(documentation: &str, out: &mut String) { if let Some(rest) = line.strip_prefix("- `") { let option = rest.trim_end().trim_end_matches('`'); - assert!( - Options::metadata().get(option).is_some(), - "unknown option {option}" - ); + assert!(Options::metadata().has(option), "unknown option {option}"); let anchor = option.replace('.', "-"); out.push_str(&format!("- [`{option}`][{option}]\n")); diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index d52d41318c7f4..11cf7c8423f51 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -1,9 +1,68 @@ //! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. //! //! Used for . -use itertools::Itertools; +use std::fmt::Write; + use ruff_workspace::options::Options; -use ruff_workspace::options_base::{OptionEntry, OptionField}; +use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit}; + +pub(crate) fn generate() -> String { + let mut output = String::new(); + generate_set(&mut output, &Set::Toplevel(Options::metadata())); + + output +} + +fn generate_set(output: &mut String, set: &Set) { + writeln!(output, "### {title}\n", title = set.title()).unwrap(); + + let mut visitor = CollectOptionsVisitor::default(); + set.metadata().record(&mut visitor); + + let (mut fields, mut sets) = (visitor.fields, visitor.groups); + + fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + + // Generate the fields. + for (name, field) in &fields { + emit_field(output, name, field, set.name()); + output.push_str("---\n\n"); + } + + // Generate all the sub-sets. + for (set_name, sub_set) in &sets { + generate_set(output, &Set::Named(set_name, *sub_set)); + } +} + +enum Set<'a> { + Toplevel(OptionSet), + Named(&'a str, OptionSet), +} + +impl<'a> Set<'a> { + fn name(&self) -> Option<&'a str> { + match self { + Set::Toplevel(_) => None, + Set::Named(name, _) => Some(name), + } + } + + fn title(&self) -> &'a str { + match self { + Set::Toplevel(_) => "Top-level", + Set::Named(name, _) => name, + } + } + + fn metadata(&self) -> &OptionSet { + match self { + Set::Toplevel(set) => set, + Set::Named(_, set) => set, + } + } +} fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) { // if there's a group name, we need to add it to the anchor @@ -37,38 +96,18 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: output.push('\n'); } -pub(crate) fn generate() -> String { - let mut output: String = "### Top-level\n\n".into(); - - let sorted_options: Vec<_> = Options::metadata() - .into_iter() - .sorted_by_key(|(name, _)| *name) - .collect(); - - // Generate all the top-level fields. - for (name, entry) in &sorted_options { - let OptionEntry::Field(field) = entry else { - continue; - }; - emit_field(&mut output, name, field, None); - output.push_str("---\n\n"); - } +#[derive(Default)] +struct CollectOptionsVisitor { + groups: Vec<(String, OptionSet)>, + fields: Vec<(String, OptionField)>, +} - // Generate all the sub-groups. - for (group_name, entry) in &sorted_options { - let OptionEntry::Group(fields) = entry else { - continue; - }; - output.push_str(&format!("### {group_name}\n")); - output.push('\n'); - for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) { - let OptionEntry::Field(field) = entry else { - continue; - }; - emit_field(&mut output, name, field, Some(group_name)); - output.push_str("---\n\n"); - } +impl Visit for CollectOptionsVisitor { + fn record_set(&mut self, name: &str, group: OptionSet) { + self.groups.push((name.to_owned(), group)); } - output + fn record_field(&mut self, name: &str, field: OptionField) { + self.fields.push((name.to_owned(), field)); + } } diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 76deeeb8b2643..82addce497d4d 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -9,6 +9,7 @@ use ruff_diagnostics::AutofixKind; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix; use ruff_workspace::options::Options; +use ruff_workspace::options_base::OptionsMetadata; const FIX_SYMBOL: &str = "🛠️"; const PREVIEW_SYMBOL: &str = "🧪"; @@ -104,10 +105,7 @@ pub(crate) fn generate() -> String { table_out.push('\n'); } - if Options::metadata() - .iter() - .any(|(name, _)| name == &linter.name()) - { + if Options::metadata().has(linter.name()) { table_out.push_str(&format!( "For related settings, see [{}](settings.md#{}).", linter.name(), diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 0326103f7b7fa..5cd13ea7cfd8a 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -50,14 +50,11 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result crate::options_base::OptionGroup { - const OPTIONS: [(&'static str, crate::options_base::OptionEntry); #options_len] = [#(#output),*]; - crate::options_base::OptionGroup::new(&OPTIONS) + impl crate::options_base::OptionsMetadata for #ident { + fn record(visit: &mut dyn crate::options_base::Visit) { + #(#output);* } } }) @@ -92,7 +89,7 @@ fn handle_option_group(field: &Field) -> syn::Result { let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); Ok(quote_spanned!( - ident.span() => (#kebab_name, crate::options_base::OptionEntry::Group(#path::metadata())) + ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>())) )) } _ => Err(syn::Error::new( @@ -150,12 +147,14 @@ fn handle_option( let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); Ok(quote_spanned!( - ident.span() => (#kebab_name, crate::options_base::OptionEntry::Field(crate::options_base::OptionField { - doc: &#doc, - default: &#default, - value_type: &#value_type, - example: &#example, - })) + ident.span() => { + visit.record_field(#kebab_name, crate::options_base::OptionField{ + doc: &#doc, + default: &#default, + value_type: &#value_type, + example: &#example, + }) + } )) } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 6097f05449ec3..497801529f03d 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -7,6 +7,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; +use crate::options_base::{OptionsMetadata, Visit}; use ruff_linter::line_width::{LineLength, TabSize}; use ruff_linter::rules::flake8_pytest_style::settings::SettingsError; use ruff_linter::rules::flake8_pytest_style::types; @@ -2399,10 +2400,6 @@ pub enum FormatOrOutputFormat { } impl FormatOrOutputFormat { - pub const fn metadata() -> crate::options_base::OptionGroup { - FormatOptions::metadata() - } - pub const fn as_output_format(&self) -> Option { match self { FormatOrOutputFormat::Format(_) => None, @@ -2411,6 +2408,12 @@ impl FormatOrOutputFormat { } } +impl OptionsMetadata for FormatOrOutputFormat { + fn record(visit: &mut dyn Visit) { + FormatOptions::record(visit); + } +} + #[derive( Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, CombineOptions, )] diff --git a/crates/ruff_workspace/src/options_base.rs b/crates/ruff_workspace/src/options_base.rs index 7a91117beccec..60269a72ca0ed 100644 --- a/crates/ruff_workspace/src/options_base.rs +++ b/crates/ruff_workspace/src/options_base.rs @@ -1,167 +1,295 @@ -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; -#[derive(Debug, Eq, PartialEq)] +/// Visits [`OptionsMetadata`]. +/// +/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata. +pub trait Visit { + /// Visits an [`OptionField`] value named `name`. + fn record_field(&mut self, name: &str, field: OptionField); + + /// Visits an [`OptionSet`] value named `name`. + fn record_set(&mut self, name: &str, group: OptionSet); +} + +/// Returns metadata for its options. +pub trait OptionsMetadata { + /// Visits the options metadata of this object by calling `visit` for each option. + fn record(visit: &mut dyn Visit); + + /// Returns the extracted metadata. + fn metadata() -> OptionSet + where + Self: Sized + 'static, + { + OptionSet::of::() + } +} + +/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`]. +#[derive(Clone, PartialEq, Eq, Debug)] pub enum OptionEntry { + /// A single option. Field(OptionField), - Group(OptionGroup), + + /// A set of options + Set(OptionSet), } impl Display for OptionEntry { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - OptionEntry::Field(field) => field.fmt(f), - OptionEntry::Group(group) => group.fmt(f), + OptionEntry::Set(set) => std::fmt::Display::fmt(set, f), + OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f), } } } -#[derive(Debug, Eq, PartialEq)] -pub struct OptionGroup(&'static [(&'static str, OptionEntry)]); +/// A set of options. +/// +/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing +/// [`OptionsMetadata`]. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct OptionSet { + record: fn(&mut dyn Visit), +} -impl OptionGroup { - pub const fn new(options: &'static [(&'static str, OptionEntry)]) -> Self { - Self(options) +impl OptionSet { + pub fn of() -> Self + where + T: OptionsMetadata + 'static, + { + Self { record: T::record } } - pub fn iter(&self) -> std::slice::Iter<(&str, OptionEntry)> { - self.into_iter() + /// Visits the options in this set by calling `visit` for each option. + pub fn record(&self, visit: &mut dyn Visit) { + let record = self.record; + record(visit); } - /// Get an option entry by its fully-qualified name - /// (e.g. `foo.bar` refers to the `bar` option in the `foo` group). + /// Returns `true` if this set has an option that resolves to `name`. + /// + /// The name can be separated by `.` to find a nested option. /// /// ## Examples /// - /// ### Find a direct child + /// ### Test for the existence of a child option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField}; - /// - /// const OPTIONS: [(&'static str, OptionEntry); 2] = [ - /// ("ignore_names", OptionEntry::Field(OptionField { - /// doc: "ignore_doc", - /// default: "ignore_default", - /// value_type: "value_type", - /// example: "ignore code" - /// })), - /// - /// ("global_names", OptionEntry::Field(OptionField { - /// doc: "global_doc", - /// default: "global_default", - /// value_type: "value_type", - /// example: "global code" - /// })) - /// ]; - /// - /// let group = OptionGroup::new(&OPTIONS); - /// - /// let ignore_names = group.get("ignore_names"); - /// - /// match ignore_names { - /// None => panic!("Expect option 'ignore_names' to be Some"), - /// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group), - /// Some(OptionEntry::Field(field)) => { - /// assert_eq!("ignore_doc", field.doc); + /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }); /// } /// } /// - /// assert_eq!(None, group.get("not_existing_option")); + /// assert!(WithOptions::metadata().has("ignore-git-ignore")); + /// assert!(!WithOptions::metadata().has("does-not-exist")); /// ``` - /// - /// ### Find a nested options + /// ### Test for the existence of a nested option /// /// ```rust - /// # use ruff_workspace::options_base::{OptionGroup, OptionEntry, OptionField}; - /// - /// const IGNORE_OPTIONS: [(&'static str, OptionEntry); 2] = [ - /// ("names", OptionEntry::Field(OptionField { - /// doc: "ignore_name_doc", - /// default: "ignore_name_default", - /// value_type: "value_type", - /// example: "ignore name code" - /// })), - /// - /// ("extensions", OptionEntry::Field(OptionField { - /// doc: "ignore_extensions_doc", - /// default: "ignore_extensions_default", - /// value_type: "value_type", - /// example: "ignore extensions code" - /// })) - /// ]; - /// - /// const OPTIONS: [(&'static str, OptionEntry); 2] = [ - /// ("ignore", OptionEntry::Group(OptionGroup::new(&IGNORE_OPTIONS))), - /// - /// ("global_names", OptionEntry::Field(OptionField { - /// doc: "global_doc", - /// default: "global_default", - /// value_type: "value_type", - /// example: "global code" - /// })) - /// ]; - /// - /// let group = OptionGroup::new(&OPTIONS); - /// - /// let ignore_names = group.get("ignore.names"); - /// - /// match ignore_names { - /// None => panic!("Expect option 'ignore.names' to be Some"), - /// Some(OptionEntry::Group(group)) => panic!("Expected 'ignore_names' to be a field but found group {}", group), - /// Some(OptionEntry::Field(field)) => { - /// assert_eq!("ignore_name_doc", field.doc); + /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }); + /// + /// visit.record_set("format", Nested::metadata()); /// } /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }); + /// } + /// } + /// + /// assert!(Root::metadata().has("format.hard-tabs")); + /// assert!(!Root::metadata().has("format.spaces")); + /// assert!(!Root::metadata().has("lint.hard-tabs")); /// ``` - pub fn get(&self, name: &str) -> Option<&OptionEntry> { - let mut parts = name.split('.').peekable(); - - let mut options = self.iter(); + pub fn has(&self, name: &str) -> bool { + self.find(name).is_some() + } - loop { - let part = parts.next()?; + /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise. + /// + /// The name can be separated by `.` to find a nested option. + /// + /// ## Examples + /// + /// ### Find a child option + /// + /// ```rust + /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// static IGNORE_GIT_IGNORE: OptionField = OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); + /// } + /// } + /// + /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))); + /// assert_eq!(WithOptions::metadata().find("does-not-exist"), None); + /// ``` + /// ### Find a nested option + /// + /// ```rust + /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// static HARD_TABS: OptionField = OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// }); + /// + /// visit.record_set("format", Nested::metadata()); + /// } + /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", HARD_TABS.clone()); + /// } + /// } + /// + /// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone()))); + /// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata()))); + /// assert_eq!(Root::metadata().find("format.spaces"), None); + /// assert_eq!(Root::metadata().find("lint.hard-tabs"), None); + /// ``` + pub fn find(&self, name: &str) -> Option { + struct FindOptionVisitor<'a> { + option: Option, + parts: std::str::Split<'a, char>, + needle: &'a str, + } - let (_, field) = options.find(|(name, _)| *name == part)?; + impl Visit for FindOptionVisitor<'_> { + fn record_set(&mut self, name: &str, set: OptionSet) { + if self.option.is_none() && name == self.needle { + if let Some(next) = self.parts.next() { + self.needle = next; + set.record(self); + } else { + self.option = Some(OptionEntry::Set(set)); + } + } + } - match (parts.peek(), field) { - (None, field) => return Some(field), - (Some(..), OptionEntry::Field(..)) => return None, - (Some(..), OptionEntry::Group(group)) => { - options = group.iter(); + fn record_field(&mut self, name: &str, field: OptionField) { + if self.option.is_none() && name == self.needle { + if self.parts.next().is_none() { + self.option = Some(OptionEntry::Field(field)); + } } } } + + let mut parts = name.split('.'); + + if let Some(first) = parts.next() { + let mut visitor = FindOptionVisitor { + parts, + needle: first, + option: None, + }; + + self.record(&mut visitor); + visitor.option + } else { + None + } } } -impl<'a> IntoIterator for &'a OptionGroup { - type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>; - type Item = &'a (&'a str, OptionEntry); +/// Visitor that writes out the names of all fields and sets. +struct DisplayVisitor<'fmt, 'buf> { + f: &'fmt mut Formatter<'buf>, + result: std::fmt::Result, +} + +impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> { + fn new(f: &'fmt mut Formatter<'buf>) -> Self { + Self { f, result: Ok(()) } + } - fn into_iter(self) -> Self::IntoIter { - self.0.iter() + fn finish(self) -> std::fmt::Result { + self.result } } -impl IntoIterator for OptionGroup { - type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>; - type Item = &'static (&'static str, OptionEntry); +impl Visit for DisplayVisitor<'_, '_> { + fn record_set(&mut self, name: &str, _: OptionSet) { + self.result = self.result.and_then(|_| writeln!(self.f, "{name}")); + } - fn into_iter(self) -> Self::IntoIter { - self.0.iter() + fn record_field(&mut self, name: &str, _: OptionField) { + self.result = self.result.and_then(|_| writeln!(self.f, "{name}")); } } -impl Display for OptionGroup { +impl Display for OptionSet { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (name, _) in self { - writeln!(f, "{name}")?; - } + let mut visitor = DisplayVisitor::new(f); + self.record(&mut visitor); + visitor.finish() + } +} - Ok(()) +impl Debug for OptionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct OptionField { pub doc: &'static str, pub default: &'static str,