Repository: dtolnay/thiserror
Pull request #350 (2024-11-04 21:36:59)

Add test of r#source that is not Error::source
Without `r#source`:

```console
error[E0599]: the method `as_dyn_error` exists for type `char`, but its trait bounds were not satisfied
  --> tests/test_source.rs:72:9
   |
72 |         source: char,
   |         ^^^^^^ method cannot be called on `char` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `char: std::error::Error`
           which is required by `char: AsDynError<'_>`
```


Patch:
diff --git a/impl/src/fmt.rs b/impl/src/fmt.rs
index aa69986..3d1394c 100644
--- a/impl/src/fmt.rs
+++ b/impl/src/fmt.rs
@@ -1,7 +1,6 @@
 use crate::ast::Field;
 use crate::attr::{Display, Trait};
 use crate::scan_expr::scan_expr;
-use crate::unraw::IdentUnraw;
 use proc_macro2::{TokenStream, TokenTree};
 use quote::{format_ident, quote, quote_spanned};
 use std::collections::{BTreeSet as Set, HashMap as Map};
@@ -58,13 +57,14 @@ impl Display<'_> {
                         Err(_) => return,
                     };
                     if !member_index.contains_key(&member) {
-                        out += int;
+                        out += &int;
                         continue;
                     }
                     member
                 }
                 'a'..='z' | 'A'..='Z' | '_' => {
-                    let ident = Ident::new(take_ident(&mut read), span);
+                    let mut ident = take_ident(&mut read);
+                    ident.set_span(span);
                     Member::Named(ident)
                 }
                 _ => continue,
@@ -87,10 +87,14 @@ impl Display<'_> {
                 };
                 implied_bounds.insert((field, bound));
             }
-            let formatvar = IdentUnraw::new(match &member {
+            let local = match &member {
                 Member::Unnamed(index) => format_ident!("_{}", index),
                 Member::Named(ident) => ident.clone(),
-            });
+            };
+            let mut formatvar = local.clone();
+            if formatvar.to_string().starts_with("r#") {
+                formatvar = format_ident!("r_{}", formatvar);
+            }
             out += &formatvar.to_string();
             if !named_args.insert(formatvar.clone()) {
                 // Already specified in the format argument list.
@@ -99,7 +103,6 @@ impl Display<'_> {
             if !has_trailing_comma {
                 args.extend(quote_spanned!(span=> ,));
             }
-            let local = formatvar.to_local();
             args.extend(quote_spanned!(span=> #formatvar = #local));
             if read.starts_with('}') && member_index.contains_key(&member) {
                 has_bonus_display = true;
@@ -117,7 +120,7 @@ impl Display<'_> {
 }
 
 struct FmtArguments {
-    named: Set<IdentUnraw>,
+    named: Set<Ident>,
     unnamed: bool,
 }
 
@@ -155,7 +158,7 @@ fn try_explicit_named_args(input: ParseStream) -> Result<FmtArguments> {
             break;
         }
         if input.peek(Ident::peek_any) && input.peek2(Token![=]) && !input.peek2(Token![==]) {
-            let ident: IdentUnraw = input.parse()?;
+            let ident = input.call(Ident::parse_any)?;
             input.parse::<Token![=]>()?;
             args.named.insert(ident);
         } else {
@@ -187,7 +190,7 @@ fn fallback_explicit_named_args(input: ParseStream) -> Result<FmtArguments> {
             && !input.peek3(Token![==])
         {
             input.parse::<Token![,]>()?;
-            let ident: IdentUnraw = input.parse()?;
+            let ident = input.call(Ident::parse_any)?;
             input.parse::<Token![=]>()?;
             args.named.insert(ident);
         }
@@ -214,28 +217,35 @@ fn is_syn_full() -> bool {
     }
 }
 
-fn take_int<'a>(read: &mut &'a str) -> &'a str {
-    let mut int_len = 0;
-    for ch in read.chars() {
+fn take_int(read: &mut &str) -> String {
+    let mut int = String::new();
+    for (i, ch) in read.char_indices() {
         match ch {
-            '0'..='9' => int_len += 1,
-            _ => break,
+            '0'..='9' => int.push(ch),
+            _ => {
+                *read = &read[i..];
+                break;
+            }
         }
     }
-    let (int, rest) = read.split_at(int_len);
-    *read = rest;
     int
 }
 
-fn take_ident<'a>(read: &mut &'a str) -> &'a str {
-    let mut ident_len = 0;
-    for ch in read.chars() {
+fn take_ident(read: &mut &str) -> Ident {
+    let mut ident = String::new();
+    let raw = read.starts_with("r#");
+    if raw {
+        ident.push_str("r#");
+        *read = &read[2..];
+    }
+    for (i, ch) in read.char_indices() {
         match ch {
-            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident_len += 1,
-            _ => break,
+            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
+            _ => {
+                *read = &read[i..];
+                break;
+            }
         }
     }
-    let (ident, rest) = read.split_at(ident_len);
-    *read = rest;
-    ident
+    Ident::parse_any.parse_str(&ident).unwrap()
 }
diff --git a/impl/src/lib.rs b/impl/src/lib.rs
index a86242d..7d7c6e3 100644
--- a/impl/src/lib.rs
+++ b/impl/src/lib.rs
@@ -26,7 +26,6 @@ mod generics;
 mod prop;
 mod scan_expr;
 mod span;
-mod unraw;
 mod valid;
 
 use proc_macro::TokenStream;
diff --git a/impl/src/unraw.rs b/impl/src/unraw.rs
deleted file mode 100644
index 1fcaf6c..0000000
--- a/impl/src/unraw.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use proc_macro2::{Ident, Span, TokenStream};
-use quote::ToTokens;
-use std::cmp::Ordering;
-use std::fmt::{self, Display};
-use syn::ext::IdentExt as _;
-use syn::parse::{Parse, ParseStream, Result};
-
-#[derive(Clone)]
-#[repr(transparent)]
-pub(crate) struct IdentUnraw(Ident);
-
-impl IdentUnraw {
-    pub fn new(ident: Ident) -> Self {
-        IdentUnraw(ident)
-    }
-
-    pub fn to_local(&self) -> Ident {
-        let unraw = self.0.unraw();
-        let repr = unraw.to_string();
-        if syn::parse_str::<Ident>(&repr).is_err() {
-            if let "_" | "super" | "self" | "Self" | "crate" = repr.as_str() {
-                // Some identifiers are never allowed to appear as raw, like r#self and r#_.
-            } else {
-                return Ident::new_raw(&repr, Span::call_site());
-            }
-        }
-        unraw
-    }
-}
-
-impl Display for IdentUnraw {
-    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-        Display::fmt(&self.0.unraw(), formatter)
-    }
-}
-
-impl Eq for IdentUnraw {}
-
-impl PartialEq for IdentUnraw {
-    fn eq(&self, other: &Self) -> bool {
-        PartialEq::eq(&self.0.unraw(), &other.0.unraw())
-    }
-}
-
-impl Ord for IdentUnraw {
-    fn cmp(&self, other: &Self) -> Ordering {
-        Ord::cmp(&self.0.unraw(), &other.0.unraw())
-    }
-}
-
-impl PartialOrd for IdentUnraw {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(Self::cmp(self, other))
-    }
-}
-
-impl Parse for IdentUnraw {
-    fn parse(input: ParseStream) -> Result<Self> {
-        input.call(Ident::parse_any).map(IdentUnraw::new)
-    }
-}
-
-impl ToTokens for IdentUnraw {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        self.0.unraw().to_tokens(tokens);
-    }
-}


Test patch:
diff --git a/tests/test_display.rs b/tests/test_display.rs
index 27bb72c..89bdc4a 100644
--- a/tests/test_display.rs
+++ b/tests/test_display.rs
@@ -279,7 +279,7 @@ fn test_macro_rules() {
 #[test]
 fn test_raw() {
     #[derive(Error, Debug)]
-    #[error("braced raw error: {fn}")]
+    #[error("braced raw error: {r#fn}")]
     struct Error {
         r#fn: &'static str,
     }
@@ -291,7 +291,7 @@ fn test_raw() {
 fn test_raw_enum() {
     #[derive(Error, Debug)]
     enum Error {
-        #[error("braced raw error: {fn}")]
+        #[error("braced raw error: {r#fn}")]
         Braced { r#fn: &'static str },
     }
 
@@ -299,21 +299,23 @@ fn test_raw_enum() {
 }
 
 #[test]
-fn test_keyword() {
+fn test_raw_conflict() {
     #[derive(Error, Debug)]
-    #[error("error: {type}", type = 1)]
-    struct Error;
+    enum Error {
+        #[error("braced raw error: {r#func}, {func}", func = "U")]
+        Braced { r#func: &'static str },
+    }
 
-    assert("error: 1", Error);
+    assert("braced raw error: T, U", Error::Braced { r#func: "T" });
 }
 
 #[test]
-fn test_self() {
+fn test_keyword() {
     #[derive(Error, Debug)]
-    #[error("error: {self:?}")]
+    #[error("error: {type}", type = 1)]
     struct Error;
 
-    assert("error: Error", Error);
+    assert("error: 1", Error);
 }
 
 #[test]
diff --git a/tests/test_source.rs b/tests/test_source.rs
index 637f4ac..29968be 100644
--- a/tests/test_source.rs
+++ b/tests/test_source.rs
@@ -63,3 +63,20 @@ error_from_macro! {
     #[error("Something")]
     Variant(#[from] io::Error)
 }
+
+#[test]
+fn test_not_source() {
+    #[derive(Error, Debug)]
+    #[error("{source} ==> {destination}")]
+    pub struct NotSource {
+        r#source: char,
+        destination: char,
+    }
+
+    let error = NotSource {
+        source: 'S',
+        destination: 'D',
+    };
+    assert_eq!(error.to_string(), "S ==> D");
+    assert!(error.source().is_none());
+}
diff --git a/tests/ui/display-underscore.rs b/tests/ui/display-underscore.rs
deleted file mode 100644
index 335614b..0000000
--- a/tests/ui/display-underscore.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-#[error("{_}")]
-pub struct Error;
-
-fn main() {}
diff --git a/tests/ui/display-underscore.stderr b/tests/ui/display-underscore.stderr
deleted file mode 100644
index 36882b9..0000000
--- a/tests/ui/display-underscore.stderr
+++ /dev/null
@@ -1,7 +0,0 @@
-error: invalid format string: invalid argument name `_`
- --> tests/ui/display-underscore.rs:4:11
-  |
-4 | #[error("{_}")]
-  |           ^ invalid argument name in format string
-  |
-  = note: argument name cannot be a single underscore
diff --git a/tests/ui/raw-identifier.rs b/tests/ui/raw-identifier.rs
deleted file mode 100644
index e7e66d0..0000000
--- a/tests/ui/raw-identifier.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-#[error("error: {r#fn}")]
-pub struct Error {
-    r#fn: &'static str,
-}
-
-fn main() {
-    let r#fn = "...";
-    let _ = format!("error: {r#fn}");
-}
diff --git a/tests/ui/raw-identifier.stderr b/tests/ui/raw-identifier.stderr
deleted file mode 100644
index a3ce94d..0000000
--- a/tests/ui/raw-identifier.stderr
+++ /dev/null
@@ -1,21 +0,0 @@
-error: invalid format string: raw identifiers are not supported
- --> tests/ui/raw-identifier.rs:4:18
-  |
-4 | #[error("error: {r#fn}")]
-  |                  --^^
-  |                  |
-  |                  raw identifier used here in format string
-  |                  help: remove the `r#`
-  |
-  = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
-
-error: invalid format string: raw identifiers are not supported
-  --> tests/ui/raw-identifier.rs:11:30
-   |
-11 |     let _ = format!("error: {r#fn}");
-   |                              --^^
-   |                              |
-   |                              raw identifier used here in format string
-   |                              help: remove the `r#`
-   |
-   = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`