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,