sycamore_view_parser/
codegen.rs

1//! Codegen for `view!` macro.
2//!
3//! Implementation note: We are not using the `quote::ToTokens` trait because we need to pass
4//! additional information to the codegen such as which mode (Client, Hydrate, SSR), etc...
5
6use proc_macro2::TokenStream;
7use quote::quote;
8use syn::{Expr, Pat};
9
10use crate::ir::{DynNode, Node, Prop, PropType, Root, TagIdent, TagNode, TextNode};
11
12pub struct Codegen {
13    // TODO: configure mode: Client, Hydrate, SSR
14}
15
16impl Codegen {
17    pub fn root(&self, root: &Root) -> TokenStream {
18        match &root.0[..] {
19            [] => quote! {
20                ::sycamore::rt::View::new()
21            },
22            [node] => self.node(node),
23            nodes => {
24                let nodes = nodes.iter().map(|node| self.node(node));
25                quote! {
26                    ::std::convert::Into::<::sycamore::rt::View>::into(::std::vec![#(#nodes),*])
27                }
28            }
29        }
30    }
31
32    /// Generate a `View` from a `Node`.
33    pub fn node(&self, node: &Node) -> TokenStream {
34        match node {
35            Node::Tag(tag) => {
36                if is_component(&tag.ident) {
37                    self.component(tag)
38                } else {
39                    self.element(tag)
40                }
41            }
42            Node::Text(TextNode { value }) => quote! {
43                ::std::convert::Into::<::sycamore::rt::View>::into(#value)
44            },
45            Node::Dyn(DynNode { value }) => {
46                let is_dynamic = is_dyn(value);
47                if is_dynamic {
48                    quote! {
49                        ::sycamore::rt::View::from_dynamic(
50                            move || ::std::convert::Into::<::sycamore::rt::View>::into(#value)
51                        )
52                    }
53                } else {
54                    quote! {
55                        ::std::convert::Into::<::sycamore::rt::View>::into(#value)
56                    }
57                }
58            }
59        }
60    }
61
62    pub fn element(&self, element: &TagNode) -> TokenStream {
63        let TagNode {
64            ident,
65            props,
66            children,
67        } = element;
68
69        let attributes = props.iter().map(|attr| self.attribute(attr));
70
71        let children = children
72            .0
73            .iter()
74            .map(|child| self.node(child))
75            .collect::<Vec<_>>();
76
77        match ident {
78            TagIdent::Path(tag) => {
79                assert!(tag.get_ident().is_some(), "elements must be an ident");
80                quote! {
81                    ::sycamore::rt::View::from(
82                        ::sycamore::rt::tags::#tag().children(::std::vec![#(#children),*])#(#attributes)*
83                    )
84                }
85            }
86            TagIdent::Hyphenated(tag) => quote! {
87                ::sycamore::rt::View::from(
88                    ::sycamore::rt::custom_element(#tag).children(::std::vec![#(#children),*])#(#attributes)*
89                )
90            },
91        }
92    }
93
94    pub fn attribute(&self, attr: &Prop) -> TokenStream {
95        let value = &attr.value;
96        let is_dynamic = is_dyn(value);
97        let dyn_value = if is_dynamic {
98            quote! { move || #value }
99        } else {
100            quote! { #value }
101        };
102        match &attr.ty {
103            PropType::Plain { ident } => {
104                quote! { .#ident(#dyn_value) }
105            }
106            PropType::PlainHyphenated { ident } => {
107                quote! { .attr(#ident, #dyn_value) }
108            }
109            PropType::PlainQuoted { ident } => {
110                quote! { .attr(#ident, #dyn_value) }
111            }
112            PropType::Directive { dir, ident } => match dir.to_string().as_str() {
113                "on" => quote! { .on(::sycamore::rt::events::#ident, #value) },
114                "prop" => {
115                    let ident = ident.to_string();
116                    quote! { .prop(#ident, #dyn_value) }
117                }
118                "bind" => quote! { .bind(::sycamore::rt::bind::#ident, #value) },
119                _ => syn::Error::new(dir.span(), format!("unknown directive `{dir}`"))
120                    .to_compile_error(),
121            },
122            PropType::Ref => quote! { .r#ref(#value) },
123            PropType::Spread => quote! { .spread(#value) },
124        }
125    }
126
127    pub fn component(
128        &self,
129        TagNode {
130            ident,
131            props,
132            children,
133        }: &TagNode,
134    ) -> TokenStream {
135        let ident = match ident {
136            TagIdent::Path(path) => path,
137            TagIdent::Hyphenated(_) => unreachable!("hyphenated tags are not components"),
138        };
139
140        let plain = props
141            .iter()
142            .filter_map(|prop| match &prop.ty {
143                PropType::Plain { ident } => Some((ident, prop.value.clone())),
144                _ => None,
145            })
146            .collect::<Vec<_>>();
147        let plain_names = plain.iter().map(|(ident, _)| ident);
148        let plain_values = plain.iter().map(|(_, value)| value);
149
150        let other_props = props
151            .iter()
152            .filter(|prop| !matches!(&prop.ty, PropType::Plain { .. }))
153            .collect::<Vec<_>>();
154        let other_attributes = other_props.iter().map(|prop| self.attribute(prop));
155
156        let children_quoted = if children.0.is_empty() {
157            quote! {}
158        } else {
159            let codegen = Codegen {};
160            let children = codegen.root(children);
161            quote! {
162                .children(
163                    ::sycamore::rt::Children::new(move || {
164                        #children
165                    })
166                )
167            }
168        };
169        quote! {{
170            let __component = &#ident; // We do this to make sure the compiler can infer the value for `<G>`.
171            ::sycamore::rt::component_scope(move || ::sycamore::rt::Component::create(
172                __component,
173                ::sycamore::rt::element_like_component_builder(__component)
174                    #(.#plain_names(#plain_values))*
175                    #(#other_attributes)*
176                    #children_quoted
177                    .build()
178            ))
179        }}
180    }
181}
182
183fn is_component(ident: &TagIdent) -> bool {
184    match ident {
185        TagIdent::Path(path) => {
186            path.get_ident().is_none()
187                || path
188                    .get_ident()
189                    .unwrap()
190                    .to_string()
191                    .chars()
192                    .next()
193                    .unwrap()
194                    .is_ascii_uppercase()
195        }
196        // A hyphenated tag is always a custom-element and therefore never a component.
197        TagIdent::Hyphenated(_) => false,
198    }
199}
200
201fn is_dyn(ex: &Expr) -> bool {
202    match ex {
203        Expr::Lit(_) | Expr::Closure(_) | Expr::Path(_) => false,
204
205        Expr::Field(f) => is_dyn(&f.base),
206        Expr::Paren(p) => is_dyn(&p.expr),
207        Expr::Group(g) => is_dyn(&g.expr),
208        Expr::Tuple(t) => t.elems.iter().any(is_dyn),
209        Expr::Array(a) => a.elems.iter().any(is_dyn),
210        Expr::Repeat(r) => is_dyn(&r.expr) || is_dyn(&r.len),
211        Expr::Struct(s) => s.fields.iter().any(|fv: &syn::FieldValue| is_dyn(&fv.expr)),
212
213        Expr::Cast(c) => is_dyn(&c.expr),
214        Expr::Macro(m) => is_dyn_macro(&m.mac),
215        Expr::Block(b) => is_dyn_block(&b.block),
216        Expr::Const(_const_block) => false,
217
218        Expr::Loop(l) => is_dyn_block(&l.body),
219        Expr::While(w) => is_dyn(&w.cond) || is_dyn_block(&w.body),
220        Expr::ForLoop(f) => is_dyn_pattern(&f.pat) || is_dyn(&f.expr) || is_dyn_block(&f.body),
221        Expr::Break(_) | Expr::Continue(_) => false,
222
223        Expr::Let(e) => is_dyn_pattern(&e.pat) || is_dyn(&e.expr),
224
225        Expr::Match(m) => {
226            is_dyn(&m.expr)
227                || m.arms.iter().any(|a: &syn::Arm| {
228                    is_dyn_pattern(&a.pat)
229                        || a.guard.as_ref().is_some_and(|(_, g_expr)| is_dyn(g_expr))
230                        || is_dyn(&a.body)
231                })
232        }
233
234        Expr::If(i) => {
235            is_dyn(&i.cond)
236                || is_dyn_block(&i.then_branch)
237                || i.else_branch.as_ref().is_some_and(|(_, e)| is_dyn(e))
238        }
239
240        Expr::Unary(u) => is_dyn(&u.expr),
241        Expr::Binary(b) => is_dyn(&b.left) || is_dyn(&b.right),
242        Expr::Index(i) => is_dyn(&i.expr) || is_dyn(&i.index),
243        Expr::Range(r) => {
244            r.start.as_deref().is_some_and(is_dyn) || r.end.as_deref().is_some_and(is_dyn)
245        }
246
247        _ => true,
248    }
249}
250
251fn is_dyn_pattern(pat: &Pat) -> bool {
252    match pat {
253        Pat::Wild(_) | Pat::Lit(_) | Pat::Path(_) | Pat::Rest(_) | Pat::Type(_) | Pat::Const(_) => {
254            false
255        }
256
257        Pat::Paren(p) => is_dyn_pattern(&p.pat),
258        Pat::Or(o) => o.cases.iter().any(is_dyn_pattern),
259        Pat::Tuple(t) => t.elems.iter().any(is_dyn_pattern),
260        Pat::TupleStruct(s) => s.elems.iter().any(is_dyn_pattern),
261        Pat::Slice(s) => s.elems.iter().any(is_dyn_pattern),
262        Pat::Range(r) => {
263            r.start.as_deref().is_some_and(is_dyn) || r.end.as_deref().is_some_and(is_dyn)
264        }
265
266        Pat::Reference(r) => r.mutability.is_some(),
267        Pat::Ident(id) => {
268            (id.by_ref.is_some() && id.mutability.is_some())
269                || id
270                    .subpat
271                    .as_ref()
272                    .is_some_and(|(_, pat)| is_dyn_pattern(pat))
273        }
274
275        Pat::Struct(s) => s
276            .fields
277            .iter()
278            .any(|fp: &syn::FieldPat| is_dyn_pattern(&fp.pat)),
279
280        // syn::Pat is non-exhaustive
281        _ => true,
282    }
283}
284
285fn is_dyn_macro(m: &syn::Macro) -> bool {
286    // Bodies of nested inner view! macros will be checked for dynamic
287    // parts when their own codegen is run.
288
289    // TODO(MSRV >= 1.82): use `is_none_or` and remove the `allow(clippy::nonminimal_bool)`
290    #[allow(clippy::nonminimal_bool)]
291    !m.path.get_ident().is_some_and(|ident| ident == "view")
292}
293
294fn is_dyn_block(block: &syn::Block) -> bool {
295    block.stmts.iter().any(|s: &syn::Stmt| match s {
296        syn::Stmt::Expr(ex, _) => is_dyn(ex),
297        syn::Stmt::Macro(m) => is_dyn_macro(&m.mac),
298        syn::Stmt::Local(loc) => {
299            is_dyn_pattern(&loc.pat)
300                || loc.init.as_ref().is_some_and(|i| {
301                    is_dyn(&i.expr) || i.diverge.as_ref().is_some_and(|(_, ex)| is_dyn(ex))
302                })
303        }
304        syn::Stmt::Item(_) => false,
305    })
306}