瀏覽代碼

Improve compile errors for async commands without `Result` return type (#6124)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Pascal Sommer 2 年之前
父節點
當前提交
d68a25e32e
共有 2 個文件被更改,包括 65 次插入1 次删除
  1. 5 0
      .changes/improve-async-cmd-error-message.md
  2. 60 1
      core/tauri-macros/src/command/wrapper.rs

+ 5 - 0
.changes/improve-async-cmd-error-message.md

@@ -0,0 +1,5 @@
+---
+"tauri-macros": 'patch:enhance'
+---
+
+Improve compiler error message when generating an async command that has a reference input and don't return a Result.

+ 60 - 1
core/tauri-macros/src/command/wrapper.rs

@@ -5,7 +5,7 @@
 use heck::{ToLowerCamelCase, ToSnakeCase};
 use proc_macro::TokenStream;
 use proc_macro2::TokenStream as TokenStream2;
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, quote_spanned};
 use syn::{
   ext::IdentExt,
   parse::{Parse, ParseStream},
@@ -103,6 +103,63 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
     resolver: format_ident!("__tauri_resolver__"),
   };
 
+  // Tauri currently doesn't support async commands that take a reference as input and don't return
+  // a result. See: https://github.com/tauri-apps/tauri/issues/2533
+  //
+  // For now, we provide an informative error message to the user in that case. Once #2533 is
+  // resolved, this check can be removed.
+  let mut async_command_check = TokenStream2::new();
+  if function.sig.asyncness.is_some() {
+    // This check won't catch all possible problems but it should catch the most common ones.
+    let mut ref_argument_span = None;
+
+    for arg in &function.sig.inputs {
+      if let syn::FnArg::Typed(pat) = arg {
+        match &*pat.ty {
+          syn::Type::Reference(_) => {
+            ref_argument_span = Some(pat.span());
+          }
+          syn::Type::Path(path) => {
+            // Check if the type contains a lifetime argument
+            let last = path.path.segments.last().unwrap();
+            if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
+              if args
+                .args
+                .iter()
+                .any(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
+              {
+                ref_argument_span = Some(pat.span());
+              }
+            }
+          }
+          _ => {}
+        }
+
+        if let Some(span) = ref_argument_span {
+          if let syn::ReturnType::Type(_, return_type) = &function.sig.output {
+            // To check if the return type is `Result` we require it to check a trait that is
+            // only implemented by `Result`. That way we don't exclude renamed result types
+            // which we wouldn't otherwise be able to detect purely from the token stream.
+            // The "error message" displayed to the user is simply the trait name.
+            async_command_check = quote_spanned! {return_type.span() =>
+              #[allow(unreachable_code, clippy::diverging_sub_expression)]
+              const _: () = if false {
+                trait AsyncCommandMustReturnResult {}
+                impl<A, B> AsyncCommandMustReturnResult for Result<A, B> {}
+                let _check: #return_type = unreachable!();
+                let _: &dyn AsyncCommandMustReturnResult = &_check;
+              };
+            };
+          } else {
+            return quote_spanned! {
+              span => compile_error!("async commands that contain references as inputs must return a `Result`");
+            }.into();
+          }
+        }
+      }
+    }
+  }
+
   // body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
   let body = syn::parse::<WrapperAttributes>(attributes)
     .map(|mut attrs| {
@@ -121,6 +178,8 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
 
   // Rely on rust 2018 edition to allow importing a macro from a path.
   quote!(
+    #async_command_check
+
     #function
 
     #maybe_macro_export