1use proc_macro2::{Span, TokenStream};
2use proc_macro_error::emit_error;
3use quote::quote;
4use syn::parse::{Parse, ParseStream};
5use syn::spanned::Spanned;
6use syn::{
7 visit_mut, AttrStyle, Attribute, Block, Expr, ExprPath, File, Ident, Item, ItemFn, LitStr,
8 Meta, MetaNameValue, ReturnType, Signature, Stmt, Token, Type,
9};
10
11mod body;
12mod lifetime;
13mod signature;
14
15pub use body::BodyRewriter;
16use signature::HookSignature;
17
18#[derive(Clone)]
19pub struct HookFn {
20 inner: ItemFn,
21}
22
23impl Parse for HookFn {
24 fn parse(input: ParseStream) -> syn::Result<Self> {
25 let func: ItemFn = input.parse()?;
26
27 let sig = func.sig.clone();
28
29 if sig.asyncness.is_some() {
30 emit_error!(sig.asyncness, "async functions can't be hooks");
31 }
32
33 if sig.constness.is_some() {
34 emit_error!(sig.constness, "const functions can't be hooks");
35 }
36
37 if sig.abi.is_some() {
38 emit_error!(sig.abi, "extern functions can't be hooks");
39 }
40
41 if sig.unsafety.is_some() {
42 emit_error!(sig.unsafety, "unsafe functions can't be hooks");
43 }
44
45 if !sig.ident.to_string().starts_with("use_") {
46 emit_error!(sig.ident, "hooks must have a name starting with `use_`");
47 }
48
49 Ok(Self { inner: func })
50 }
51}
52
53impl HookFn {
54 fn doc_attr(&self) -> Attribute {
55 let span = self.inner.span();
56
57 let sig_formatted = prettyplease::unparse(&File {
58 shebang: None,
59 attrs: vec![],
60 items: vec![Item::Fn(ItemFn {
61 block: Box::new(Block {
62 brace_token: Default::default(),
63 stmts: vec![Stmt::Expr(
64 Expr::Path(ExprPath {
65 attrs: vec![],
66 qself: None,
67 path: Ident::new("__yew_macro_dummy_function_body__", span).into(),
68 }),
69 None,
70 )],
71 }),
72 ..self.inner.clone()
73 })],
74 });
75
76 let literal = LitStr::new(
77 &format!(
78 r#"
79# Note
80
81When used in function components and hooks, this hook is equivalent to:
82
83```
84{}
85```
86"#,
87 sig_formatted.replace(
88 "__yew_macro_dummy_function_body__",
89 "/* implementation omitted */"
90 )
91 ),
92 span,
93 );
94
95 Attribute {
96 pound_token: Default::default(),
97 style: AttrStyle::Outer,
98 bracket_token: Default::default(),
99 meta: Meta::NameValue(MetaNameValue {
100 path: Ident::new("doc", span).into(),
101 eq_token: Token,
102 value: Expr::Lit(syn::ExprLit {
103 attrs: vec![],
104 lit: literal.into(),
105 }),
106 }),
107 }
108 }
109}
110
111pub fn hook_impl(hook: HookFn) -> syn::Result<TokenStream> {
112 let doc_attr = hook.doc_attr();
113
114 let HookFn { inner: original_fn } = hook;
115
116 let ItemFn {
117 ref vis,
118 ref sig,
119 ref block,
120 ref attrs,
121 } = original_fn;
122 let mut block = *block.clone();
123
124 let hook_sig = HookSignature::rewrite(sig);
125
126 let Signature {
127 ref fn_token,
128 ref ident,
129 ref inputs,
130 output: ref hook_return_type,
131 ref generics,
132 ..
133 } = hook_sig.sig;
134
135 let output_type = &hook_sig.output_type;
136
137 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
138 let call_generics = hook_sig.call_generics();
139
140 let ctx_ident = Ident::new("_ctx", Span::mixed_site());
142
143 let mut body_rewriter = BodyRewriter::new(ctx_ident.clone());
144 visit_mut::visit_block_mut(&mut body_rewriter, &mut block);
145
146 let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site());
147 let input_args = hook_sig.input_args();
148
149 let inner_fn_rt = match &sig.output {
151 ReturnType::Default => None,
152 ReturnType::Type(rarrow, _) => Some(quote! { #rarrow #output_type }),
153 };
154
155 let inner_fn = quote! { fn #inner_fn_ident #generics (#ctx_ident: &mut ::yew::functional::HookContext, #inputs) #inner_fn_rt #where_clause #block };
156
157 let inner_type_impl = if hook_sig.needs_boxing {
158 let with_output = !matches!(hook_sig.output_type, Type::ImplTrait(_),);
159 let inner_fn_rt = with_output.then_some(&inner_fn_rt);
160 let output_type = with_output.then_some(&output_type);
161
162 let hook_lifetime = &hook_sig.hook_lifetime;
163 let hook_lifetime_plus = quote! { #hook_lifetime + };
164
165 let boxed_inner_ident = Ident::new("boxed_inner", Span::mixed_site());
166 let boxed_fn_type = quote! { ::std::boxed::Box<dyn #hook_lifetime_plus ::std::ops::FnOnce(&mut ::yew::functional::HookContext) #inner_fn_rt> };
167
168 let as_boxed_fn = with_output.then(|| quote! { as #boxed_fn_type });
169
170 let generic_types = generics.type_params().map(|t| &t.ident);
171
172 quote! {
174 let #boxed_inner_ident = ::std::boxed::Box::new(
175 move |#ctx_ident: &mut ::yew::functional::HookContext| #inner_fn_rt {
176 #inner_fn_ident :: <#(#generic_types,)*> (#ctx_ident, #(#input_args,)*)
177 }
178 ) #as_boxed_fn;
179
180 ::yew::functional::BoxedHook::<#hook_lifetime, #output_type>::new(#boxed_inner_ident)
181 }
182 } else {
183 let input_types = hook_sig.input_types();
184
185 let args_ident = Ident::new("args", Span::mixed_site());
186 let hook_struct_name = Ident::new("HookProvider", Span::mixed_site());
187
188 let phantom_types = hook_sig.phantom_types();
189 let phantom_lifetimes = hook_sig.phantom_lifetimes();
190
191 quote! {
192 struct #hook_struct_name #generics #where_clause {
193 _marker: ::std::marker::PhantomData<( #(#phantom_types,)* #(#phantom_lifetimes,)* )>,
194 #args_ident: (#(#input_types,)*),
195 }
196
197 #[automatically_derived]
198 impl #impl_generics ::yew::functional::Hook for #hook_struct_name #ty_generics #where_clause {
199 type Output = #output_type;
200
201 fn run(mut self, #ctx_ident: &mut ::yew::functional::HookContext) -> Self::Output {
202 let (#(#input_args,)*) = self.#args_ident;
203
204 #inner_fn_ident #call_generics (#ctx_ident, #(#input_args,)*)
205 }
206 }
207
208 #[automatically_derived]
209 impl #impl_generics #hook_struct_name #ty_generics #where_clause {
210 fn new(#inputs) -> Self {
211 #hook_struct_name {
212 _marker: ::std::marker::PhantomData,
213 #args_ident: (#(#input_args,)*),
214 }
215 }
216 }
217
218 #hook_struct_name #call_generics ::new(#(#input_args,)*)
219 }
220 };
221
222 let output = quote! {
225 #[cfg(not(doctest))]
226 #(#attrs)*
227 #doc_attr
228 #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause {
229 #inner_fn
230
231 #inner_type_impl
232 }
233
234 #[cfg(doctest)]
235 #original_fn
236 };
237
238 Ok(output)
239}