Repository: dtolnay/paste (rust)

Issue:
Support paste in cfg feature context
I wonder if there is a way to express the following macro:

```rust
#[macro_export]
macro_rules! checked_feature {
    ($feature:ident, $current_protocol_version:expr) => {{
        paste! {
            #[cfg(feature = "protocol_feature_" $feature:snake)]
            let is_feature_enabled = PROTOCOL_FEATURES_TO_VERSION_MAPPING[&ProtocolFeature::$feature] >= $current_protocol_version;
            #[cfg(not(feature = "protocol_feature_" $feature:snake))]
            let is_feature_enabled = false;
            is_feature_enabled
        }
    }};
}
```

This gives me quite a cryptic message when use:

```rust
let q = checked_feature!(ForwardChunkParts, _protocol_version);
```

```
error: expected `,`, found `ForwardChunkParts`
   --> chain/chunks/src/lib.rs:654:34
    |
654 |                 checked_feature!(ForwardChunkParts, _protocol_version);
    |                 -----------------^^^^^^^^^^^^^^^^^--------------------
    |                 |                |
    |                 |                unexpected token
    |                 expected `,`
```

I have tried various combinations of `stringify!` and it seems that Rust just does not like anything but plain string literal there. I assume that the `#[doc]` support might be quite close to what I want here for the features.

Even if I use `#[cfg(feature = stringify!($feature))]` I get:

```
error: expected unsuffixed literal or identifier, found `stringify`
```

Pull request description:
Support paste in arbitrary name-value attributes
Fixes #56.

