rsx_block.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. //! An arbitrary block parser.
  2. //!
  3. //! Is meant to parse the contents of a block that is either a component or an element.
  4. //! We put these together to cut down on code duplication and make the parsers a bit more resilient.
  5. //!
  6. //! This involves custom structs for name, attributes, and children, as well as a custom parser for the block itself.
  7. //! It also bubbles out diagnostics if it can to give better errors.
  8. use crate::innerlude::*;
  9. use proc_macro2::Span;
  10. use proc_macro2_diagnostics::SpanDiagnosticExt;
  11. use syn::{
  12. ext::IdentExt,
  13. parse::{Parse, ParseBuffer, ParseStream},
  14. spanned::Spanned,
  15. token::{self, Brace},
  16. Expr, Ident, LitStr, Token,
  17. };
  18. /// An item in the form of
  19. ///
  20. /// {
  21. /// attributes,
  22. /// ..spreads,
  23. /// children
  24. /// }
  25. ///
  26. /// Does not make any guarnatees about the contents of the block - this is meant to be verified by the
  27. /// element/component impls themselves.
  28. ///
  29. /// The name of the block is expected to be parsed by the parent parser. It will accept items out of
  30. /// order if possible and then bubble up diagnostics to the parent. This lets us give better errors
  31. /// and autocomplete
  32. #[derive(PartialEq, Eq, Clone, Debug, Default)]
  33. pub struct RsxBlock {
  34. pub brace: token::Brace,
  35. pub attributes: Vec<Attribute>,
  36. pub spreads: Vec<Spread>,
  37. pub children: Vec<BodyNode>,
  38. pub diagnostics: Diagnostics,
  39. }
  40. impl Parse for RsxBlock {
  41. fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
  42. let content: ParseBuffer;
  43. let brace = syn::braced!(content in input);
  44. RsxBlock::parse_inner(&content, brace)
  45. }
  46. }
  47. impl RsxBlock {
  48. /// Only parse the children of the block - all others will be rejected
  49. pub fn parse_children(content: &ParseBuffer) -> syn::Result<Self> {
  50. let mut nodes = vec![];
  51. let mut diagnostics = Diagnostics::new();
  52. while !content.is_empty() {
  53. nodes.push(Self::parse_body_node_with_comma_diagnostics(
  54. content,
  55. &mut diagnostics,
  56. )?);
  57. }
  58. Ok(Self {
  59. children: nodes,
  60. diagnostics,
  61. ..Default::default()
  62. })
  63. }
  64. pub fn parse_inner(content: &ParseBuffer, brace: token::Brace) -> syn::Result<Self> {
  65. let mut items = vec![];
  66. let mut diagnostics = Diagnostics::new();
  67. // If we are after attributes, we can try to provide better completions and diagnostics
  68. // by parsing the following nodes as body nodes if they are ambiguous, we can parse them as body nodes
  69. let mut after_attributes = false;
  70. // Lots of manual parsing but it's important to do it all here to give the best diagnostics possible
  71. // We can do things like lookaheads, peeking, etc. to give better errors and autocomplete
  72. // We allow parsing in any order but complain if its done out of order.
  73. // Autofmt will fortunately fix this for us in most cases
  74. //
  75. // We do this by parsing the unambiguous cases first and then do some clever lookahead to parse the rest
  76. while !content.is_empty() {
  77. // Parse spread attributes
  78. if content.peek(Token![..]) {
  79. let dots = content.parse::<Token![..]>()?;
  80. // in case someone tries to do ...spread which is not valid
  81. if let Ok(extra) = content.parse::<Token![.]>() {
  82. diagnostics.push(
  83. extra
  84. .span()
  85. .error("Spread expressions only take two dots - not 3! (..spread)"),
  86. );
  87. }
  88. let expr = content.parse::<Expr>()?;
  89. let attr = Spread {
  90. expr,
  91. dots,
  92. dyn_idx: DynIdx::default(),
  93. comma: content.parse().ok(),
  94. };
  95. if !content.is_empty() && attr.comma.is_none() {
  96. diagnostics.push(
  97. attr.span()
  98. .error("Attributes must be separated by commas")
  99. .help("Did you forget a comma?"),
  100. );
  101. }
  102. items.push(RsxItem::Spread(attr));
  103. after_attributes = true;
  104. continue;
  105. }
  106. // Parse unambiguous attributes - these can't be confused with anything
  107. if (content.peek(LitStr) || content.peek(Ident) || content.peek(Ident::peek_any))
  108. && content.peek2(Token![:])
  109. && !content.peek3(Token![:])
  110. {
  111. let attr = content.parse::<Attribute>()?;
  112. if !content.is_empty() && attr.comma.is_none() {
  113. diagnostics.push(
  114. attr.span()
  115. .error("Attributes must be separated by commas")
  116. .help("Did you forget a comma?"),
  117. );
  118. }
  119. items.push(RsxItem::Attribute(attr));
  120. continue;
  121. }
  122. // Eagerly match on completed children, generally
  123. if content.peek(LitStr)
  124. | content.peek(Token![for])
  125. | content.peek(Token![if])
  126. | content.peek(Token![match])
  127. | content.peek(token::Brace)
  128. // web components
  129. | (content.peek(Ident::peek_any) && content.peek2(Token![-]))
  130. // elements
  131. | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace)))
  132. // components
  133. | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace) || content.peek2(Token![::])))
  134. {
  135. items.push(RsxItem::Child(
  136. Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
  137. ));
  138. if !content.is_empty() && content.peek(Token![,]) {
  139. let comma = content.parse::<Token![,]>()?;
  140. diagnostics.push(
  141. comma.span().warning(
  142. "Elements and text nodes do not need to be separated by commas.",
  143. ),
  144. );
  145. }
  146. after_attributes = true;
  147. continue;
  148. }
  149. // Parse shorthand attributes
  150. // todo: this might cause complications with partial expansion... think more about the cases
  151. // where we can imagine expansion and what better diagnostics we can provide
  152. if Self::peek_lowercase_ident(&content)
  153. && !content.peek2(Brace)
  154. && !content.peek2(Token![:]) // regular attributes / components with generics
  155. && !content.peek2(Token![-]) // web components
  156. && !content.peek2(Token![<]) // generics on components
  157. // generics on components
  158. && !content.peek2(Token![::])
  159. {
  160. let attribute = content.parse::<Attribute>()?;
  161. if !content.is_empty() && attribute.comma.is_none() {
  162. diagnostics.push(
  163. attribute
  164. .span()
  165. .error("Attributes must be separated by commas")
  166. .help("Did you forget a comma?"),
  167. );
  168. }
  169. items.push(RsxItem::Attribute(attribute));
  170. continue;
  171. }
  172. // Finally just attempt a bodynode parse
  173. items.push(RsxItem::Child(
  174. Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
  175. ))
  176. }
  177. // Validate the order of the items
  178. RsxBlock::validate(&items, &mut diagnostics);
  179. // todo: maybe make this a method such that the rsxblock is lossless
  180. // Decompose into attributes, spreads, and children
  181. let mut attributes = vec![];
  182. let mut spreads = vec![];
  183. let mut children = vec![];
  184. for item in items {
  185. match item {
  186. RsxItem::Attribute(attr) => attributes.push(attr),
  187. RsxItem::Spread(spread) => spreads.push(spread),
  188. RsxItem::Child(child) => children.push(child),
  189. }
  190. }
  191. Ok(Self {
  192. attributes,
  193. children,
  194. spreads,
  195. brace,
  196. diagnostics,
  197. })
  198. }
  199. // Parse a body node with diagnostics for unnecessary trailing commas
  200. fn parse_body_node_with_comma_diagnostics(
  201. content: &ParseBuffer,
  202. diagnostics: &mut Diagnostics,
  203. ) -> syn::Result<BodyNode> {
  204. let body_node = content.parse::<BodyNode>()?;
  205. if !content.is_empty() && content.peek(Token![,]) {
  206. let comma = content.parse::<Token![,]>()?;
  207. diagnostics.push(
  208. comma
  209. .span()
  210. .warning("Elements and text nodes do not need to be separated by commas."),
  211. );
  212. }
  213. Ok(body_node)
  214. }
  215. fn peek_lowercase_ident(stream: &ParseStream) -> bool {
  216. let Ok(ident) = stream.fork().parse::<Ident>() else {
  217. return false;
  218. };
  219. ident
  220. .to_string()
  221. .chars()
  222. .next()
  223. .unwrap()
  224. .is_ascii_lowercase()
  225. }
  226. /// Ensure the ordering of the items is correct
  227. /// - Attributes must come before children
  228. /// - Spreads must come before children
  229. /// - Spreads must come after attributes
  230. ///
  231. /// div {
  232. /// key: "value",
  233. /// ..props,
  234. /// "Hello, world!"
  235. /// }
  236. fn validate(items: &[RsxItem], diagnostics: &mut Diagnostics) {
  237. #[derive(Debug, PartialEq, Eq)]
  238. enum ValidationState {
  239. Attributes,
  240. Spreads,
  241. Children,
  242. }
  243. use ValidationState::*;
  244. let mut state = ValidationState::Attributes;
  245. for item in items.iter() {
  246. match item {
  247. RsxItem::Attribute(_) => {
  248. if state == Children || state == Spreads {
  249. diagnostics.push(
  250. item.span()
  251. .error("Attributes must come before children in an element"),
  252. );
  253. }
  254. state = Attributes;
  255. }
  256. RsxItem::Spread(_) => {
  257. if state == Children {
  258. diagnostics.push(
  259. item.span()
  260. .error("Spreads must come before children in an element"),
  261. );
  262. }
  263. state = Spreads;
  264. }
  265. RsxItem::Child(_) => {
  266. state = Children;
  267. }
  268. }
  269. }
  270. }
  271. }
  272. pub enum RsxItem {
  273. Attribute(Attribute),
  274. Spread(Spread),
  275. Child(BodyNode),
  276. }
  277. impl RsxItem {
  278. pub fn span(&self) -> Span {
  279. match self {
  280. RsxItem::Attribute(attr) => attr.span(),
  281. RsxItem::Spread(spread) => spread.dots.span(),
  282. RsxItem::Child(child) => child.span(),
  283. }
  284. }
  285. }
  286. #[cfg(test)]
  287. mod tests {
  288. use super::*;
  289. use quote::quote;
  290. #[test]
  291. fn basic_cases() {
  292. let input = quote! {
  293. { "Hello, world!" }
  294. };
  295. let block: RsxBlock = syn::parse2(input).unwrap();
  296. assert_eq!(block.attributes.len(), 0);
  297. assert_eq!(block.children.len(), 1);
  298. let input = quote! {
  299. {
  300. key: "value",
  301. onclick: move |_| {
  302. "Hello, world!"
  303. },
  304. ..spread,
  305. "Hello, world!"
  306. }
  307. };
  308. let block: RsxBlock = syn::parse2(input).unwrap();
  309. dbg!(block);
  310. let complex_element = quote! {
  311. {
  312. key: "value",
  313. onclick2: move |_| {
  314. "Hello, world!"
  315. },
  316. thing: if true { "value" },
  317. otherthing: if true { "value" } else { "value" },
  318. onclick: move |_| {
  319. "Hello, world!"
  320. },
  321. ..spread,
  322. ..spread1
  323. ..spread2,
  324. "Hello, world!"
  325. }
  326. };
  327. let _block: RsxBlock = syn::parse2(complex_element).unwrap();
  328. let complex_component = quote! {
  329. {
  330. key: "value",
  331. onclick2: move |_| {
  332. "Hello, world!"
  333. },
  334. ..spread,
  335. "Hello, world!"
  336. }
  337. };
  338. let _block: RsxBlock = syn::parse2(complex_component).unwrap();
  339. }
  340. /// Some tests of partial expansion to give better autocomplete
  341. #[test]
  342. fn partial_cases() {
  343. let with_hander = quote! {
  344. {
  345. onclick: move |_| {
  346. some.
  347. }
  348. }
  349. };
  350. let _block: RsxBlock = syn::parse2(with_hander).unwrap();
  351. }
  352. /// Ensure the hotreload scoring algorithm works as expected
  353. #[test]
  354. fn hr_score() {
  355. let _block = quote! {
  356. {
  357. a: "value {cool}",
  358. b: "{cool} value",
  359. b: "{cool} {thing} value",
  360. b: "{thing} value",
  361. }
  362. };
  363. // loop { accumulate perfect matches }
  364. // stop when all matches are equally valid
  365. //
  366. // Remove new attr one by one as we find its perfect match. If it doesn't have a perfect match, we
  367. // score it instead.
  368. quote! {
  369. // start with
  370. div {
  371. div { class: "other {abc} {def} {hij}" } // 1, 1, 1
  372. div { class: "thing {abc} {def}" } // 1, 1, 1
  373. // div { class: "thing {abc}" } // 1, 0, 1
  374. }
  375. // end with
  376. div {
  377. h1 {
  378. class: "thing {abc}" // 1, 1, MAX
  379. }
  380. h1 {
  381. class: "thing {hij}" // 1, 1, MAX
  382. }
  383. // h2 {
  384. // class: "thing {def}" // 1, 1, 0
  385. // }
  386. // h3 {
  387. // class: "thing {def}" // 1, 1, 0
  388. // }
  389. }
  390. // how about shuffling components, for, if, etc
  391. Component {
  392. class: "thing {abc}",
  393. other: "other {abc} {def}",
  394. }
  395. Component {
  396. class: "thing",
  397. other: "other",
  398. }
  399. Component {
  400. class: "thing {abc}",
  401. other: "other",
  402. }
  403. Component {
  404. class: "thing {abc}",
  405. other: "other {abc} {def}",
  406. }
  407. };
  408. }
  409. #[test]
  410. fn kitchen_sink_parse() {
  411. let input = quote! {
  412. // Elements
  413. {
  414. class: "hello",
  415. id: "node-{node_id}",
  416. ..props,
  417. // Text Nodes
  418. "Hello, world!"
  419. // Exprs
  420. {rsx! { "hi again!" }}
  421. for item in 0..10 {
  422. // "Second"
  423. div { "cool-{item}" }
  424. }
  425. Link {
  426. to: "/home",
  427. class: "link {is_ready}",
  428. "Home"
  429. }
  430. if false {
  431. div { "hi again!?" }
  432. } else if true {
  433. div { "its cool?" }
  434. } else {
  435. div { "not nice !" }
  436. }
  437. }
  438. };
  439. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  440. }
  441. #[test]
  442. fn simple_comp_syntax() {
  443. let input = quote! {
  444. { class: "inline-block mr-4", icons::icon_14 {} }
  445. };
  446. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  447. }
  448. #[test]
  449. fn with_sutter() {
  450. let input = quote! {
  451. {
  452. div {}
  453. d
  454. div {}
  455. }
  456. };
  457. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  458. }
  459. #[test]
  460. fn looks_like_prop_but_is_expr() {
  461. let input = quote! {
  462. {
  463. a: "asd".to_string(),
  464. // b can be omitted, and it will be filled with its default value
  465. c: "asd".to_string(),
  466. d: Some("asd".to_string()),
  467. e: Some("asd".to_string()),
  468. }
  469. };
  470. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  471. }
  472. #[test]
  473. fn no_comma_diagnostics() {
  474. let input = quote! {
  475. { a, ..ComponentProps { a: 1, b: 2, c: 3, children: VNode::empty(), onclick: Default::default() } }
  476. };
  477. let parsed: RsxBlock = syn::parse2(input).unwrap();
  478. assert!(parsed.diagnostics.is_empty());
  479. }
  480. #[test]
  481. fn proper_attributes() {
  482. let input = quote! {
  483. {
  484. onclick: action,
  485. href,
  486. onmounted: onmounted,
  487. prevent_default,
  488. class,
  489. rel,
  490. target: tag_target,
  491. aria_current,
  492. ..attributes,
  493. {children}
  494. }
  495. };
  496. let parsed: RsxBlock = syn::parse2(input).unwrap();
  497. dbg!(parsed.attributes);
  498. }
  499. #[test]
  500. fn reserved_attributes() {
  501. let input = quote! {
  502. {
  503. label {
  504. for: "blah",
  505. }
  506. }
  507. };
  508. let parsed: RsxBlock = syn::parse2(input).unwrap();
  509. dbg!(parsed.attributes);
  510. }
  511. #[test]
  512. fn diagnostics_check() {
  513. let input = quote::quote! {
  514. {
  515. class: "foo bar"
  516. "Hello world"
  517. }
  518. };
  519. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  520. }
  521. #[test]
  522. fn incomplete_components() {
  523. let input = quote::quote! {
  524. {
  525. some::cool::Component
  526. }
  527. };
  528. let _parsed: RsxBlock = syn::parse2(input).unwrap();
  529. }
  530. #[test]
  531. fn incomplete_root_elements() {
  532. use syn::parse::Parser;
  533. let input = quote::quote! {
  534. di
  535. };
  536. let parsed = RsxBlock::parse_children.parse2(input).unwrap();
  537. let children = parsed.children;
  538. assert_eq!(children.len(), 1);
  539. if let BodyNode::Element(parsed) = &children[0] {
  540. assert_eq!(
  541. parsed.name,
  542. ElementName::Ident(Ident::new("di", Span::call_site()))
  543. );
  544. } else {
  545. panic!("expected element, got {:?}", children);
  546. }
  547. assert!(parsed.diagnostics.is_empty());
  548. }
  549. }