Repository: dtolnay/cxx (language: rust)

Issue:
Namespaces with raw-escaped reserved Rust words generate invalid C++
<!--  ❤️  -->

Given the following example:

```rust
#[cxx::bridge]
pub(crate) mod ffi {
    unsafe extern "C++" {
        #[namespace = ::my::company::r#box::implementation]
        type MyBox;
    }
}
```

The following code is generated:


```cpp
::std::shared_ptr<:my::company::r#box::implementation> MyBox;
```

```cpp
namespace r#box { ... }
```

These are both invalid and generate the following errors:
```
<SNIP>:16: error: expected '{'        
    namespace r#box {                                                                                             
               ^                                                                                                  
<SNIP>: error: expected unqualified-id                           
<SNIP>: error: no member named 'r' in namespace 'my::company'                                                                   
  ::std::shared_ptr<::my::company::r#box::implementation::MyBox> my_box;       
                    ~~~~~~~~~~~~~~~^                            
3 errors generated.                        
```

Arguably, cxx should strip out the rust-specific `r#` and generate C++ code as if there wasn't an escape from rust to begin with.

Maybe related: #287 


Discussion / hints:
If anyone's running into this issue, you can quote the namespace attribute value without the raw-escape token to allow Rust to permit it as well:

```rust
#[cxx::bridge]
pub(crate) mod ffi {
    unsafe extern "C++" {
        #[namespace = "::my::company::box::implementation"]
        type MyBox;
    }
}
```

As the need to quote is a limitation from Rust rather than cxx.

Fix patch:
diff --git a/macro/src/type_id.rs b/macro/src/type_id.rs
index 74f0c1c92..7bca67b18 100644
--- a/macro/src/type_id.rs
+++ b/macro/src/type_id.rs
@@ -1,6 +1,7 @@
 use crate::syntax::qualified::QualifiedName;
 use proc_macro2::{TokenStream, TokenTree};
 use quote::{format_ident, quote, ToTokens};