Patch:
diff --git a/src/attr.rs b/src/attr.rs
new file mode 100644
index 0000000..6ad0526
--- /dev/null
+++ b/src/attr.rs
@@ -0,0 +1,142 @@
+use crate::error::Result;
+use crate::segment::{self, Segment};
+use proc_macro::{Delimiter, Group, Span, TokenStream, TokenTree};
+use std::iter;
+use std::mem;
+use std::str::FromStr;
+
+pub fn expand_attr(
+    attr: TokenStream,
+    span: Span,
+    contains_paste: &mut bool,
+) -> Result<TokenStream> {
+    let mut tokens = attr.clone().into_iter();
+    match tokens.next() {
+        Some(TokenTree::Ident(..)) => {}
+        _ => return Ok(attr),
+    }
+
+    let group = match tokens.next() {
+        Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => {
+            let mut count = 0;
+            if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) {
+                if count > 1 {
+                    *contains_paste = true;
+                    return do_paste_name_value_attr(attr, span);
+                }
+            }
+            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);
+    }
+
+    let mut group_contains_paste = false;
+    let mut expanded = TokenStream::new();
+    let mut nested_attr = TokenStream::new();
+    for tt in group.stream().into_iter() {
+        match &tt {
+            TokenTree::Punct(punct) if punct.as_char() == ',' => {
+                expanded.extend(expand_attr(
+                    nested_attr,
+                    group.span(),
+                    &mut group_contains_paste,
+                )?);
+                expanded.extend(iter::once(tt));
+                nested_attr = TokenStream::new();
+            }
+            _ => nested_attr.extend(iter::once(tt)),
+        }
+    }
+
+    if !nested_attr.is_empty() {
+        expanded.extend(expand_attr(
+            nested_attr,
+            group.span(),
+            &mut group_contains_paste,
+        )?);
+    }
+
+    if group_contains_paste {
+        *contains_paste = true;
+        let mut group = Group::new(Delimiter::Parenthesis, expanded);
+        group.set_span(span);
+        Ok(attr
+            .into_iter()
+            // Just keep the initial ident in `#[ident(...)]`.
+            .take(1)
+            .chain(iter::once(TokenTree::Group(group)))
+            .collect())
+    } else {
+        Ok(attr)
+    }
+}
+
+fn do_paste_name_value_attr(attr: TokenStream, span: Span) -> Result<TokenStream> {
+    let mut expanded = TokenStream::new();
+    let mut tokens = attr.into_iter().peekable();
+    expanded.extend(tokens.by_ref().take(2)); // `doc =`
+
+    let mut segments = segment::parse(&mut tokens)?;
+
+    for segment in &mut segments {
+        if let Segment::String(string) = segment {
+            if let Some(open_quote) = string.value.find('"') {
+                if open_quote == 0 {
+                    string.value.truncate(string.value.len() - 1);
+                    string.value.remove(0);
+                } else {
+                    let begin = open_quote + 1;
+                    let end = string.value.rfind('"').unwrap();
+                    let raw_string = mem::replace(&mut string.value, String::new());
+                    for ch in raw_string[begin..end].chars() {
+                        string.value.extend(ch.escape_default());
+                    }
+                }
+            }
+        }
+    }
+
+    let mut lit = segment::paste(&segments)?;
+    lit.insert(0, '"');
+    lit.push('"');
+
+    let mut lit = TokenStream::from_str(&lit)
+        .unwrap()
+        .into_iter()
+        .next()
+        .unwrap();
+    lit.set_span(span);
+    expanded.extend(iter::once(lit));
+    Ok(expanded)
+}
+
+fn is_stringlike(token: &TokenTree) -> bool {
+    match token {
+        TokenTree::Ident(_) => true,
+        TokenTree::Literal(literal) => {
+            let repr = literal.to_string();
+            !repr.starts_with('b') && !repr.starts_with('\'')
+        }
+        TokenTree::Group(group) => {
+            if group.delimiter() != Delimiter::None {
+                return false;
+            }
+            let mut inner = group.stream().into_iter();
+            match inner.next() {
+                Some(first) => inner.next().is_none() && is_stringlike(&first),
+                None => false,
+            }
+        }
+        TokenTree::Punct(punct) => punct.as_char() == '\'' || punct.as_char() == ':',
+    }
+}
diff --git a/src/doc.rs b/src/doc.rs
deleted file mode 100644
index 5fa2ad9..0000000
--- a/src/doc.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use crate::error::Result;
-use crate::segment::{self, Segment};
-use proc_macro::{Delimiter, Span, TokenStream, TokenTree};
-use std::iter;
-use std::mem;
-use std::str::FromStr;
-
-pub fn is_pasted_doc(input: &TokenStream) -> bool {
-    #[derive(PartialEq)]
-    enum State {
-        Init,
-        Doc,
-        Equal,
-        First,
-        Rest,
-    }
-
-    let mut state = State::Init;
-    for tt in input.clone() {
-        state = match (state, &tt) {
-            (State::Init, TokenTree::Ident(ident)) if ident.to_string() == "doc" => State::Doc,
-            (State::Doc, TokenTree::Punct(punct)) if punct.as_char() == '=' => State::Equal,
-            (State::Equal, tt) if is_stringlike(tt) => State::First,
-            (State::First, tt) | (State::Rest, tt) if is_stringlike(tt) => State::Rest,
-            _ => return false,
-        };
-    }
-
-    state == State::Rest
-}
-
-pub fn do_paste_doc(attr: &TokenStream, span: Span) -> Result<TokenStream> {
-    let mut expanded = TokenStream::new();
-    let mut tokens = attr.clone().into_iter().peekable();
-    expanded.extend(tokens.by_ref().take(2)); // `doc =`
-
-    let mut segments = segment::parse(&mut tokens)?;
-
-    for segment in &mut segments {
-        if let Segment::String(string) = segment {
-            if let Some(open_quote) = string.value.find('"') {
-                if open_quote == 0 {
-                    string.value.truncate(string.value.len() - 1);
-                    string.value.remove(0);
-                } else {
-                    let begin = open_quote + 1;
-                    let end = string.value.rfind('"').unwrap();
-                    let raw_string = mem::replace(&mut string.value, String::new());
-                    for ch in raw_string[begin..end].chars() {
-                        string.value.extend(ch.escape_default());
-                    }
-                }
-            }
-        }
-    }
-
-    let mut lit = segment::paste(&segments)?;
-    lit.insert(0, '"');
-    lit.push('"');
-
-    let mut lit = TokenStream::from_str(&lit)
-        .unwrap()
-        .into_iter()
-        .next()
-        .unwrap();
-    lit.set_span(span);
-    expanded.extend(iter::once(lit));
-    Ok(expanded)
-}
-
-fn is_stringlike(token: &TokenTree) -> bool {
-    match token {
-        TokenTree::Ident(_) => true,
-        TokenTree::Literal(literal) => {
-            let repr = literal.to_string();
-            !repr.starts_with('b') && !repr.starts_with('\'')
-        }
-        TokenTree::Group(group) => {
-            if group.delimiter() != Delimiter::None {
-                return false;
-            }
-            let mut inner = group.stream().into_iter();
-            match inner.next() {
-                Some(first) => inner.next().is_none() && is_stringlike(&first),
-                None => false,
-            }
-        }
-        TokenTree::Punct(punct) => punct.as_char() == '\'' || punct.as_char() == ':',
-    }
-}
diff --git a/src/lib.rs b/src/lib.rs
index e342a33..01d6070 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -140,11 +140,11 @@
 
 extern crate proc_macro;
 
