Alioth Code Coverage

help.rs0.00%

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashSet;
16use std::ffi::{CStr, CString, OsStr, OsString};
17use std::num::NonZero;
18use std::path::{Path, PathBuf};
19
20pub use serde_aco_derive::Help;
21
22#[derive(Debug)]
23pub struct FieldHelp {
24 pub ident: &'static str,
25 pub doc: &'static str,
26 pub ty: TypedHelp,
27}
28
29#[derive(Debug)]
30pub enum TypedHelp {
31 Struct {
32 name: &'static str,
33 fields: &'static [FieldHelp],
34 },
35 Enum {
36 name: &'static str,
37 variants: &'static [FieldHelp],
38 },
39 String,
40 Int,
41 Float,
42 Bool,
43 Unit,
44 Custom {
45 desc: &'static str,
46 },
47 Array(&'static TypedHelp),
48 Option(&'static TypedHelp),
49}
50
51pub trait Help {
52 const HELP: TypedHelp;
53}
54
55macro_rules! impl_help_for_num_types {
56 ($help_type:ident, $($ty:ty),+) => {
57 $(impl Help for $ty {
58 const HELP: TypedHelp = TypedHelp::$help_type;
59 })+
60 $(impl Help for NonZero<$ty> {
61 const HELP: TypedHelp = TypedHelp::$help_type;
62 })+
63 };
64}
65
66macro_rules! impl_help_for_types {
67 ($help_type:ident, $($ty:ty),+) => {
68 $(impl Help for $ty {
69 const HELP: TypedHelp = TypedHelp::$help_type;
70 })+
71 };
72}
73
74macro_rules! impl_help_for_array_types {
75 ($($ty:ty),+) => {
76 $(impl<T> Help for $ty where T: Help {
77 const HELP: TypedHelp = TypedHelp::Array(&T::HELP);
78 })+
79 };
80}
81
82impl_help_for_num_types!(
83 Int, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
84);
85impl_help_for_types!(Float, f32, f64);
86impl_help_for_types!(Bool, bool);
87impl_help_for_types!(
88 String,
89 &str,
90 Box<str>,
91 String,
92 CStr,
93 CString,
94 &OsStr,
95 OsString,
96 &Path,
97 Box<Path>,
98 PathBuf
99);
100impl_help_for_array_types!(&[T], Box<[T]>, Vec<T>);
101
102impl<T> Help for Option<T>
103where
104 T: Help,
105{
106 const HELP: TypedHelp = TypedHelp::Option(&T::HELP);
107}
108
109#[derive(Debug, Default)]
110struct ExtraHelp<'a> {
111 types: HashSet<&'static str>,
112 helps: Vec<&'a TypedHelp>,
113}
114
115fn add_value_type(s: &mut String, v: &TypedHelp) {
116 let type_s = match v {
117 TypedHelp::Bool => "bool",
118 TypedHelp::Int => "integer",
119 TypedHelp::Float => "float",
120 TypedHelp::String => "string",
121 TypedHelp::Unit => todo!(),
122 TypedHelp::Custom { desc } => desc,
123 TypedHelp::Struct { name, .. } => name,
124 TypedHelp::Enum { name, .. } => name,
125 TypedHelp::Option(o) => return add_value_type(s, o),
126 TypedHelp::Array(t) => {
127 s.push_str("array<");
128 add_value_type(s, t);
129 s.push('>');
130 return;
131 }
132 };
133 s.push_str(type_s);
134}
135
136fn add_extra_help<'a>(extra: &mut ExtraHelp<'a>, v: &'a TypedHelp) {
137 let v = match v {
138 TypedHelp::Array(t) => t,
139 TypedHelp::Option(t) => t,
140 _ => v,
141 };
142 let (TypedHelp::Enum {
143 name,
144 variants: fields,
145 }
146 | TypedHelp::Struct { name, fields }) = v
147 else {
148 return;
149 };
150 if extra.types.insert(name) {
151 extra.helps.push(v);
152 for f in fields.iter() {
153 add_extra_help(extra, &f.ty);
154 }
155 }
156}
157
158fn extra_help(s: &mut String, v: &TypedHelp) {
159 s.push_str("# ");
160 match v {
161 TypedHelp::Struct { name, fields } => {
162 struct_help(s, &mut None, name, fields, 2);
163 }
164 TypedHelp::Enum { name, variants } => {
165 enum_help(s, &mut None, name, variants, 2);
166 }
167 _ => unreachable!(),
168 }
169}
170
171fn next_line(s: &mut String, indent: usize) {
172 s.push('\n');
173 for _ in 0..indent {
174 s.push(' ');
175 }
176}
177
178fn one_key_val<'a>(s: &mut String, extra: &mut Option<&mut ExtraHelp<'a>>, f: &'a FieldHelp) {
179 if f.ident.is_empty() {
180 let fields = match f.ty {
181 TypedHelp::Enum { variants, .. } => variants,
182 _ => unreachable!(),
183 };
184 s.push('(');
185 let mut need_separator = false;
186 for field in fields {
187 if need_separator {
188 s.push('|');
189 } else {
190 need_separator = true;
191 }
192 one_key_val(s, extra, field)
193 }
194 s.push(')');
195 } else {
196 s.push_str(f.ident);
197 s.push_str("=<");
198 add_value_type(s, &f.ty);
199 s.push('>');
200 if let Some(extra) = extra {
201 add_extra_help(extra, &f.ty)
202 }
203 }
204}
205
206fn key_val_pairs<'a>(
207 s: &mut String,
208 extra: &mut Option<&mut ExtraHelp<'a>>,
209 variant: &str,
210 fields: &'a [FieldHelp],
211) {
212 let mut add_comma = false;
213 if !variant.is_empty() {
214 s.push_str(variant);
215 add_comma = true;
216 }
217 for f in fields.iter() {
218 if add_comma {
219 s.push(',');
220 } else {
221 add_comma = true;
222 }
223 one_key_val(s, extra, f);
224 }
225}
226
227fn value_helps(s: &mut String, indent: usize, width: usize, fields: &[FieldHelp]) {
228 for f in fields.iter() {
229 if f.ident.is_empty() {
230 let fields = match f.ty {
231 TypedHelp::Enum { variants, .. } => variants,
232 _ => unreachable!(),
233 };
234 value_helps(s, indent, width, fields)
235 } else if f.doc.is_empty() {
236 continue;
237 } else {
238 next_line(s, indent);
239 let mut first_line = true;
240 for line in f.doc.lines() {
241 if first_line {
242 s.push_str(&format!("- {:width$}\t{}", f.ident, line, width = width));
243 first_line = false;
244 } else {
245 next_line(s, indent + width + 2);
246 s.push('\t');
247 s.push_str(line);
248 }
249 }
250 }
251 }
252}
253
254fn fields_ident_len_max(fields: &[FieldHelp]) -> Option<usize> {
255 let ident_len = |field: &FieldHelp| {
256 if !field.ident.is_empty() {
257 return Some(field.ident.len());
258 }
259 match field.ty {
260 TypedHelp::Enum { variants, .. } => fields_ident_len_max(variants),
261 TypedHelp::Struct { fields, .. } => fields_ident_len_max(fields),
262 _ => unreachable!(),
263 }
264 };
265
266 fields.iter().flat_map(ident_len).max()
267}
268
269fn field_helps(s: &mut String, indent: usize, fields: &[FieldHelp]) {
270 let Some(width) = fields_ident_len_max(fields) else {
271 return;
272 };
273 value_helps(s, indent, width, fields)
274}
275
276fn struct_help<'a>(
277 s: &mut String,
278 extra: &mut Option<&mut ExtraHelp<'a>>,
279 desc: &str,
280 fields: &'a [FieldHelp],
281 indent: usize,
282) {
283 s.push_str(desc);
284 next_line(s, indent);
285 s.push_str("* ");
286 key_val_pairs(s, extra, "", fields);
287 field_helps(s, indent + 2, fields);
288}
289
290fn enum_all_unit_help(s: &mut String, variants: &[FieldHelp], indent: usize) -> bool {
291 if variants.iter().any(|f| !matches!(f.ty, TypedHelp::Unit)) {
292 return false;
293 }
294 let Some(width) = variants.iter().map(|f| f.ident.len()).max() else {
295 return false;
296 };
297 for variant in variants.iter() {
298 next_line(s, indent);
299 s.push_str(&format!(
300 "* {:width$}\t{}",
301 variant.ident,
302 variant.doc,
303 width = width
304 ));
305 }
306 true
307}
308
309fn enum_help<'a>(
310 s: &mut String,
311 extra: &mut Option<&mut ExtraHelp<'a>>,
312 doc: &str,
313 variants: &'a [FieldHelp],
314 indent: usize,
315) {
316 s.push_str(doc);
317 if enum_all_unit_help(s, variants, indent) {
318 return;
319 }
320 if variants.is_empty() {
321 next_line(s, indent);
322 s.push_str("No options available");
323 }
324 for variant in variants.iter() {
325 next_line(s, indent);
326 s.push_str("* ");
327 match &variant.ty {
328 TypedHelp::Struct { fields, .. } => {
329 key_val_pairs(s, extra, variant.ident, fields);
330 next_line(s, indent + 2);
331 s.push_str(variant.doc);
332 field_helps(s, indent + 2, fields);
333 }
334 TypedHelp::Unit => {
335 s.push_str(variant.ident);
336 next_line(s, indent + 2);
337 s.push_str(variant.doc);
338 }
339 TypedHelp::String
340 | TypedHelp::Int
341 | TypedHelp::Float
342 | TypedHelp::Bool
343 | TypedHelp::Custom { .. } => {
344 s.push_str(variant.ident);
345 s.push_str(",<");
346 add_value_type(s, &variant.ty);
347 s.push('>');
348 next_line(s, indent + 2);
349 s.push_str(variant.doc);
350 }
351 _ => todo!("{:?}", variant.ty),
352 };
353 }
354}
355
356pub fn help_text<T: Help>(doc: &str) -> String {
357 let help = T::HELP;
358 let mut s = String::new();
359 let mut extra = ExtraHelp::default();
360 match &help {
361 TypedHelp::Struct { fields, .. } => {
362 struct_help(&mut s, &mut Some(&mut extra), doc, fields, 0);
363 }
364 TypedHelp::Enum { variants, .. } => {
365 enum_help(&mut s, &mut Some(&mut extra), doc, variants, 0)
366 }
367 _ => unreachable!("{:?}", help),
368 }
369 for h in extra.helps {
370 next_line(&mut s, 0);
371 extra_help(&mut s, h);
372 }
373 s
374}
375