diff --git a/.changes/improve-async-cmd-error-message.md b/.changes/improve-async-cmd-error-message.md new file mode 100644 index 000000000..48bbbb02c --- /dev/null +++ b/.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. diff --git a/core/tauri-macros/src/command/wrapper.rs b/core/tauri-macros/src/command/wrapper.rs index f876fa0d4..008e0b8c7 100644 --- a/core/tauri-macros/src/command/wrapper.rs +++ b/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 AsyncCommandMustReturnResult for Result {} + 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::(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