1use 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 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::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 fn validate_component_path(&mut self) {
121 let path = &self.name;
122
123 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 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 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 fn validate_component_spread(&mut self) {
162 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 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 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 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 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 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
327fn 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));
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 #[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 #[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 assert_eq!(component.get_key(), Some(&parse_quote!("{value}")));
502
503 let properties = component
505 .component_props()
506 .map(|attr| attr.name.to_string())
507 .collect::<Vec<_>>();
508 assert_eq!(properties, ["to", "class"]);
509 }
510}