+use syn::ext::IdentExt;
 
 pub enum Crate {
     Cxx,
@@ -24,7 +25,7 @@ pub fn expand(krate: Crate, arg: QualifiedName) -> TokenStream {
         if !ids.is_empty() {
             ids.push(quote!(()));
         }
-        for ch in word.to_string().chars() {
+        for ch in word.unraw().to_string().chars() {
             ids.push(match ch {
                 'A'..='Z' | 'a'..='z' => {
                     let t = format_ident!("{}", ch);
diff --git a/syntax/qualified.rs b/syntax/qualified.rs
index 96f07c19e..5f182fa9b 100644
--- a/syntax/qualified.rs
+++ b/syntax/qualified.rs
@@ -1,5 +1,5 @@
 use syn::ext::IdentExt;
-use syn::parse::{ParseStream, Result};
+use syn::parse::{Error, ParseStream, Result};
 use syn::{Ident, LitStr, Token};
 
 pub struct QualifiedName {
@@ -8,21 +8,8 @@ pub struct QualifiedName {
 
 impl QualifiedName {
     pub fn parse_unquoted(input: ParseStream) -> Result<Self> {
-        let mut segments = Vec::new();
-        let mut trailing_punct = true;
-        let leading_colons: Option<Token![::]> = input.parse()?;
-        while trailing_punct && input.peek(Ident::peek_any) {
-            let ident = Ident::parse_any(input)?;
-            segments.push(ident);
-            let colons: Option<Token![::]> = input.parse()?;
-            trailing_punct = colons.is_some();
-        }
-        if segments.is_empty() && leading_colons.is_none() {
-            return Err(input.error("expected path"));
-        } else if trailing_punct {
-            return Err(input.error("expected path segment"));
-        }
-        Ok(QualifiedName { segments })
+        let allow_raw = true;
+        parse_unquoted(input, allow_raw)
     }
 
     pub fn parse_quoted_or_unquoted(input: ParseStream) -> Result<Self> {
@@ -32,10 +19,41 @@ impl QualifiedName {
                 let segments = Vec::new();
                 Ok(QualifiedName { segments })
             } else {
-                lit.parse_with(Self::parse_unquoted)
+                lit.parse_with(|input: ParseStream| {
+                    let allow_raw = false;
+                    parse_unquoted(input, allow_raw)
+                })
             }
         } else {
             Self::parse_unquoted(input)
         }
     }
 }
+
+fn parse_unquoted(input: ParseStream, allow_raw: bool) -> Result<QualifiedName> {
+    let mut segments = Vec::new();
+    let mut trailing_punct = true;
+    let leading_colons: Option<Token![::]> = input.parse()?;
+    while trailing_punct && input.peek(Ident::peek_any) {
+        let mut ident = Ident::parse_any(input)?;
+        if let Some(unraw) = ident.to_string().strip_prefix("r#") {
+            if !allow_raw {
+                let msg = format!(
+                    "raw identifier `{}` is not allowed in a quoted namespace; use `{}`, or remove quotes",
+                    ident, unraw,
+                );
+                return Err(Error::new(ident.span(), msg));
+            }
+            ident = Ident::new(unraw, ident.span());
+        }
+        segments.push(ident);
+        let colons: Option<Token![::]> = input.parse()?;
+        trailing_punct = colons.is_some();
+    }
+    if segments.is_empty() && leading_colons.is_none() {
+        return Err(input.error("expected path"));
+    } else if trailing_punct {
+        return Err(input.error("expected path segment"));
+    }
+    Ok(QualifiedName { segments })
+}


Test patch:
diff --git a/tests/ui/raw_ident_namespace.rs b/tests/ui/raw_ident_namespace.rs
new file mode 100644
index 000000000..2c8286c70
--- /dev/null
+++ b/tests/ui/raw_ident_namespace.rs
@@ -0,0 +1,53 @@
+use cxx::{type_id, ExternType};
+
+#[repr(transparent)]
+pub struct QuotedRaw(usize);
+
+unsafe impl ExternType for QuotedRaw {
+    type Id = type_id!("org::r#box::implementation::QuotedRaw");
+    type Kind = cxx::kind::Trivial;
+}
+
+#[repr(transparent)]
+pub struct QuotedKeyword(usize);
+
+unsafe impl ExternType for QuotedKeyword {
+    type Id = type_id!("org::box::implementation::QuotedKeyword");
+    type Kind = cxx::kind::Trivial;
+}
+
+#[repr(transparent)]
+pub struct UnquotedRaw(usize);
+
+unsafe impl ExternType for UnquotedRaw {
+    type Id = type_id!(org::r#box::implementation::UnquotedRaw);
+    type Kind = cxx::kind::Trivial;
+}
+
+#[repr(transparent)]
+pub struct UnquotedKeyword(usize);
+
+unsafe impl ExternType for UnquotedKeyword {
+    type Id = type_id!(org::box::implementation::UnquotedKeyword);
+    type Kind = cxx::kind::Trivial;
+}
+
+#[cxx::bridge]
+pub mod ffi {
+    extern "C++" {
+        #[namespace = "org::r#box::implementation"]
+        type QuotedRaw = crate::QuotedRaw;
+
+        #[namespace = "org::box::implementation"]
+        type QuotedKeyword = crate::QuotedKeyword;
+
+        #[namespace = org::r#box::implementation]
+        type UnquotedRaw = crate::UnquotedRaw;
+
+        // Not allowed by rustc (independent of cxx):
+        // #[namespace = org::box::implementation]
+        // type UnquotedKeyword = crate::UnquotedKeyword;
+    }
+}
+
+fn main() {}
diff --git a/tests/ui/raw_ident_namespace.stderr b/tests/ui/raw_ident_namespace.stderr
new file mode 100644
index 000000000..86c8b6fd6
--- /dev/null
+++ b/tests/ui/raw_ident_namespace.stderr
@@ -0,0 +1,11 @@
+error: raw identifier `r#box` is not allowed in a quoted namespace; use `box`, or remove quotes
+ --> tests/ui/raw_ident_namespace.rs:7:24
+  |
+7 |     type Id = type_id!("org::r#box::implementation::QuotedRaw");
+  |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: raw identifier `r#box` is not allowed in a quoted namespace; use `box`, or remove quotes
+  --> tests/ui/raw_ident_namespace.rs:38:23
+   |
+38 |         #[namespace = "org::r#box::implementation"]
+   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^