blob: ae640a70a915bad34ec1fa7e32503dc168ee372a [file] [log] [blame]
// 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, )*],
}
}
}