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}