diff --git a/src/attr.rs b/src/attr.rs index 443daa5a46..0a2b01616d 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -6,13 +6,111 @@ use std::iter; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Attribute { pub style: AttrStyle, - pub value: MetaItem, + + /// The path of the attribute. + /// + /// E.g. `derive` in `#[derive(Copy)]` + /// E.g. `crate::precondition` in `#[crate::precondition x < 5]` + pub path: Path, + + /// Any tokens after the path. + /// + /// E.g. `( Copy )` in `#[derive(Copy)]` + /// E.g. `x < 5` in `#[crate::precondition x < 5]` + pub tts: Vec, + pub is_sugared_doc: bool, } impl Attribute { - pub fn name(&self) -> &str { - self.value.name() + /// Parses the tokens after the path as a [`MetaItem`](enum.MetaItem.html) if possible. + pub fn meta_item(&self) -> Option { + let name = if self.path.segments.len() == 1 { + &self.path.segments[0].ident + } else { + return None; + }; + + if self.tts.is_empty() { + return Some(MetaItem::Word(name.clone())); + } + + if self.tts.len() == 1 { + if let TokenTree::Delimited(Delimited { delim: DelimToken::Paren, ref tts }) = self.tts[0] { + fn nested_meta_item_from_tokens(tts: &[TokenTree]) -> Option<(NestedMetaItem, &[TokenTree])> { + assert!(!tts.is_empty()); + + match tts[0] { + TokenTree::Token(Token::Literal(ref lit)) => { + Some((NestedMetaItem::Literal(lit.clone()), &tts[1..])) + } + + TokenTree::Token(Token::Ident(ref ident)) => { + if tts.len() >= 3 { + if let TokenTree::Token(Token::Eq) = tts[1] { + if let TokenTree::Token(Token::Literal(ref lit)) = tts[2] { + return Some((NestedMetaItem::MetaItem(MetaItem::NameValue(ident.clone(), lit.clone())), &tts[3..])); + } + } + } + + if tts.len() >= 2 { + if let TokenTree::Delimited(Delimited { delim: DelimToken::Paren, tts: ref inner_tts }) = tts[1] { + return match list_of_nested_meta_items_from_tokens(vec![], inner_tts) { + Some(nested_meta_items) => { + Some((NestedMetaItem::MetaItem(MetaItem::List(ident.clone(), nested_meta_items)), &tts[2..])) + } + + None => None + }; + } + } + + Some((NestedMetaItem::MetaItem(MetaItem::Word(ident.clone())), &tts[1..])) + } + + _ => None + } + } + + fn list_of_nested_meta_items_from_tokens(mut result: Vec, tts: &[TokenTree]) -> Option> { + if tts.is_empty() { + return Some(result); + } + + match nested_meta_item_from_tokens(tts) { + Some((nested_meta_item, rest)) => { + result.push(nested_meta_item); + if rest.is_empty() { + list_of_nested_meta_items_from_tokens(result, rest) + } + else if let TokenTree::Token(Token::Comma) = rest[0] { + list_of_nested_meta_items_from_tokens(result, &rest[1..]) + } + else { + None + } + } + + None => None + } + } + + if let Some(nested_meta_items) = list_of_nested_meta_items_from_tokens(vec![], tts) { + return Some(MetaItem::List(name.clone(), nested_meta_items)); + } + } + } + + if self.tts.len() == 2 { + if let TokenTree::Token(Token::Eq) = self.tts[0] { + if let TokenTree::Token(Token::Literal(ref lit)) = self.tts[1] { + return Some(MetaItem::NameValue(name.clone(), lit.clone())); + } + } + } + + None } } @@ -109,22 +207,31 @@ impl<'a, T> FilterAttrs<'a> for T #[cfg(feature = "parsing")] pub mod parsing { use super::*; - use ident::parsing::ident; - use lit::parsing::lit; + use lit::{Lit, StrStyle}; + use mac::{Token, TokenTree}; + use mac::parsing::token_trees; use synom::space::{block_comment, whitespace}; + use ty::parsing::mod_style_path; #[cfg(feature = "full")] named!(pub inner_attr -> Attribute, alt!( do_parse!( punct!("#") >> punct!("!") >> - punct!("[") >> - meta_item: meta_item >> - punct!("]") >> - (Attribute { - style: AttrStyle::Inner, - value: meta_item, - is_sugared_doc: false, + path_and_tts: delimited!( + punct!("["), + tuple!(mod_style_path, token_trees), + punct!("]") + ) >> + ({ + let (path, tts) = path_and_tts; + + Attribute { + style: AttrStyle::Inner, + path: path, + tts: tts, + is_sugared_doc: false, + } }) ) | @@ -133,10 +240,11 @@ pub mod parsing { content: take_until!("\n") >> (Attribute { style: AttrStyle::Inner, - value: MetaItem::NameValue( - "doc".into(), - format!("//!{}", content).into(), - ), + path: "doc".into(), + tts: vec![ + TokenTree::Token(Token::Eq), + TokenTree::Token(Token::Literal(Lit::Str(format!("//!{}", content).into(), StrStyle::Cooked))), + ], is_sugared_doc: true, }) ) @@ -147,10 +255,11 @@ pub mod parsing { com: block_comment >> (Attribute { style: AttrStyle::Inner, - value: MetaItem::NameValue( - "doc".into(), - com.into(), - ), + path: "doc".into(), + tts: vec![ + TokenTree::Token(Token::Eq), + TokenTree::Token(Token::Literal(Lit::Str(com.into(), StrStyle::Cooked))), + ], is_sugared_doc: true, }) ) @@ -159,13 +268,20 @@ pub mod parsing { named!(pub outer_attr -> Attribute, alt!( do_parse!( punct!("#") >> - punct!("[") >> - meta_item: meta_item >> - punct!("]") >> - (Attribute { - style: AttrStyle::Outer, - value: meta_item, - is_sugared_doc: false, + path_and_tts: delimited!( + punct!("["), + tuple!(mod_style_path, token_trees), + punct!("]") + ) >> + ({ + let (path, tts) = path_and_tts; + + Attribute { + style: AttrStyle::Outer, + path: path, + tts: tts, + is_sugared_doc: false, + } }) ) | @@ -175,10 +291,11 @@ pub mod parsing { content: take_until!("\n") >> (Attribute { style: AttrStyle::Outer, - value: MetaItem::NameValue( - "doc".into(), - format!("///{}", content).into(), - ), + path: "doc".into(), + tts: vec![ + TokenTree::Token(Token::Eq), + TokenTree::Token(Token::Literal(Lit::Str(format!("///{}", content).into(), StrStyle::Cooked))), + ], is_sugared_doc: true, }) ) @@ -189,74 +306,65 @@ pub mod parsing { com: block_comment >> (Attribute { style: AttrStyle::Outer, - value: MetaItem::NameValue( - "doc".into(), - com.into(), - ), + path: "doc".into(), + tts: vec![ + TokenTree::Token(Token::Eq), + TokenTree::Token(Token::Literal(Lit::Str(com.into(), StrStyle::Cooked))), + ], is_sugared_doc: true, }) ) )); - - named!(meta_item -> MetaItem, alt!( - do_parse!( - id: ident >> - punct!("(") >> - inner: terminated_list!(punct!(","), nested_meta_item) >> - punct!(")") >> - (MetaItem::List(id, inner)) - ) - | - do_parse!( - name: ident >> - punct!("=") >> - value: lit >> - (MetaItem::NameValue(name, value)) - ) - | - map!(ident, MetaItem::Word) - )); - - named!(nested_meta_item -> NestedMetaItem, alt!( - meta_item => { NestedMetaItem::MetaItem } - | - lit => { NestedMetaItem::Literal } - )); } #[cfg(feature = "printing")] mod printing { use super::*; use lit::{Lit, StrStyle}; + use mac::{Token, TokenTree}; use quote::{Tokens, ToTokens}; + use ty::Path; impl ToTokens for Attribute { fn to_tokens(&self, tokens: &mut Tokens) { - if let Attribute { style, - value: MetaItem::NameValue(ref name, - Lit::Str(ref value, StrStyle::Cooked)), - is_sugared_doc: true } = *self { - if name == "doc" { - match style { - AttrStyle::Inner if value.starts_with("//!") => { - tokens.append(&format!("{}\n", value)); - return; - } - AttrStyle::Inner if value.starts_with("/*!") => { - tokens.append(value); - return; - } - AttrStyle::Outer if value.starts_with("///") => { - tokens.append(&format!("{}\n", value)); - return; - } - AttrStyle::Outer if value.starts_with("/**") => { - tokens.append(value); - return; + // If this was a sugared doc, emit it in its original form instead of `#[doc = "..."]` + match *self { + Attribute { + style, + path: Path { global: false, ref segments }, + ref tts, + is_sugared_doc: true, + } if segments.len() == 1 && + segments[0].ident == "doc" && + segments[0].parameters.is_empty() && + tts.len() == 2 => + { + if let TokenTree::Token(Token::Eq) = self.tts[0] { + if let TokenTree::Token(Token::Literal(Lit::Str(ref value, StrStyle::Cooked))) = self.tts[1] { + match style { + AttrStyle::Inner if value.starts_with("//!") => { + tokens.append(&format!("{}\n", value)); + return; + } + AttrStyle::Inner if value.starts_with("/*!") => { + tokens.append(value); + return; + } + AttrStyle::Outer if value.starts_with("///") => { + tokens.append(&format!("{}\n", value)); + return; + } + AttrStyle::Outer if value.starts_with("/**") => { + tokens.append(value); + return; + } + _ => {} + } } - _ => {} } } + + _ => {} } tokens.append("#"); @@ -264,7 +372,8 @@ mod printing { tokens.append("!"); } tokens.append("["); - self.value.to_tokens(tokens); + self.path.to_tokens(tokens); + tokens.append_all(&self.tts); tokens.append("]"); } } diff --git a/src/ty.rs b/src/ty.rs index 4055b6a60c..7b7d3afbb1 100644 --- a/src/ty.rs +++ b/src/ty.rs @@ -519,6 +519,27 @@ pub mod parsing { ), Into::into) )); + named!(pub mod_style_path -> Path, do_parse!( + global: option!(punct!("::")) >> + segments: separated_nonempty_list!(punct!("::"), mod_style_path_segment) >> + (Path { + global: global.is_some(), + segments: segments, + }) + )); + + named!(mod_style_path_segment -> PathSegment, alt!( + map!(ident, Into::into) + | + map!(alt!( + keyword!("super") + | + keyword!("self") + | + keyword!("Self") + ), Into::into) + )); + named!(type_binding -> TypeBinding, do_parse!( id: ident >> punct!("=") >> diff --git a/tests/test_generics.rs b/tests/test_generics.rs index 0cdce4e37d..800daa7395 100644 --- a/tests/test_generics.rs +++ b/tests/test_generics.rs @@ -21,7 +21,8 @@ fn test_split_for_impl() { ty_params: vec![TyParam { attrs: vec![Attribute { style: AttrStyle::Outer, - value: MetaItem::Word("may_dangle".into()), + path: "may_dangle".into(), + tts: vec![], is_sugared_doc: false, }], ident: Ident::new("T"), diff --git a/tests/test_macro_input.rs b/tests/test_macro_input.rs index b209bc6a99..6f40a00803 100644 --- a/tests/test_macro_input.rs +++ b/tests/test_macro_input.rs @@ -30,14 +30,17 @@ fn test_struct() { ident: "Item".into(), vis: Visibility::Public, attrs: vec![Attribute { - style: AttrStyle::Outer, - value: MetaItem::List("derive".into(), - vec![ - NestedMetaItem::MetaItem(MetaItem::Word("Debug".into())), - NestedMetaItem::MetaItem(MetaItem::Word("Clone".into())), - ]), - is_sugared_doc: false, - }], + style: AttrStyle::Outer, + path: "derive".into(), + tts: vec![ + TokenTree::Delimited(Delimited { delim: DelimToken::Paren, tts: vec![ + TokenTree::Token(Token::Ident("Debug".into())), + TokenTree::Token(Token::Comma), + TokenTree::Token(Token::Ident("Clone".into())), + ]}) + ], + is_sugared_doc: false, + }], generics: Generics::default(), body: Body::Struct(VariantData::Struct(vec![Field { ident: Some("ident".into()), @@ -68,7 +71,16 @@ fn test_struct() { }])), }; - assert_eq!(expected, parse_macro_input(raw).unwrap()); + let actual = parse_macro_input(raw).unwrap(); + + assert_eq!(expected, actual); + + let expected_meta_item = MetaItem::List("derive".into(), vec![ + NestedMetaItem::MetaItem(MetaItem::Word("Debug".into())), + NestedMetaItem::MetaItem(MetaItem::Word("Clone".into())), + ]); + + assert_eq!(expected_meta_item, actual.attrs[0].meta_item().unwrap()); } #[test] @@ -94,18 +106,20 @@ fn test_enum() { attrs: vec![ Attribute { style: AttrStyle::Outer, - value: MetaItem::NameValue( - "doc".into(), - Lit::Str( + path: "doc".into(), + tts: vec![ + TokenTree::Token(Token::Eq), + TokenTree::Token(Token::Literal(Lit::Str( "/// See the std::result module documentation for details.".into(), StrStyle::Cooked, - ), - ), + ))), + ], is_sugared_doc: true, }, Attribute { style: AttrStyle::Outer, - value: MetaItem::Word("must_use".into()), + path: "must_use".into(), + tts: vec![], is_sugared_doc: false, }, ], @@ -185,5 +199,120 @@ fn test_enum() { ]), }; - assert_eq!(expected, parse_macro_input(raw).unwrap()); + let actual = parse_macro_input(raw).unwrap(); + + assert_eq!(expected, actual); + + let expected_meta_items = vec![ + MetaItem::NameValue("doc".into(), Lit::Str( + "/// See the std::result module documentation for details.".into(), + StrStyle::Cooked, + )), + MetaItem::Word("must_use".into()), + ]; + + let actual_meta_items: Vec<_> = actual.attrs.into_iter().map(|attr| attr.meta_item().unwrap()).collect(); + + assert_eq!(expected_meta_items, actual_meta_items); +} + +#[test] +fn test_attr_with_path() { + let raw =r#" + #[::attr_args::identity + fn main() { assert_eq!(foo(), "Hello, world!"); }] + struct Dummy; + "#; + + let expected = MacroInput { + ident: "Dummy".into(), + vis: Visibility::Inherited, + attrs: vec![Attribute { + style: AttrStyle::Outer, + path: Path { global: true, segments: vec!["attr_args".into(), "identity".into()] }, + tts: vec![ + TokenTree::Token(Token::Ident("fn".into())), + TokenTree::Token(Token::Ident("main".into())), + TokenTree::Delimited(Delimited { delim: DelimToken::Paren, tts: vec![] }), + TokenTree::Delimited(Delimited { delim: DelimToken::Brace, tts: vec![ + TokenTree::Token(Token::Ident("assert_eq".into())), + TokenTree::Token(Token::Not), + TokenTree::Delimited(Delimited { delim: DelimToken::Paren, tts: vec![ + TokenTree::Token(Token::Ident("foo".into())), + TokenTree::Delimited(Delimited { delim: DelimToken::Paren, tts: vec![] }), + TokenTree::Token(Token::Comma), + TokenTree::Token(Token::Literal(Lit::Str("Hello, world!".into(), StrStyle::Cooked))), + ]}), + TokenTree::Token(Token::Semi), + ]}) + ], + is_sugared_doc: false, + }], + generics: Generics::default(), + body: Body::Struct(VariantData::Unit), + }; + + let actual = parse_macro_input(raw).unwrap(); + + assert_eq!(expected, actual); + + assert!(actual.attrs[0].meta_item().is_none()); +} + +#[test] +fn test_attr_with_non_mod_style_path() { + let raw =r#" + #[inert ] + struct S; + "#; + + let expected = MacroInput { + ident: "S".into(), + vis: Visibility::Inherited, + attrs: vec![Attribute { + style: AttrStyle::Outer, + path: Path { global: false, segments: vec!["inert".into()] }, + tts: vec![ + TokenTree::Token(Token::Lt), + TokenTree::Token(Token::Ident("T".into())), + TokenTree::Token(Token::Gt), + ], + is_sugared_doc: false, + }], + generics: Generics::default(), + body: Body::Struct(VariantData::Unit), + }; + + let actual = parse_macro_input(raw).unwrap(); + + assert_eq!(expected, actual); + + assert!(actual.attrs[0].meta_item().is_none()); +} + +#[test] +fn test_attr_with_mod_style_path_with_self() { + let raw =r#" + #[foo::self] + struct S; + "#; + + let expected = MacroInput { + ident: "S".into(), + vis: Visibility::Inherited, + attrs: vec![Attribute { + style: AttrStyle::Outer, + path: Path { global: false, segments: vec!["foo".into(), "self".into()] }, + tts: vec![], + is_sugared_doc: false, + }], + generics: Generics::default(), + body: Body::Struct(VariantData::Unit), + }; + + let actual = parse_macro_input(raw).unwrap(); + + assert_eq!(expected, actual); + + assert!(actual.attrs[0].meta_item().is_none()); } diff --git a/tests/test_meta_item.rs b/tests/test_meta_item.rs new file mode 100644 index 0000000000..58a8eff532 --- /dev/null +++ b/tests/test_meta_item.rs @@ -0,0 +1,54 @@ +extern crate syn; +use syn::*; + +#[test] +fn test_meta_item_word() { + run_test("#[foo]", &MetaItem::Word("foo".into())) +} + +#[test] +fn test_meta_item_name_value() { + run_test("#[foo = 5]", &MetaItem::NameValue("foo".into(), + Lit::Int(5, IntTy::Unsuffixed))) +} + +#[test] +fn test_meta_item_list_lit() { + run_test("#[foo(5)]", &MetaItem::List("foo".into(), vec![ + NestedMetaItem::Literal(Lit::Int(5, IntTy::Unsuffixed)), + ])) +} + +#[test] +fn test_meta_item_list_word() { + run_test("#[foo(bar)]", &MetaItem::List("foo".into(), vec![ + NestedMetaItem::MetaItem(MetaItem::Word("bar".into())), + ])) +} + +#[test] +fn test_meta_item_list_name_value() { + run_test("#[foo(bar = 5)]", &MetaItem::List("foo".into(), vec![ + NestedMetaItem::MetaItem(MetaItem::NameValue("bar".into(), + Lit::Int(5, IntTy::Unsuffixed))), + ])) +} + +#[test] +fn test_meta_item_multiple() { + run_test("#[foo(word, name = 5, list(name2 = 6), word2)]", &MetaItem::List("foo".into(), vec![ + NestedMetaItem::MetaItem(MetaItem::Word("word".into())), + NestedMetaItem::MetaItem(MetaItem::NameValue("name".into(), + Lit::Int(5, IntTy::Unsuffixed))), + NestedMetaItem::MetaItem(MetaItem::List("list".into(), vec![ + NestedMetaItem::MetaItem(MetaItem::NameValue("name2".into(), + Lit::Int(6, IntTy::Unsuffixed))) + ])), + NestedMetaItem::MetaItem(MetaItem::Word("word2".into())), + ])) +} + +fn run_test(input: &str, expected: &MetaItem) { + let attr = parse_outer_attr(input).unwrap(); + assert_eq!(expected, &attr.meta_item().unwrap()); +}