• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2020 Google LLC All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 
5 use {
6     crate::errors::Errors,
7     proc_macro2::Span,
8     std::collections::hash_map::{Entry, HashMap},
9 };
10 
11 /// Attributes applied to a field of a `#![derive(FromArgs)]` struct.
12 #[derive(Default)]
13 pub struct FieldAttrs {
14     pub default: Option<syn::LitStr>,
15     pub description: Option<Description>,
16     pub from_str_fn: Option<syn::Ident>,
17     pub field_type: Option<FieldType>,
18     pub long: Option<syn::LitStr>,
19     pub short: Option<syn::LitChar>,
20     pub arg_name: Option<syn::LitStr>,
21     pub greedy: Option<syn::Path>,
22     pub hidden_help: bool,
23 }
24 
25 /// The purpose of a particular field on a `#![derive(FromArgs)]` struct.
26 #[derive(Copy, Clone, Eq, PartialEq)]
27 pub enum FieldKind {
28     /// Switches are booleans that are set to "true" by passing the flag.
29     Switch,
30     /// Options are `--key value`. They may be optional (using `Option`),
31     /// or repeating (using `Vec`), or required (neither `Option` nor `Vec`)
32     Option,
33     /// Subcommand fields (of which there can be at most one) refer to enums
34     /// containing one of several potential subcommands. They may be optional
35     /// (using `Option`) or required (no `Option`).
36     SubCommand,
37     /// Positional arguments are parsed literally if the input
38     /// does not begin with `-` or `--` and is not a subcommand.
39     /// They are parsed in declaration order, and only the last positional
40     /// argument in a type may be an `Option`, `Vec`, or have a default value.
41     Positional,
42 }
43 
44 /// The type of a field on a `#![derive(FromArgs)]` struct.
45 ///
46 /// This is a simple wrapper around `FieldKind` which includes the `syn::Ident`
47 /// of the attribute containing the field kind.
48 pub struct FieldType {
49     pub kind: FieldKind,
50     pub ident: syn::Ident,
51 }
52 
53 /// A description of a `#![derive(FromArgs)]` struct.
54 ///
55 /// Defaults to the docstring if one is present, or `#[argh(description = "...")]`
56 /// if one is provided.
57 pub struct Description {
58     /// Whether the description was an explicit annotation or whether it was a doc string.
59     pub explicit: bool,
60     pub content: syn::LitStr,
61 }
62 
63 impl FieldAttrs {
parse(errors: &Errors, field: &syn::Field) -> Self64     pub fn parse(errors: &Errors, field: &syn::Field) -> Self {
65         let mut this = Self::default();
66 
67         for attr in &field.attrs {
68             if is_doc_attr(attr) {
69                 parse_attr_doc(errors, attr, &mut this.description);
70                 continue;
71             }
72 
73             let ml = if let Some(ml) = argh_attr_to_meta_list(errors, attr) {
74                 ml
75             } else {
76                 continue;
77             };
78 
79             for meta in &ml.nested {
80                 let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
81 
82                 let name = meta.path();
83                 if name.is_ident("arg_name") {
84                     if let Some(m) = errors.expect_meta_name_value(meta) {
85                         this.parse_attr_arg_name(errors, m);
86                     }
87                 } else if name.is_ident("default") {
88                     if let Some(m) = errors.expect_meta_name_value(meta) {
89                         this.parse_attr_default(errors, m);
90                     }
91                 } else if name.is_ident("description") {
92                     if let Some(m) = errors.expect_meta_name_value(meta) {
93                         parse_attr_description(errors, m, &mut this.description);
94                     }
95                 } else if name.is_ident("from_str_fn") {
96                     if let Some(m) = errors.expect_meta_list(meta) {
97                         this.parse_attr_from_str_fn(errors, m);
98                     }
99                 } else if name.is_ident("long") {
100                     if let Some(m) = errors.expect_meta_name_value(meta) {
101                         this.parse_attr_long(errors, m);
102                     }
103                 } else if name.is_ident("option") {
104                     parse_attr_field_type(errors, meta, FieldKind::Option, &mut this.field_type);
105                 } else if name.is_ident("short") {
106                     if let Some(m) = errors.expect_meta_name_value(meta) {
107                         this.parse_attr_short(errors, m);
108                     }
109                 } else if name.is_ident("subcommand") {
110                     parse_attr_field_type(
111                         errors,
112                         meta,
113                         FieldKind::SubCommand,
114                         &mut this.field_type,
115                     );
116                 } else if name.is_ident("switch") {
117                     parse_attr_field_type(errors, meta, FieldKind::Switch, &mut this.field_type);
118                 } else if name.is_ident("positional") {
119                     parse_attr_field_type(
120                         errors,
121                         meta,
122                         FieldKind::Positional,
123                         &mut this.field_type,
124                     );
125                 } else if name.is_ident("greedy") {
126                     this.greedy = Some(name.clone());
127                 } else if name.is_ident("hidden_help") {
128                     this.hidden_help = true;
129                 } else {
130                     errors.err(
131                         &meta,
132                         concat!(
133                             "Invalid field-level `argh` attribute\n",
134                             "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `greedy`, ",
135                             "`long`, `option`, `short`, `subcommand`, `switch`, `hidden_help`",
136                         ),
137                     );
138                 }
139             }
140         }
141 
142         if let (Some(default), Some(field_type)) = (&this.default, &this.field_type) {
143             match field_type.kind {
144                 FieldKind::Option | FieldKind::Positional => {}
145                 FieldKind::SubCommand | FieldKind::Switch => errors.err(
146                     default,
147                     "`default` may only be specified on `#[argh(option)]` \
148                      or `#[argh(positional)]` fields",
149                 ),
150             }
151         }
152 
153         match (&this.greedy, this.field_type.as_ref().map(|f| f.kind)) {
154             (Some(_), Some(FieldKind::Positional)) => {}
155             (Some(greedy), Some(_)) => errors.err(
156                 &greedy,
157                 "`greedy` may only be specified on `#[argh(positional)]` \
158                     fields",
159             ),
160             _ => {}
161         }
162 
163         if let Some(d) = &this.description {
164             check_option_description(errors, d.content.value().trim(), d.content.span());
165         }
166 
167         this
168     }
169 
parse_attr_from_str_fn(&mut self, errors: &Errors, m: &syn::MetaList)170     fn parse_attr_from_str_fn(&mut self, errors: &Errors, m: &syn::MetaList) {
171         parse_attr_fn_name(errors, m, "from_str_fn", &mut self.from_str_fn)
172     }
173 
parse_attr_default(&mut self, errors: &Errors, m: &syn::MetaNameValue)174     fn parse_attr_default(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
175         parse_attr_single_string(errors, m, "default", &mut self.default);
176     }
177 
parse_attr_arg_name(&mut self, errors: &Errors, m: &syn::MetaNameValue)178     fn parse_attr_arg_name(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
179         parse_attr_single_string(errors, m, "arg_name", &mut self.arg_name);
180     }
181 
parse_attr_long(&mut self, errors: &Errors, m: &syn::MetaNameValue)182     fn parse_attr_long(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
183         parse_attr_single_string(errors, m, "long", &mut self.long);
184         let long = self.long.as_ref().unwrap();
185         let value = long.value();
186         check_long_name(errors, long, &value);
187     }
188 
parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue)189     fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
190         if let Some(first) = &self.short {
191             errors.duplicate_attrs("short", first, m);
192         } else if let Some(lit_char) = errors.expect_lit_char(&m.lit) {
193             self.short = Some(lit_char.clone());
194             if !lit_char.value().is_ascii() {
195                 errors.err(lit_char, "Short names must be ASCII");
196             }
197         }
198     }
199 }
200 
check_long_name(errors: &Errors, spanned: &impl syn::spanned::Spanned, value: &str)201 pub(crate) fn check_long_name(errors: &Errors, spanned: &impl syn::spanned::Spanned, value: &str) {
202     if !value.is_ascii() {
203         errors.err(spanned, "Long names must be ASCII");
204     }
205     if !value.chars().all(|c| c.is_lowercase() || c == '-' || c.is_ascii_digit()) {
206         errors.err(spanned, "Long names must be lowercase");
207     }
208 }
209 
parse_attr_fn_name( errors: &Errors, m: &syn::MetaList, attr_name: &str, slot: &mut Option<syn::Ident>, )210 fn parse_attr_fn_name(
211     errors: &Errors,
212     m: &syn::MetaList,
213     attr_name: &str,
214     slot: &mut Option<syn::Ident>,
215 ) {
216     if let Some(first) = slot {
217         errors.duplicate_attrs(attr_name, first, m);
218     }
219 
220     if m.nested.len() != 1 {
221         errors.err(&m.nested, "Expected a single argument containing the function name");
222         return;
223     }
224 
225     // `unwrap` will not fail because of the call above
226     let nested = m.nested.first().unwrap();
227     if let Some(path) = errors.expect_nested_meta(nested).and_then(|m| errors.expect_meta_word(m)) {
228         *slot = path.get_ident().cloned();
229     }
230 }
231 
parse_attr_field_type( errors: &Errors, meta: &syn::Meta, kind: FieldKind, slot: &mut Option<FieldType>, )232 fn parse_attr_field_type(
233     errors: &Errors,
234     meta: &syn::Meta,
235     kind: FieldKind,
236     slot: &mut Option<FieldType>,
237 ) {
238     if let Some(path) = errors.expect_meta_word(meta) {
239         if let Some(first) = slot {
240             errors.duplicate_attrs("field kind", &first.ident, path);
241         } else if let Some(word) = path.get_ident() {
242             *slot = Some(FieldType { kind, ident: word.clone() });
243         }
244     }
245 }
246 
247 // Whether the attribute is one like `#[<name> ...]`
is_matching_attr(name: &str, attr: &syn::Attribute) -> bool248 fn is_matching_attr(name: &str, attr: &syn::Attribute) -> bool {
249     attr.path.segments.len() == 1 && attr.path.segments[0].ident == name
250 }
251 
252 /// Checks for `#[doc ...]`, which is generated by doc comments.
is_doc_attr(attr: &syn::Attribute) -> bool253 fn is_doc_attr(attr: &syn::Attribute) -> bool {
254     is_matching_attr("doc", attr)
255 }
256 
257 /// Checks for `#[argh ...]`
is_argh_attr(attr: &syn::Attribute) -> bool258 fn is_argh_attr(attr: &syn::Attribute) -> bool {
259     is_matching_attr("argh", attr)
260 }
261 
attr_to_meta_subtype<R: Clone>( errors: &Errors, attr: &syn::Attribute, f: impl FnOnce(&syn::Meta) -> Option<&R>, ) -> Option<R>262 fn attr_to_meta_subtype<R: Clone>(
263     errors: &Errors,
264     attr: &syn::Attribute,
265     f: impl FnOnce(&syn::Meta) -> Option<&R>,
266 ) -> Option<R> {
267     match attr.parse_meta() {
268         Ok(meta) => f(&meta).cloned(),
269         Err(e) => {
270             errors.push(e);
271             None
272         }
273     }
274 }
275 
attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList>276 fn attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
277     attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_list(m))
278 }
279 
attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaNameValue>280 fn attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaNameValue> {
281     attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_name_value(m))
282 }
283 
284 /// Filters out non-`#[argh(...)]` attributes and converts to `syn::MetaList`.
argh_attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList>285 fn argh_attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
286     if !is_argh_attr(attr) {
287         return None;
288     }
289     attr_to_meta_list(errors, attr)
290 }
291 
292 /// Represents a `#[derive(FromArgs)]` type's top-level attributes.
293 #[derive(Default)]
294 pub struct TypeAttrs {
295     pub is_subcommand: Option<syn::Ident>,
296     pub name: Option<syn::LitStr>,
297     pub description: Option<Description>,
298     pub examples: Vec<syn::LitStr>,
299     pub notes: Vec<syn::LitStr>,
300     pub error_codes: Vec<(syn::LitInt, syn::LitStr)>,
301 }
302 
303 impl TypeAttrs {
304     /// Parse top-level `#[argh(...)]` attributes
parse(errors: &Errors, derive_input: &syn::DeriveInput) -> Self305     pub fn parse(errors: &Errors, derive_input: &syn::DeriveInput) -> Self {
306         let mut this = TypeAttrs::default();
307 
308         for attr in &derive_input.attrs {
309             if is_doc_attr(attr) {
310                 parse_attr_doc(errors, attr, &mut this.description);
311                 continue;
312             }
313 
314             let ml = if let Some(ml) = argh_attr_to_meta_list(errors, attr) {
315                 ml
316             } else {
317                 continue;
318             };
319 
320             for meta in &ml.nested {
321                 let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
322 
323                 let name = meta.path();
324                 if name.is_ident("description") {
325                     if let Some(m) = errors.expect_meta_name_value(meta) {
326                         parse_attr_description(errors, m, &mut this.description);
327                     }
328                 } else if name.is_ident("error_code") {
329                     if let Some(m) = errors.expect_meta_list(meta) {
330                         this.parse_attr_error_code(errors, m);
331                     }
332                 } else if name.is_ident("example") {
333                     if let Some(m) = errors.expect_meta_name_value(meta) {
334                         this.parse_attr_example(errors, m);
335                     }
336                 } else if name.is_ident("name") {
337                     if let Some(m) = errors.expect_meta_name_value(meta) {
338                         this.parse_attr_name(errors, m);
339                     }
340                 } else if name.is_ident("note") {
341                     if let Some(m) = errors.expect_meta_name_value(meta) {
342                         this.parse_attr_note(errors, m);
343                     }
344                 } else if name.is_ident("subcommand") {
345                     if let Some(ident) = errors.expect_meta_word(meta).and_then(|p| p.get_ident()) {
346                         this.parse_attr_subcommand(errors, ident);
347                     }
348                 } else {
349                     errors.err(
350                         &meta,
351                         concat!(
352                             "Invalid type-level `argh` attribute\n",
353                             "Expected one of: `description`, `error_code`, `example`, `name`, ",
354                             "`note`, `subcommand`",
355                         ),
356                     );
357                 }
358             }
359         }
360 
361         this.check_error_codes(errors);
362         this
363     }
364 
365     /// Checks that error codes are within range for `i32` and that they are
366     /// never duplicated.
check_error_codes(&self, errors: &Errors)367     fn check_error_codes(&self, errors: &Errors) {
368         // map from error code to index
369         let mut map: HashMap<u64, usize> = HashMap::new();
370         for (index, (lit_int, _lit_str)) in self.error_codes.iter().enumerate() {
371             let value = match lit_int.base10_parse::<u64>() {
372                 Ok(v) => v,
373                 Err(e) => {
374                     errors.push(e);
375                     continue;
376                 }
377             };
378             if value > (std::i32::MAX as u64) {
379                 errors.err(lit_int, "Error code out of range for `i32`");
380             }
381             match map.entry(value) {
382                 Entry::Occupied(previous) => {
383                     let previous_index = *previous.get();
384                     let (previous_lit_int, _previous_lit_str) = &self.error_codes[previous_index];
385                     errors.err(lit_int, &format!("Duplicate error code {}", value));
386                     errors.err(
387                         previous_lit_int,
388                         &format!("Error code {} previously defined here", value),
389                     );
390                 }
391                 Entry::Vacant(slot) => {
392                     slot.insert(index);
393                 }
394             }
395         }
396     }
397 
parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList)398     fn parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList) {
399         if ml.nested.len() != 2 {
400             errors.err(&ml, "Expected two arguments, an error number and a string description");
401             return;
402         }
403 
404         let err_code = &ml.nested[0];
405         let err_msg = &ml.nested[1];
406 
407         let err_code = errors.expect_nested_lit(err_code).and_then(|l| errors.expect_lit_int(l));
408         let err_msg = errors.expect_nested_lit(err_msg).and_then(|l| errors.expect_lit_str(l));
409 
410         if let (Some(err_code), Some(err_msg)) = (err_code, err_msg) {
411             self.error_codes.push((err_code.clone(), err_msg.clone()));
412         }
413     }
414 
parse_attr_example(&mut self, errors: &Errors, m: &syn::MetaNameValue)415     fn parse_attr_example(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
416         parse_attr_multi_string(errors, m, &mut self.examples)
417     }
418 
parse_attr_name(&mut self, errors: &Errors, m: &syn::MetaNameValue)419     fn parse_attr_name(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
420         parse_attr_single_string(errors, m, "name", &mut self.name);
421         if let Some(name) = &self.name {
422             if name.value() == "help" {
423                 errors.err(name, "Custom `help` commands are not supported.");
424             }
425         }
426     }
427 
parse_attr_note(&mut self, errors: &Errors, m: &syn::MetaNameValue)428     fn parse_attr_note(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
429         parse_attr_multi_string(errors, m, &mut self.notes)
430     }
431 
parse_attr_subcommand(&mut self, errors: &Errors, ident: &syn::Ident)432     fn parse_attr_subcommand(&mut self, errors: &Errors, ident: &syn::Ident) {
433         if let Some(first) = &self.is_subcommand {
434             errors.duplicate_attrs("subcommand", first, ident);
435         } else {
436             self.is_subcommand = Some(ident.clone());
437         }
438     }
439 }
440 
441 /// Represents an enum variant's attributes.
442 #[derive(Default)]
443 pub struct VariantAttrs {
444     pub is_dynamic: Option<syn::Path>,
445 }
446 
447 impl VariantAttrs {
448     /// Parse enum variant `#[argh(...)]` attributes
parse(errors: &Errors, variant: &syn::Variant) -> Self449     pub fn parse(errors: &Errors, variant: &syn::Variant) -> Self {
450         let mut this = VariantAttrs::default();
451 
452         let fields = match &variant.fields {
453             syn::Fields::Named(fields) => Some(&fields.named),
454             syn::Fields::Unnamed(fields) => Some(&fields.unnamed),
455             syn::Fields::Unit => None,
456         };
457 
458         for field in fields.into_iter().flatten() {
459             for attr in &field.attrs {
460                 if is_argh_attr(attr) {
461                     err_unused_enum_attr(errors, attr);
462                 }
463             }
464         }
465 
466         for attr in &variant.attrs {
467             let ml = if let Some(ml) = argh_attr_to_meta_list(errors, attr) {
468                 ml
469             } else {
470                 continue;
471             };
472 
473             for meta in &ml.nested {
474                 let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
475 
476                 let name = meta.path();
477                 if name.is_ident("dynamic") {
478                     if let Some(prev) = this.is_dynamic.as_ref() {
479                         errors.duplicate_attrs("dynamic", prev, meta);
480                     } else {
481                         this.is_dynamic = errors.expect_meta_word(meta).cloned();
482                     }
483                 } else {
484                     errors.err(
485                         &meta,
486                         "Invalid variant-level `argh` attribute\n\
487                          Variants can only have the #[argh(dynamic)] attribute.",
488                     );
489                 }
490             }
491         }
492 
493         this
494     }
495 }
496 
check_option_description(errors: &Errors, desc: &str, span: Span)497 fn check_option_description(errors: &Errors, desc: &str, span: Span) {
498     let chars = &mut desc.trim().chars();
499     match (chars.next(), chars.next()) {
500         (Some(x), _) if x.is_lowercase() => {}
501         // If both the first and second letter are not lowercase,
502         // this is likely an initialism which should be allowed.
503         (Some(x), Some(y)) if !x.is_lowercase() && !y.is_lowercase() => {}
504         _ => {
505             errors.err_span(span, "Descriptions must begin with a lowercase letter");
506         }
507     }
508 }
509 
parse_attr_single_string( errors: &Errors, m: &syn::MetaNameValue, name: &str, slot: &mut Option<syn::LitStr>, )510 fn parse_attr_single_string(
511     errors: &Errors,
512     m: &syn::MetaNameValue,
513     name: &str,
514     slot: &mut Option<syn::LitStr>,
515 ) {
516     if let Some(first) = slot {
517         errors.duplicate_attrs(name, first, m);
518     } else if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
519         *slot = Some(lit_str.clone());
520     }
521 }
522 
parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut Vec<syn::LitStr>)523 fn parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut Vec<syn::LitStr>) {
524     if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
525         list.push(lit_str.clone());
526     }
527 }
528 
parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Description>)529 fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Description>) {
530     let nv = if let Some(nv) = attr_to_meta_name_value(errors, attr) {
531         nv
532     } else {
533         return;
534     };
535 
536     // Don't replace an existing description.
537     if slot.as_ref().map(|d| d.explicit).unwrap_or(false) {
538         return;
539     }
540 
541     if let Some(lit_str) = errors.expect_lit_str(&nv.lit) {
542         let lit_str = if let Some(previous) = slot {
543             let previous = &previous.content;
544             let previous_span = previous.span();
545             syn::LitStr::new(&(previous.value() + &*lit_str.value()), previous_span)
546         } else {
547             lit_str.clone()
548         };
549         *slot = Some(Description { explicit: false, content: lit_str });
550     }
551 }
552 
parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option<Description>)553 fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option<Description>) {
554     let lit_str = if let Some(lit_str) = errors.expect_lit_str(&m.lit) { lit_str } else { return };
555 
556     // Don't allow multiple explicit (non doc-comment) descriptions
557     if let Some(description) = slot {
558         if description.explicit {
559             errors.duplicate_attrs("description", &description.content, lit_str);
560         }
561     }
562 
563     *slot = Some(Description { explicit: true, content: lit_str.clone() });
564 }
565 
566 /// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]`
567 /// attribute and that it does not have any other type-level `#[argh(...)]` attributes.
check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span)568 pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) {
569     let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes } = type_attrs;
570 
571     // Ensure that `#[argh(subcommand)]` is present.
572     if is_subcommand.is_none() {
573         errors.err_span(
574             *type_span,
575             concat!(
576                 "`#![derive(FromArgs)]` on `enum`s can only be used to enumerate subcommands.\n",
577                 "Consider adding `#[argh(subcommand)]` to the `enum` declaration.",
578             ),
579         );
580     }
581 
582     // Error on all other type-level attributes.
583     if let Some(name) = name {
584         err_unused_enum_attr(errors, name);
585     }
586     if let Some(description) = description {
587         if description.explicit {
588             err_unused_enum_attr(errors, &description.content);
589         }
590     }
591     if let Some(example) = examples.first() {
592         err_unused_enum_attr(errors, example);
593     }
594     if let Some(note) = notes.first() {
595         err_unused_enum_attr(errors, note);
596     }
597     if let Some(err_code) = error_codes.first() {
598         err_unused_enum_attr(errors, &err_code.0);
599     }
600 }
601 
err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned)602 fn err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned) {
603     errors.err(
604         location,
605         concat!(
606             "Unused `argh` attribute on `#![derive(FromArgs)]` enum. ",
607             "Such `enum`s can only be used to dispatch to subcommands, ",
608             "and should only contain the #[argh(subcommand)] attribute.",
609         ),
610     );
611 }
612