diff options
Diffstat (limited to 'third_party/rust/clap_derive/src/attrs.rs')
-rw-r--r-- | third_party/rust/clap_derive/src/attrs.rs | 985 |
1 files changed, 985 insertions, 0 deletions
diff --git a/third_party/rust/clap_derive/src/attrs.rs b/third_party/rust/clap_derive/src/attrs.rs new file mode 100644 index 0000000000..aaee167e31 --- /dev/null +++ b/third_party/rust/clap_derive/src/attrs.rs @@ -0,0 +1,985 @@ +// Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>, +// Kevin Knapp (@kbknapp) <kbknapp@gmail.com>, and +// Ana Hobden (@hoverbear) <operator@hoverbear.org> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// This work was derived from Structopt (https://github.com/TeXitoi/structopt) +// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the +// MIT/Apache 2.0 license. + +use crate::{ + parse::*, + utils::{process_doc_comment, Sp, Ty}, +}; + +use std::env; + +use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use proc_macro2::{self, Span, TokenStream}; +use proc_macro_error::abort; +use quote::{quote, quote_spanned, ToTokens}; +use syn::{ + self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Field, Ident, LitStr, MetaNameValue, + Type, Variant, +}; + +/// Default casing style for generated arguments. +pub const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab; + +/// Default casing style for environment variables +pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake; + +#[derive(Clone)] +pub struct Attrs { + name: Name, + casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ty: Option<Type>, + doc_comment: Vec<Method>, + methods: Vec<Method>, + parser: Sp<Parser>, + verbatim_doc_comment: Option<Ident>, + next_display_order: Option<Method>, + next_help_heading: Option<Method>, + help_heading: Option<Method>, + is_enum: bool, + has_custom_parser: bool, + kind: Sp<Kind>, +} + +impl Attrs { + pub fn from_struct( + span: Span, + attrs: &[Attribute], + name: Name, + argument_casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ) -> Self { + let mut res = Self::new(span, name, None, argument_casing, env_casing); + res.push_attrs(attrs); + res.push_doc_comment(attrs, "about"); + + if res.has_custom_parser { + abort!( + res.parser.span(), + "`parse` attribute is only allowed on fields" + ); + } + match &*res.kind { + Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), + Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"), + Kind::Arg(_) => res, + Kind::FromGlobal(_) => abort!(res.kind.span(), "from_global is only allowed on fields"), + Kind::Flatten => abort!(res.kind.span(), "flatten is only allowed on fields"), + Kind::ExternalSubcommand => abort!( + res.kind.span(), + "external_subcommand is only allowed on fields" + ), + } + } + + pub fn from_variant( + variant: &Variant, + struct_casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ) -> Self { + let name = variant.ident.clone(); + let mut res = Self::new( + variant.span(), + Name::Derived(name), + None, + struct_casing, + env_casing, + ); + res.push_attrs(&variant.attrs); + res.push_doc_comment(&variant.attrs, "about"); + + match &*res.kind { + Kind::Flatten => { + if res.has_custom_parser { + abort!( + res.parser.span(), + "parse attribute is not allowed for flattened entry" + ); + } + if res.has_explicit_methods() { + abort!( + res.kind.span(), + "methods are not allowed for flattened entry" + ); + } + + // ignore doc comments + res.doc_comment = vec![]; + } + + Kind::ExternalSubcommand => (), + + Kind::Subcommand(_) => { + if res.has_custom_parser { + abort!( + res.parser.span(), + "parse attribute is not allowed for subcommand" + ); + } + + use syn::Fields::*; + use syn::FieldsUnnamed; + let field_ty = match variant.fields { + Named(_) => { + abort!(variant.span(), "structs are not allowed for subcommand"); + } + Unit => abort!(variant.span(), "unit-type is not allowed for subcommand"), + Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { + &unnamed[0].ty + } + Unnamed(..) => { + abort!( + variant, + "non single-typed tuple is not allowed for subcommand" + ) + } + }; + let ty = Ty::from_syn_ty(field_ty); + match *ty { + Ty::OptionOption => { + abort!( + field_ty, + "Option<Option<T>> type is not allowed for subcommand" + ); + } + Ty::OptionVec => { + abort!( + field_ty, + "Option<Vec<T>> type is not allowed for subcommand" + ); + } + _ => (), + } + + res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span()); + } + Kind::Skip(_) => (), + Kind::FromGlobal(_) => { + abort!(res.kind.span(), "from_global is not supported on variants"); + } + Kind::Arg(_) => (), + } + + res + } + + pub fn from_arg_enum_variant( + variant: &Variant, + argument_casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ) -> Self { + let mut res = Self::new( + variant.span(), + Name::Derived(variant.ident.clone()), + None, + argument_casing, + env_casing, + ); + res.push_attrs(&variant.attrs); + res.push_doc_comment(&variant.attrs, "help"); + + if res.has_custom_parser { + abort!( + res.parser.span(), + "`parse` attribute is only allowed on fields" + ); + } + match &*res.kind { + Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), + Kind::Skip(_) => res, + Kind::Arg(_) => res, + Kind::FromGlobal(_) => abort!(res.kind.span(), "from_global is only allowed on fields"), + Kind::Flatten => abort!(res.kind.span(), "flatten is only allowed on fields"), + Kind::ExternalSubcommand => abort!( + res.kind.span(), + "external_subcommand is only allowed on fields" + ), + } + } + + pub fn from_field( + field: &Field, + struct_casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ) -> Self { + let name = field.ident.clone().unwrap(); + let mut res = Self::new( + field.span(), + Name::Derived(name), + Some(field.ty.clone()), + struct_casing, + env_casing, + ); + res.push_attrs(&field.attrs); + res.push_doc_comment(&field.attrs, "help"); + + match &*res.kind { + Kind::Flatten => { + if res.has_custom_parser { + abort!( + res.parser.span(), + "parse attribute is not allowed for flattened entry" + ); + } + if res.has_explicit_methods() { + abort!( + res.kind.span(), + "methods are not allowed for flattened entry" + ); + } + + // ignore doc comments + res.doc_comment = vec![]; + } + + Kind::ExternalSubcommand => { + abort! { res.kind.span(), + "`external_subcommand` can be used only on enum variants" + } + } + + Kind::Subcommand(_) => { + if res.has_custom_parser { + abort!( + res.parser.span(), + "parse attribute is not allowed for subcommand" + ); + } + if res.has_explicit_methods() { + abort!( + res.kind.span(), + "methods in attributes are not allowed for subcommand" + ); + } + + let ty = Ty::from_syn_ty(&field.ty); + match *ty { + Ty::OptionOption => { + abort!( + field.ty, + "Option<Option<T>> type is not allowed for subcommand" + ); + } + Ty::OptionVec => { + abort!( + field.ty, + "Option<Vec<T>> type is not allowed for subcommand" + ); + } + _ => (), + } + + res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span()); + } + Kind::Skip(_) => { + if res.has_explicit_methods() { + abort!( + res.kind.span(), + "methods are not allowed for skipped fields" + ); + } + } + Kind::FromGlobal(orig_ty) => { + let ty = Ty::from_syn_ty(&field.ty); + res.kind = Sp::new(Kind::FromGlobal(ty), orig_ty.span()); + } + Kind::Arg(orig_ty) => { + let mut ty = Ty::from_syn_ty(&field.ty); + if res.has_custom_parser { + match *ty { + Ty::Option | Ty::Vec | Ty::OptionVec => (), + _ => ty = Sp::new(Ty::Other, ty.span()), + } + } + + match *ty { + Ty::Bool => { + if res.is_positional() && !res.has_custom_parser { + abort!(field.ty, + "`bool` cannot be used as positional parameter with default parser"; + help = "if you want to create a flag add `long` or `short`"; + help = "If you really want a boolean parameter \ + add an explicit parser, for example `parse(try_from_str)`"; + note = "see also https://github.com/clap-rs/clap/blob/master/examples/derive_ref/custom-bool.md"; + ) + } + if res.is_enum { + abort!(field.ty, "`arg_enum` is meaningless for bool") + } + if let Some(m) = res.find_default_method() { + abort!(m.name, "default_value is meaningless for bool") + } + if let Some(m) = res.find_method("required") { + abort!(m.name, "required is meaningless for bool") + } + } + Ty::Option => { + if let Some(m) = res.find_default_method() { + abort!(m.name, "default_value is meaningless for Option") + } + } + Ty::OptionOption => { + if res.is_positional() { + abort!( + field.ty, + "Option<Option<T>> type is meaningless for positional argument" + ) + } + } + Ty::OptionVec => { + if res.is_positional() { + abort!( + field.ty, + "Option<Vec<T>> type is meaningless for positional argument" + ) + } + } + + _ => (), + } + res.kind = Sp::new(Kind::Arg(ty), orig_ty.span()); + } + } + + res + } + + fn new( + default_span: Span, + name: Name, + ty: Option<Type>, + casing: Sp<CasingStyle>, + env_casing: Sp<CasingStyle>, + ) -> Self { + Self { + name, + ty, + casing, + env_casing, + doc_comment: vec![], + methods: vec![], + parser: Parser::default_spanned(default_span), + verbatim_doc_comment: None, + next_display_order: None, + next_help_heading: None, + help_heading: None, + is_enum: false, + has_custom_parser: false, + kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span), + } + } + + fn push_method(&mut self, name: Ident, arg: impl ToTokens) { + if name == "name" { + self.name = Name::Assigned(quote!(#arg)); + } else { + self.methods.push(Method::new(name, quote!(#arg))); + } + } + + fn push_attrs(&mut self, attrs: &[Attribute]) { + use ClapAttr::*; + + let parsed = parse_clap_attributes(attrs); + for attr in &parsed { + let attr = attr.clone(); + match attr { + Short(ident) => { + self.push_method(ident, self.name.clone().translate_char(*self.casing)); + } + + Long(ident) => { + self.push_method(ident, self.name.clone().translate(*self.casing)); + } + + Env(ident) => { + self.push_method(ident, self.name.clone().translate(*self.env_casing)); + } + + ArgEnum(_) => self.is_enum = true, + + FromGlobal(ident) => { + let ty = Sp::call_site(Ty::Other); + let kind = Sp::new(Kind::FromGlobal(ty), ident.span()); + self.set_kind(kind); + } + + Subcommand(ident) => { + let ty = Sp::call_site(Ty::Other); + let kind = Sp::new(Kind::Subcommand(ty), ident.span()); + self.set_kind(kind); + } + + ExternalSubcommand(ident) => { + let kind = Sp::new(Kind::ExternalSubcommand, ident.span()); + self.set_kind(kind); + } + + Flatten(ident) => { + let kind = Sp::new(Kind::Flatten, ident.span()); + self.set_kind(kind); + } + + Skip(ident, expr) => { + let kind = Sp::new(Kind::Skip(expr), ident.span()); + self.set_kind(kind); + } + + VerbatimDocComment(ident) => self.verbatim_doc_comment = Some(ident), + + DefaultValueT(ident, expr) => { + let ty = if let Some(ty) = self.ty.as_ref() { + ty + } else { + abort!( + ident, + "#[clap(default_value_t)] (without an argument) can be used \ + only on field level"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + }; + + let val = if let Some(expr) = expr { + quote!(#expr) + } else { + quote!(<#ty as ::std::default::Default>::default()) + }; + + let val = if parsed.iter().any(|a| matches!(a, ArgEnum(_))) { + quote_spanned!(ident.span()=> { + { + let val: #ty = #val; + clap::ArgEnum::to_possible_value(&val).unwrap().get_name() + } + }) + } else { + quote_spanned!(ident.span()=> { + clap::lazy_static::lazy_static! { + static ref DEFAULT_VALUE: &'static str = { + let val: #ty = #val; + let s = ::std::string::ToString::to_string(&val); + ::std::boxed::Box::leak(s.into_boxed_str()) + }; + } + *DEFAULT_VALUE + }) + }; + + let raw_ident = Ident::new("default_value", ident.span()); + self.methods.push(Method::new(raw_ident, val)); + } + + DefaultValueOsT(ident, expr) => { + let ty = if let Some(ty) = self.ty.as_ref() { + ty + } else { + abort!( + ident, + "#[clap(default_value_os_t)] (without an argument) can be used \ + only on field level"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + }; + + let val = if let Some(expr) = expr { + quote!(#expr) + } else { + quote!(<#ty as ::std::default::Default>::default()) + }; + + let val = if parsed.iter().any(|a| matches!(a, ArgEnum(_))) { + quote_spanned!(ident.span()=> { + { + let val: #ty = #val; + clap::ArgEnum::to_possible_value(&val).unwrap().get_name() + } + }) + } else { + quote_spanned!(ident.span()=> { + clap::lazy_static::lazy_static! { + static ref DEFAULT_VALUE: &'static ::std::ffi::OsStr = { + let val: #ty = #val; + let s: ::std::ffi::OsString = val.into(); + ::std::boxed::Box::leak(s.into_boxed_os_str()) + }; + } + *DEFAULT_VALUE + }) + }; + + let raw_ident = Ident::new("default_value_os", ident.span()); + self.methods.push(Method::new(raw_ident, val)); + } + + NextDisplayOrder(ident, expr) => { + self.next_display_order = Some(Method::new(ident, quote!(#expr))); + } + + HelpHeading(ident, expr) => { + self.help_heading = Some(Method::new(ident, quote!(#expr))); + } + NextHelpHeading(ident, expr) => { + self.next_help_heading = Some(Method::new(ident, quote!(#expr))); + } + + About(ident) => { + if let Some(method) = Method::from_env(ident, "CARGO_PKG_DESCRIPTION") { + self.methods.push(method); + } + } + + Author(ident) => { + if let Some(method) = Method::from_env(ident, "CARGO_PKG_AUTHORS") { + self.methods.push(method); + } + } + + Version(ident) => { + if let Some(method) = Method::from_env(ident, "CARGO_PKG_VERSION") { + self.methods.push(method); + } + } + + NameLitStr(name, lit) => { + self.push_method(name, lit); + } + + NameExpr(name, expr) => { + self.push_method(name, expr); + } + + MethodCall(name, args) => self.push_method(name, quote!(#(#args),*)), + + RenameAll(_, casing_lit) => { + self.casing = CasingStyle::from_lit(casing_lit); + } + + RenameAllEnv(_, casing_lit) => { + self.env_casing = CasingStyle::from_lit(casing_lit); + } + + Parse(ident, spec) => { + self.has_custom_parser = true; + self.parser = Parser::from_spec(ident, spec); + } + } + } + } + + fn push_doc_comment(&mut self, attrs: &[Attribute], name: &str) { + use syn::Lit::*; + use syn::Meta::*; + + let comment_parts: Vec<_> = attrs + .iter() + .filter(|attr| attr.path.is_ident("doc")) + .filter_map(|attr| { + if let Ok(NameValue(MetaNameValue { lit: Str(s), .. })) = attr.parse_meta() { + Some(s.value()) + } else { + // non #[doc = "..."] attributes are not our concern + // we leave them for rustc to handle + None + } + }) + .collect(); + + self.doc_comment = + process_doc_comment(comment_parts, name, self.verbatim_doc_comment.is_none()); + } + + fn set_kind(&mut self, kind: Sp<Kind>) { + if let Kind::Arg(_) = *self.kind { + self.kind = kind; + } else { + abort!( + kind.span(), + "`subcommand`, `flatten`, `external_subcommand` and `skip` cannot be used together" + ); + } + } + + pub fn find_method(&self, name: &str) -> Option<&Method> { + self.methods.iter().find(|m| m.name == name) + } + + pub fn find_default_method(&self) -> Option<&Method> { + self.methods + .iter() + .find(|m| m.name == "default_value" || m.name == "default_value_os") + } + + /// generate methods from attributes on top of struct or enum + pub fn initial_top_level_methods(&self) -> TokenStream { + let next_display_order = self.next_display_order.as_ref().into_iter(); + let next_help_heading = self.next_help_heading.as_ref().into_iter(); + let help_heading = self.help_heading.as_ref().into_iter(); + quote!( + #(#next_display_order)* + #(#next_help_heading)* + #(#help_heading)* + ) + } + + pub fn final_top_level_methods(&self) -> TokenStream { + let methods = &self.methods; + let doc_comment = &self.doc_comment; + + quote!( #(#doc_comment)* #(#methods)*) + } + + /// generate methods on top of a field + pub fn field_methods(&self, supports_long_help: bool) -> proc_macro2::TokenStream { + let methods = &self.methods; + let help_heading = self.help_heading.as_ref().into_iter(); + match supports_long_help { + true => { + let doc_comment = &self.doc_comment; + quote!( #(#doc_comment)* #(#help_heading)* #(#methods)* ) + } + false => { + let doc_comment = self + .doc_comment + .iter() + .filter(|mth| mth.name != "long_help"); + quote!( #(#doc_comment)* #(#help_heading)* #(#methods)* ) + } + } + } + + pub fn next_display_order(&self) -> TokenStream { + let next_display_order = self.next_display_order.as_ref().into_iter(); + quote!( #(#next_display_order)* ) + } + + pub fn next_help_heading(&self) -> TokenStream { + let next_help_heading = self.next_help_heading.as_ref().into_iter(); + let help_heading = self.help_heading.as_ref().into_iter(); + quote!( #(#next_help_heading)* #(#help_heading)* ) + } + + #[cfg(feature = "unstable-v4")] + pub fn id(&self) -> TokenStream { + self.name.clone().raw() + } + + #[cfg(not(feature = "unstable-v4"))] + pub fn id(&self) -> TokenStream { + self.cased_name() + } + + pub fn cased_name(&self) -> TokenStream { + self.name.clone().translate(*self.casing) + } + + pub fn value_name(&self) -> TokenStream { + self.name.clone().translate(CasingStyle::ScreamingSnake) + } + + pub fn parser(&self) -> &Sp<Parser> { + &self.parser + } + + pub fn kind(&self) -> Sp<Kind> { + self.kind.clone() + } + + pub fn is_enum(&self) -> bool { + self.is_enum + } + + pub fn ignore_case(&self) -> TokenStream { + let method = self.find_method("ignore_case"); + + if let Some(method) = method { + method.args.clone() + } else { + quote! { false } + } + } + + pub fn casing(&self) -> Sp<CasingStyle> { + self.casing.clone() + } + + pub fn env_casing(&self) -> Sp<CasingStyle> { + self.env_casing.clone() + } + + pub fn is_positional(&self) -> bool { + self.methods + .iter() + .all(|m| m.name != "long" && m.name != "short") + } + + pub fn has_explicit_methods(&self) -> bool { + self.methods + .iter() + .any(|m| m.name != "help" && m.name != "long_help") + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +pub enum Kind { + Arg(Sp<Ty>), + FromGlobal(Sp<Ty>), + Subcommand(Sp<Ty>), + Flatten, + Skip(Option<Expr>), + ExternalSubcommand, +} + +#[derive(Clone)] +pub struct Method { + name: Ident, + args: TokenStream, +} + +impl Method { + pub fn new(name: Ident, args: TokenStream) -> Self { + Method { name, args } + } + + fn from_env(ident: Ident, env_var: &str) -> Option<Self> { + let mut lit = match env::var(env_var) { + Ok(val) => { + if val.is_empty() { + return None; + } + LitStr::new(&val, ident.span()) + } + Err(_) => { + abort!(ident, + "cannot derive `{}` from Cargo.toml", ident; + note = "`{}` environment variable is not set", env_var; + help = "use `{} = \"...\"` to set {} manually", ident, ident; + ); + } + }; + + if ident == "author" { + let edited = process_author_str(&lit.value()); + lit = LitStr::new(&edited, lit.span()); + } + + Some(Method::new(ident, quote!(#lit))) + } +} + +impl ToTokens for Method { + fn to_tokens(&self, ts: &mut proc_macro2::TokenStream) { + let Method { ref name, ref args } = self; + + let tokens = quote!( .#name(#args) ); + + tokens.to_tokens(ts); + } +} + +/// replace all `:` with `, ` when not inside the `<>` +/// +/// `"author1:author2:author3" => "author1, author2, author3"` +/// `"author1 <http://website1.com>:author2" => "author1 <http://website1.com>, author2" +fn process_author_str(author: &str) -> String { + let mut res = String::with_capacity(author.len()); + let mut inside_angle_braces = 0usize; + + for ch in author.chars() { + if inside_angle_braces > 0 && ch == '>' { + inside_angle_braces -= 1; + res.push(ch); + } else if ch == '<' { + inside_angle_braces += 1; + res.push(ch); + } else if inside_angle_braces == 0 && ch == ':' { + res.push_str(", "); + } else { + res.push(ch); + } + } + + res +} + +#[derive(Clone)] +pub struct Parser { + pub kind: Sp<ParserKind>, + pub func: TokenStream, +} + +impl Parser { + fn default_spanned(span: Span) -> Sp<Self> { + let kind = Sp::new(ParserKind::TryFromStr, span); + let func = quote_spanned!(span=> ::std::str::FromStr::from_str); + Sp::new(Parser { kind, func }, span) + } + + fn from_spec(parse_ident: Ident, spec: ParserSpec) -> Sp<Self> { + use self::ParserKind::*; + + let kind = match &*spec.kind.to_string() { + "from_str" => FromStr, + "try_from_str" => TryFromStr, + "from_os_str" => FromOsStr, + "try_from_os_str" => TryFromOsStr, + "from_occurrences" => FromOccurrences, + "from_flag" => FromFlag, + s => abort!(spec.kind.span(), "unsupported parser `{}`", s), + }; + + let func = match spec.parse_func { + None => match kind { + FromStr | FromOsStr => { + quote_spanned!(spec.kind.span()=> ::std::convert::From::from) + } + TryFromStr => quote_spanned!(spec.kind.span()=> ::std::str::FromStr::from_str), + TryFromOsStr => abort!( + spec.kind.span(), + "you must set parser for `try_from_os_str` explicitly" + ), + FromOccurrences => quote_spanned!(spec.kind.span()=> { |v| v as _ }), + FromFlag => quote_spanned!(spec.kind.span()=> ::std::convert::From::from), + }, + + Some(func) => match func { + Expr::Path(_) => quote!(#func), + _ => abort!(func, "`parse` argument must be a function path"), + }, + }; + + let kind = Sp::new(kind, spec.kind.span()); + let parser = Parser { kind, func }; + Sp::new(parser, parse_ident.span()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ParserKind { + FromStr, + TryFromStr, + FromOsStr, + TryFromOsStr, + FromOccurrences, + FromFlag, +} + +/// Defines the casing for the attributes long representation. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum CasingStyle { + /// Indicate word boundaries with uppercase letter, excluding the first word. + Camel, + /// Keep all letters lowercase and indicate word boundaries with hyphens. + Kebab, + /// Indicate word boundaries with uppercase letter, including the first word. + Pascal, + /// Keep all letters uppercase and indicate word boundaries with underscores. + ScreamingSnake, + /// Keep all letters lowercase and indicate word boundaries with underscores. + Snake, + /// Keep all letters lowercase and remove word boundaries. + Lower, + /// Keep all letters uppercase and remove word boundaries. + Upper, + /// Use the original attribute name defined in the code. + Verbatim, +} + +impl CasingStyle { + fn from_lit(name: LitStr) -> Sp<Self> { + use self::CasingStyle::*; + + let normalized = name.value().to_upper_camel_case().to_lowercase(); + let cs = |kind| Sp::new(kind, name.span()); + + match normalized.as_ref() { + "camel" | "camelcase" => cs(Camel), + "kebab" | "kebabcase" => cs(Kebab), + "pascal" | "pascalcase" => cs(Pascal), + "screamingsnake" | "screamingsnakecase" => cs(ScreamingSnake), + "snake" | "snakecase" => cs(Snake), + "lower" | "lowercase" => cs(Lower), + "upper" | "uppercase" => cs(Upper), + "verbatim" | "verbatimcase" => cs(Verbatim), + s => abort!(name, "unsupported casing: `{}`", s), + } + } +} + +#[derive(Clone)] +pub enum Name { + Derived(Ident), + Assigned(TokenStream), +} + +impl Name { + #[cfg(feature = "unstable-v4")] + pub fn raw(self) -> TokenStream { + match self { + Name::Assigned(tokens) => tokens, + Name::Derived(ident) => { + let s = ident.unraw().to_string(); + quote_spanned!(ident.span()=> #s) + } + } + } + + pub fn translate(self, style: CasingStyle) -> TokenStream { + use CasingStyle::*; + + match self { + Name::Assigned(tokens) => tokens, + Name::Derived(ident) => { + let s = ident.unraw().to_string(); + let s = match style { + Pascal => s.to_upper_camel_case(), + Kebab => s.to_kebab_case(), + Camel => s.to_lower_camel_case(), + ScreamingSnake => s.to_shouty_snake_case(), + Snake => s.to_snake_case(), + Lower => s.to_snake_case().replace('_', ""), + Upper => s.to_shouty_snake_case().replace('_', ""), + Verbatim => s, + }; + quote_spanned!(ident.span()=> #s) + } + } + } + + pub fn translate_char(self, style: CasingStyle) -> TokenStream { + use CasingStyle::*; + + match self { + Name::Assigned(tokens) => quote!( (#tokens).chars().next().unwrap() ), + Name::Derived(ident) => { + let s = ident.unraw().to_string(); + let s = match style { + Pascal => s.to_upper_camel_case(), + Kebab => s.to_kebab_case(), + Camel => s.to_lower_camel_case(), + ScreamingSnake => s.to_shouty_snake_case(), + Snake => s.to_snake_case(), + Lower => s.to_snake_case(), + Upper => s.to_shouty_snake_case(), + Verbatim => s, + }; + + let s = s.chars().next().unwrap(); + quote_spanned!(ident.span()=> #s) + } + } + } +} |