dioxus_rsx/
partial_closure.rs

1use crate::PartialExpr;
2use proc_macro2::TokenStream;
3use quote::ToTokens;
4use std::hash::{Hash, Hasher};
5use syn::{
6    parse::{Parse, ParseStream},
7    punctuated::Punctuated,
8    Attribute, Expr, Pat, PatType, Result, ReturnType, Token, Type,
9};
10use syn::{BoundLifetimes, ExprClosure};
11
12/// A closure whose body might not be valid rust code but we want to interpret it regardless.
13/// This lets us provide expansions in way more cases than normal closures at the expense of an
14/// increased mainteance burden and complexity.
15///
16/// We do our best to reuse the same logic from partial exprs for the body of the PartialClosure.
17/// The code here is simply stolen from `syn::ExprClosure` and lightly modified to work with
18/// PartialExprs. We only removed the attrs field and changed the body to be a PartialExpr.
19/// Otherwise, it's a direct copy of the original.
20#[derive(Debug, Clone)]
21pub struct PartialClosure {
22    pub lifetimes: Option<BoundLifetimes>,
23    pub constness: Option<Token![const]>,
24    pub movability: Option<Token![static]>,
25    pub asyncness: Option<Token![async]>,
26    pub capture: Option<Token![move]>,
27    pub or1_token: Token![|],
28    pub inputs: Punctuated<Pat, Token![,]>,
29    pub or2_token: Token![|],
30    pub output: ReturnType,
31    pub body: PartialExpr,
32}
33
34impl Parse for PartialClosure {
35    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
36        let lifetimes: Option<BoundLifetimes> = input.parse()?;
37        let constness: Option<Token![const]> = input.parse()?;
38        let movability: Option<Token![static]> = input.parse()?;
39        let asyncness: Option<Token![async]> = input.parse()?;
40        let capture: Option<Token![move]> = input.parse()?;
41        let or1_token: Token![|] = input.parse()?;
42
43        let mut inputs = Punctuated::new();
44        loop {
45            if input.peek(Token![|]) {
46                break;
47            }
48            let value = closure_arg(input)?;
49            inputs.push_value(value);
50            if input.peek(Token![|]) {
51                break;
52            }
53            let punct: Token![,] = input.parse()?;
54            inputs.push_punct(punct);
55        }
56
57        let or2_token: Token![|] = input.parse()?;
58
59        let output = if input.peek(Token![->]) {
60            let arrow_token: Token![->] = input.parse()?;
61            let ty: Type = input.parse()?;
62            ReturnType::Type(arrow_token, Box::new(ty))
63        } else {
64            ReturnType::Default
65        };
66
67        let body = PartialExpr::parse(input)?;
68
69        Ok(PartialClosure {
70            lifetimes,
71            constness,
72            movability,
73            asyncness,
74            capture,
75            or1_token,
76            inputs,
77            or2_token,
78            output,
79            body,
80        })
81    }
82}
83
84impl ToTokens for PartialClosure {
85    fn to_tokens(&self, tokens: &mut TokenStream) {
86        self.lifetimes.to_tokens(tokens);
87        self.constness.to_tokens(tokens);
88        self.movability.to_tokens(tokens);
89        self.asyncness.to_tokens(tokens);
90        self.capture.to_tokens(tokens);
91        self.or1_token.to_tokens(tokens);
92        self.inputs.to_tokens(tokens);
93        self.or2_token.to_tokens(tokens);
94        self.output.to_tokens(tokens);
95        self.body.to_tokens(tokens);
96    }
97}
98
99impl PartialEq for PartialClosure {
100    fn eq(&self, other: &Self) -> bool {
101        self.lifetimes == other.lifetimes
102            && self.constness == other.constness
103            && self.movability == other.movability
104            && self.asyncness == other.asyncness
105            && self.capture == other.capture
106            && self.or1_token == other.or1_token
107            && self.inputs == other.inputs
108            && self.or2_token == other.or2_token
109            && self.output == other.output
110            && self.body == other.body
111    }
112}
113
114impl Eq for PartialClosure {}
115impl Hash for PartialClosure {
116    fn hash<H: Hasher>(&self, state: &mut H) {
117        self.lifetimes.hash(state);
118        self.constness.hash(state);
119        self.movability.hash(state);
120        self.asyncness.hash(state);
121        self.capture.hash(state);
122        self.or1_token.hash(state);
123        self.inputs.hash(state);
124        self.or2_token.hash(state);
125        self.output.hash(state);
126        self.body.hash(state);
127    }
128}
129
130impl PartialClosure {
131    /// Convert this partial closure into a full closure if it is valid
132    /// Returns err if the internal tokens can't be parsed as a closure
133    pub fn as_expr(&self) -> Result<Expr> {
134        let expr_closure = ExprClosure {
135            attrs: Vec::new(),
136            asyncness: self.asyncness,
137            capture: self.capture,
138            inputs: self.inputs.clone(),
139            output: self.output.clone(),
140            lifetimes: self.lifetimes.clone(),
141            constness: self.constness,
142            movability: self.movability,
143            or1_token: self.or1_token,
144            or2_token: self.or2_token,
145
146            // try to lower the body to an expression - if might fail if it can't
147            body: Box::new(self.body.as_expr()?),
148        };
149
150        Ok(Expr::Closure(expr_closure))
151    }
152}
153
154/// This might look complex but it is just a ripoff of the `syn::ExprClosure` implementation. AFAIK
155/// This code is not particularly accessible from outside syn... so it lives here. sorry
156fn closure_arg(input: ParseStream) -> Result<Pat> {
157    let attrs = input.call(Attribute::parse_outer)?;
158    let mut pat = Pat::parse_single(input)?;
159
160    if input.peek(Token![:]) {
161        Ok(Pat::Type(PatType {
162            attrs,
163            pat: Box::new(pat),
164            colon_token: input.parse()?,
165            ty: input.parse()?,
166        }))
167    } else {
168        match &mut pat {
169            Pat::Const(pat) => pat.attrs = attrs,
170            Pat::Ident(pat) => pat.attrs = attrs,
171            Pat::Lit(pat) => pat.attrs = attrs,
172            Pat::Macro(pat) => pat.attrs = attrs,
173            Pat::Or(pat) => pat.attrs = attrs,
174            Pat::Paren(pat) => pat.attrs = attrs,
175            Pat::Path(pat) => pat.attrs = attrs,
176            Pat::Range(pat) => pat.attrs = attrs,
177            Pat::Reference(pat) => pat.attrs = attrs,
178            Pat::Rest(pat) => pat.attrs = attrs,
179            Pat::Slice(pat) => pat.attrs = attrs,
180            Pat::Struct(pat) => pat.attrs = attrs,
181            Pat::Tuple(pat) => pat.attrs = attrs,
182            Pat::TupleStruct(pat) => pat.attrs = attrs,
183            Pat::Wild(pat) => pat.attrs = attrs,
184            Pat::Type(_) => unreachable!(),
185            Pat::Verbatim(_) => {}
186            _ => {}
187        }
188        Ok(pat)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use quote::quote;
196
197    #[test]
198    fn parses() {
199        let doesnt_parse: Result<ExprClosure> = syn::parse2(quote! {
200            |a, b| { method. }
201        });
202
203        // regular closures can't parse as partial closures
204        assert!(doesnt_parse.is_err());
205
206        let parses: Result<PartialClosure> = syn::parse2(quote! {
207            |a, b| { method. }
208        });
209
210        // but ours can - we just can't format it out
211        let parses = parses.unwrap();
212        dbg!(parses.to_token_stream().to_string());
213    }
214
215    #[test]
216    fn parses_real_world() {
217        let parses: Result<PartialClosure> = syn::parse2(quote! {
218            move |_| {
219                let mut sidebar = SHOW_SIDEBAR.write();
220                *sidebar = !*sidebar;
221            }
222        });
223
224        // but ours can - we just can't format it out
225        let parses = parses.unwrap();
226        dbg!(parses.to_token_stream().to_string());
227        parses.as_expr().unwrap();
228
229        let parses: Result<PartialClosure> = syn::parse2(quote! {
230            move |_| {
231                rsx! {
232                    div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
233                        "gomg"
234                        "hi!!"
235                        "womh"
236                    }
237                };
238                println!("hi")
239            }
240        });
241        parses.unwrap().as_expr().unwrap();
242    }
243
244    #[test]
245    fn partial_eqs() {
246        let a: PartialClosure = syn::parse2(quote! {
247            move |e| {
248                println!("clicked!");
249            }
250        })
251        .unwrap();
252
253        let b: PartialClosure = syn::parse2(quote! {
254            move |e| {
255                println!("clicked!");
256            }
257        })
258        .unwrap();
259
260        let c: PartialClosure = syn::parse2(quote! {
261            move |e| {
262                println!("unclicked");
263            }
264        })
265        .unwrap();
266
267        assert_eq!(a, b);
268        assert_ne!(a, c);
269    }
270
271    /// Ensure our ToTokens impl is the same as the one in syn
272    #[test]
273    fn same_to_tokens() {
274        let a: PartialClosure = syn::parse2(quote! {
275            move |e| {
276                println!("clicked!");
277            }
278        })
279        .unwrap();
280
281        let b: PartialClosure = syn::parse2(quote! {
282            move |e| {
283                println!("clicked!");
284            }
285        })
286        .unwrap();
287
288        let c: ExprClosure = syn::parse2(quote! {
289            move |e| {
290                println!("clicked!");
291            }
292        })
293        .unwrap();
294
295        assert_eq!(
296            a.to_token_stream().to_string(),
297            b.to_token_stream().to_string()
298        );
299
300        assert_eq!(
301            a.to_token_stream().to_string(),
302            c.to_token_stream().to_string()
303        );
304
305        let a: PartialClosure = syn::parse2(quote! {
306            move |e| println!("clicked!")
307        })
308        .unwrap();
309
310        let b: ExprClosure = syn::parse2(quote! {
311            move |e| println!("clicked!")
312        })
313        .unwrap();
314
315        assert_eq!(
316            a.to_token_stream().to_string(),
317            b.to_token_stream().to_string()
318        );
319    }
320}