1use serde::{Serialize, Serializer};
6use std::collections::BTreeMap;
7
8use std::fmt::{Debug, Display, Formatter};
9
10pub trait Visit {
14 fn record_field(&mut self, name: &str, field: OptionField);
16
17 fn record_set(&mut self, name: &str, group: OptionSet);
19}
20
21pub trait OptionsMetadata {
23 fn record(visit: &mut dyn Visit);
25
26 fn documentation() -> Option<&'static str> {
27 None
28 }
29
30 fn metadata() -> OptionSet
32 where
33 Self: Sized + 'static,
34 {
35 OptionSet::of::<Self>()
36 }
37}
38
39impl<T> OptionsMetadata for Option<T>
40where
41 T: OptionsMetadata,
42{
43 fn record(visit: &mut dyn Visit) {
44 T::record(visit);
45 }
46}
47
48#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
50#[serde(untagged)]
51pub enum OptionEntry {
52 Field(OptionField),
54
55 Set(OptionSet),
57}
58
59impl Display for OptionEntry {
60 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Set(set) => std::fmt::Display::fmt(set, f),
63 Self::Field(field) => std::fmt::Display::fmt(&field, f),
64 }
65 }
66}
67
68#[derive(Copy, Clone)]
73pub struct OptionSet {
74 record: fn(&mut dyn Visit),
75 doc: fn() -> Option<&'static str>,
76}
77
78impl PartialEq for OptionSet {
79 fn eq(&self, other: &Self) -> bool {
80 std::ptr::fn_addr_eq(self.record, other.record) && std::ptr::fn_addr_eq(self.doc, other.doc)
81 }
82}
83
84impl Eq for OptionSet {}
85
86impl OptionSet {
87 pub fn of<T>() -> Self
88 where
89 T: OptionsMetadata + 'static,
90 {
91 Self {
92 record: T::record,
93 doc: T::documentation,
94 }
95 }
96
97 pub fn record(&self, visit: &mut dyn Visit) {
99 let record = self.record;
100 record(visit);
101 }
102
103 pub fn documentation(&self) -> Option<&'static str> {
104 let documentation = self.doc;
105 documentation()
106 }
107
108 pub fn has(&self, name: &str) -> bool {
112 self.find(name).is_some()
113 }
114
115 pub fn find(&self, name: &str) -> Option<OptionEntry> {
119 struct FindOptionVisitor<'a> {
120 option: Option<OptionEntry>,
121 parts: std::str::Split<'a, char>,
122 needle: &'a str,
123 }
124
125 impl Visit for FindOptionVisitor<'_> {
126 fn record_set(&mut self, name: &str, set: OptionSet) {
127 if self.option.is_none() && name == self.needle {
128 if let Some(next) = self.parts.next() {
129 self.needle = next;
130 set.record(self);
131 } else {
132 self.option = Some(OptionEntry::Set(set));
133 }
134 }
135 }
136
137 fn record_field(&mut self, name: &str, field: OptionField) {
138 if self.option.is_none() && name == self.needle {
139 if self.parts.next().is_none() {
140 self.option = Some(OptionEntry::Field(field));
141 }
142 }
143 }
144 }
145
146 let mut parts = name.split('.');
147
148 if let Some(first) = parts.next() {
149 let mut visitor = FindOptionVisitor {
150 parts,
151 needle: first,
152 option: None,
153 };
154
155 self.record(&mut visitor);
156 visitor.option
157 } else {
158 None
159 }
160 }
161}
162
163struct DisplayVisitor<'fmt, 'buf> {
165 f: &'fmt mut Formatter<'buf>,
166 result: std::fmt::Result,
167}
168
169impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> {
170 fn new(f: &'fmt mut Formatter<'buf>) -> Self {
171 Self { f, result: Ok(()) }
172 }
173
174 fn finish(self) -> std::fmt::Result {
175 self.result
176 }
177}
178
179impl Visit for DisplayVisitor<'_, '_> {
180 fn record_set(&mut self, name: &str, _: OptionSet) {
181 self.result = self.result.and_then(|()| writeln!(self.f, "{name}"));
182 }
183
184 fn record_field(&mut self, name: &str, field: OptionField) {
185 self.result = self.result.and_then(|()| {
186 write!(self.f, "{name}")?;
187
188 if field.deprecated.is_some() {
189 write!(self.f, " (deprecated)")?;
190 }
191
192 writeln!(self.f)
193 });
194 }
195}
196
197impl Display for OptionSet {
198 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
199 let mut visitor = DisplayVisitor::new(f);
200 self.record(&mut visitor);
201 visitor.finish()
202 }
203}
204
205struct SerializeVisitor<'a> {
206 entries: &'a mut BTreeMap<String, OptionField>,
207}
208
209impl Visit for SerializeVisitor<'_> {
210 fn record_set(&mut self, name: &str, set: OptionSet) {
211 let mut entries = BTreeMap::new();
213 let mut visitor = SerializeVisitor {
214 entries: &mut entries,
215 };
216 set.record(&mut visitor);
217
218 for (key, value) in entries {
220 self.entries.insert(format!("{name}.{key}"), value);
221 }
222 }
223
224 fn record_field(&mut self, name: &str, field: OptionField) {
225 self.entries.insert(name.to_string(), field);
226 }
227}
228
229impl Serialize for OptionSet {
230 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231 where
232 S: Serializer,
233 {
234 let mut entries = BTreeMap::new();
235 let mut visitor = SerializeVisitor {
236 entries: &mut entries,
237 };
238 self.record(&mut visitor);
239 entries.serialize(serializer)
240 }
241}
242
243impl Debug for OptionSet {
244 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245 Display::fmt(self, f)
246 }
247}
248
249#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
250pub struct OptionField {
251 pub doc: &'static str,
252 pub default: &'static str,
254 pub value_type: &'static str,
256 pub scope: Option<&'static str>,
258 pub example: &'static str,
259 pub deprecated: Option<Deprecated>,
260 pub possible_values: Option<Vec<PossibleValue>>,
261 pub uv_toml_only: bool,
263}
264
265#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
266pub struct Deprecated {
267 pub since: Option<&'static str>,
268 pub message: Option<&'static str>,
269}
270
271impl Display for OptionField {
272 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
273 writeln!(f, "{}", self.doc)?;
274 writeln!(f)?;
275
276 writeln!(f, "Default value: {}", self.default)?;
277
278 if let Some(possible_values) = self
279 .possible_values
280 .as_ref()
281 .filter(|values| !values.is_empty())
282 {
283 writeln!(f, "Possible values:")?;
284 writeln!(f)?;
285 for value in possible_values {
286 writeln!(f, "- {value}")?;
287 }
288 } else {
289 writeln!(f, "Type: {}", self.value_type)?;
290 }
291
292 if let Some(deprecated) = &self.deprecated {
293 write!(f, "Deprecated")?;
294
295 if let Some(since) = deprecated.since {
296 write!(f, " (since {since})")?;
297 }
298
299 if let Some(message) = deprecated.message {
300 write!(f, ": {message}")?;
301 }
302
303 writeln!(f)?;
304 }
305
306 writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
307 }
308}
309
310#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
313pub struct PossibleValue {
314 pub name: String,
315 pub help: Option<String>,
316}
317
318impl Display for PossibleValue {
319 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320 write!(f, "`\"{}\"`", self.name)?;
321 if let Some(help) = &self.help {
322 write!(f, ": {help}")?;
323 }
324 Ok(())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_has_child_option() {
334 struct WithOptions;
335
336 impl OptionsMetadata for WithOptions {
337 fn record(visit: &mut dyn Visit) {
338 visit.record_field(
339 "ignore-git-ignore",
340 OptionField {
341 doc: "Whether Ruff should respect the gitignore file",
342 default: "false",
343 value_type: "bool",
344 example: "",
345 scope: None,
346 deprecated: None,
347 possible_values: None,
348 uv_toml_only: false,
349 },
350 );
351 }
352 }
353
354 assert!(WithOptions::metadata().has("ignore-git-ignore"));
355 assert!(!WithOptions::metadata().has("does-not-exist"));
356 }
357
358 #[test]
359 fn test_has_nested_option() {
360 struct Root;
361
362 impl OptionsMetadata for Root {
363 fn record(visit: &mut dyn Visit) {
364 visit.record_field(
365 "ignore-git-ignore",
366 OptionField {
367 doc: "Whether Ruff should respect the gitignore file",
368 default: "false",
369 value_type: "bool",
370 example: "",
371 scope: None,
372 deprecated: None,
373 possible_values: None,
374 uv_toml_only: false,
375 },
376 );
377
378 visit.record_set("format", Nested::metadata());
379 }
380 }
381
382 struct Nested;
383
384 impl OptionsMetadata for Nested {
385 fn record(visit: &mut dyn Visit) {
386 visit.record_field(
387 "hard-tabs",
388 OptionField {
389 doc: "Use hard tabs for indentation and spaces for alignment.",
390 default: "false",
391 value_type: "bool",
392 example: "",
393 scope: None,
394 deprecated: None,
395 possible_values: None,
396 uv_toml_only: false,
397 },
398 );
399 }
400 }
401
402 assert!(Root::metadata().has("format.hard-tabs"));
403 assert!(!Root::metadata().has("format.spaces"));
404 assert!(!Root::metadata().has("lint.hard-tabs"));
405 }
406
407 #[test]
408 fn test_find_child_option() {
409 struct WithOptions;
410
411 static IGNORE_GIT_IGNORE: OptionField = OptionField {
412 doc: "Whether Ruff should respect the gitignore file",
413 default: "false",
414 value_type: "bool",
415 example: "",
416 scope: None,
417 deprecated: None,
418 possible_values: None,
419 uv_toml_only: false,
420 };
421
422 impl OptionsMetadata for WithOptions {
423 fn record(visit: &mut dyn Visit) {
424 visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
425 }
426 }
427
428 assert_eq!(
429 WithOptions::metadata().find("ignore-git-ignore"),
430 Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))
431 );
432 assert_eq!(WithOptions::metadata().find("does-not-exist"), None);
433 }
434
435 #[test]
436 fn test_find_nested_option() {
437 static HARD_TABS: OptionField = OptionField {
438 doc: "Use hard tabs for indentation and spaces for alignment.",
439 default: "false",
440 value_type: "bool",
441 example: "",
442 scope: None,
443 deprecated: None,
444 possible_values: None,
445 uv_toml_only: false,
446 };
447
448 struct Root;
449
450 impl OptionsMetadata for Root {
451 fn record(visit: &mut dyn Visit) {
452 visit.record_field(
453 "ignore-git-ignore",
454 OptionField {
455 doc: "Whether Ruff should respect the gitignore file",
456 default: "false",
457 value_type: "bool",
458 example: "",
459 scope: None,
460 deprecated: None,
461 possible_values: None,
462 uv_toml_only: false,
463 },
464 );
465
466 visit.record_set("format", Nested::metadata());
467 }
468 }
469
470 struct Nested;
471
472 impl OptionsMetadata for Nested {
473 fn record(visit: &mut dyn Visit) {
474 visit.record_field("hard-tabs", HARD_TABS.clone());
475 }
476 }
477
478 assert_eq!(
479 Root::metadata().find("format.hard-tabs"),
480 Some(OptionEntry::Field(HARD_TABS.clone()))
481 );
482 assert_eq!(
483 Root::metadata().find("format"),
484 Some(OptionEntry::Set(Nested::metadata()))
485 );
486 assert_eq!(Root::metadata().find("format.spaces"), None);
487 assert_eq!(Root::metadata().find("lint.hard-tabs"), None);
488 }
489}