Skip to content

Commit d68a25e

Browse files
Improve compile errors for async commands without Result return type (#6124)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent 154ccbe commit d68a25e

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-macros": 'patch:enhance'
3+
---
4+
5+
Improve compiler error message when generating an async command that has a reference input and don't return a Result.

‎core/tauri-macros/src/command/wrapper.rs‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use heck::{ToLowerCamelCase, ToSnakeCase};
66
use proc_macro::TokenStream;
77
use proc_macro2::TokenStream as TokenStream2;
8-
use quote::{format_ident, quote};
8+
use quote::{format_ident, quote, quote_spanned};
99
use syn::{
1010
ext::IdentExt,
1111
parse::{Parse, ParseStream},
@@ -103,6 +103,63 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
103103
resolver: format_ident!("__tauri_resolver__"),
104104
};
105105

106+
// Tauri currently doesn't support async commands that take a reference as input and don't return
107+
// a result. See: https://github.com/tauri-apps/tauri/issues/2533
108+
//
109+
// For now, we provide an informative error message to the user in that case. Once #2533 is
110+
// resolved, this check can be removed.
111+
let mut async_command_check = TokenStream2::new();
112+
if function.sig.asyncness.is_some() {
113+
// This check won't catch all possible problems but it should catch the most common ones.
114+
let mut ref_argument_span = None;
115+
116+
for arg in &function.sig.inputs {
117+
if let syn::FnArg::Typed(pat) = arg {
118+
match &*pat.ty {
119+
syn::Type::Reference(_) => {
120+
ref_argument_span = Some(pat.span());
121+
}
122+
syn::Type::Path(path) => {
123+
// Check if the type contains a lifetime argument
124+
let last = path.path.segments.last().unwrap();
125+
if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
126+
if args
127+
.args
128+
.iter()
129+
.any(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
130+
{
131+
ref_argument_span = Some(pat.span());
132+
}
133+
}
134+
}
135+
_ => {}
136+
}
137+
138+
if let Some(span) = ref_argument_span {
139+
if let syn::ReturnType::Type(_, return_type) = &function.sig.output {
140+
// To check if the return type is `Result` we require it to check a trait that is
141+
// only implemented by `Result`. That way we don't exclude renamed result types
142+
// which we wouldn't otherwise be able to detect purely from the token stream.
143+
// The "error message" displayed to the user is simply the trait name.
144+
async_command_check = quote_spanned! {return_type.span() =>
145+
#[allow(unreachable_code, clippy::diverging_sub_expression)]
146+
const _: () = if false {
147+
trait AsyncCommandMustReturnResult {}
148+
impl<A, B> AsyncCommandMustReturnResult for Result<A, B> {}
149+
let _check: #return_type = unreachable!();
150+
let _: &dyn AsyncCommandMustReturnResult = &_check;
151+
};
152+
};
153+
} else {
154+
return quote_spanned! {
155+
span => compile_error!("async commands that contain references as inputs must return a `Result`");
156+
}.into();
157+
}
158+
}
159+
}
160+
}
161+
}
162+
106163
// body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
107164
let body = syn::parse::<WrapperAttributes>(attributes)
108165
.map(|mut attrs| {
@@ -121,6 +178,8 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
121178

122179
// Rely on rust 2018 edition to allow importing a macro from a path.
123180
quote!(
181+
#async_command_check
182+
124183
#function
125184

126185
#maybe_macro_export

0 commit comments

Comments
 (0)