diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8f72598fa6..c8fe963caa 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -12,8 +12,6 @@ license = "MIT" [lib] name = "ruff" -crate-type = ["cdylib", "rlib"] -doctest = false [dependencies] ruff_cache = { path = "../ruff_cache" } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 19e8db7f9d..d74c0d4c97 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -803,7 +803,7 @@ impl Linter { } } -#[derive(is_macro::Is)] +#[derive(is_macro::Is, Copy, Clone)] pub enum LintSource { Ast, Io, @@ -818,9 +818,9 @@ pub enum LintSource { impl Rule { /// The source for the diagnostic (either the AST, the filesystem, or the /// physical lines). - pub const fn lint_source(&self) -> &'static LintSource { + pub const fn lint_source(&self) -> LintSource { match self { - Rule::UnusedNOQA => &LintSource::Noqa, + Rule::UnusedNOQA => LintSource::Noqa, Rule::BlanketNOQA | Rule::BlanketTypeIgnore | Rule::DocLineTooLong @@ -836,7 +836,7 @@ impl Rule { | Rule::ShebangWhitespace | Rule::TrailingWhitespace | Rule::IndentationContainsTabs - | Rule::BlankLineContainsWhitespace => &LintSource::PhysicalLines, + | Rule::BlankLineContainsWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring | Rule::AmbiguousUnicodeCharacterString @@ -855,10 +855,10 @@ impl Rule { | Rule::UselessSemicolon | Rule::MultipleStatementsOnOneLineSemicolon | Rule::TrailingCommaProhibited - | Rule::TypeCommentInStub => &LintSource::Tokens, - Rule::IOError => &LintSource::Io, - Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports, - Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => &LintSource::Filesystem, + | Rule::TypeCommentInStub => LintSource::Tokens, + Rule::IOError => LintSource::Io, + Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports, + Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem, #[cfg(feature = "logical_lines")] Rule::IndentationWithInvalidMultiple | Rule::IndentationWithInvalidMultipleComment @@ -890,8 +890,8 @@ impl Rule { | Rule::WhitespaceAfterOpenBracket | Rule::WhitespaceBeforeCloseBracket | Rule::WhitespaceBeforeParameters - | Rule::WhitespaceBeforePunctuation => &LintSource::LogicalLines, - _ => &LintSource::Ast, + | Rule::WhitespaceBeforePunctuation => LintSource::LogicalLines, + _ => LintSource::Ast, } } } diff --git a/crates/ruff/src/settings/options_base.rs b/crates/ruff/src/settings/options_base.rs index 7be2617839..2450f73bfb 100644 --- a/crates/ruff/src/settings/options_base.rs +++ b/crates/ruff/src/settings/options_base.rs @@ -1,42 +1,180 @@ -pub trait ConfigurationOptions { - fn get_available_options() -> Vec<(&'static str, OptionEntry)>; +use std::fmt::{Display, Formatter}; - /// Get an option entry by its fully-qualified name - /// (e.g. `foo.bar` refers to the `bar` option in the `foo` group). - fn get(name: Option<&str>) -> Option { - let mut entries = Self::get_available_options(); +#[derive(Debug, Eq, PartialEq)] +pub enum OptionEntry { + Field(OptionField), + Group(OptionGroup), +} - let mut parts_iter = name.into_iter().flat_map(|s| s.split('.')); - - while let Some(part) = parts_iter.next() { - let (_, field) = entries.into_iter().find(|(name, _)| *name == part)?; - match field { - OptionEntry::Field(..) => { - if parts_iter.next().is_some() { - return None; - } - - return Some(field); - } - OptionEntry::Group(fields) => { - entries = fields; - } - } +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), } - Some(OptionEntry::Group(entries)) } } -#[derive(Debug)] -pub enum OptionEntry { - Field(OptionField), - Group(Vec<(&'static str, OptionEntry)>), +#[derive(Debug, Eq, PartialEq)] +pub struct OptionGroup(&'static [(&'static str, OptionEntry)]); + +impl OptionGroup { + pub const fn new(options: &'static [(&'static str, OptionEntry)]) -> Self { + Self(options) + } + + pub fn iter(&self) -> std::slice::Iter<(&str, OptionEntry)> { + self.into_iter() + } + + /// Get an option entry by its fully-qualified name + /// (e.g. `foo.bar` refers to the `bar` option in the `foo` group). + /// + /// ## Examples + /// + /// ### Find a direct child + /// + /// ```rust + /// # use ruff::settings::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); + /// } + /// } + /// + /// assert_eq!(None, group.get("not_existing_option")); + /// ``` + /// + /// ### Find a nested options + /// + ///```rust + /// # use ruff::settings::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); + /// } + /// } + /// ``` + pub fn get(&self, name: &str) -> Option<&OptionEntry> { + let mut parts = name.split('.').peekable(); + + let mut options = self.iter(); + + loop { + let part = parts.next()?; + + let (_, field) = options.find(|(name, _)| *name == part)?; + + match (parts.peek(), field) { + (None, field) => return Some(field), + (Some(..), OptionEntry::Field(..)) => return None, + (Some(..), OptionEntry::Group(group)) => { + options = group.iter(); + } + } + } + } } -#[derive(Debug)] +impl<'a> IntoIterator for &'a OptionGroup { + type Item = &'a (&'a str, OptionEntry); + type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for OptionGroup { + type Item = &'static (&'static str, OptionEntry); + type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl Display for OptionGroup { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (name, _) in self.iter() { + writeln!(f, "{name}")?; + } + + Ok(()) + } +} + +#[derive(Debug, Eq, PartialEq)] pub struct OptionField { pub doc: &'static str, pub default: &'static str, pub value_type: &'static str, pub example: &'static str, } + +impl Display for OptionField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.doc)?; + writeln!(f)?; + writeln!(f, "Default value: {}", self.default)?; + writeln!(f, "Type: {}", self.value_type)?; + writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) + } +} diff --git a/crates/ruff_cli/src/commands/config.rs b/crates/ruff_cli/src/commands/config.rs index 5520a75c25..a2db72a85f 100644 --- a/crates/ruff_cli/src/commands/config.rs +++ b/crates/ruff_cli/src/commands/config.rs @@ -1,35 +1,19 @@ use crate::ExitStatus; -use ruff::settings::{ - options::Options, - options_base::{ConfigurationOptions, OptionEntry, OptionField}, -}; +use ruff::settings::options::Options; #[allow(clippy::print_stdout)] pub(crate) fn config(key: Option<&str>) -> ExitStatus { - let Some(entry) = Options::get(key) else { - println!("Unknown option"); - return ExitStatus::Error; - }; - - match entry { - OptionEntry::Field(OptionField { - doc, - default, - value_type, - example, - }) => { - println!("{doc}"); - println!(); - println!("Default value: {default}"); - println!("Type: {value_type}"); - println!("Example usage:\n```toml\n{example}\n```"); - } - OptionEntry::Group(entries) => { - for (name, _) in entries { - println!("{name}"); + match key { + None => print!("{}", Options::metadata()), + Some(key) => match Options::metadata().get(key) { + None => { + println!("Unknown option"); + return ExitStatus::Error; } - } + Some(entry) => { + print!("{entry}"); + } + }, } - ExitStatus::Success } diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs index b7af1f4d68..038a23c14b 100644 --- a/crates/ruff_dev/src/generate_docs.rs +++ b/crates/ruff_dev/src/generate_docs.rs @@ -10,7 +10,6 @@ use strum::IntoEnumIterator; use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff::settings::options::Options; -use ruff::settings::options_base::ConfigurationOptions; use ruff_diagnostics::Availability; use crate::ROOT_DIR; @@ -92,7 +91,7 @@ fn process_documentation(documentation: &str, out: &mut String) { let option = rest.trim_end().trim_end_matches('`'); assert!( - Options::get(Some(option)).is_some(), + Options::metadata().get(option).is_some(), "unknown option {option}" ); diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 68f6ca6b62..8e6fe6f077 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -1,7 +1,7 @@ //! Generate a Markdown-compatible listing of configuration options. use itertools::Itertools; use ruff::settings::options::Options; -use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField}; +use ruff::settings::options_base::{OptionEntry, OptionField}; fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) { output.push_str(&format!("#### [`{name}`](#{name})\n")); @@ -27,8 +27,10 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: pub fn generate() -> String { let mut output: String = "### Top-level\n\n".into(); - let mut sorted_options = Options::get_available_options(); - sorted_options.sort_by_key(|(name, _)| *name); + 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 { diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 484724842a..1fb8da74d7 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -44,15 +44,17 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result }; } - Ok(quote! { - use crate::settings::options_base::{OptionEntry, OptionField, ConfigurationOptions}; + let options_len = output.len(); - #[automatically_derived] - impl ConfigurationOptions for #ident { - fn get_available_options() -> Vec<(&'static str, OptionEntry)> { - vec![#(#output),*] - } - } + Ok(quote! { + use crate::settings::options_base::{OptionEntry, OptionField, OptionGroup}; + + impl #ident { + pub const fn metadata() -> OptionGroup { + const OPTIONS: [(&'static str, OptionEntry); #options_len] = [#(#output),*]; + OptionGroup::new(&OPTIONS) + } + } }) } _ => Err(syn::Error::new( @@ -86,7 +88,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, OptionEntry::Group(#path::get_available_options())) + ident.span() => (#kebab_name, OptionEntry::Group(#path::metadata())) )) } _ => Err(syn::Error::new(