Repository: autometrics-dev/autometrics-rs (Rust)

Issue:
returning `Result<impl Trait>` from a `#[autometrics]`'d function results in compile error
if one has a function that looks like this:

```rust
#[autometrics]
async fn hello() -> Result<impl ToString, std::io::Error> {
    // ...
}
```

the compiler will fail to compile it with:

```
error[E0562]: `impl Trait` only allowed in function and inherent method return types, not in variable bindings
  --> src/main.rs:11:28
   |
11 | async fn hello() -> Result<impl ToString, std::io::Error> {
   |                            ^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0562`.
error: could not compile `playground` (bin "playground") due to previous error
```

removing the `#[autometrics]` macro ends up making the program compile successfully.

playgrounds:
- smallest example: https://www.rustexplorer.com/b/52t7cy
- with `actix-web`: https://www.rustexplorer.com/b/4vsrgc




Discussion:
looking at what code `autometrics` generates using `cargo expand` reveals the culprit pretty easily:

```rust
let result: std::result::Result<impl ToString, std::io::Error> = autometrics::__private::CALLER
                                ^^^^^^^^^^^^^

```
Yeah, I should have seen that coming.

The fix is "basically" expanding [this match statement](https://github.com/autometrics-dev/autometrics-rs/blob/dd902ac2665cdf0c2685f23ab928c8664c2dca27/autometrics-macros/src/lib.rs#L88-L92) so that when the return type is a generic type, it also checks all the type arguments to make sure there's no impl either

I won't have time to deal with it this week, but you can definitely try to change this match statement if you feel like it, the whole logic to fix that issue lives in that match statement



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b230d83..589134b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,7 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 
--
+- Return types on functions annotated with `#[autometrics]` containing generic 
+  `impl` types in their type arguments (`fn() -> Result<impl ToString, impl std::error::Error>`)
+  no longer fail to compile.
 
 ### Security
 
diff --git a/autometrics-macros/src/lib.rs b/autometrics-macros/src/lib.rs
index f22ff991..cfb39a9f 100644
--- a/autometrics-macros/src/lib.rs
+++ b/autometrics-macros/src/lib.rs
@@ -3,7 +3,10 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
 use proc_macro2::TokenStream;
 use quote::quote;
 use std::env;
-use syn::{parse_macro_input, ImplItem, ItemFn, ItemImpl, Result, ReturnType, Type};
+use syn::{
+    parse_macro_input, GenericArgument, ImplItem, ItemFn, ItemImpl, PathArguments, Result,
+    ReturnType, Type,
+};
 
 mod parse;
 mod result_labels;
@@ -87,8 +90,66 @@ fn instrument_function(args: &AutometricsArgs, item: ItemFn) -> Result<TokenStre
     // `GetLabels` implementor.
     let return_type = match sig.output {
         ReturnType::Default => quote! { : () },
-        ReturnType::Type(_, ref t) if matches!(t.as_ref(), &Type::ImplTrait(_)) => quote! {},
-        ReturnType::Type(_, ref t) => quote! { : #t },
+        ReturnType::Type(_, ref t) => match t.as_ref() {
+            Type::ImplTrait(_) => quote! {},
+            Type::Path(path) => {
+                let mut ts = vec![];
+                let mut first = true;
+
+                for segment in &path.path.segments {
+                    let ident = &segment.ident;
+                    let args = &segment.arguments;
+
+                    // special handling in case the type is angle bracket with a `impl` trait
+                    // in such a case, we would run into the following error
+                    //
+                    // ```
+                    // error[E0562]: `impl Trait` only allowed in function and inherent method return types, not in variable bindings
+                    //   --> src/main.rs:11:28
+                    //    |
+                    // 11 | async fn hello() -> Result<impl ToString, std::io::Error> {
+                    //    |                            ^^^^^^^^^^^^^
+                    // ```
+                    //
+                    // this whole block just re-creates the angle bracketed `<impl ToString, std::io::Error>`
+                    // manually but the trait `impl` replaced with an infer `_`, which fixes this issue
+                    let suffix = match args {
+                        PathArguments::AngleBracketed(brackets) => {
+                            let mut ts = vec![];
+
+                            for args in &brackets.args {
+                                ts.push(match args {
+                                    GenericArgument::Type(ty)
+                                        if matches!(ty, Type::ImplTrait(_)) =>
+                                    {
+                                        quote! { _ }
+                                    }
+                                    generic_arg => quote! { #generic_arg },
+                                });
+                            }
+
+                            quote! { ::<#(#ts),*> }
+                        }
+                        _ => quote! {},
+                    };
+
+                    // primitive way to check whenever this is the first iteration or not
+                    // as on the first iteration, we don't want to prepend `::`,
+                    // as types may be local and/or imported and then couldn't be found
+                    if !first {
+                        ts.push(quote! { :: });
+                    } else {
+                        first = false;
+                    }
+
+                    ts.push(quote! { #ident });
+                    ts.push(quote! { #suffix });
+                }
+
+                quote! { : #(#ts)* }
+            }
+            _ => quote! { : #t },
+        },
     };
 
     // Wrap the body of the original function, using a slightly different approach based on whether the function is async


Test patch:
diff --git a/autometrics/tests/compile_test.rs b/autometrics/tests/compile_test.rs
new file mode 100644
index 00000000..7edb0cbe
--- /dev/null
+++ b/autometrics/tests/compile_test.rs
@@ -0,0 +1,45 @@
+use std::io;
+use autometrics::autometrics;
+
+// general purpose `Result`, part of the std prelude.
+// notice both `Ok` and `Err` generic type arguments are explicitly provided
+#[autometrics]
+fn issue_121_a() -> Result<impl ToString, io::Error> {
+    Ok("a")
+}
+
+// specialized `Result` which is part of std but not part of the std prelude.
+// notice there is only an explicit `Ok` type in the generic args, the `Err` generic argument
+// is type-defined
+#[autometrics]
+fn issue_121_b() -> io::Result<impl ToString> {
+    Ok("b")
+}
+
+// specialized `Result` which is part of a foreign crate
+// notice there is only an explicit `Ok` type in the generic args, the `Err` generic argument
+// is type-defined in the foreign crate
+#[autometrics]
+fn issue_121_c() -> ::http::Result<impl ToString> {
+    // CODE STYLE: please keep return formatted this way (with the leading `::`)
+    Ok("c")
+}
+
+// Result where both `Ok` and `Error` are `impl` types
+#[autometrics]
+fn issue_121_d() -> Result<impl ToString, impl std::error::Error> {
+    if true {
+        Ok("d")
+    } else {
+        Err(io::Error::new(io::ErrorKind::Other, "issue 121d"))
+    }
+}
+
+#[test]
+fn invoke_issue_121() {
+    // we need to handle all three code generation cases
+    issue_121_a().unwrap();
+    issue_121_b().unwrap();
+    issue_121_c().unwrap();
+    issue_121_d().unwrap();
+}