dioxus_rsx/
component.rs

1//! Parse components into the VNode::Component variant
2//!
3//! Uses the regular robust RsxBlock parser and then validates the component, emitting errors as
4//! diagnostics. This was refactored from a straightforward parser to this validation approach so
5//! that we can emit errors as diagnostics instead of returning results.
6//!
7//! Using this approach we can provide *much* better errors as well as partial expansion wherever
8//! possible.
9//!
10//! It does lead to the code actually being larger than it was before, but it should be much easier
11//! to work with and extend. To add new syntax, we add it to the RsxBlock parser and then add a
12//! validation step here. This does make using the component as a source of truth not as good, but
13//! oddly enoughly, we want the tree to actually be capable of being technically invalid. This is not
14//! usual for building in Rust - you want strongly typed things to be valid - but in this case, we
15//! want to accept all sorts of malformed input and then provide the best possible error messages.
16//!
17//! If you're generally parsing things, you'll just want to parse and then check if it's valid.
18
19use crate::innerlude::*;
20use proc_macro2::TokenStream as TokenStream2;
21use proc_macro2_diagnostics::SpanDiagnosticExt;
22use quote::{quote, ToTokens, TokenStreamExt};
23use std::{collections::HashSet, vec};
24use syn::{
25    parse::{Parse, ParseStream},
26    spanned::Spanned,
27    token, AngleBracketedGenericArguments, Expr, Ident, PathArguments, Result,
28};
29
30#[derive(PartialEq, Eq, Clone, Debug)]
31pub struct Component {
32    pub name: syn::Path,
33    pub generics: Option<AngleBracketedGenericArguments>,
34    pub fields: Vec<Attribute>,
35    pub component_literal_dyn_idx: Vec<DynIdx>,
36    pub spreads: Vec<Spread>,
37    pub brace: Option<token::Brace>,
38    pub children: TemplateBody,
39    pub dyn_idx: DynIdx,
40    pub diagnostics: Diagnostics,
41}
42
43impl Parse for Component {
44    fn parse(input: ParseStream) -> Result<Self> {
45        let mut name = input.parse::<syn::Path>()?;
46        let generics = normalize_path(&mut name);
47
48        if !input.peek(token::Brace) {
49            return Ok(Self::empty(name, generics));
50        };
51
52        let RsxBlock {
53            attributes: fields,
54            children,
55            brace,
56            spreads,
57            diagnostics,
58        } = input.parse::<RsxBlock>()?;
59
60        let literal_properties_count = fields
61            .iter()
62            .filter(|attr| matches!(attr.value, AttributeValue::AttrLiteral(_)))
63            .count();
64        let component_literal_dyn_idx = vec![DynIdx::default(); literal_properties_count];
65
66        let mut component = Self {
67            dyn_idx: DynIdx::default(),
68            children: TemplateBody::new(children),
69            name,
70            generics,
71            fields,
72            brace: Some(brace),
73            component_literal_dyn_idx,
74            spreads,
75            diagnostics,
76        };
77
78        // We've received a valid rsx block, but it's not necessarily a valid component
79        // validating it will dump diagnostics into the output
80        component.validate_component_path();
81        component.validate_fields();
82        component.validate_component_spread();
83
84        Ok(component)
85    }
86}
87
88impl ToTokens for Component {
89    fn to_tokens(&self, tokens: &mut TokenStream2) {
90        let Self { name, generics, .. } = self;
91
92        // Create props either from manual props or from the builder approach
93        let props = self.create_props();
94
95        // Make sure we emit any errors
96        let diagnostics = &self.diagnostics;
97
98        tokens.append_all(quote! {
99            dioxus_core::DynamicNode::Component({
100
101                // todo: ensure going through the trait actually works
102                // we want to avoid importing traits
103                // use dioxus_core::prelude::Properties;
104                use dioxus_core::prelude::Properties;
105                let __comp = ({
106                    #props
107                }).into_vcomponent(
108                    #name #generics,
109                );
110                #diagnostics
111                __comp
112            })
113        })
114    }
115}
116
117impl Component {
118    // Make sure this a proper component path (uppercase ident, a path, or contains an underscorea)
119    // This should be validated by the RsxBlock parser when it peeks bodynodes
120    fn validate_component_path(&mut self) {
121        let path = &self.name;
122
123        // First, ensure the path is not a single lowercase ident with no underscores
124        if path.segments.len() == 1 {
125            let seg = path.segments.first().unwrap();
126            if seg.ident.to_string().chars().next().unwrap().is_lowercase()
127                && !seg.ident.to_string().contains('_')
128            {
129                self.diagnostics.push(seg.ident.span().error(
130                    "Component names must be uppercase, contain an underscore, or abe a path.",
131                ));
132            }
133        }
134
135        // ensure path segments doesn't have PathArguments, only the last
136        // segment is allowed to have one.
137        if path
138            .segments
139            .iter()
140            .take(path.segments.len() - 1)
141            .any(|seg| seg.arguments != PathArguments::None)
142        {
143            self.diagnostics.push(path.span().error(
144                "Component names must not have path arguments. Only the last segment is allowed to have one.",
145            ));
146        }
147
148        // ensure last segment only have value of None or AngleBracketed
149        if !matches!(
150            path.segments.last().unwrap().arguments,
151            PathArguments::None | PathArguments::AngleBracketed(_)
152        ) {
153            self.diagnostics.push(
154                path.span()
155                    .error("Component names must have no arguments or angle bracketed arguments."),
156            );
157        }
158    }
159
160    // Make sure the spread argument is being used as props spreading
161    fn validate_component_spread(&mut self) {
162        // Next, ensure that there's only one spread argument in the attributes *and* it's the last one
163        for spread in self.spreads.iter().skip(1) {
164            self.diagnostics.push(
165                spread
166                    .expr
167                    .span()
168                    .error("Only one set of manual props is allowed for a component."),
169            );
170        }
171    }
172
173    pub fn get_key(&self) -> Option<&AttributeValue> {
174        self.fields
175            .iter()
176            .find(|attr| attr.name.is_likely_key())
177            .map(|attr| &attr.value)
178    }
179
180    /// Ensure there's no duplicate props - this will be a compile error but we can move it to a
181    /// diagnostic, thankfully
182    fn validate_fields(&mut self) {
183        let mut seen = HashSet::new();
184
185        for field in self.fields.iter() {
186            match &field.name {
187                AttributeName::Custom(_) => {}
188                AttributeName::BuiltIn(k) => {
189                    if !seen.contains(k) {
190                        seen.insert(k);
191                    } else {
192                        self.diagnostics.push(k.span().error(
193                            "Duplicate prop field found. Only one prop field per name is allowed.",
194                        ));
195                    }
196                }
197                AttributeName::Spread(_) => {
198                    unreachable!(
199                        "Spread attributes should be handled in the spread validation step."
200                    )
201                }
202            }
203        }
204    }
205
206    /// Create the tokens we'll use for the props of the component
207    ///
208    /// todo: don't create the tokenstream from scratch and instead dump it into the existing streama
209    fn create_props(&self) -> TokenStream2 {
210        let manual_props = self.manual_props();
211
212        let name = &self.name;
213        let generics = &self.generics;
214
215        let mut tokens = if let Some(props) = manual_props.as_ref() {
216            quote! { let mut __manual_props = #props; }
217        } else {
218            quote! { fc_to_builder(#name #generics) }
219        };
220
221        tokens.append_all(self.add_fields_to_builder(
222            manual_props.map(|_| Ident::new("__manual_props", proc_macro2::Span::call_site())),
223        ));
224
225        if !self.children.is_empty() {
226            let children = &self.children;
227            if manual_props.is_some() {
228                tokens.append_all(quote! { __manual_props.children = { #children }; })
229            } else {
230                tokens.append_all(quote! { .children( { #children } ) })
231            }
232        }
233
234        if manual_props.is_some() {
235            tokens.append_all(quote! { __manual_props })
236        } else {
237            tokens.append_all(quote! { .build() })
238        }
239
240        tokens
241    }
242
243    fn manual_props(&self) -> Option<&Expr> {
244        self.spreads.first().map(|spread| &spread.expr)
245    }
246
247    // Iterate over the props of the component (without spreads, key, and custom attributes)
248    pub fn component_props(&self) -> impl Iterator<Item = &Attribute> {
249        self.fields
250            .iter()
251            .filter(move |attr| !attr.name.is_likely_key())
252    }
253
254    fn add_fields_to_builder(&self, manual_props: Option<Ident>) -> TokenStream2 {
255        let mut dynamic_literal_index = 0;
256        let mut tokens = TokenStream2::new();
257        for attribute in self.component_props() {
258            let release_value = attribute.value.to_token_stream();
259
260            // In debug mode, we try to grab the value from the dynamic literal pool if possible
261            let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
262                let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
263                dynamic_literal_index += 1;
264                let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
265                quote! {
266                    {
267                        #[cfg(debug_assertions)]
268                        {
269                            #debug_value
270                        }
271                        #[cfg(not(debug_assertions))]
272                        {
273                            #release_value
274                        }
275                    }
276                }
277            } else {
278                release_value
279            };
280
281            match &attribute.name {
282                AttributeName::BuiltIn(name) => {
283                    if let Some(manual_props) = &manual_props {
284                        tokens.append_all(quote! { #manual_props.#name = #value; })
285                    } else {
286                        tokens.append_all(quote! { .#name(#value) })
287                    }
288                }
289                AttributeName::Custom(name) => {
290                    if manual_props.is_some() {
291                        tokens.append_all(name.span().error(
292                            "Custom attributes are not supported for components that are spread",
293                        ).emit_as_expr_tokens());
294                    } else {
295                        tokens.append_all(quote! { .push_attribute(#name, None, #value, false) })
296                    }
297                }
298                // spreads are handled elsewhere
299                AttributeName::Spread(_) => {}
300            }
301        }
302
303        tokens
304    }
305
306    fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
307        let mut diagnostics = Diagnostics::new();
308        diagnostics.push(
309            name.span()
310                .error("Components must have a body")
311                .help("Components must have a body, for example `Component {}`"),
312        );
313        Component {
314            name,
315            generics,
316            brace: None,
317            fields: vec![],
318            spreads: vec![],
319            children: TemplateBody::new(vec![]),
320            component_literal_dyn_idx: vec![],
321            dyn_idx: DynIdx::default(),
322            diagnostics,
323        }
324    }
325}
326
327/// Normalize the generics of a path
328///
329/// Ensure there's a `::` after the last segment if there are generics
330fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments> {
331    let seg = name.segments.last_mut()?;
332
333    let mut generics = match seg.arguments.clone() {
334        PathArguments::AngleBracketed(args) => {
335            seg.arguments = PathArguments::None;
336            Some(args)
337        }
338        _ => None,
339    };
340
341    if let Some(generics) = generics.as_mut() {
342        use syn::Token;
343        generics.colon2_token = Some(Token![::](proc_macro2::Span::call_site()));
344    }
345
346    generics
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use prettier_please::PrettyUnparse;
353    use syn::parse_quote;
354
355    /// Ensure we can parse a component
356    #[test]
357    fn parses() {
358        let input = quote! {
359            MyComponent {
360                key: "value {something}",
361                prop: "value",
362                ..props,
363                div {
364                    "Hello, world!"
365                }
366            }
367        };
368
369        let component: Component = syn::parse2(input).unwrap();
370
371        dbg!(component);
372
373        let input_without_manual_props = quote! {
374            MyComponent {
375                key: "value {something}",
376                prop: "value",
377                div { "Hello, world!" }
378            }
379        };
380
381        let component: Component = syn::parse2(input_without_manual_props).unwrap();
382        dbg!(component);
383    }
384
385    /// Ensure we reject invalid forms
386    ///
387    /// Maybe want to snapshot the errors?
388    #[test]
389    fn rejects() {
390        let input = quote! {
391            myComponent {
392                key: "value",
393                prop: "value",
394                prop: "other",
395                ..props,
396                ..other_props,
397                div {
398                    "Hello, world!"
399                }
400            }
401        };
402
403        let component: Component = syn::parse2(input).unwrap();
404        dbg!(component.diagnostics);
405    }
406
407    #[test]
408    fn to_tokens_properly() {
409        let input = quote! {
410            MyComponent {
411                key: "value {something}",
412                prop: "value",
413                prop: "value",
414                prop: "value",
415                prop: "value",
416                prop: 123,
417                ..props,
418                div { "Hello, world!" }
419            }
420        };
421
422        let component: Component = syn::parse2(input).unwrap();
423        println!("{}", component.to_token_stream());
424    }
425
426    #[test]
427    fn to_tokens_no_manual_props() {
428        let input_without_manual_props = quote! {
429            MyComponent {
430                key: "value {something}",
431                named: "value {something}",
432                prop: "value",
433                count: 1,
434                div { "Hello, world!" }
435            }
436        };
437        let component: Component = syn::parse2(input_without_manual_props).unwrap();
438        println!("{}", component.to_token_stream().pretty_unparse());
439    }
440
441    #[test]
442    fn generics_params() {
443        let input_without_children = quote! {
444             Outlet::<R> {}
445        };
446        let component: crate::CallBody = syn::parse2(input_without_children).unwrap();
447        println!("{}", component.to_token_stream().pretty_unparse());
448    }
449
450    #[test]
451    fn generics_no_fish() {
452        let name = quote! { Outlet<R> };
453        let mut p = syn::parse2::<syn::Path>(name).unwrap();
454        let generics = normalize_path(&mut p);
455        assert!(generics.is_some());
456
457        let input_without_children = quote! {
458            div {
459                Component<Generic> {}
460            }
461        };
462        let component: BodyNode = syn::parse2(input_without_children).unwrap();
463        println!("{}", component.to_token_stream().pretty_unparse());
464    }
465
466    #[test]
467    fn fmt_passes_properly() {
468        let input = quote! {
469            Link { to: Route::List, class: "pure-button", "Go back" }
470        };
471
472        let component: Component = syn::parse2(input).unwrap();
473
474        println!("{}", component.to_token_stream().pretty_unparse());
475    }
476
477    #[test]
478    fn incomplete_components() {
479        let input = quote::quote! {
480            some::cool::Component
481        };
482
483        let _parsed: Component = syn::parse2(input).unwrap();
484
485        let input = quote::quote! {
486            some::cool::C
487        };
488
489        let _parsed: syn::Path = syn::parse2(input).unwrap();
490    }
491
492    #[test]
493    fn identifies_key() {
494        let input = quote! {
495            Link { key: "{value}", to: Route::List, class: "pure-button", "Go back" }
496        };
497
498        let component: Component = syn::parse2(input).unwrap();
499
500        // The key should exist
501        assert_eq!(component.get_key(), Some(&parse_quote!("{value}")));
502
503        // The key should not be included in the properties
504        let properties = component
505            .component_props()
506            .map(|attr| attr.name.to_string())
507            .collect::<Vec<_>>();
508        assert_eq!(properties, ["to", "class"]);
509    }
510}