use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{
parenthesized, parse2, parse_quote, token, Attribute, Expr, Ident, LitChar, LitStr, Path,
PathArguments, PathSegment, Result, Token, Type,
};
use crate::utils::{to_kebab_case, LineIter};
use super::{
as_long_name, as_short_name, parse_optional_arg, split_type, ConsumerAttr, Doc, Name,
PostprAttr, Shape,
};
#[derive(Debug, Clone)]
pub struct Field {
ty: Type,
naming: Vec<Name>,
env: Option<Box<Expr>>,
consumer: Option<ConsumerAttr>,
external: Option<Path>,
name: Option<Ident>,
postpr: Vec<PostprAttr>,
help: Option<String>,
}
fn check_stage(prev: &mut usize, new: usize, keyword: &Ident) -> Result<()> {
let stages = ["naming", "consumer", "external", "postprocessing"];
if *prev > new {
return Err(syn::Error::new(
keyword.span(),
format!(
"keyword `{}` is at stage `{}` can't follow previous stage ({})",
keyword,
stages[new - 1],
stages[*prev - 1]
),
));
}
if new == 3 && *prev != 0 {
return Err(syn::Error::new(
keyword.span(),
"Processing chain must start with external if external is present".to_owned(),
));
}
if *prev == 2 && new == 2 {
return Err(syn::Error::new(
keyword.span(),
"You can have only one consumer".to_owned(),
));
}
*prev = new;
Ok(())
}
fn parse_arg<T: Parse>(input: ParseStream) -> Result<T> {
let content;
let _ = parenthesized!(content in input);
content.parse::<T>()
}
pub fn parse_opt_arg<T: Parse>(input: ParseStream) -> Result<Option<T>> {
if input.peek(token::Paren) {
let content;
let _ = parenthesized!(content in input);
Ok(Some(content.parse::<T>()?))
} else {
Ok(None)
}
}
#[inline(never)]
pub fn parse_lit_char(input: ParseStream) -> Result<LitChar> {
parse_arg(input)
}
#[inline(never)]
pub fn parse_lit_str(input: ParseStream) -> Result<LitStr> {
parse_arg(input)
}
#[inline(never)]
pub fn parse_ident(input: ParseStream) -> Result<Ident> {
parse_arg(input)
}
#[inline(never)]
pub fn parse_path(input: ParseStream) -> Result<Path> {
parse_arg(input)
}
#[inline(never)]
pub fn parse_expr(input: ParseStream) -> Result<Box<Expr>> {
Ok(Box::new(parse_arg(input)?))
}
impl Field {
pub fn var_name(&self, ix: usize) -> Ident {
let name = &self.name;
match name {
Some(name) => name.clone(),
None => Ident::new(&format!("f{}", ix), Span::call_site()),
}
}
#[allow(clippy::too_many_lines)]
pub fn make(ty: Type, name: Option<Ident>, attrs: Vec<Attribute>) -> Result<Self> {
let mut res = Field {
ty,
external: None,
name,
naming: Vec::new(),
env: None,
consumer: None,
postpr: Vec::new(),
help: None,
};
let mut help = Vec::new();
let mut stage = 0;
for attr in attrs {
if attr.path.is_ident("doc") {
help.push(parse2::<Doc>(attr.tokens)?.0);
} else if attr.path.is_ident("bpaf") {
#[allow(clippy::cognitive_complexity)]
attr.parse_args_with(|input: ParseStream| loop {
if input.is_empty() {
break Ok(());
}
let input_copy = input.fork();
let keyword = input.parse::<Ident>()?;
let span = keyword.span();
let content;
if keyword == "long" {
check_stage(&mut stage, 1, &keyword)?;
res.naming.push(if input.peek(token::Paren) {
let lit = parse_lit_str(input)?;
Name::Long(lit)
} else if let Some(name) = res.name.as_ref() {
Name::Long(as_long_name(name))
} else {
break Err(
input_copy.error("unnamed field needs to have a name specified")
);
});
} else if keyword == "short" {
check_stage(&mut stage, 1, &keyword)?;
res.naming.push(if input.peek(token::Paren) {
let lit = parse_lit_char(input)?;
Name::Short(lit)
} else if let Some(name) = res.name.as_ref() {
Name::Short(as_short_name(name))
} else {
break Err(
input_copy.error("unnamed field needs to have a name specified")
);
});
} else if keyword == "env" {
check_stage(&mut stage, 1, &keyword)?;
res.env = Some(parse_expr(input)?);
} else if keyword == "argument" {
check_stage(&mut stage, 2, &keyword)?;
let ty = if input.peek(token::Colon) {
input.parse::<token::Colon>()?;
input.parse::<token::Colon>()?;
input.parse::<token::Lt>()?;
let ty = input.parse::<Type>()?;
input.parse::<token::Gt>()?;
Some(Box::new(ty))
} else {
None
};
let arg = parse_optional_arg(input)?;
res.consumer = Some(ConsumerAttr::Arg(arg, ty));
} else if keyword == "positional" {
check_stage(&mut stage, 2, &keyword)?;
let ty = if input.peek(token::Colon) {
input.parse::<token::Colon>()?;
input.parse::<token::Colon>()?;
input.parse::<token::Lt>()?;
let ty = input.parse::<Type>()?;
input.parse::<token::Gt>()?;
Some(Box::new(ty))
} else {
None
};
let arg = parse_optional_arg(input)?;
res.consumer = Some(ConsumerAttr::Pos(arg, ty));
} else if keyword == "any" {
check_stage(&mut stage, 2, &keyword)?;
let ty = if input.peek(token::Colon) {
input.parse::<token::Colon>()?;
input.parse::<token::Colon>()?;
input.parse::<token::Lt>()?;
let ty = input.parse::<Type>()?;
input.parse::<token::Gt>()?;
Some(Box::new(ty))
} else {
None
};
let arg = parse_optional_arg(input)?;
res.consumer = Some(ConsumerAttr::Any(arg, ty));
} else if keyword == "switch" {
check_stage(&mut stage, 2, &keyword)?;
res.consumer = Some(ConsumerAttr::Switch);
} else if keyword == "flag" {
check_stage(&mut stage, 2, &keyword)?;
let _ = parenthesized!(content in input);
let a = content.parse()?;
content.parse::<token::Comma>()?;
let b = content.parse()?;
res.consumer = Some(ConsumerAttr::Flag(Box::new(a), Box::new(b)));
} else if keyword == "external" {
check_stage(&mut stage, 3, &keyword)?;
if input.peek(token::Paren) {
res.external = Some(parse_path(input)?);
} else if let Some(name) = &res.name {
res.external = Some(ident_to_path(name.clone()));
} else {
break Err(
input_copy.error("unnamed fields needs to have a name specified")
);
}
} else if keyword == "guard" {
check_stage(&mut stage, 4, &keyword)?;
let _ = parenthesized!(content in input);
let guard_fn = content.parse::<Path>()?;
let _ = content.parse::<Token![,]>()?;
let msg = content.parse::<Expr>()?;
res.postpr
.push(PostprAttr::Guard(span, guard_fn, Box::new(msg)));
} else if keyword == "fallback" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr
.push(PostprAttr::Fallback(span, parse_expr(input)?));
} else if keyword == "fallback_with" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr
.push(PostprAttr::FallbackWith(span, parse_expr(input)?));
} else if keyword == "parse" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Parse(span, parse_path(input)?));
} else if keyword == "map" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Map(span, parse_path(input)?));
} else if keyword == "complete" {
check_stage(&mut stage, 4, &keyword)?;
let f = parse_path(input)?;
res.postpr.push(PostprAttr::Complete(span, f));
} else if keyword == "complete_shell" {
check_stage(&mut stage, 4, &keyword)?;
let f = parse_expr(input)?;
res.postpr.push(PostprAttr::CompleteShell(span, f));
} else if keyword == "many" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Many(span, None));
} else if keyword == "some" {
check_stage(&mut stage, 4, &keyword)?;
let lit = parse_lit_str(input)?;
res.postpr.push(PostprAttr::Many(span, Some(lit)));
} else if keyword == "optional" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Optional(span));
} else if keyword == "hide" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Hide(span));
} else if keyword == "hide_usage" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::HideUsage(span));
} else if keyword == "catch" {
check_stage(&mut stage, 4, &keyword)?;
res.postpr.push(PostprAttr::Catch(span));
} else if keyword == "group_help" {
check_stage(&mut stage, 4, &keyword)?;
let expr = parse_expr(input)?;
res.postpr.push(PostprAttr::GroupHelp(span, expr));
} else {
return Err(input_copy.error("Unexpected attribute"));
}
if !input.is_empty() {
input.parse::<token::Comma>()?;
}
})?;
}
}
res.implicit_naming();
res.implicit_consumer()?;
res.help = LineIter::from(&help[..]).next();
if !(res.external.is_some() || res.consumer.is_some()) {
return Err(syn::Error::new(
res.ty.span(),
"Not sure how to parse this field, make a ticket?",
));
}
Ok(res)
}
}
impl Field {
fn implicit_naming(&mut self) {
if self.external.is_some() || !self.naming.is_empty() {
return;
}
if let Some(cons) = &self.consumer {
match cons {
ConsumerAttr::Any(_, _) | ConsumerAttr::Pos(_, _) => return,
ConsumerAttr::Arg(_, _)
| ConsumerAttr::Switch
| ConsumerAttr::Flag(_, _)
| ConsumerAttr::ReqFlag(_) => {}
}
}
let name = match &self.name {
Some(name) => to_kebab_case(&name.to_string()),
None => return,
};
self.naming.push(if name.chars().nth(1).is_some() {
Name::Long(LitStr::new(&name, self.name.span()))
} else {
let c = name.chars().next().unwrap();
Name::Short(LitChar::new(c, self.name.span()))
});
}
fn implicit_consumer(&mut self) -> Result<()> {
if self.external.is_some() {
return Ok(());
}
let derive_postpr = if let Some(wrong) = self.postpr.iter().find(|i| !i.can_derive()) {
if self.consumer.is_none() {
return Err(syn::Error::new(
wrong.span(),
"Can't derive implicit consumer with this annotation present",
));
}
false
} else {
true
};
let ty = match split_type(&self.ty) {
Shape::Optional(ty) => {
if derive_postpr {
self.postpr.insert(0, PostprAttr::Optional(ty.span()));
}
ty
}
Shape::Multiple(ty) => {
if derive_postpr {
self.postpr.insert(0, PostprAttr::Many(ty.span(), None));
}
ty
}
Shape::Bool => {
if self.naming.is_empty() {
return Err(syn::Error::new(
self.ty.span(),
"Can't derive consumer for unnamed boolean field",
));
}
if self.consumer.is_none() {
self.consumer = Some(ConsumerAttr::Switch);
}
return Ok(());
}
Shape::Direct(ty) => ty,
Shape::Unit => {
if self.consumer.is_none() {
self.consumer = Some(ConsumerAttr::ReqFlag(parse_quote!(())));
return Ok(());
}
self.ty.clone()
}
};
if let Some(cons) = &self.consumer {
match cons {
ConsumerAttr::Any(l, None) => {
self.consumer = Some(ConsumerAttr::Any(l.clone(), Some(Box::new(ty.clone()))));
}
ConsumerAttr::Arg(l, None) => {
self.consumer = Some(ConsumerAttr::Arg(l.clone(), Some(Box::new(ty.clone()))));
}
ConsumerAttr::Pos(l, None) => {
self.consumer = Some(ConsumerAttr::Pos(l.clone(), Some(Box::new(ty.clone()))));
}
_ => {}
}
}
match &self.consumer {
Some(cons) => match cons {
ConsumerAttr::Arg(..)
| ConsumerAttr::Switch
| ConsumerAttr::Flag(_, _)
| ConsumerAttr::ReqFlag(_) => {
if self.naming.is_empty() {
return Err(syn::Error::new(
self.ty.span(),
"This consumer needs a name, you can specify it with long(\"name\") or short('n')",
));
}
}
ConsumerAttr::Pos(..) | ConsumerAttr::Any(..) => {}
},
None => {
let arg = LitStr::new("ARG", ty.span());
if self.naming.is_empty() {
self.consumer = Some(ConsumerAttr::Pos(arg, Some(Box::new(ty))));
} else {
self.consumer = Some(ConsumerAttr::Arg(arg, Some(Box::new(ty))));
}
}
}
Ok(())
}
}
impl ToTokens for Field {
fn to_tokens(&self, tokens: &mut TokenStream) {
if let Some(ext) = &self.external {
quote!(#ext()).to_tokens(tokens);
} else if !self.naming.is_empty() {
let name = &self.naming[0];
quote!(::bpaf::#name).to_tokens(tokens);
for rest in &self.naming[1..] {
quote!(.#rest).to_tokens(tokens);
}
if let Some(env) = &self.env {
quote!(.env(#env)).to_tokens(tokens);
}
if let Some(help) = &self.help {
quote!(.help(#help)).to_tokens(tokens);
}
if let Some(cons) = &self.consumer {
quote!(.#cons).to_tokens(tokens);
}
} else if let Some(pos) = &self.consumer {
quote!(::bpaf::#pos).to_tokens(tokens);
if let Some(help) = &self.help {
quote!(.help(#help)).to_tokens(tokens);
}
} else {
unreachable!("implicit_consumer bug?");
}
for postpr in &self.postpr {
quote!(.#postpr).to_tokens(tokens);
}
}
}
fn ident_to_path(ident: Ident) -> Path {
Path {
leading_colon: None,
segments: vec![PathSegment {
ident,
arguments: PathArguments::None,
}]
.into_iter()
.collect(),
}
}