Skip to main content

ruff_options_metadata/
lib.rs

1use std::fmt::{Debug, Display, Formatter};
2
3/// Visits [`OptionsMetadata`].
4///
5/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata.
6pub trait Visit {
7    /// Visits an [`OptionField`] value named `name`.
8    fn record_field(&mut self, name: &str, field: OptionField);
9
10    /// Visits an [`OptionSet`] value named `name`.
11    fn record_set(&mut self, name: &str, group: OptionSet);
12}
13
14/// Returns metadata for its options.
15pub trait OptionsMetadata {
16    /// Visits the options metadata of this object by calling `visit` for each option.
17    fn record(visit: &mut dyn Visit);
18
19    fn documentation() -> Option<&'static str> {
20        None
21    }
22
23    /// Returns the extracted metadata.
24    fn metadata() -> OptionSet
25    where
26        Self: Sized + 'static,
27    {
28        OptionSet::of::<Self>()
29    }
30}
31
32impl<T> OptionsMetadata for Option<T>
33where
34    T: OptionsMetadata,
35{
36    fn record(visit: &mut dyn Visit) {
37        T::record(visit);
38    }
39}
40
41/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
42#[derive(Clone, Debug)]
43#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))]
44pub enum OptionEntry {
45    /// A single option.
46    Field(OptionField),
47
48    /// A set of options.
49    Set(OptionSet),
50}
51
52impl OptionEntry {
53    pub fn into_field(self) -> Option<OptionField> {
54        match self {
55            OptionEntry::Field(field) => Some(field),
56            OptionEntry::Set(_) => None,
57        }
58    }
59}
60
61impl Display for OptionEntry {
62    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63        match self {
64            OptionEntry::Set(set) => std::fmt::Display::fmt(set, f),
65            OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f),
66        }
67    }
68}
69
70/// A set of options.
71///
72/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing
73/// [`OptionsMetadata`].
74#[derive(Copy, Clone)]
75pub struct OptionSet {
76    record: fn(&mut dyn Visit),
77    doc: fn() -> Option<&'static str>,
78}
79
80impl OptionSet {
81    pub fn of<T>() -> Self
82    where
83        T: OptionsMetadata + 'static,
84    {
85        Self {
86            record: T::record,
87            doc: T::documentation,
88        }
89    }
90
91    /// Visits the options in this set by calling `visit` for each option.
92    pub fn record(&self, visit: &mut dyn Visit) {
93        let record = self.record;
94        record(visit);
95    }
96
97    pub fn documentation(&self) -> Option<&'static str> {
98        let documentation = self.doc;
99        documentation()
100    }
101
102    /// Returns `true` if this set has an option that resolves to `name`.
103    ///
104    /// The name can be separated by `.` to find a nested option.
105    ///
106    /// ## Examples
107    ///
108    /// ### Test for the existence of a child option
109    ///
110    /// ```rust
111    /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
112    ///
113    /// struct WithOptions;
114    ///
115    /// impl OptionsMetadata for WithOptions {
116    ///     fn record(visit: &mut dyn Visit) {
117    ///         visit.record_field("ignore-git-ignore", OptionField {
118    ///             doc: "Whether Ruff should respect the gitignore file",
119    ///             default: "false",
120    ///             value_type: "bool",
121    ///             example: "",
122    ///             scope: None,
123    ///             deprecated: None,
124    ///         });
125    ///     }
126    /// }
127    ///
128    /// assert!(WithOptions::metadata().has("ignore-git-ignore"));
129    /// assert!(!WithOptions::metadata().has("does-not-exist"));
130    /// ```
131    /// ### Test for the existence of a nested option
132    ///
133    /// ```rust
134    /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
135    ///
136    /// struct Root;
137    ///
138    /// impl OptionsMetadata for Root {
139    ///     fn record(visit: &mut dyn Visit) {
140    ///         visit.record_field("ignore-git-ignore", OptionField {
141    ///             doc: "Whether Ruff should respect the gitignore file",
142    ///             default: "false",
143    ///             value_type: "bool",
144    ///             example: "",
145    ///             scope: None,
146    ///             deprecated: None
147    ///         });
148    ///
149    ///         visit.record_set("format", Nested::metadata());
150    ///     }
151    /// }
152    ///
153    /// struct Nested;
154    ///
155    /// impl OptionsMetadata for Nested {
156    ///     fn record(visit: &mut dyn Visit) {
157    ///         visit.record_field("hard-tabs", OptionField {
158    ///             doc: "Use hard tabs for indentation and spaces for alignment.",
159    ///             default: "false",
160    ///             value_type: "bool",
161    ///             example: "",
162    ///             scope: None,
163    ///             deprecated: None
164    ///         });
165    ///     }
166    /// }
167    ///
168    /// assert!(Root::metadata().has("format.hard-tabs"));
169    /// assert!(!Root::metadata().has("format.spaces"));
170    /// assert!(!Root::metadata().has("lint.hard-tabs"));
171    /// ```
172    pub fn has(&self, name: &str) -> bool {
173        self.find(name).is_some()
174    }
175
176    /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise.
177    ///
178    /// The name can be separated by `.` to find a nested option.
179    ///
180    /// ## Examples
181    ///
182    /// ### Find a child option
183    ///
184    /// ```rust
185    /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
186    ///
187    /// struct WithOptions;
188    ///
189    /// static IGNORE_GIT_IGNORE: OptionField = OptionField {
190    ///     doc: "Whether Ruff should respect the gitignore file",
191    ///     default: "false",
192    ///     value_type: "bool",
193    ///     example: "",
194    ///     scope: None,
195    ///     deprecated: None
196    ///  };
197    ///
198    /// impl OptionsMetadata for WithOptions {
199    ///     fn record(visit: &mut dyn Visit) {
200    ///         visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
201    ///     }
202    /// }
203    ///
204    /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore").and_then(OptionEntry::into_field), Some(IGNORE_GIT_IGNORE.clone()));
205    /// assert!(WithOptions::metadata().find("does-not-exist").is_none());
206    /// ```
207    /// ### Find a nested option
208    ///
209    /// ```rust
210    /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
211    ///
212    /// static HARD_TABS: OptionField = OptionField {
213    ///     doc: "Use hard tabs for indentation and spaces for alignment.",
214    ///     default: "false",
215    ///     value_type: "bool",
216    ///     example: "",
217    ///     scope: None,
218    ///     deprecated: None
219    /// };
220    ///
221    /// struct Root;
222    ///
223    /// impl OptionsMetadata for Root {
224    ///     fn record(visit: &mut dyn Visit) {
225    ///         visit.record_field("ignore-git-ignore", OptionField {
226    ///             doc: "Whether Ruff should respect the gitignore file",
227    ///             default: "false",
228    ///             value_type: "bool",
229    ///             example: "",
230    ///             scope: None,
231    ///             deprecated: None
232    ///         });
233    ///
234    ///         visit.record_set("format", Nested::metadata());
235    ///     }
236    /// }
237    ///
238    /// struct Nested;
239    ///
240    /// impl OptionsMetadata for Nested {
241    ///     fn record(visit: &mut dyn Visit) {
242    ///         visit.record_field("hard-tabs", HARD_TABS.clone());
243    ///     }
244    /// }
245    ///
246    /// assert_eq!(Root::metadata().find("format.hard-tabs").and_then(OptionEntry::into_field), Some(HARD_TABS.clone()));
247    /// assert!(matches!(Root::metadata().find("format"), Some(OptionEntry::Set(_))));
248    /// assert!(Root::metadata().find("format.spaces").is_none());
249    /// assert!(Root::metadata().find("lint.hard-tabs").is_none());
250    /// ```
251    pub fn find(&self, name: &str) -> Option<OptionEntry> {
252        struct FindOptionVisitor<'a> {
253            option: Option<OptionEntry>,
254            parts: std::str::Split<'a, char>,
255            needle: &'a str,
256        }
257
258        impl Visit for FindOptionVisitor<'_> {
259            fn record_set(&mut self, name: &str, set: OptionSet) {
260                if self.option.is_none() && name == self.needle {
261                    if let Some(next) = self.parts.next() {
262                        self.needle = next;
263                        set.record(self);
264                    } else {
265                        self.option = Some(OptionEntry::Set(set));
266                    }
267                }
268            }
269
270            fn record_field(&mut self, name: &str, field: OptionField) {
271                if self.option.is_none() && name == self.needle {
272                    if self.parts.next().is_none() {
273                        self.option = Some(OptionEntry::Field(field));
274                    }
275                }
276            }
277        }
278
279        let mut parts = name.split('.');
280
281        if let Some(first) = parts.next() {
282            let mut visitor = FindOptionVisitor {
283                parts,
284                needle: first,
285                option: None,
286            };
287
288            self.record(&mut visitor);
289            visitor.option
290        } else {
291            None
292        }
293    }
294
295    pub fn collect_fields(&self) -> Vec<(String, OptionField)> {
296        struct FieldsCollector(Vec<(String, OptionField)>);
297
298        impl Visit for FieldsCollector {
299            fn record_field(&mut self, name: &str, field: OptionField) {
300                self.0.push((name.to_string(), field));
301            }
302
303            fn record_set(&mut self, _name: &str, _group: OptionSet) {}
304        }
305
306        let mut visitor = FieldsCollector(vec![]);
307        self.record(&mut visitor);
308        visitor.0
309    }
310}
311
312/// Visitor that writes out the names of all fields and sets.
313struct DisplayVisitor<'fmt, 'buf> {
314    f: &'fmt mut Formatter<'buf>,
315    result: std::fmt::Result,
316}
317
318impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> {
319    fn new(f: &'fmt mut Formatter<'buf>) -> Self {
320        Self { f, result: Ok(()) }
321    }
322
323    fn finish(self) -> std::fmt::Result {
324        self.result
325    }
326}
327
328impl Visit for DisplayVisitor<'_, '_> {
329    fn record_set(&mut self, name: &str, _: OptionSet) {
330        self.result = self.result.and_then(|()| writeln!(self.f, "{name}"));
331    }
332
333    fn record_field(&mut self, name: &str, field: OptionField) {
334        self.result = self.result.and_then(|()| {
335            write!(self.f, "{name}")?;
336
337            if field.deprecated.is_some() {
338                write!(self.f, " (deprecated)")?;
339            }
340
341            writeln!(self.f)
342        });
343    }
344}
345
346impl Display for OptionSet {
347    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
348        let mut visitor = DisplayVisitor::new(f);
349        self.record(&mut visitor);
350        visitor.finish()
351    }
352}
353
354impl Debug for OptionSet {
355    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
356        Display::fmt(self, f)
357    }
358}
359
360#[derive(Debug, Eq, PartialEq, Clone)]
361#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
362pub struct OptionField {
363    pub doc: &'static str,
364    /// Ex) `"false"`
365    pub default: &'static str,
366    /// Ex) `"bool"`
367    pub value_type: &'static str,
368    /// Ex) `"per-file-ignores"`
369    pub scope: Option<&'static str>,
370    pub example: &'static str,
371    pub deprecated: Option<Deprecated>,
372}
373
374#[derive(Debug, Clone, Eq, PartialEq)]
375#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
376pub struct Deprecated {
377    pub since: Option<&'static str>,
378    pub message: Option<&'static str>,
379}
380
381impl Display for OptionField {
382    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
383        writeln!(f, "{}", self.doc)?;
384        writeln!(f)?;
385        writeln!(f, "Default value: {}", self.default)?;
386        writeln!(f, "Type: {}", self.value_type)?;
387
388        if let Some(deprecated) = &self.deprecated {
389            write!(f, "Deprecated")?;
390
391            if let Some(since) = deprecated.since {
392                write!(f, " (since {since})")?;
393            }
394
395            if let Some(message) = deprecated.message {
396                write!(f, ": {message}")?;
397            }
398
399            writeln!(f)?;
400        }
401
402        writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
403    }
404}
405
406#[cfg(feature = "serde")]
407mod serde {
408    use super::{OptionField, OptionSet, Visit};
409    use serde::{Serialize, Serializer};
410    use std::collections::BTreeMap;
411
412    impl Serialize for OptionSet {
413        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
414        where
415            S: Serializer,
416        {
417            let mut entries = BTreeMap::new();
418            let mut visitor = SerializeVisitor {
419                entries: &mut entries,
420            };
421            self.record(&mut visitor);
422            entries.serialize(serializer)
423        }
424    }
425
426    struct SerializeVisitor<'a> {
427        entries: &'a mut BTreeMap<String, OptionField>,
428    }
429
430    impl Visit for SerializeVisitor<'_> {
431        fn record_set(&mut self, name: &str, set: OptionSet) {
432            // Collect the entries of the set.
433            let mut entries = BTreeMap::new();
434            let mut visitor = SerializeVisitor {
435                entries: &mut entries,
436            };
437            set.record(&mut visitor);
438
439            // Insert the set into the entries.
440            for (key, value) in entries {
441                self.entries.insert(format!("{name}.{key}"), value);
442            }
443        }
444
445        fn record_field(&mut self, name: &str, field: OptionField) {
446            self.entries.insert(name.to_string(), field);
447        }
448    }
449}