Merge pull request #513 from carllerche/parse-meta
Implement Parse for meta item types
diff --git a/src/attr.rs b/src/attr.rs
index 2e0cccb..bfc41d9 100644
--- a/src/attr.rs
+++ b/src/attr.rs
@@ -11,7 +11,9 @@
use std::iter;
-use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree};
+#[cfg(not(feature = "parsing"))]
+use proc_macro2::{Delimiter, Spacing, TokenTree};
+use proc_macro2::TokenStream;
#[cfg(feature = "parsing")]
use parse::{ParseStream, Result};
@@ -146,32 +148,61 @@
impl Attribute {
/// Parses the tokens after the path as a [`Meta`](enum.Meta.html) if
/// possible.
+ ///
+ /// Deprecated; use `parse_meta` instead.
+ #[doc(hidden)]
pub fn interpret_meta(&self) -> Option<Meta> {
- let name = if self.path.segments.len() == 1 {
- &self.path.segments.first().unwrap().value().ident
- } else {
- return None;
- };
-
- if self.tts.is_empty() {
- return Some(Meta::Word(name.clone()));
+ #[cfg(feature = "parsing")]
+ {
+ self.parse_meta().ok()
}
- let tts = self.tts.clone().into_iter().collect::<Vec<_>>();
+ #[cfg(not(feature = "parsing"))]
+ {
+ let name = if self.path.segments.len() == 1 {
+ &self.path.segments.first().unwrap().value().ident
+ } else {
+ return None;
+ };
- if tts.len() == 1 {
- if let Some(meta) = Attribute::extract_meta_list(name.clone(), &tts[0]) {
- return Some(meta);
+ if self.tts.is_empty() {
+ return Some(Meta::Word(name.clone()));
}
- }
- if tts.len() == 2 {
- if let Some(meta) = Attribute::extract_name_value(name.clone(), &tts[0], &tts[1]) {
- return Some(meta);
+ let tts = self.tts.clone().into_iter().collect::<Vec<_>>();
+
+ if tts.len() == 1 {
+ if let Some(meta) = Attribute::extract_meta_list(name.clone(), &tts[0]) {
+ return Some(meta);
+ }
}
+
+ if tts.len() == 2 {
+ if let Some(meta) = Attribute::extract_name_value(name.clone(), &tts[0], &tts[1]) {
+ return Some(meta);
+ }
+ }
+
+ None
+ }
+ }
+
+ /// Parses the tokens after the path as a [`Meta`](enum.Meta.html) if
+ /// possible.
+ #[cfg(feature = "parsing")]
+ pub fn parse_meta(&self) -> Result<Meta> {
+ if let Some(ref colon) = self.path.leading_colon {
+ return Err(Error::new(colon.spans[0], "expected meta identifier"));
}
- None
+ let first_segment = self.path.segments.first().expect("paths have at least one segment");
+ if let Some(colon) = first_segment.punct() {
+ return Err(Error::new(colon.spans[0], "expected meta value"));
+ }
+ let ident = first_segment.value().ident.clone();
+
+ let parser = |input: ParseStream| parsing::parse_meta_after_ident(ident, input);
+ parse::Parser::parse2(parser, self.tts.clone())
}
/// Parses zero or more outer attributes from the stream.
@@ -200,6 +231,7 @@
Ok(attrs)
}
+ #[cfg(not(feature = "parsing"))]
fn extract_meta_list(ident: Ident, tt: &TokenTree) -> Option<Meta> {
let g = match *tt {
TokenTree::Group(ref g) => g,
@@ -220,6 +252,7 @@
}))
}
+ #[cfg(not(feature = "parsing"))]
fn extract_name_value(ident: Ident, a: &TokenTree, b: &TokenTree) -> Option<Meta> {
let a = match *a {
TokenTree::Punct(ref o) => o,
@@ -256,6 +289,7 @@
}
}
+#[cfg(not(feature = "parsing"))]
fn nested_meta_item_from_tokens(tts: &[TokenTree]) -> Option<(NestedMeta, &[TokenTree])> {
assert!(!tts.is_empty());
@@ -297,6 +331,7 @@
}
}
+#[cfg(not(feature = "parsing"))]
fn list_of_nested_meta_items_from_tokens(
mut tts: &[TokenTree],
) -> Option<Punctuated<NestedMeta, Token![,]>> {
@@ -438,6 +473,48 @@
}
}
+/// Conventional argument type associated with an invocation of an attribute
+/// macro.
+///
+/// For example if we are developing an attribute macro that is intended to be
+/// invoked on function items as follows:
+///
+/// ```rust
+/// # const IGNORE: &str = stringify! {
+/// #[my_attribute(path = "/v1/refresh")]
+/// # };
+/// pub fn refresh() {
+/// /* ... */
+/// }
+/// ```
+///
+/// The implementation of this macro would want to parse its attribute arguments
+/// as type `AttributeArgs`.
+///
+/// ```rust
+/// #[macro_use]
+/// extern crate syn;
+///
+/// extern crate proc_macro;
+///
+/// use proc_macro::TokenStream;
+/// use syn::{AttributeArgs, ItemFn};
+///
+/// # const IGNORE: &str = stringify! {
+/// #[proc_macro_attribute]
+/// # };
+/// pub fn my_attribute(args: TokenStream, input: TokenStream) -> TokenStream {
+/// let args = parse_macro_input!(args as AttributeArgs);
+/// let input = parse_macro_input!(input as ItemFn);
+///
+/// /* ... */
+/// # "".parse().unwrap()
+/// }
+/// #
+/// # fn main() {}
+/// ```
+pub type AttributeArgs = Vec<NestedMeta>;
+
pub trait FilterAttrs<'a> {
type Ret: Iterator<Item = &'a Attribute>;
@@ -476,7 +553,8 @@
pub mod parsing {
use super::*;
- use parse::{ParseStream, Result};
+ use ext::IdentExt;
+ use parse::{Parse, ParseStream, Result};
#[cfg(feature = "full")]
use private;
@@ -510,6 +588,68 @@
attrs
}
}
+
+ impl Parse for Meta {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ident = input.call(Ident::parse_any)?;
+ parse_meta_after_ident(ident, input)
+ }
+ }
+
+ impl Parse for MetaList {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ident = input.call(Ident::parse_any)?;
+ parse_meta_list_after_ident(ident, input)
+ }
+ }
+
+ impl Parse for MetaNameValue {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ident = input.call(Ident::parse_any)?;
+ parse_meta_name_value_after_ident(ident, input)
+ }
+ }
+
+ impl Parse for NestedMeta {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ahead = input.fork();
+
+ if ahead.peek(Lit) {
+ input.parse().map(NestedMeta::Literal)
+ } else if ahead.call(Ident::parse_any).is_ok() {
+ input.parse().map(NestedMeta::Meta)
+ } else {
+ Err(input.error("expected identifier or literal"))
+ }
+ }
+ }
+
+ pub fn parse_meta_after_ident(ident: Ident, input: ParseStream) -> Result<Meta> {
+ if input.peek(token::Paren) {
+ parse_meta_list_after_ident(ident, input).map(Meta::List)
+ } else if input.peek(Token![=]) {
+ parse_meta_name_value_after_ident(ident, input).map(Meta::NameValue)
+ } else {
+ Ok(Meta::Word(ident))
+ }
+ }
+
+ fn parse_meta_list_after_ident(ident: Ident, input: ParseStream) -> Result<MetaList> {
+ let content;
+ Ok(MetaList {
+ ident: ident,
+ paren_token: parenthesized!(content in input),
+ nested: content.parse_terminated(NestedMeta::parse)?,
+ })
+ }
+
+ fn parse_meta_name_value_after_ident(ident: Ident, input: ParseStream) -> Result<MetaNameValue> {
+ Ok(MetaNameValue {
+ ident: ident,
+ eq_token: input.parse()?,
+ lit: input.parse()?,
+ })
+ }
}
#[cfg(feature = "printing")]
diff --git a/src/lib.rs b/src/lib.rs
index dc07fea..ae290a5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -327,7 +327,7 @@
#[cfg(any(feature = "full", feature = "derive"))]
mod attr;
#[cfg(any(feature = "full", feature = "derive"))]
-pub use attr::{AttrStyle, Attribute, Meta, MetaList, MetaNameValue, NestedMeta};
+pub use attr::{AttrStyle, Attribute, AttributeArgs, Meta, MetaList, MetaNameValue, NestedMeta};
#[cfg(any(feature = "full", feature = "derive"))]
mod data;
@@ -448,6 +448,15 @@
#[doc(hidden)]
pub mod parse_quote;
+// Not public API except the `parse_macro_input!` macro.
+#[cfg(all(
+ not(all(target_arch = "wasm32", target_os = "unknown")),
+ feature = "parsing",
+ feature = "proc-macro"
+))]
+#[doc(hidden)]
+pub mod parse_macro_input;
+
#[cfg(all(feature = "parsing", feature = "printing"))]
pub mod spanned;
@@ -765,58 +774,3 @@
file.shebang = shebang;
Ok(file)
}
-
-/// Parse the input TokenStream of a macro, triggering a compile error if the
-/// tokens fail to parse.
-///
-/// Refer to the [`parse` module] documentation for more details about parsing
-/// in Syn.
-///
-/// [`parse` module]: parse/index.html
-///
-/// # Intended usage
-///
-/// ```rust
-/// #[macro_use]
-/// extern crate syn;
-///
-/// extern crate proc_macro;
-///
-/// use proc_macro::TokenStream;
-/// use syn::parse::{Parse, ParseStream, Result};
-///
-/// struct MyMacroInput {
-/// /* ... */
-/// }
-///
-/// impl Parse for MyMacroInput {
-/// fn parse(input: ParseStream) -> Result<Self> {
-/// /* ... */
-/// # Ok(MyMacroInput {})
-/// }
-/// }
-///
-/// # const IGNORE: &str = stringify! {
-/// #[proc_macro]
-/// # };
-/// pub fn my_macro(tokens: TokenStream) -> TokenStream {
-/// let input = parse_macro_input!(tokens as MyMacroInput);
-///
-/// /* ... */
-/// # "".parse().unwrap()
-/// }
-/// #
-/// # fn main() {}
-/// ```
-#[cfg(feature = "proc-macro")]
-#[macro_export]
-macro_rules! parse_macro_input {
- ($tokenstream:ident as $ty:ty) => {
- match $crate::parse::<$ty>($tokenstream) {
- $crate::export::Ok(data) => data,
- $crate::export::Err(err) => {
- return $crate::export::TokenStream::from(err.to_compile_error());
- }
- };
- };
-}
diff --git a/src/parse_macro_input.rs b/src/parse_macro_input.rs
new file mode 100644
index 0000000..3c26787
--- /dev/null
+++ b/src/parse_macro_input.rs
@@ -0,0 +1,102 @@
+/// Parse the input TokenStream of a macro, triggering a compile error if the
+/// tokens fail to parse.
+///
+/// Refer to the [`parse` module] documentation for more details about parsing
+/// in Syn.
+///
+/// [`parse` module]: parse/index.html
+///
+/// # Intended usage
+///
+/// ```rust
+/// #[macro_use]
+/// extern crate syn;
+///
+/// extern crate proc_macro;
+///
+/// use proc_macro::TokenStream;
+/// use syn::parse::{Parse, ParseStream, Result};
+///
+/// struct MyMacroInput {
+/// /* ... */
+/// }
+///
+/// impl Parse for MyMacroInput {
+/// fn parse(input: ParseStream) -> Result<Self> {
+/// /* ... */
+/// # Ok(MyMacroInput {})
+/// }
+/// }
+///
+/// # const IGNORE: &str = stringify! {
+/// #[proc_macro]
+/// # };
+/// pub fn my_macro(tokens: TokenStream) -> TokenStream {
+/// let input = parse_macro_input!(tokens as MyMacroInput);
+///
+/// /* ... */
+/// # "".parse().unwrap()
+/// }
+/// #
+/// # fn main() {}
+/// ```
+#[macro_export]
+macro_rules! parse_macro_input {
+ ($tokenstream:ident as $ty:ty) => {
+ match $crate::parse_macro_input::parse::<$ty>($tokenstream) {
+ $crate::export::Ok(data) => data,
+ $crate::export::Err(err) => {
+ return $crate::export::TokenStream::from(err.to_compile_error());
+ }
+ };
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Can parse any type that implements Parse.
+
+use parse::{Parse, ParseStream, Parser, Result};
+use proc_macro::TokenStream;
+
+// Not public API.
+#[doc(hidden)]
+pub fn parse<T: ParseMacroInput>(token_stream: TokenStream) -> Result<T> {
+ T::parse.parse(token_stream)
+}
+
+// Not public API.
+#[doc(hidden)]
+pub trait ParseMacroInput: Sized {
+ fn parse(input: ParseStream) -> Result<Self>;
+}
+
+impl<T: Parse> ParseMacroInput for T {
+ fn parse(input: ParseStream) -> Result<Self> {
+ <T as Parse>::parse(input)
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Any other types that we want `parse_macro_input!` to be able to parse.
+
+use AttributeArgs;
+
+impl ParseMacroInput for AttributeArgs {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let mut metas = Vec::new();
+
+ loop {
+ if input.is_empty() {
+ break;
+ }
+ let value = input.parse()?;
+ metas.push(value);
+ if input.is_empty() {
+ break;
+ }
+ input.parse::<Token![,]>()?;
+ }
+
+ Ok(metas)
+ }
+}
diff --git a/tests/test_meta_item.rs b/tests/test_meta_item.rs
index f3c75a9..30b06cc 100644
--- a/tests/test_meta_item.rs
+++ b/tests/test_meta_item.rs
@@ -10,6 +10,7 @@
extern crate proc_macro2;
extern crate syn;
+extern crate quote;
use proc_macro2::{Ident, Literal, Span};
use syn::parse::Parser;
@@ -187,9 +188,126 @@
)
}
+#[test]
+fn test_parse_meta_item_word() {
+ let raw = "hello";
+
+ let expected = Meta::Word(ident("hello"));
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+}
+
+#[test]
+fn test_parse_meta_name_value() {
+ let raw = "foo = 5";
+
+ let expected = MetaNameValue {
+ ident: ident("foo").into(),
+ eq_token: Default::default(),
+ lit: lit(Literal::i32_unsuffixed(5)),
+ };
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+
+ let expected = Meta::NameValue(expected);
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+}
+
+#[test]
+fn test_parse_meta_item_list_lit() {
+ let raw = "foo(5)";
+
+ let expected = MetaList {
+ ident: ident("foo").into(),
+ paren_token: Default::default(),
+ nested: punctuated![NestedMeta::Literal(lit(Literal::i32_unsuffixed(5)))],
+ };
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+
+ let expected = Meta::List(expected);
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+}
+
+#[test]
+fn test_parse_meta_item_multiple() {
+ let raw = "foo(word, name = 5, list(name2 = 6), word2)";
+
+ let expected = MetaList {
+ ident: ident("foo").into(),
+ paren_token: Default::default(),
+ nested: punctuated![
+ NestedMeta::Meta(Meta::Word(ident("word").into())),
+ NestedMeta::Meta(
+ MetaNameValue {
+ ident: ident("name").into(),
+ eq_token: Default::default(),
+ lit: lit(Literal::i32_unsuffixed(5)),
+ }
+ .into(),
+ ),
+ NestedMeta::Meta(
+ MetaList {
+ ident: ident("list").into(),
+ paren_token: Default::default(),
+ nested: punctuated![NestedMeta::Meta(
+ MetaNameValue {
+ ident: ident("name2").into(),
+ eq_token: Default::default(),
+ lit: lit(Literal::i32_unsuffixed(6)),
+ }
+ .into(),
+ )],
+ }
+ .into(),
+ ),
+ NestedMeta::Meta(Meta::Word(ident("word2").into())),
+ ],
+ };
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+
+ let expected = Meta::List(expected);
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+}
+
+#[test]
+fn test_parse_nested_meta() {
+ let raw = "5";
+
+ let expected = NestedMeta::Literal(lit(Literal::i32_unsuffixed(5)));
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+
+ let raw = "list(name2 = 6)";
+
+ let expected = NestedMeta::Meta(
+ MetaList {
+ ident: ident("list").into(),
+ paren_token: Default::default(),
+ nested: punctuated![NestedMeta::Meta(
+ MetaNameValue {
+ ident: ident("name2").into(),
+ eq_token: Default::default(),
+ lit: lit(Literal::i32_unsuffixed(6)),
+ }
+ .into(),
+ )],
+ }
+ .into(),
+ );
+
+ assert_eq!(expected, syn::parse_str(raw).unwrap());
+}
+
fn run_test<T: Into<Meta>>(input: &str, expected: T) {
let attrs = Attribute::parse_outer.parse_str(input).unwrap();
assert_eq!(attrs.len(), 1);
let attr = attrs.into_iter().next().unwrap();
- assert_eq!(expected.into(), attr.interpret_meta().unwrap());
+ let expected = expected.into();
+ assert_eq!(expected, attr.interpret_meta().unwrap());
+ assert_eq!(expected, attr.parse_meta().unwrap());
}