component.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. use proc_macro2::TokenStream;
  2. use quote::{format_ident, quote, ToTokens, TokenStreamExt};
  3. use syn::parse::{Parse, ParseStream};
  4. use syn::punctuated::Punctuated;
  5. use syn::spanned::Spanned;
  6. use syn::*;
  7. pub struct ComponentBody {
  8. pub item_fn: ItemFn,
  9. pub options: ComponentMacroOptions,
  10. }
  11. impl Parse for ComponentBody {
  12. fn parse(input: ParseStream) -> Result<Self> {
  13. let item_fn: ItemFn = input.parse()?;
  14. validate_component_fn(&item_fn)?;
  15. Ok(Self {
  16. item_fn,
  17. options: ComponentMacroOptions::default(),
  18. })
  19. }
  20. }
  21. impl ComponentBody {
  22. pub fn with_options(mut self, options: ComponentMacroOptions) -> Self {
  23. self.options = options;
  24. self
  25. }
  26. }
  27. impl ToTokens for ComponentBody {
  28. fn to_tokens(&self, tokens: &mut TokenStream) {
  29. // https://github.com/DioxusLabs/dioxus/issues/1938
  30. // If there's only one input and the input is `props: Props`, we don't need to generate a props struct
  31. // Just attach the non_snake_case attribute to the function
  32. // eventually we'll dump this metadata into devtooling that lets us find all these components
  33. //
  34. // Components can also use the struct pattern to "inline" their props.
  35. // Freya uses this a bunch (because it's clean),
  36. // e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed
  37. if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() {
  38. let comp_fn = &self.item_fn;
  39. tokens.append_all(allow_camel_case_for_fn_ident(comp_fn).into_token_stream());
  40. return;
  41. }
  42. let comp_fn = self.comp_fn();
  43. // If there's no props declared, we simply omit the props argument
  44. // This is basically so you can annotate the App component with #[component] and still be compatible with the
  45. // launch signatures that take fn() -> Element
  46. let props_struct = match self.item_fn.sig.inputs.is_empty() {
  47. // No props declared, so we don't need to generate a props struct
  48. true => quote! {},
  49. // Props declared, so we generate a props struct and then also attach the doc attributes to it
  50. false => {
  51. let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
  52. let (props_struct, props_impls) = self.props_struct();
  53. quote! {
  54. #[doc = #doc]
  55. #[allow(missing_docs)]
  56. #props_struct
  57. #(#props_impls)*
  58. }
  59. }
  60. };
  61. let completion_hints = self.completion_hints();
  62. tokens.append_all(quote! {
  63. #props_struct
  64. #comp_fn
  65. #completion_hints
  66. });
  67. }
  68. }
  69. impl ComponentBody {
  70. // build a new item fn, transforming the original item fn
  71. fn comp_fn(&self) -> ItemFn {
  72. let ComponentBody { item_fn, .. } = self;
  73. let ItemFn {
  74. attrs,
  75. vis,
  76. sig,
  77. block,
  78. } = item_fn;
  79. let Signature {
  80. inputs,
  81. ident: fn_ident,
  82. generics,
  83. output: fn_output,
  84. ..
  85. } = sig;
  86. let Generics { where_clause, .. } = generics;
  87. let (_, impl_generics, _) = generics.split_for_impl();
  88. let generics_turbofish = impl_generics.as_turbofish();
  89. // We generate a struct with the same name as the component but called `Props`
  90. let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
  91. // We pull in the field names from the original function signature, but need to strip off the mutability
  92. let struct_field_names = inputs.iter().map(rebind_mutability);
  93. let props_docs = self.props_docs(inputs.iter().collect());
  94. let inlined_props_argument = if inputs.is_empty() {
  95. quote! {}
  96. } else {
  97. quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics }
  98. };
  99. // Defer to the lazy_body if we're using lazy
  100. let body: TokenStream = if self.options.lazy {
  101. self.lazy_body(
  102. &struct_ident,
  103. generics,
  104. &impl_generics,
  105. fn_output,
  106. where_clause,
  107. &inlined_props_argument,
  108. block,
  109. )
  110. } else {
  111. quote! { #block }
  112. };
  113. // We need a props type to exist even if the inputs are empty with lazy components
  114. let emit_props = if self.options.lazy {
  115. if inputs.is_empty() {
  116. quote! {props: ()}
  117. } else {
  118. quote!(props: #struct_ident #impl_generics)
  119. }
  120. } else {
  121. inlined_props_argument
  122. };
  123. // The extra nest is for the snake case warning to kick back in
  124. parse_quote! {
  125. #(#attrs)*
  126. #(#props_docs)*
  127. #[allow(non_snake_case)]
  128. #vis fn #fn_ident #generics (#emit_props) #fn_output #where_clause {
  129. {
  130. // In debug mode we can detect if the user is calling the component like a function
  131. dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
  132. #body
  133. }
  134. }
  135. }
  136. }
  137. /// Generate the body of the lazy component
  138. ///
  139. /// This extracts the body into a new component that is wrapped in a lazy loader
  140. #[allow(clippy::too_many_arguments)]
  141. fn lazy_body(
  142. &self,
  143. struct_ident: &Ident,
  144. generics: &Generics,
  145. impl_generics: &TypeGenerics,
  146. fn_output: &ReturnType,
  147. where_clause: &Option<WhereClause>,
  148. inlined_props_argument: &TokenStream,
  149. block: &Block,
  150. ) -> TokenStream {
  151. let fn_ident = &self.item_fn.sig.ident;
  152. let inputs = &self.item_fn.sig.inputs;
  153. let lazy_name = format_ident!("Lazy{fn_ident}");
  154. let out_ty = match &self.item_fn.sig.output {
  155. ReturnType::Default => quote! { () },
  156. ReturnType::Type(_, ty) => quote! { #ty },
  157. };
  158. let props_ty = if inputs.is_empty() {
  159. quote! { () }
  160. } else {
  161. quote! { #struct_ident #impl_generics }
  162. };
  163. let anon_props = if inputs.is_empty() {
  164. quote! { props: () }
  165. } else {
  166. quote! { #inlined_props_argument}
  167. };
  168. quote! {
  169. fn #lazy_name #generics (#anon_props) #fn_output #where_clause {
  170. #block
  171. }
  172. dioxus::config_macros::maybe_wasm_split! {
  173. if wasm_split {
  174. {
  175. static __MODULE: wasm_split::LazyLoader<#props_ty, #out_ty> =
  176. wasm_split::lazy_loader!(extern "lazy" fn #lazy_name(props: #props_ty,) -> #out_ty);
  177. use_resource(|| async move { __MODULE.load().await }).suspend()?;
  178. __MODULE.call(props).unwrap()
  179. }
  180. } else {
  181. {
  182. #lazy_name(props)
  183. }
  184. }
  185. }
  186. }
  187. }
  188. /// Build an associated struct for the props of the component
  189. ///
  190. /// This will expand to the typed-builder implementation that we have vendored in this crate.
  191. /// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion.
  192. /// TODO: cache these tokens since this codegen is rather expensive (lots of tokens)
  193. ///
  194. /// We try our best to transfer over any declared doc attributes from the original function signature onto the
  195. /// props struct fields.
  196. fn props_struct(&self) -> (ItemStruct, Vec<ItemImpl>) {
  197. let ItemFn { vis, sig, .. } = &self.item_fn;
  198. let Signature {
  199. inputs,
  200. ident,
  201. generics,
  202. ..
  203. } = sig;
  204. let generic_arguments = if !generics.params.is_empty() {
  205. let generic_arguments = generics
  206. .params
  207. .iter()
  208. .map(make_prop_struct_generics)
  209. .collect::<Punctuated<_, Token![,]>>();
  210. quote! { <#generic_arguments> }
  211. } else {
  212. quote! {}
  213. };
  214. let where_clause = &generics.where_clause;
  215. let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
  216. let struct_field_idents = inputs
  217. .iter()
  218. .map(make_prop_struct_field_idents)
  219. .collect::<Vec<_>>();
  220. let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
  221. let item_struct = parse_quote! {
  222. #[derive(Props)]
  223. #[allow(non_camel_case_types)]
  224. #vis struct #struct_ident #generics #where_clause {
  225. #(#struct_fields),*
  226. }
  227. };
  228. let item_impl_clone = parse_quote! {
  229. impl #generics ::core::clone::Clone for #struct_ident #generic_arguments #where_clause {
  230. #[inline]
  231. fn clone(&self) -> Self {
  232. Self {
  233. #(#struct_field_idents: ::core::clone::Clone::clone(&self.#struct_field_idents)),*
  234. }
  235. }
  236. }
  237. };
  238. let item_impl_partial_eq = parse_quote! {
  239. impl #generics ::core::cmp::PartialEq for #struct_ident #generic_arguments #where_clause {
  240. #[inline]
  241. fn eq(&self, other: &Self) -> bool {
  242. #(
  243. self.#struct_field_idents == other.#struct_field_idents &&
  244. )*
  245. true
  246. }
  247. }
  248. };
  249. (item_struct, vec![item_impl_clone, item_impl_partial_eq])
  250. }
  251. /// Convert a list of function arguments into a list of doc attributes for the props struct
  252. ///
  253. /// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring.
  254. fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
  255. let fn_ident = &self.item_fn.sig.ident;
  256. if inputs.is_empty() {
  257. return Vec::new();
  258. }
  259. let arg_docs = inputs
  260. .iter()
  261. .filter_map(|f| build_doc_fields(f))
  262. .collect::<Vec<_>>();
  263. let mut props_docs = Vec::with_capacity(5);
  264. let props_def_link = fn_ident.to_string() + "Props";
  265. let header =
  266. format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
  267. props_docs.push(parse_quote! {
  268. #[doc = #header]
  269. });
  270. for arg in arg_docs {
  271. let DocField {
  272. arg_name,
  273. arg_type,
  274. deprecation,
  275. input_arg_doc,
  276. } = arg;
  277. let arg_name = strip_pat_mutability(arg_name).to_token_stream().to_string();
  278. let arg_type = crate::utils::format_type_string(arg_type);
  279. let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
  280. .replace("\n\n", "</p><p>");
  281. let prop_def_link = format!("{props_def_link}::{arg_name}");
  282. let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
  283. if let Some(deprecation) = deprecation {
  284. arg_doc.push_str("<p>👎 Deprecated");
  285. if let Some(since) = deprecation.since {
  286. arg_doc.push_str(&format!(" since {since}"));
  287. }
  288. if let Some(note) = deprecation.note {
  289. let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
  290. let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
  291. arg_doc.push_str(&format!(": {note}"));
  292. }
  293. arg_doc.push_str("</p>");
  294. if !input_arg_doc.is_empty() {
  295. arg_doc.push_str("<hr/>");
  296. }
  297. }
  298. if !input_arg_doc.is_empty() {
  299. arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
  300. }
  301. props_docs.push(parse_quote! { #[doc = #arg_doc] });
  302. }
  303. props_docs
  304. }
  305. fn is_explicit_props_ident(&self) -> bool {
  306. if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
  307. if let Pat::Ident(ident) = pat.as_ref() {
  308. return ident.ident == "props";
  309. }
  310. }
  311. false
  312. }
  313. fn has_struct_parameter_pattern(&self) -> bool {
  314. if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
  315. if matches!(pat.as_ref(), Pat::Struct(_)) {
  316. return true;
  317. }
  318. }
  319. false
  320. }
  321. // We generate an extra enum to help us autocomplete the braces after the component.
  322. // This is a bit of a hack, but it's the only way to get the braces to autocomplete.
  323. fn completion_hints(&self) -> TokenStream {
  324. let comp_fn = &self.item_fn.sig.ident;
  325. let completions_mod = Ident::new(&format!("{}_completions", comp_fn), comp_fn.span());
  326. let vis = &self.item_fn.vis;
  327. quote! {
  328. #[allow(non_snake_case)]
  329. #[doc(hidden)]
  330. mod #completions_mod {
  331. #[doc(hidden)]
  332. #[allow(non_camel_case_types)]
  333. /// This enum is generated to help autocomplete the braces after the component. It does nothing
  334. pub enum Component {
  335. #comp_fn {}
  336. }
  337. }
  338. #[allow(unused)]
  339. #vis use #completions_mod::Component::#comp_fn;
  340. }
  341. }
  342. }
  343. struct DocField<'a> {
  344. arg_name: &'a Pat,
  345. arg_type: &'a Type,
  346. deprecation: Option<crate::utils::DeprecatedAttribute>,
  347. input_arg_doc: String,
  348. }
  349. fn build_doc_fields(f: &FnArg) -> Option<DocField> {
  350. let FnArg::Typed(pt) = f else { unreachable!() };
  351. let arg_doc = pt
  352. .attrs
  353. .iter()
  354. .filter_map(|attr| {
  355. // TODO: Error reporting
  356. // Check if the path of the attribute is "doc"
  357. if !is_attr_doc(attr) {
  358. return None;
  359. };
  360. let Meta::NameValue(meta_name_value) = &attr.meta else {
  361. return None;
  362. };
  363. let Expr::Lit(doc_lit) = &meta_name_value.value else {
  364. return None;
  365. };
  366. let Lit::Str(doc_lit_str) = &doc_lit.lit else {
  367. return None;
  368. };
  369. Some(doc_lit_str.value())
  370. })
  371. .fold(String::new(), |mut doc, next_doc_line| {
  372. doc.push('\n');
  373. doc.push_str(&next_doc_line);
  374. doc
  375. });
  376. Some(DocField {
  377. arg_name: &pt.pat,
  378. arg_type: &pt.ty,
  379. deprecation: pt.attrs.iter().find_map(|attr| {
  380. if !attr.path().is_ident("deprecated") {
  381. return None;
  382. }
  383. let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
  384. match res {
  385. Err(e) => panic!("{}", e.to_string()),
  386. Ok(v) => Some(v),
  387. }
  388. }),
  389. input_arg_doc: arg_doc,
  390. })
  391. }
  392. fn validate_component_fn(item_fn: &ItemFn) -> Result<()> {
  393. // Do some validation....
  394. // 1. Ensure the component returns *something*
  395. if item_fn.sig.output == ReturnType::Default {
  396. return Err(Error::new(
  397. item_fn.sig.output.span(),
  398. "Must return a <dioxus_core::Element>".to_string(),
  399. ));
  400. }
  401. // 2. make sure there's no lifetimes on the component - we don't know how to handle those
  402. if item_fn.sig.generics.lifetimes().count() > 0 {
  403. return Err(Error::new(
  404. item_fn.sig.generics.span(),
  405. "Lifetimes are not supported in components".to_string(),
  406. ));
  407. }
  408. // 3. we can't handle async components
  409. if item_fn.sig.asyncness.is_some() {
  410. return Err(Error::new(
  411. item_fn.sig.asyncness.span(),
  412. "Async components are not supported".to_string(),
  413. ));
  414. }
  415. // 4. we can't handle const components
  416. if item_fn.sig.constness.is_some() {
  417. return Err(Error::new(
  418. item_fn.sig.constness.span(),
  419. "Const components are not supported".to_string(),
  420. ));
  421. }
  422. // 5. no receiver parameters
  423. if item_fn
  424. .sig
  425. .inputs
  426. .iter()
  427. .any(|f| matches!(f, FnArg::Receiver(_)))
  428. {
  429. return Err(Error::new(
  430. item_fn.sig.inputs.span(),
  431. "Receiver parameters are not supported".to_string(),
  432. ));
  433. }
  434. Ok(())
  435. }
  436. /// Convert a function arg with a given visibility (provided by the function) and then generate a field for the
  437. /// associated props struct.
  438. fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
  439. // There's no receivers (&self) allowed in the component body
  440. let FnArg::Typed(pt) = f else { unreachable!() };
  441. let arg_pat = match pt.pat.as_ref() {
  442. // rip off mutability
  443. // todo: we actually don't want any of the extra bits of the field pattern
  444. Pat::Ident(f) => {
  445. let mut f = f.clone();
  446. f.mutability = None;
  447. quote! { #f }
  448. }
  449. a => quote! { #a },
  450. };
  451. let PatType {
  452. attrs,
  453. ty,
  454. colon_token,
  455. ..
  456. } = pt;
  457. quote! {
  458. #(#attrs)*
  459. #vis #arg_pat #colon_token #ty
  460. }
  461. }
  462. /// Get ident from a function arg
  463. fn make_prop_struct_field_idents(f: &FnArg) -> &Ident {
  464. // There's no receivers (&self) allowed in the component body
  465. let FnArg::Typed(pt) = f else { unreachable!() };
  466. match pt.pat.as_ref() {
  467. // rip off mutability
  468. // todo: we actually don't want any of the extra bits of the field pattern
  469. Pat::Ident(f) => &f.ident,
  470. _ => unreachable!(),
  471. }
  472. }
  473. fn make_prop_struct_generics(generics: &GenericParam) -> TokenStream {
  474. match generics {
  475. GenericParam::Type(ty) => {
  476. let ident = &ty.ident;
  477. quote! { #ident }
  478. }
  479. GenericParam::Lifetime(lifetime) => {
  480. let lifetime = &lifetime.lifetime;
  481. quote! { #lifetime }
  482. }
  483. GenericParam::Const(c) => {
  484. let ident = &c.ident;
  485. quote! { #ident }
  486. }
  487. }
  488. }
  489. fn rebind_mutability(f: &FnArg) -> TokenStream {
  490. // There's no receivers (&self) allowed in the component body
  491. let FnArg::Typed(pt) = f else { unreachable!() };
  492. let immutable = strip_pat_mutability(&pt.pat);
  493. quote!(mut #immutable)
  494. }
  495. fn strip_pat_mutability(pat: &Pat) -> Pat {
  496. let mut pat = pat.clone();
  497. // rip off mutability, but still write it out eventually
  498. if let Pat::Ident(ref mut pat_ident) = &mut pat {
  499. pat_ident.mutability = None;
  500. }
  501. pat
  502. }
  503. /// Checks if the attribute is a `#[doc]` attribute.
  504. fn is_attr_doc(attr: &Attribute) -> bool {
  505. attr.path() == &parse_quote!(doc)
  506. }
  507. fn keep_up_to_n_consecutive_chars(
  508. input: &str,
  509. n_of_consecutive_chars_allowed: usize,
  510. target_char: char,
  511. ) -> String {
  512. let mut output = String::new();
  513. let mut prev_char: Option<char> = None;
  514. let mut consecutive_count = 0;
  515. for c in input.chars() {
  516. match prev_char {
  517. Some(prev) if c == target_char && prev == target_char => {
  518. if consecutive_count < n_of_consecutive_chars_allowed {
  519. output.push(c);
  520. consecutive_count += 1;
  521. }
  522. }
  523. _ => {
  524. output.push(c);
  525. prev_char = Some(c);
  526. consecutive_count = 1;
  527. }
  528. }
  529. }
  530. output
  531. }
  532. /// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler.
  533. fn allow_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn {
  534. let mut clone = item_fn.clone();
  535. let block = &item_fn.block;
  536. clone.attrs.push(parse_quote! { #[allow(non_snake_case)] });
  537. clone.block = parse_quote! {
  538. {
  539. #block
  540. }
  541. };
  542. clone
  543. }
  544. #[derive(Default)]
  545. pub struct ComponentMacroOptions {
  546. pub lazy: bool,
  547. }
  548. impl Parse for ComponentMacroOptions {
  549. fn parse(input: ParseStream) -> Result<Self> {
  550. let mut lazy_load = false;
  551. while !input.is_empty() {
  552. let ident = input.parse::<Ident>()?;
  553. let ident_name = ident.to_string();
  554. if ident_name == "lazy" {
  555. lazy_load = true;
  556. } else if ident_name == "no_case_check" {
  557. // we used to have this?
  558. } else {
  559. return Err(Error::new(
  560. ident.span(),
  561. "Unknown option for component macro",
  562. ));
  563. }
  564. if input.peek(Token![,]) {
  565. input.parse::<Token![,]>()?;
  566. }
  567. }
  568. Ok(Self { lazy: lazy_load })
  569. }
  570. }