Repository: dtolnay/paste
Pull request #59 (2020-12-10 01:13:57)

Handle attribute with non-ident path
#57 only applied to attributes whose path is a single ident, as in `#[attr(...)]`. This PR extends to work inside of `#[path::to::attr(...)]` as well.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index e0b2223..63cb4c6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ readme = "README.md"
 proc-macro = true
 
 [dev-dependencies]
+paste-test-suite = { version = "0", path = "tests/macros" }
 rustversion = "1.0"
 trybuild = "1.0"
 
diff --git a/src/attr.rs b/src/attr.rs
index 2d23730..be64a79 100644
--- a/src/attr.rs
+++ b/src/attr.rs
@@ -11,28 +11,50 @@ pub fn expand_attr(
     contains_paste: &mut bool,
 ) -> Result<TokenStream> {
     let mut tokens = attr.clone().into_iter();
-    match tokens.next() {
-        Some(TokenTree::Ident(..)) => {}
-        _ => return Ok(attr),
-    }
+    let mut leading_colons = 0; // $(::)?
+    let mut leading_path = 0; // $($ident)::+
 
-    let group = match tokens.next() {
-        Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
-            let mut count = 0;
-            if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) && count > 1 {
-                *contains_paste = true;
-                return do_paste_name_value_attr(attr, span);
+    let mut token;
+    let group = loop {
+        token = tokens.next();
+        match token {
+            // colon after `$(:)?`
+            Some(TokenTree::Punct(ref punct))
+                if punct.as_char() == ':' && leading_colons < 2 && leading_path == 0 =>
+            {
+                leading_colons += 1;
+            }
+            // ident after `$(::)? $($ident ::)*`
+            Some(TokenTree::Ident(_)) if leading_colons != 1 && leading_path % 3 == 0 => {
+                leading_path += 1;
+            }
+            // colon after `$(::)? $($ident ::)* $ident $(:)?`
+            Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' && leading_path % 3 > 0 => {
+                leading_path += 1;
+            }
+            // eq+value after `$(::)? $($ident)::+`
+            Some(TokenTree::Punct(ref punct))
+                if punct.as_char() == '=' && leading_path % 3 == 1 =>
+            {
+                let mut count = 0;
+                if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) && count > 1 {
+                    *contains_paste = true;
+                    let leading = leading_colons + leading_path;
+                    return do_paste_name_value_attr(attr, span, leading);
+                }
+                return Ok(attr);
             }
-            return Ok(attr);
+            // parens after `$(::)? $($ident)::+`
+            Some(TokenTree::Group(ref group))
+                if group.delimiter() == Delimiter::Parenthesis && leading_path % 3 == 1 =>
+            {
+                break group;
+            }
+            // bail out
+            _ => return Ok(attr),
         }
-        Some(TokenTree::Group(group)) => group,
-        _ => return Ok(attr),
     };
 
-    if group.delimiter() != Delimiter::Parenthesis {
-        return Ok(attr);
-    }
-
     // There can't be anything else after the first group in a valid attribute.
     if tokens.next().is_some() {
         return Ok(attr);
@@ -71,7 +93,7 @@ pub fn expand_attr(
         Ok(attr
             .into_iter()
             // Just keep the initial ident in `#[ident(...)]`.
-            .take(1)
+            .take(leading_colons + leading_path)
             .chain(iter::once(TokenTree::Group(group)))
             .collect())
     } else {
@@ -79,10 +101,10 @@ pub fn expand_attr(
     }
 }
 
-fn do_paste_name_value_attr(attr: TokenStream, span: Span) -> Result<TokenStream> {
+fn do_paste_name_value_attr(attr: TokenStream, span: Span, leading: usize) -> Result<TokenStream> {
     let mut expanded = TokenStream::new();
     let mut tokens = attr.into_iter().peekable();
-    expanded.extend(tokens.by_ref().take(2)); // `doc =`
+    expanded.extend(tokens.by_ref().take(leading + 1)); // `doc =`
 
     let mut segments = segment::parse(&mut tokens)?;
 


Test patch:
diff --git a/tests/macros/Cargo.toml b/tests/macros/Cargo.toml
new file mode 100644
index 0000000..ba2ce2d
--- /dev/null
+++ b/tests/macros/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "paste-test-suite"
+version = "0.0.0"
+edition = "2018"
+publish = false
+
+[lib]
+path = "lib.rs"
+proc-macro = true
diff --git a/tests/macros/lib.rs b/tests/macros/lib.rs
new file mode 100644
index 0000000..b49a5b4
--- /dev/null
+++ b/tests/macros/lib.rs
@@ -0,0 +1,25 @@
+extern crate proc_macro;
+
+use proc_macro::{TokenStream, TokenTree};
+
+#[proc_macro_attribute]
+pub fn paste_test(args: TokenStream, input: TokenStream) -> TokenStream {
+    let mut iter = args.clone().into_iter();
+    match iter.next() {
+        Some(TokenTree::Ident(_)) => {}
+        _ => panic!("{}", args),
+    }
+    match iter.next() {
+        Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {}
+        _ => panic!("{}", args),
+    }
+    match iter.next() {
+        Some(TokenTree::Literal(ref literal)) if literal.to_string().starts_with('"') => {}
+        _ => panic!("{}", args),
+    }
+    match iter.next() {
+        None => {}
+        _ => panic!("{}", args),
+    }
+    input
+}
diff --git a/tests/test_attr.rs b/tests/test_attr.rs
index fd67e2a..01abbfe 100644
--- a/tests/test_attr.rs
+++ b/tests/test_attr.rs
@@ -1,4 +1,23 @@
 use paste::paste;
+use paste_test_suite::paste_test;
+
+#[test]
+fn test_attr() {
+    paste! {
+        #[paste_test(k = "val" "ue")]
+        struct A;
+
+        #[paste_test_suite::paste_test(k = "val" "ue")]
+        struct B;
+
+        #[::paste_test_suite::paste_test(k = "val" "ue")]
+        struct C;
+    }
+
+    let _ = A;
+    let _ = B;
+    let _ = C;
+}
 
 #[test]
 fn test_paste_cfg() {