component.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. use proc_macro2::TokenStream;
  2. use quote::{quote, ToTokens, TokenStreamExt};
  3. use syn::parse::{Parse, ParseStream};
  4. use syn::spanned::Spanned;
  5. use syn::*;
  6. pub struct ComponentBody {
  7. pub item_fn: ItemFn,
  8. }
  9. impl Parse for ComponentBody {
  10. fn parse(input: ParseStream) -> Result<Self> {
  11. let item_fn: ItemFn = input.parse()?;
  12. validate_component_fn_signature(&item_fn)?;
  13. Ok(Self { item_fn })
  14. }
  15. }
  16. impl ToTokens for ComponentBody {
  17. fn to_tokens(&self, tokens: &mut TokenStream) {
  18. // https://github.com/DioxusLabs/dioxus/issues/1938
  19. // If there's only one input and the input is `props: Props`, we don't need to generate a props struct
  20. // Just attach the non_snake_case attribute to the function
  21. // eventually we'll dump this metadata into devtooling that lets us find all these components
  22. if self.is_explicit_props_ident() {
  23. let comp_fn = &self.item_fn;
  24. tokens.append_all(quote! {
  25. #[allow(non_snake_case)]
  26. #comp_fn
  27. });
  28. return;
  29. }
  30. let comp_fn = self.comp_fn();
  31. // If there's no props declared, we simply omit the props argument
  32. // This is basically so you can annotate the App component with #[component] and still be compatible with the
  33. // launch signatures that take fn() -> Element
  34. let props_struct = match self.item_fn.sig.inputs.is_empty() {
  35. // No props declared, so we don't need to generate a props struct
  36. true => quote! {},
  37. // Props declared, so we generate a props struct and thatn also attach the doc attributes to it
  38. false => {
  39. let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
  40. let props_struct = self.props_struct();
  41. quote! {
  42. #[doc = #doc]
  43. #props_struct
  44. }
  45. }
  46. };
  47. tokens.append_all(quote! {
  48. #props_struct
  49. #[allow(non_snake_case)]
  50. #comp_fn
  51. });
  52. }
  53. }
  54. impl ComponentBody {
  55. // build a new item fn, transforming the original item fn
  56. fn comp_fn(&self) -> ItemFn {
  57. let ComponentBody { item_fn, .. } = self;
  58. let ItemFn {
  59. attrs,
  60. vis,
  61. sig,
  62. block,
  63. } = item_fn;
  64. let Signature {
  65. inputs,
  66. ident: fn_ident,
  67. generics,
  68. output: fn_output,
  69. asyncness,
  70. ..
  71. } = sig;
  72. let Generics { where_clause, .. } = generics;
  73. let (_, ty_generics, _) = generics.split_for_impl();
  74. // We generate a struct with the same name as the component but called `Props`
  75. let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
  76. // We pull in the field names from the original function signature, but need to strip off the mutability
  77. let struct_field_names = inputs.iter().filter_map(rebind_mutability);
  78. let props_docs = self.props_docs(inputs.iter().skip(1).collect());
  79. // Don't generate the props argument if there are no inputs
  80. // This means we need to skip adding the argument to the function signature, and also skip the expanded struct
  81. let props_ident = match inputs.is_empty() {
  82. true => quote! {},
  83. false => quote! { mut __props: #struct_ident #ty_generics },
  84. };
  85. let expanded_struct = match inputs.is_empty() {
  86. true => quote! {},
  87. false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
  88. };
  89. parse_quote! {
  90. #(#attrs)*
  91. #(#props_docs)*
  92. #asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause {
  93. #expanded_struct
  94. #block
  95. }
  96. }
  97. }
  98. /// Build an associated struct for the props of the component
  99. ///
  100. /// This will expand to the typed-builder implementation that we have vendored in this crate.
  101. /// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion.
  102. /// TODO: cache these tokens since this codegen is rather expensive (lots of tokens)
  103. ///
  104. /// We try our best to transfer over any declared doc attributes from the original function signature onto the
  105. /// props struct fields.
  106. fn props_struct(&self) -> ItemStruct {
  107. let ItemFn { vis, sig, .. } = &self.item_fn;
  108. let Signature {
  109. inputs,
  110. ident,
  111. generics,
  112. ..
  113. } = sig;
  114. let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
  115. let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
  116. parse_quote! {
  117. #[derive(Props, Clone, PartialEq)]
  118. #[allow(non_camel_case_types)]
  119. #vis struct #struct_ident #generics {
  120. #(#struct_fields),*
  121. }
  122. }
  123. }
  124. /// Convert a list of function arguments into a list of doc attributes for the props struct
  125. ///
  126. /// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring.
  127. fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
  128. let fn_ident = &self.item_fn.sig.ident;
  129. if inputs.len() <= 1 {
  130. return Vec::new();
  131. }
  132. let arg_docs = inputs
  133. .iter()
  134. .filter_map(|f| build_doc_fields(f))
  135. .collect::<Vec<_>>();
  136. let mut props_docs = Vec::with_capacity(5);
  137. let props_def_link = fn_ident.to_string() + "Props";
  138. let header =
  139. format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
  140. props_docs.push(parse_quote! {
  141. #[doc = #header]
  142. });
  143. for arg in arg_docs {
  144. let DocField {
  145. arg_name,
  146. arg_type,
  147. deprecation,
  148. input_arg_doc,
  149. } = arg;
  150. let arg_name = arg_name.into_token_stream().to_string();
  151. let arg_type = crate::utils::format_type_string(arg_type);
  152. let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
  153. .replace("\n\n", "</p><p>");
  154. let prop_def_link = format!("{props_def_link}::{arg_name}");
  155. let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
  156. if let Some(deprecation) = deprecation {
  157. arg_doc.push_str("<p>👎 Deprecated");
  158. if let Some(since) = deprecation.since {
  159. arg_doc.push_str(&format!(" since {since}"));
  160. }
  161. if let Some(note) = deprecation.note {
  162. let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
  163. let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
  164. arg_doc.push_str(&format!(": {note}"));
  165. }
  166. arg_doc.push_str("</p>");
  167. if !input_arg_doc.is_empty() {
  168. arg_doc.push_str("<hr/>");
  169. }
  170. }
  171. if !input_arg_doc.is_empty() {
  172. arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
  173. }
  174. props_docs.push(parse_quote! { #[doc = #arg_doc] });
  175. }
  176. props_docs
  177. }
  178. fn is_explicit_props_ident(&self) -> bool {
  179. if self.item_fn.sig.inputs.len() == 1 {
  180. if let FnArg::Typed(PatType { pat, .. }) = &self.item_fn.sig.inputs[0] {
  181. if let Pat::Ident(ident) = pat.as_ref() {
  182. return ident.ident == "props";
  183. }
  184. }
  185. }
  186. false
  187. }
  188. }
  189. struct DocField<'a> {
  190. arg_name: &'a Pat,
  191. arg_type: &'a Type,
  192. deprecation: Option<crate::utils::DeprecatedAttribute>,
  193. input_arg_doc: String,
  194. }
  195. fn build_doc_fields(f: &FnArg) -> Option<DocField> {
  196. let FnArg::Typed(pt) = f else { unreachable!() };
  197. let arg_doc = pt
  198. .attrs
  199. .iter()
  200. .filter_map(|attr| {
  201. // TODO: Error reporting
  202. // Check if the path of the attribute is "doc"
  203. if !is_attr_doc(attr) {
  204. return None;
  205. };
  206. let Meta::NameValue(meta_name_value) = &attr.meta else {
  207. return None;
  208. };
  209. let Expr::Lit(doc_lit) = &meta_name_value.value else {
  210. return None;
  211. };
  212. let Lit::Str(doc_lit_str) = &doc_lit.lit else {
  213. return None;
  214. };
  215. Some(doc_lit_str.value())
  216. })
  217. .fold(String::new(), |mut doc, next_doc_line| {
  218. doc.push('\n');
  219. doc.push_str(&next_doc_line);
  220. doc
  221. });
  222. Some(DocField {
  223. arg_name: &pt.pat,
  224. arg_type: &pt.ty,
  225. deprecation: pt.attrs.iter().find_map(|attr| {
  226. if attr.path() != &parse_quote!(deprecated) {
  227. return None;
  228. }
  229. let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
  230. match res {
  231. Err(e) => panic!("{}", e.to_string()),
  232. Ok(v) => Some(v),
  233. }
  234. }),
  235. input_arg_doc: arg_doc,
  236. })
  237. }
  238. fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
  239. // Do some validation....
  240. // 1. Ensure the component returns *something*
  241. if item_fn.sig.output == ReturnType::Default {
  242. return Err(Error::new(
  243. item_fn.sig.output.span(),
  244. "Must return a <dioxus_core::Element>".to_string(),
  245. ));
  246. }
  247. // 2. make sure there's no lifetimes on the component - we don't know how to handle those
  248. if item_fn.sig.generics.lifetimes().count() > 0 {
  249. return Err(Error::new(
  250. item_fn.sig.generics.span(),
  251. "Lifetimes are not supported in components".to_string(),
  252. ));
  253. }
  254. // 3. we can't handle async components
  255. if item_fn.sig.asyncness.is_some() {
  256. return Err(Error::new(
  257. item_fn.sig.asyncness.span(),
  258. "Async components are not supported".to_string(),
  259. ));
  260. }
  261. // 4. we can't handle const components
  262. if item_fn.sig.constness.is_some() {
  263. return Err(Error::new(
  264. item_fn.sig.constness.span(),
  265. "Const components are not supported".to_string(),
  266. ));
  267. }
  268. // 5. no receiver parameters
  269. if item_fn
  270. .sig
  271. .inputs
  272. .iter()
  273. .any(|f| matches!(f, FnArg::Receiver(_)))
  274. {
  275. return Err(Error::new(
  276. item_fn.sig.inputs.span(),
  277. "Receiver parameters are not supported".to_string(),
  278. ));
  279. }
  280. Ok(())
  281. }
  282. /// Convert a function arg with a given visibility (provided by the function) and then generate a field for the
  283. /// associated props struct.
  284. fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
  285. // There's no receivers (&self) allowed in the component body
  286. let FnArg::Typed(pt) = f else { unreachable!() };
  287. let arg_pat = match pt.pat.as_ref() {
  288. // rip off mutability
  289. // todo: we actually don't want any of the extra bits of the field pattern
  290. Pat::Ident(f) => {
  291. let mut f = f.clone();
  292. f.mutability = None;
  293. quote! { #f }
  294. }
  295. a => quote! { #a },
  296. };
  297. let PatType {
  298. attrs,
  299. ty,
  300. colon_token,
  301. ..
  302. } = pt;
  303. quote! {
  304. #(#attrs)*
  305. #vis #arg_pat #colon_token #ty
  306. }
  307. }
  308. fn rebind_mutability(f: &FnArg) -> Option<TokenStream> {
  309. // There's no receivers (&self) allowed in the component body
  310. let FnArg::Typed(pt) = f else { unreachable!() };
  311. let pat = &pt.pat;
  312. let mut pat = pat.clone();
  313. // rip off mutability, but still write it out eventually
  314. if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
  315. pat_ident.mutability = None;
  316. }
  317. Some(quote!(mut #pat))
  318. }
  319. /// Checks if the attribute is a `#[doc]` attribute.
  320. fn is_attr_doc(attr: &Attribute) -> bool {
  321. attr.path() == &parse_quote!(doc)
  322. }
  323. fn keep_up_to_n_consecutive_chars(
  324. input: &str,
  325. n_of_consecutive_chars_allowed: usize,
  326. target_char: char,
  327. ) -> String {
  328. let mut output = String::new();
  329. let mut prev_char: Option<char> = None;
  330. let mut consecutive_count = 0;
  331. for c in input.chars() {
  332. match prev_char {
  333. Some(prev) if c == target_char && prev == target_char => {
  334. if consecutive_count < n_of_consecutive_chars_allowed {
  335. output.push(c);
  336. consecutive_count += 1;
  337. }
  338. }
  339. _ => {
  340. output.push(c);
  341. prev_char = Some(c);
  342. consecutive_count = 1;
  343. }
  344. }
  345. }
  346. output
  347. }