diff options
Diffstat (limited to 'src/args_info.rs')
-rw-r--r-- | src/args_info.rs | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/src/args_info.rs b/src/args_info.rs new file mode 100644 index 0000000..ae640a7 --- /dev/null +++ b/src/args_info.rs @@ -0,0 +1,356 @@ +// Copyright (c) 2023 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use crate::{ + enum_only_single_field_unnamed_variants, + errors::Errors, + help::require_description, + parse_attrs::{check_enum_type_attrs, FieldAttrs, FieldKind, TypeAttrs, VariantAttrs}, + Optionality, StructField, +}; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::LitStr; + +/// Implement the derive macro for ArgsInfo. +pub(crate) fn impl_args_info(input: &syn::DeriveInput) -> TokenStream { + let errors = &Errors::default(); + + // parse the types + let type_attrs = &TypeAttrs::parse(errors, input); + + // Based on the type generate the appropriate code. + let mut output_tokens = match &input.data { + syn::Data::Struct(ds) => { + impl_arg_info_struct(errors, &input.ident, type_attrs, &input.generics, ds) + } + syn::Data::Enum(de) => { + impl_arg_info_enum(errors, &input.ident, type_attrs, &input.generics, de) + } + syn::Data::Union(_) => { + errors.err(input, "`#[derive(ArgsInfo)]` cannot be applied to unions"); + TokenStream::new() + } + }; + errors.to_tokens(&mut output_tokens); + output_tokens +} + +/// Implement the ArgsInfo trait for a struct annotated with argh attributes. +fn impl_arg_info_struct( + errors: &Errors, + name: &syn::Ident, + type_attrs: &TypeAttrs, + generic_args: &syn::Generics, + ds: &syn::DataStruct, +) -> TokenStream { + // Collect the fields, skipping fields that are not supported. + let fields = match &ds.fields { + syn::Fields::Named(fields) => fields, + syn::Fields::Unnamed(_) => { + errors.err( + &ds.struct_token, + "`#![derive(ArgsInfo)]` is not currently supported on tuple structs", + ); + return TokenStream::new(); + } + syn::Fields::Unit => { + errors.err(&ds.struct_token, "#![derive(ArgsInfo)]` cannot be applied to unit structs"); + return TokenStream::new(); + } + }; + + // Map the fields into StructField objects. + let fields: Vec<_> = fields + .named + .iter() + .filter_map(|field| { + let attrs = FieldAttrs::parse(errors, field); + StructField::new(errors, field, attrs) + }) + .collect(); + + let impl_span = Span::call_site(); + + // Generate the implementation of `get_args_info()` for this struct. + let args_info = impl_args_info_data(name, errors, type_attrs, &fields); + + // Split out the generics info for the impl declaration. + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); + + quote_spanned! { impl_span => + #[automatically_derived] + impl #impl_generics argh::ArgsInfo for #name #ty_generics #where_clause { + fn get_args_info() -> argh::CommandInfoWithArgs { + #args_info + } + } + } +} + +/// Implement ArgsInfo for an enum. The enum is a collection of subcommands. +fn impl_arg_info_enum( + errors: &Errors, + name: &syn::Ident, + type_attrs: &TypeAttrs, + generic_args: &syn::Generics, + de: &syn::DataEnum, +) -> TokenStream { + // Validate the enum is OK for argh. + check_enum_type_attrs(errors, type_attrs, &de.enum_token.span); + + // Ensure that `#[argh(subcommand)]` is present. + if type_attrs.is_subcommand.is_none() { + errors.err_span( + de.enum_token.span, + concat!( + "`#![derive(ArgsInfo)]` on `enum`s can only be used to enumerate subcommands.\n", + "Consider adding `#[argh(subcommand)]` to the `enum` declaration.", + ), + ); + } + + // One of the variants can be annotated as providing dynamic subcommands. + // We treat this differently since we need to call a function at runtime + // to determine the subcommands provided. + let mut dynamic_type_and_variant = None; + + // An enum variant like `<name>(<ty>)`. This is used to collect + // the type of the variant for each subcommand. + struct ArgInfoVariant<'a> { + ty: &'a syn::Type, + } + + let variants: Vec<ArgInfoVariant<'_>> = de + .variants + .iter() + .filter_map(|variant| { + let name = &variant.ident; + let ty = enum_only_single_field_unnamed_variants(errors, &variant.fields)?; + if VariantAttrs::parse(errors, variant).is_dynamic.is_some() { + if dynamic_type_and_variant.is_some() { + errors.err(variant, "Only one variant can have the `dynamic` attribute"); + } + dynamic_type_and_variant = Some((ty, name)); + None + } else { + Some(ArgInfoVariant { ty }) + } + }) + .collect(); + + let dynamic_subcommands = if let Some((dynamic_type, _)) = dynamic_type_and_variant { + quote! { + <#dynamic_type as argh::DynamicSubCommand>::commands().iter() + .map(|s| + SubCommandInfo { + name: s.name, + command: CommandInfoWithArgs { + name: s.name, + description: s.description, + ..Default::default() + } + }).collect() + } + } else { + quote! { vec![]} + }; + + let variant_ty_info = variants.iter().map(|t| { + let ty = t.ty; + quote!( + argh::SubCommandInfo { + name: #ty::get_args_info().name, + command: #ty::get_args_info() + } + ) + }); + + let cmd_name = if let Some(id) = &type_attrs.name { + id.clone() + } else { + LitStr::new("", Span::call_site()) + }; + + let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); + + quote! { + #[automatically_derived] + impl #impl_generics argh::ArgsInfo for #name #ty_generics #where_clause { + fn get_args_info() -> argh::CommandInfoWithArgs { + + let mut the_subcommands = vec![#(#variant_ty_info),*]; + let mut dynamic_commands = #dynamic_subcommands; + + the_subcommands.append(&mut dynamic_commands); + + + argh::CommandInfoWithArgs { + name: #cmd_name, + /// A short description of the command's functionality. + description: " enum of subcommands", + commands: the_subcommands, + ..Default::default() + } + } // end of get_args_ifo + } // end of impl ArgsInfo + } +} + +fn impl_args_info_data<'a>( + name: &proc_macro2::Ident, + errors: &Errors, + type_attrs: &TypeAttrs, + fields: &'a [StructField<'a>], +) -> TokenStream { + let mut subcommands_iter = + fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); + + let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); + for dup_subcommand in subcommands_iter { + errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); + } + + let impl_span = Span::call_site(); + + let mut positionals = vec![]; + let mut flags = vec![]; + + // Add the implicit --help flag + flags.push(quote! { + argh::FlagInfo { + short: None, + long: "--help", + description: "display usage information", + optionality: argh::Optionality::Optional, + kind: argh::FlagInfoKind::Switch, + hidden: false + } + }); + + for field in fields { + let optionality = match field.optionality { + Optionality::None => quote! { argh::Optionality::Required }, + Optionality::Defaulted(_) => quote! { argh::Optionality::Optional }, + Optionality::Optional => quote! { argh::Optionality::Optional }, + Optionality::Repeating if field.attrs.greedy.is_some() => { + quote! { argh::Optionality::Greedy } + } + Optionality::Repeating => quote! { argh::Optionality::Repeating }, + }; + + match field.kind { + FieldKind::Positional => { + let name = field.positional_arg_name(); + + let description = if let Some(desc) = &field.attrs.description { + desc.content.value().trim().to_owned() + } else { + String::new() + }; + let hidden = field.attrs.hidden_help; + + positionals.push(quote! { + argh::PositionalInfo { + name: #name, + description: #description, + optionality: #optionality, + hidden: #hidden, + } + }); + } + FieldKind::Switch | FieldKind::Option => { + let short = if let Some(short) = &field.attrs.short { + quote! { Some(#short) } + } else { + quote! { None } + }; + + let long = field.long_name.as_ref().expect("missing long name for option"); + + let description = require_description( + errors, + field.name.span(), + &field.attrs.description, + "field", + ); + + let kind = if field.kind == FieldKind::Switch { + quote! { + argh::FlagInfoKind::Switch + } + } else { + let arg_name = if let Some(arg_name) = &field.attrs.arg_name { + quote! { #arg_name } + } else { + let arg_name = long.trim_start_matches("--"); + quote! { #arg_name } + }; + + quote! { + argh::FlagInfoKind::Option { + arg_name: #arg_name, + } + } + }; + + let hidden = field.attrs.hidden_help; + + flags.push(quote! { + argh::FlagInfo { + short: #short, + long: #long, + description: #description, + optionality: #optionality, + kind: #kind, + hidden: #hidden, + } + }); + } + FieldKind::SubCommand => {} + } + } + + let empty_str = syn::LitStr::new("", Span::call_site()); + let type_name = LitStr::new(&name.to_string(), Span::call_site()); + let subcommand_name = if type_attrs.is_subcommand.is_some() { + type_attrs.name.as_ref().unwrap_or_else(|| { + errors.err(name, "`#[argh(name = \"...\")]` attribute is required for subcommands"); + &empty_str + }) + } else { + &type_name + }; + + let subcommand = if let Some(subcommand) = subcommand { + let subcommand_ty = subcommand.ty_without_wrapper; + quote! { + #subcommand_ty::get_subcommands() + } + } else { + quote! {vec![]} + }; + + let description = + require_description(errors, Span::call_site(), &type_attrs.description, "type"); + let examples = type_attrs.examples.iter().map(|e| quote! { #e }); + let notes = type_attrs.notes.iter().map(|e| quote! { #e }); + + let error_codes = type_attrs.error_codes.iter().map(|(code, text)| { + quote! { argh::ErrorCodeInfo{code:#code, description: #text} } + }); + + quote_spanned! { impl_span => + argh::CommandInfoWithArgs { + name: #subcommand_name, + description: #description, + examples: &[#( #examples, )*], + notes: &[#( #notes, )*], + positionals: &[#( #positionals, )*], + flags: &[#( #flags, )*], + commands: #subcommand, + error_codes: &[#( #error_codes, )*], + } + } +} |