-mod doc;
+mod attr;
 mod error;
 mod segment;
 
-use crate::doc::{do_paste_doc, is_pasted_doc};
+use crate::attr::expand_attr;
 use crate::error::{Error, Result};
 use crate::segment::Segment;
 use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
@@ -206,18 +206,17 @@ fn expand(input: TokenStream, contains_paste: &mut bool) -> Result<TokenStream>
                 } else if delimiter == Delimiter::None && is_flat_group(&content) {
                     expanded.extend(content);
                     *contains_paste = true;
-                } else if delimiter == Delimiter::Bracket
-                    && (lookbehind == Lookbehind::Pound || lookbehind == Lookbehind::PoundBang)
-                    && is_pasted_doc(&content)
-                {
-                    let pasted = do_paste_doc(&content, span)?;
-                    let mut group = Group::new(delimiter, pasted);
-                    group.set_span(span);
-                    expanded.extend(iter::once(TokenTree::Group(group)));
-                    *contains_paste = true;
                 } else {
                     let mut group_contains_paste = false;
-                    let nested = expand(content, &mut group_contains_paste)?;
+                    let nested = match delimiter {
+                        Delimiter::Bracket if lookbehind == Lookbehind::Pound => {
+                            expand_attr(content, span, &mut group_contains_paste)?
+                        }
+                        Delimiter::Bracket if lookbehind == Lookbehind::PoundBang => {
+                            expand_attr(content, span, &mut group_contains_paste)?
+                        }
+                        _ => expand(content, &mut group_contains_paste)?,
+                    };
                     let group = if group_contains_paste {
                         let mut group = Group::new(delimiter, nested);
                         group.set_span(span);


Test patch:
diff --git a/tests/test_attr.rs b/tests/test_attr.rs
new file mode 100644
index 0000000..fd67e2a
--- /dev/null
+++ b/tests/test_attr.rs
@@ -0,0 +1,22 @@
+use paste::paste;
+
+#[test]
+fn test_paste_cfg() {
+    macro_rules! m {
+        ($ret:ident, $width:expr) => {
+            paste! {
+                #[cfg(any(feature = "protocol_feature_" $ret:snake, target_pointer_width = "" $width))]
+                fn new() -> $ret { todo!() }
+            }
+        };
+    }
+
+    struct Paste;
+
+    #[cfg(target_pointer_width = "64")]
+    m!(Paste, 64);
+    #[cfg(target_pointer_width = "32")]
+    m!(Paste, 32);
+
+    let _ = new;
+}