1use crate::innerlude::*;
20use proc_macro2::TokenStream as TokenStream2;
21use proc_macro2_diagnostics::SpanDiagnosticExt;
22use quote::{quote, quote_spanned, 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 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 let props = self.create_props();
94
95 let diagnostics = &self.diagnostics;
97
98 tokens.append_all(quote! {
99 dioxus_core::DynamicNode::Component({
100
101 use dioxus_core::Properties;
104 let __comp = ({
105 #props
106 }).into_vcomponent(
107 #name #generics,
108 );
109 #diagnostics
110 __comp
111 })
112 })
113 }
114}
115
116impl Component {
117 fn validate_component_path(&mut self) {
120 let path = &self.name;
121
122 if path.segments.len() == 1 {
124 let seg = path.segments.first().unwrap();
125 if seg.ident.to_string().chars().next().unwrap().is_lowercase()
126 && !seg.ident.to_string().contains('_')
127 {
128 self.diagnostics.push(seg.ident.span().error(
129 "Component names must be uppercase, contain an underscore, or abe a path.",
130 ));
131 }
132 }
133
134 if path
137 .segments
138 .iter()
139 .take(path.segments.len() - 1)
140 .any(|seg| seg.arguments != PathArguments::None)
141 {
142 self.diagnostics.push(path.span().error(
143 "Component names must not have path arguments. Only the last segment is allowed to have one.",
144 ));
145 }
146
147 if !matches!(
149 path.segments.last().unwrap().arguments,
150 PathArguments::None | PathArguments::AngleBracketed(_)
151 ) {
152 self.diagnostics.push(
153 path.span()
154 .error("Component names must have no arguments or angle bracketed arguments."),
155 );
156 }
157 }
158
159 fn validate_component_spread(&mut self) {
161 for spread in self.spreads.iter().skip(1) {
163 self.diagnostics.push(
164 spread
165 .expr
166 .span()
167 .error("Only one set of manual props is allowed for a component."),
168 );
169 }
170 }
171
172 pub fn get_key(&self) -> Option<&AttributeValue> {
173 self.fields
174 .iter()
175 .find(|attr| attr.name.is_likely_key())
176 .map(|attr| &attr.value)
177 }
178
179 fn validate_fields(&mut self) {
182 let mut seen = HashSet::new();
183
184 for field in self.fields.iter() {
185 match &field.name {
186 AttributeName::Custom(_) => {}
187 AttributeName::BuiltIn(k) => {
188 if !seen.contains(k) {
189 seen.insert(k);
190 } else {
191 self.diagnostics.push(k.span().error(
192 "Duplicate prop field found. Only one prop field per name is allowed.",
193 ));
194 }
195 }
196 AttributeName::Spread(_) => {
197 unreachable!(
198 "Spread attributes should be handled in the spread validation step."
199 )
200 }
201 }
202 }
203 }
204
205 fn create_props(&self) -> TokenStream2 {
209 let manual_props = self.manual_props();
210
211 let name = &self.name;
212 let generics = &self.generics;
213 let inner_scope_span = self
214 .brace
215 .as_ref()
216 .map(|b| b.span.join())
217 .unwrap_or(self.name.span());
218
219 let mut tokens = if let Some(props) = manual_props.as_ref() {
220 quote_spanned! { props.span() => let mut __manual_props = #props; }
221 } else {
222 let spanned = quote_spanned! { self.name.span() => #name #generics };
225 quote! { dioxus_core::fc_to_builder(#spanned) }
226 };
227
228 tokens.append_all(self.add_fields_to_builder(
229 manual_props.map(|_| Ident::new("__manual_props", proc_macro2::Span::call_site())),
230 ));
231
232 if !self.children.is_empty() {
233 let children = &self.children;
234 if manual_props.is_some() {
236 tokens.append_all(
237 quote_spanned! { children.first_root_span() => __manual_props.children = #children; },
238 )
239 } else {
240 tokens.append_all(
241 quote_spanned! { children.first_root_span() => .children( #children ) },
242 )
243 }
244 }
245
246 if manual_props.is_some() {
247 tokens.append_all(quote! { __manual_props })
248 } else {
249 tokens.append_all(quote_spanned! { inner_scope_span => .build() })
251 }
252
253 tokens
254 }
255
256 fn manual_props(&self) -> Option<&Expr> {
257 self.spreads.first().map(|spread| &spread.expr)
258 }
259
260 pub fn component_props(&self) -> impl Iterator<Item = &Attribute> {
262 self.fields
263 .iter()
264 .filter(move |attr| !attr.name.is_likely_key())
265 }
266
267 fn add_fields_to_builder(&self, manual_props: Option<Ident>) -> TokenStream2 {
268 let mut dynamic_literal_index = 0;
269 let mut tokens = TokenStream2::new();
270 for attribute in self.component_props() {
271 let release_value = attribute.value.to_token_stream();
272
273 let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
275 let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
276 dynamic_literal_index += 1;
277 let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
278 quote! {
279 {
280 #[cfg(debug_assertions)]
281 {
282 #debug_value
283 }
284 #[cfg(not(debug_assertions))]
285 {
286 #release_value
287 }
288 }
289 }
290 } else {
291 release_value
292 };
293
294 match &attribute.name {
295 AttributeName::BuiltIn(name) => {
296 if let Some(manual_props) = &manual_props {
297 tokens.append_all(quote! { #manual_props.#name = #value; })
298 } else {
299 tokens.append_all(quote! { .#name(#value) })
300 }
301 }
302 AttributeName::Custom(name) => {
303 if manual_props.is_some() {
304 tokens.append_all(name.span().error(
305 "Custom attributes are not supported for components that are spread",
306 ).emit_as_expr_tokens());
307 } else {
308 tokens.append_all(quote! {
319 .push_attribute(#name, None, #value, false)
320 })
321 }
322 }
323 AttributeName::Spread(_) => {}
325 }
326 }
327
328 tokens
329 }
330
331 fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
332 let mut diagnostics = Diagnostics::new();
333 diagnostics.push(
334 name.span()
335 .error("Components must have a body")
336 .help("Components must have a body, for example `Component {}`"),
337 );
338 Component {
339 name,
340 generics,
341 brace: None,
342 fields: vec![],
343 spreads: vec![],
344 children: TemplateBody::new(vec![]),
345 component_literal_dyn_idx: vec![],
346 dyn_idx: DynIdx::default(),
347 diagnostics,
348 }
349 }
350}
351
352fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments> {
356 let seg = name.segments.last_mut()?;
357
358 let mut generics = match seg.arguments.clone() {
359 PathArguments::AngleBracketed(args) => {
360 seg.arguments = PathArguments::None;
361 Some(args)
362 }
363 _ => None,
364 };
365
366 if let Some(generics) = generics.as_mut() {
367 generics.colon2_token = Some(syn::Token));
368 }
369
370 generics
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use prettier_please::PrettyUnparse;
377 use syn::parse_quote;
378
379 #[test]
381 fn parses() {
382 let input = quote! {
383 MyComponent {
384 key: "value {something}",
385 prop: "value",
386 ..props,
387 div {
388 "Hello, world!"
389 }
390 }
391 };
392
393 let component: Component = syn::parse2(input).unwrap();
394
395 dbg!(component);
396
397 let input_without_manual_props = quote! {
398 MyComponent {
399 key: "value {something}",
400 prop: "value",
401 div { "Hello, world!" }
402 }
403 };
404
405 let component: Component = syn::parse2(input_without_manual_props).unwrap();
406 dbg!(component);
407 }
408
409 #[test]
413 fn rejects() {
414 let input = quote! {
415 myComponent {
416 key: "value",
417 prop: "value",
418 prop: "other",
419 ..props,
420 ..other_props,
421 div {
422 "Hello, world!"
423 }
424 }
425 };
426
427 let component: Component = syn::parse2(input).unwrap();
428 dbg!(component.diagnostics);
429 }
430
431 #[test]
432 fn to_tokens_properly() {
433 let input = quote! {
434 MyComponent {
435 key: "value {something}",
436 prop: "value",
437 prop: "value",
438 prop: "value",
439 prop: "value",
440 prop: 123,
441 ..props,
442 div { "Hello, world!" }
443 }
444 };
445
446 let component: Component = syn::parse2(input).unwrap();
447 println!("{}", component.to_token_stream());
448 }
449
450 #[test]
451 fn to_tokens_no_manual_props() {
452 let input_without_manual_props = quote! {
453 MyComponent {
454 key: "value {something}",
455 named: "value {something}",
456 prop: "value",
457 count: 1,
458 div { "Hello, world!" }
459 }
460 };
461 let component: Component = syn::parse2(input_without_manual_props).unwrap();
462 println!("{}", component.to_token_stream().pretty_unparse());
463 }
464
465 #[test]
466 fn generics_params() {
467 let input_without_children = quote! {
468 Outlet::<R> {}
469 };
470 let component: crate::CallBody = syn::parse2(input_without_children).unwrap();
471 println!("{}", component.to_token_stream().pretty_unparse());
472 }
473
474 #[test]
475 fn generics_no_fish() {
476 let name = quote! { Outlet<R> };
477 let mut p = syn::parse2::<syn::Path>(name).unwrap();
478 let generics = normalize_path(&mut p);
479 assert!(generics.is_some());
480
481 let input_without_children = quote! {
482 div {
483 Component<Generic> {}
484 }
485 };
486 let component: BodyNode = syn::parse2(input_without_children).unwrap();
487 println!("{}", component.to_token_stream().pretty_unparse());
488 }
489
490 #[test]
491 fn fmt_passes_properly() {
492 let input = quote! {
493 Link { to: Route::List, class: "pure-button", "Go back" }
494 };
495
496 let component: Component = syn::parse2(input).unwrap();
497
498 println!("{}", component.to_token_stream().pretty_unparse());
499 }
500
501 #[test]
502 fn incomplete_components() {
503 let input = quote::quote! {
504 some::cool::Component
505 };
506
507 let _parsed: Component = syn::parse2(input).unwrap();
508
509 let input = quote::quote! {
510 some::cool::C
511 };
512
513 let _parsed: syn::Path = syn::parse2(input).unwrap();
514 }
515
516 #[test]
517 fn identifies_key() {
518 let input = quote! {
519 Link { key: "{value}", to: Route::List, class: "pure-button", "Go back" }
520 };
521
522 let component: Component = syn::parse2(input).unwrap();
523
524 assert_eq!(component.get_key(), Some(&parse_quote!("{value}")));
526
527 let properties = component
529 .component_props()
530 .map(|attr| attr.name.to_string())
531 .collect::<Vec<_>>();
532 assert_eq!(properties, ["to", "class"]);
533 }
534}