From fa9d92f956403e6428e13eff175c2df173f9ceee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 22:41:40 -0800 Subject: [PATCH] Fix tests for autofmt --- examples/shorthand.rs | 25 ++ packages/autofmt/src/component.rs | 4 + packages/autofmt/src/writer.rs | 44 ++- .../autofmt/tests/samples/commentshard.rsx | 6 +- packages/autofmt/tests/samples/complex.rsx | 8 +- .../autofmt/tests/samples/ifchain_forloop.rsx | 16 +- .../autofmt/tests/samples/immediate_expr.rsx | 2 +- packages/autofmt/tests/samples/long.rsx | 8 +- packages/autofmt/tests/samples/long_exprs.rsx | 2 +- packages/autofmt/tests/samples/multirsx.rsx | 10 +- .../autofmt/tests/samples/trailing_expr.rsx | 2 +- .../autofmt/tests/wrong/multiexpr-4sp.rsx | 4 +- .../tests/wrong/multiexpr-4sp.wrong.rsx | 2 +- .../autofmt/tests/wrong/multiexpr-tab.rsx | 4 +- .../tests/wrong/multiexpr-tab.wrong.rsx | 2 +- packages/rsx/src/component.rs | 324 ++++++++++-------- packages/rsx/src/node.rs | 1 - 17 files changed, 287 insertions(+), 177 deletions(-) create mode 100644 examples/shorthand.rs diff --git a/examples/shorthand.rs b/examples/shorthand.rs new file mode 100644 index 000000000..44a1650e6 --- /dev/null +++ b/examples/shorthand.rs @@ -0,0 +1,25 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let a = 123; + let b = 456; + let c = 789; + + render! { + Component { a, b, c } + Component { a, ..ComponentProps { a: 1, b: 2, c: 3 } } + } +} + +#[component] +fn Component(cx: Scope, a: i32, b: i32, c: i32) -> Element { + render! { + div { "{a}" } + div { "{b}" } + div { "{c}" } + } +} diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 1eea2eba2..c83f9b333 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -182,6 +182,9 @@ impl Writer<'_> { s.source.as_ref().unwrap().to_token_stream() )?; } + ContentField::Shorthand(e) => { + write!(self.out, "{}", e.to_token_stream())?; + } ContentField::OnHandlerRaw(exp) => { let out = prettyplease::unparse_expr(exp); let mut lines = out.split('\n').peekable(); @@ -223,6 +226,7 @@ impl Writer<'_> { .iter() .map(|field| match &field.content { ContentField::Formatted(s) => ifmt_to_string(s).len() , + ContentField::Shorthand(e) => e.to_token_stream().to_string().len(), ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => { let formatted = prettyplease::unparse_expr(exp); let len = if formatted.contains('\n') { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index c7bf300d5..78736acfe 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -232,8 +232,48 @@ impl<'a> Writer<'a> { } fn write_if_chain(&mut self, ifchain: &IfChain) -> std::fmt::Result { - todo!() - // self.write_raw_expr(ifchain.span()) + // Recurse in place by setting the next chain + let mut branch = Some(ifchain); + + while let Some(chain) = branch { + let IfChain { + if_token, + cond, + then_branch, + else_if_branch, + else_branch, + } = chain; + + write!( + self.out, + "{} {} {{", + if_token.to_token_stream(), + prettyplease::unparse_expr(cond) + )?; + + self.write_body_indented(then_branch)?; + + if let Some(else_if_branch) = else_if_branch { + // write the closing bracket and else + self.out.tabbed_line()?; + write!(self.out, "}} else ")?; + + branch = Some(else_if_branch); + } else if let Some(else_branch) = else_branch { + self.out.tabbed_line()?; + write!(self.out, "}} else {{")?; + + self.write_body_indented(else_branch)?; + branch = None; + } else { + branch = None; + } + } + + self.out.tabbed_line()?; + write!(self.out, "}}")?; + + Ok(()) } } diff --git a/packages/autofmt/tests/samples/commentshard.rsx b/packages/autofmt/tests/samples/commentshard.rsx index f6c2fc19e..71782f39d 100644 --- a/packages/autofmt/tests/samples/commentshard.rsx +++ b/packages/autofmt/tests/samples/commentshard.rsx @@ -8,17 +8,17 @@ rsx! { "hello world" // Comments - expr1, + {expr1}, // Comments - expr2, + {expr2}, // Comments // Comments // Comments // Comments // Comments - expr3, + {expr3}, div { // todo some work in here diff --git a/packages/autofmt/tests/samples/complex.rsx b/packages/autofmt/tests/samples/complex.rsx index 1bf29a767..56c16e657 100644 --- a/packages/autofmt/tests/samples/complex.rsx +++ b/packages/autofmt/tests/samples/complex.rsx @@ -18,7 +18,7 @@ rsx! { to: "{to}", span { class: "inline-block mr-3", icons::icon_0 {} } span { "{name}" } - children.is_some().then(|| rsx! { + {children.is_some().then(|| rsx! { span { class: "inline-block ml-auto hover:bg-gray-500", onclick: move |evt| { @@ -27,9 +27,9 @@ rsx! { }, icons::icon_8 {} } - }) + })} } - div { class: "px-4", is_current.then(|| rsx!{ children }) } + div { class: "px-4", {is_current.then(|| rsx!{ children })} } } // No nesting @@ -48,5 +48,5 @@ rsx! { } } - div { asdbascasdbasd, asbdasbdabsd, asbdabsdbasdbas } + div { "asdbascasdbasd", "asbdasbdabsd", {asbdabsdbasdbas} } } diff --git a/packages/autofmt/tests/samples/ifchain_forloop.rsx b/packages/autofmt/tests/samples/ifchain_forloop.rsx index 9ec7326af..728b2d3d3 100644 --- a/packages/autofmt/tests/samples/ifchain_forloop.rsx +++ b/packages/autofmt/tests/samples/ifchain_forloop.rsx @@ -8,6 +8,20 @@ rsx! { // Some ifchain if a > 10 { // - rsx! { div {} } + div {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else { + h3 {} } } diff --git a/packages/autofmt/tests/samples/immediate_expr.rsx b/packages/autofmt/tests/samples/immediate_expr.rsx index 9e298499d..cea8dc7db 100644 --- a/packages/autofmt/tests/samples/immediate_expr.rsx +++ b/packages/autofmt/tests/samples/immediate_expr.rsx @@ -1,4 +1,4 @@ fn it_works() { - cx.render(rsx!(())) + cx.render(rsx!({()})) } diff --git a/packages/autofmt/tests/samples/long.rsx b/packages/autofmt/tests/samples/long.rsx index 1bda8a006..056e16061 100644 --- a/packages/autofmt/tests/samples/long.rsx +++ b/packages/autofmt/tests/samples/long.rsx @@ -11,7 +11,7 @@ pub fn Explainer<'a>( // pt-5 sm:pt-24 lg:pt-24 let mut right = rsx! { - div { class: "relative w-1/2", flasher } + div { class: "relative w-1/2", {flasher} } }; let align = match invert { @@ -24,7 +24,7 @@ pub fn Explainer<'a>( h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold", "{title}" } - content + {content} } }; @@ -34,8 +34,8 @@ pub fn Explainer<'a>( cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/samples/long_exprs.rsx b/packages/autofmt/tests/samples/long_exprs.rsx index fb4dab93f..490bc5541 100644 --- a/packages/autofmt/tests/samples/long_exprs.rsx +++ b/packages/autofmt/tests/samples/long_exprs.rsx @@ -6,7 +6,7 @@ rsx! { section { class: "body-font overflow-hidden dark:bg-ideblack", div { class: "container px-6 mx-auto", div { class: "-my-8 divide-y-2 divide-gray-100", - POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } }) + {POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })} } } } diff --git a/packages/autofmt/tests/samples/multirsx.rsx b/packages/autofmt/tests/samples/multirsx.rsx index 380ef3487..84340f5ba 100644 --- a/packages/autofmt/tests/samples/multirsx.rsx +++ b/packages/autofmt/tests/samples/multirsx.rsx @@ -4,20 +4,20 @@ rsx! { div {} // hi - div { abcd, ball, s } + div { "abcd", "ball", "s" } // // // - div { abcd, ball, s } + div { "abcd", "ball", "s" } // // // div { - abcd, - ball, - s, + "abcd" + "ball" + "s" // "asdasd" diff --git a/packages/autofmt/tests/samples/trailing_expr.rsx b/packages/autofmt/tests/samples/trailing_expr.rsx index 667b8bb32..a57a822ab 100644 --- a/packages/autofmt/tests/samples/trailing_expr.rsx +++ b/packages/autofmt/tests/samples/trailing_expr.rsx @@ -1,7 +1,7 @@ fn it_works() { cx.render(rsx! { div { - span { "Description: ", package.description.as_deref().unwrap_or("❌❌❌❌ missing") } + span { "Description: ", {package.description.as_deref().unwrap_or("❌❌❌❌ missing")} } } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-4sp.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx index 56c57e37e..ecb2f8bdc 100644 --- a/packages/autofmt/tests/wrong/multiexpr-4sp.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx @@ -1,8 +1,8 @@ fn ItWroks() { cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx index 5732f2cdd..15752f172 100644 --- a/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx @@ -1,5 +1,5 @@ fn ItWroks() { cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.rsx index 1fc3401c4..85f2216a1 100644 --- a/packages/autofmt/tests/wrong/multiexpr-tab.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-tab.rsx @@ -1,8 +1,8 @@ fn ItWroks() { cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx index 073107541..6872d0b43 100644 --- a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx @@ -1,5 +1,5 @@ fn ItWroks() { cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} } }) } diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index bccef2638..528a39072 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -19,6 +19,7 @@ use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, spanned::Spanned, + token::Brace, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, Token, }; @@ -32,8 +33,90 @@ pub struct Component { pub brace: syn::token::Brace, } +impl Parse for Component { + fn parse(stream: ParseStream) -> Result { + let mut name = stream.parse::()?; + Component::validate_component_path(&name)?; + + // extract the path arguments from the path into prop_gen_args + let prop_gen_args = normalize_path(&mut name); + + let content: ParseBuffer; + let brace = syn::braced!(content in stream); + + let mut fields = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + + while !content.is_empty() { + // if we splat into a component then we're merging properties + if content.peek(Token![..]) { + content.parse::()?; + manual_props = Some(content.parse()?); + } else + // + // Fields + // Named fields + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + fields.push(content.parse()?); + } else + // + // shorthand struct initialization + // Not a div {}, mod::Component {}, or web-component {} + if content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-]) + { + fields.push(content.parse()?); + } else { + children.push(content.parse()?); + } + + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + name, + prop_gen_args, + fields, + children, + manual_props, + brace, + }) + } +} + +impl ToTokens for Component { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Self { + name, + prop_gen_args, + .. + } = self; + + let builder = self + .manual_props + .as_ref() + .map(|props| self.collect_manual_props(props)) + .unwrap_or_else(|| self.collect_props()); + + let fn_name = self.fn_name(); + + tokens.append_all(quote! { + __cx.component( + #name #prop_gen_args, + #builder, + #fn_name + ) + }) + } +} + impl Component { - pub fn validate_component_path(path: &syn::Path) -> Result<()> { + fn validate_component_path(path: &syn::Path) -> Result<()> { // ensure path segments doesn't have PathArguments, only the last // segment is allowed to have one. if path @@ -67,134 +150,53 @@ impl Component { _ => None, } } -} -impl Parse for Component { - fn parse(stream: ParseStream) -> Result { - let mut name = stream.parse::()?; - Component::validate_component_path(&name)?; - - // extract the path arguments from the path into prop_gen_args - let prop_gen_args = name.segments.last_mut().and_then(|seg| { - if let PathArguments::AngleBracketed(args) = seg.arguments.clone() { - seg.arguments = PathArguments::None; - Some(args) - } else { - None + fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 { + let mut toks = quote! { let mut __manual_props = #manual_props; }; + for field in &self.fields { + if field.name == "key" { + continue; } - }); + let ComponentField { name, content } = field; + toks.append_all(quote! { __manual_props.#name = #content; }); + } + toks.append_all(quote! { __manual_props }); + quote! {{ #toks }} + } - let content: ParseBuffer; + fn collect_props(&self) -> TokenStream2 { + let name = &self.name; - // if we see a `{` then we have a block - // else parse as a function-like call - let brace = syn::braced!(content in stream); - - let mut fields = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - - while !content.is_empty() { - // if we splat into a component then we're merging properties - if content.peek(Token![..]) { - content.parse::()?; - manual_props = Some(content.parse::()?); - } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - fields.push(content.parse::()?); - } else { - children.push(content.parse::()?); - } - - if content.peek(Token![,]) { - let _ = content.parse::(); + let mut toks = match &self.prop_gen_args { + Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, + None => quote! { fc_to_builder(__cx, #name) }, + }; + for field in &self.fields { + match field.name.to_string().as_str() { + "key" => {} + _ => toks.append_all(quote! {#field}), } } + if !self.children.is_empty() { + let renderer: TemplateRenderer = TemplateRenderer { + roots: &self.children, + location: None, + }; - Ok(Self { - name, - prop_gen_args, - fields, - children, - manual_props, - brace, - }) + toks.append_all(quote! { + .children( + Some({ #renderer }) + ) + }); + } + toks.append_all(quote! { + .build() + }); + toks } -} -impl ToTokens for Component { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - let prop_gen_args = &self.prop_gen_args; - - let builder = match &self.manual_props { - Some(manual_props) => { - let mut toks = quote! { - let mut __manual_props = #manual_props; - }; - for field in &self.fields { - if field.name == "key" { - // skip keys - } else { - let name = &field.name; - let val = &field.content; - toks.append_all(quote! { - __manual_props.#name = #val; - }); - } - } - toks.append_all(quote! { - __manual_props - }); - quote! {{ - #toks - }} - } - None => { - let mut toks = match prop_gen_args { - Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, - None => quote! { fc_to_builder(__cx, #name) }, - }; - for field in &self.fields { - match field.name.to_string().as_str() { - "key" => {} - _ => toks.append_all(quote! {#field}), - } - } - - if !self.children.is_empty() { - let renderer: TemplateRenderer = TemplateRenderer { - roots: &self.children, - location: None, - }; - - toks.append_all(quote! { - .children( - Some({ #renderer }) - ) - }); - } - - toks.append_all(quote! { - .build() - }); - toks - } - }; - - let fn_name = self.name.segments.last().unwrap().ident.to_string(); - - let gen_name = match &self.prop_gen_args { - Some(gen) => quote! { #name #gen }, - None => quote! { #name }, - }; - - tokens.append_all(quote! { - __cx.component( - #gen_name, - #builder, - #fn_name - ) - }) + fn fn_name(&self) -> String { + self.name.segments.last().unwrap().ident.to_string() } } @@ -207,21 +209,47 @@ pub struct ComponentField { #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum ContentField { + Shorthand(Ident), ManExpr(Expr), Formatted(IfmtInput), OnHandlerRaw(Expr), } +impl ContentField { + fn new_from_name(name: &Ident, input: ParseStream) -> Result { + if name.to_string().starts_with("on") { + return Ok(ContentField::OnHandlerRaw(input.parse()?)); + } + + if *name == "key" { + return Ok(ContentField::Formatted(input.parse()?)); + } + + if input.peek(LitStr) { + let forked = input.fork(); + let t: LitStr = forked.parse()?; + + // the string literal must either be the end of the input or a followed by a comma + let res = + match (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) { + true => ContentField::Formatted(input.parse()?), + false => ContentField::ManExpr(input.parse()?), + }; + + return Ok(res); + } + + Ok(ContentField::ManExpr(input.parse()?)) + } +} + impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { + ContentField::Shorthand(i) => tokens.append_all(quote! { #i }), ContentField::ManExpr(e) => e.to_tokens(tokens), - ContentField::Formatted(s) => tokens.append_all(quote! { - __cx.raw_text(#s) - }), - ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { - __cx.event_handler(#e) - }), + ContentField::Formatted(s) => tokens.append_all(quote! { __cx.raw_text(#s) }), + ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.event_handler(#e) }), } } } @@ -229,30 +257,21 @@ impl ToTokens for ContentField { impl Parse for ComponentField { fn parse(input: ParseStream) -> Result { let name = Ident::parse_any(input)?; - input.parse::()?; - let content = { - if name.to_string().starts_with("on") { - ContentField::OnHandlerRaw(input.parse()?) - } else if name == "key" { - let content = ContentField::Formatted(input.parse()?); - return Ok(Self { name, content }); - } else if input.peek(LitStr) { - let forked = input.fork(); - let t: LitStr = forked.parse()?; - // the string literal must either be the end of the input or a followed by a comma - if (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) { - ContentField::Formatted(input.parse()?) - } else { - ContentField::ManExpr(input.parse()?) - } - } else { - ContentField::ManExpr(input.parse()?) - } + // if the next token is not a colon, then it's a shorthand field + if input.parse::().is_err() { + return Ok(Self { + content: ContentField::Shorthand(name.clone()), + name, + }); }; + + let content = ContentField::new_from_name(&name, input)?; + if input.peek(LitStr) || input.peek(Ident) { missing_trailing_comma!(content.span()); } + Ok(Self { name, content }) } } @@ -260,9 +279,7 @@ impl Parse for ComponentField { impl ToTokens for ComponentField { fn to_tokens(&self, tokens: &mut TokenStream2) { let ComponentField { name, content, .. } = self; - tokens.append_all(quote! { - .#name(#content) - }) + tokens.append_all(quote! { .#name(#content) }) } } @@ -281,3 +298,14 @@ fn is_literal_foramtted(lit: &LitStr) -> bool { false } + +fn normalize_path(name: &mut syn::Path) -> Option { + let seg = name.segments.last_mut()?; + match seg.arguments.clone() { + PathArguments::AngleBracketed(args) => { + seg.arguments = PathArguments::None; + Some(args) + } + _ => None, + } +} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index a6f0aea06..02fab0752 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -98,7 +98,6 @@ impl Parse for BodyNode { // Input:: {} // crate::Input:: {} if body_stream.peek(token::Brace) { - Component::validate_component_path(&path)?; return Ok(BodyNode::Component(stream.parse()?)); } }