diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index bd9e83046..b9903dc47 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -67,7 +67,8 @@ impl Writer { // check if we have a lot of attributes let attr_len = self.is_short_attrs(attributes); let is_short_attr_list = attr_len < 80; - let is_small_children = self.is_short_children(children).is_some(); + let children_len = self.is_short_children(children); + let is_small_children = children_len.is_some(); // if we have few attributes and a lot of children, place the attrs on top if is_short_attr_list && !is_small_children { @@ -85,7 +86,11 @@ impl Writer { // if we have few children and few attributes, make it a one-liner if is_short_attr_list && is_small_children { - opt_level = ShortOptimization::Oneliner; + if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 { + opt_level = ShortOptimization::Oneliner; + } else { + opt_level = ShortOptimization::PropsOnTop; + } } // If there's nothing at all, empty optimization @@ -360,8 +365,8 @@ impl Writer { Some(len) => total_count += len, None => return None, }, - BodyNode::ForLoop(_) => todo!(), - BodyNode::IfChain(_) => todo!(), + BodyNode::ForLoop(_forloop) => return None, + BodyNode::IfChain(_chain) => return None, } } diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 55a5fa9eb..4a916c71a 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -1,16 +1,16 @@ //! pretty printer for rsx! use std::fmt::{Result, Write}; +use proc_macro2::Span; + use crate::Writer; impl Writer { - pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result { + pub fn write_raw_expr(&mut self, placement: Span) -> Result { /* We want to normalize the expr to the appropriate indent level. */ - use syn::spanned::Spanned; - let placement = exp.span(); let start = placement.start(); let end = placement.end(); diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 67f087854..184de1003 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -46,6 +46,8 @@ pub fn fmt_file(contents: &str) -> Vec { use triple_accel::{levenshtein_search, Match}; for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) { + let open = end; + if k > 1 { continue; } @@ -55,7 +57,7 @@ pub fn fmt_file(contents: &str) -> Vec { continue; } - let mut indent_level = { + let indent_level = { // walk backwards from start until we find a new line let mut lines = contents[..start].lines().rev(); match lines.next() { @@ -70,31 +72,34 @@ pub fn fmt_file(contents: &str) -> Vec { } }; - let remaining = &contents[end - 1..]; - let bracket_end = find_bracket_end(remaining).unwrap(); - let sub_string = &contents[end..bracket_end + end - 1]; - last_bracket_end = bracket_end + end - 1; + let remaining = &contents[open - 1..]; + let close = find_bracket_end(remaining).unwrap(); + // Move the last bracket end to the end of this block to avoid nested blocks + last_bracket_end = close + open - 1; - let mut new = fmt_block(sub_string, indent_level).unwrap(); + // Format the substring, doesn't include the outer brackets + let substring = &remaining[1..close - 1]; + // make sure to add back whatever weird whitespace there was at the end + let mut remaining_whitespace = substring.chars().rev().take_while(|c| *c == ' ').count(); + + let mut new = fmt_block(substring, indent_level).unwrap(); + + // if the new string is not multiline, don't try to adjust the marker ending + // We want to trim off any indentation that there might be if new.len() <= 80 && !new.contains('\n') { new = format!(" {new} "); - - // if the new string is not multiline, don't try to adjust the marker ending - // We want to trim off any indentation that there might be - indent_level = 0; + remaining_whitespace = 0; } - let end_marker = end + bracket_end - indent_level * 4 - 1; - - if new == contents[end..end_marker] { + if new == substring { continue; } formatted_blocks.push(FormattedBlock { formatted: new, - start: end, - end: end_marker, + start: open, + end: last_bracket_end - remaining_whitespace - 1, }); } @@ -118,7 +123,7 @@ pub fn write_block_out(body: CallBody) -> Option { } pub fn fmt_block(block: &str, indent_level: usize) -> Option { - let body = syn::parse_str::(block).ok()?; + let body = syn::parse_str::(block).unwrap(); let mut buf = Writer { src: block.lines().map(|f| f.to_string()).collect(), diff --git a/packages/autofmt/src/util.rs b/packages/autofmt/src/util.rs index 7fd4c325c..7ffd36a9a 100644 --- a/packages/autofmt/src/util.rs +++ b/packages/autofmt/src/util.rs @@ -1,14 +1,17 @@ pub fn find_bracket_end(contents: &str) -> Option { let mut depth = 0; - for (i, c) in contents.chars().enumerate() { + let mut len = 0; + + for c in contents.chars() { + len += c.len_utf8(); if c == '{' { depth += 1; } else if c == '}' { depth -= 1; if depth == 0 { - return Some(i); + return Some(len); } } } diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 4d4aed4a3..b60224c88 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,10 +1,11 @@ -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed}; +use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop}; use proc_macro2::{LineColumn, Span}; +use quote::ToTokens; use std::{ collections::{HashMap, VecDeque}, fmt::{Result, Write}, }; -use syn::{spanned::Spanned, Expr}; +use syn::{spanned::Spanned, Expr, ExprIf}; use crate::buffer::Buffer; @@ -37,8 +38,9 @@ impl Writer { BodyNode::Element(el) => self.write_element(el), BodyNode::Component(component) => self.write_component(component), BodyNode::Text(text) => self.out.write_text(text), - BodyNode::RawExpr(exp) => self.write_raw_expr(exp), - _ => Ok(()), + BodyNode::RawExpr(exp) => self.write_raw_expr(exp.span()), + BodyNode::ForLoop(forloop) => self.write_for_loop(forloop), + BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain), } } @@ -176,6 +178,31 @@ impl Writer { .or_insert_with(|| prettyplease::unparse_expr(expr)) .as_str() } + + fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result { + write!( + self.out, + "for {} in {} {{", + forloop.pat.clone().into_token_stream(), + prettyplease::unparse_expr(&forloop.expr) + )?; + + if forloop.body.is_empty() { + write!(self.out, "}}")?; + return Ok(()); + } + + self.write_body_indented(&forloop.body)?; + + self.out.tabbed_line()?; + write!(self.out, "}}")?; + + Ok(()) + } + + fn write_if_chain(&mut self, ifchain: &ExprIf) -> std::fmt::Result { + self.write_raw_expr(ifchain.span()) + } } trait SpanLength { diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 19588fda0..26492e141 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -1,35 +1,39 @@ macro_rules! twoway { - ($val:literal => $name:ident) => { - #[test] - fn $name() { - let src = include_str!(concat!("./samples/", $val, ".rsx")); - let formatted = dioxus_autofmt::fmt_file(src); - let out = dioxus_autofmt::apply_formats(src, formatted); - pretty_assertions::assert_eq!(&src, &out); - } + ( + $( + + // doc attrs + $( #[doc = $doc:expr] )* + $name:ident + ),* + ) => { + $( + $( #[doc = $doc] )* + #[test] + fn $name() { + let src = include_str!(concat!("./samples/", stringify!($name), ".rsx")); + let formatted = dioxus_autofmt::fmt_file(src); + let out = dioxus_autofmt::apply_formats(src, formatted); + pretty_assertions::assert_eq!(&src, &out); + } + )* }; } -twoway! ("simple" => simple); - -twoway! ("comments" => comments); - -twoway! ("attributes" => attributes); - -twoway! ("manual_props" => manual_props); - -twoway! ("complex" => complex); - -twoway! ("tiny" => tiny); - -twoway! ("tinynoopt" => tinynoopt); - -twoway! ("long" => long); - -twoway! ("key" => key); - -// Disabled because we can't handle comments on exprs yet -twoway! ("multirsx" => multirsx); - -// Disabled because we can't handle comments on exprs yet -twoway! ("commentshard" => commentshard); +twoway![ + simple, + comments, + attributes, + manual_props, + complex, + tiny, + tinynoopt, + long, + key, + multirsx, + commentshard, + emoji, + messy_indent, + long_exprs, + ifchain_forloop +]; diff --git a/packages/autofmt/tests/samples/emoji.rsx b/packages/autofmt/tests/samples/emoji.rsx new file mode 100644 index 000000000..066491583 --- /dev/null +++ b/packages/autofmt/tests/samples/emoji.rsx @@ -0,0 +1,5 @@ +rsx! { + div { class: "asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd", + section { "🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀" } + } +} diff --git a/packages/autofmt/tests/samples/ifchain_forloop.rsx b/packages/autofmt/tests/samples/ifchain_forloop.rsx new file mode 100644 index 000000000..9ec7326af --- /dev/null +++ b/packages/autofmt/tests/samples/ifchain_forloop.rsx @@ -0,0 +1,13 @@ +rsx! { + // Does this work? + for i in b { + // Hey it works? + div {} + } + + // Some ifchain + if a > 10 { + // + rsx! { div {} } + } +} diff --git a/packages/autofmt/tests/samples/long_exprs.rsx b/packages/autofmt/tests/samples/long_exprs.rsx new file mode 100644 index 000000000..fb4dab93f --- /dev/null +++ b/packages/autofmt/tests/samples/long_exprs.rsx @@ -0,0 +1,17 @@ +rsx! { + div { + div { + div { + div { + 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 } }) + } + } + } + } + } + } + } +} diff --git a/packages/autofmt/tests/samples/messy_indent.rsx b/packages/autofmt/tests/samples/messy_indent.rsx new file mode 100644 index 000000000..260dd08a6 --- /dev/null +++ b/packages/autofmt/tests/samples/messy_indent.rsx @@ -0,0 +1,13 @@ +fn SaveClipboard(cx: Scope) -> Element { + rsx! { + div { class: "relative w-1/2 {align} max-w-md leading-8", + 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}" + } + } + }; + + cx.render(rsx! { + div { "hello world", "hello world", "hello world" } +}) +} diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index 54ff5f867..38aaa21ca 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -37,7 +37,9 @@ rsx! { // One level compression div { - a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" } + a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", + "Send invitation" + } } // Components diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 9651ac483..c1c8d44b7 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -41,6 +41,7 @@ serde = { version = "1", features = ["derive"], optional = true } tokio = { version = "1", features = ["full"] } dioxus = { path = "../dioxus" } pretty_assertions = "1.3.0" +rand = "0.8.5" [features] default = [] diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs new file mode 100644 index 000000000..75dd84c33 --- /dev/null +++ b/packages/core/tests/fuzzing.rs @@ -0,0 +1,305 @@ +use dioxus::prelude::Props; +use dioxus_core::*; +use std::cell::Cell; + +fn random_ns() -> Option<&'static str> { + let namespace = rand::random::() % 2; + match namespace { + 0 => None, + 1 => Some(Box::leak( + format!("ns{}", rand::random::()).into_boxed_str(), + )), + _ => unreachable!(), + } +} + +fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> { + match rand::random::() % 2 { + 0 => TemplateAttribute::Static { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value: Box::leak(format!("value{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + }, + 1 => TemplateAttribute::Dynamic { + id: { + let old_idx = *attr_idx; + *attr_idx += 1; + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn create_random_template_node( + dynamic_node_types: &mut Vec, + template_idx: &mut usize, + attr_idx: &mut usize, + depth: usize, +) -> TemplateNode<'static> { + match rand::random::() % 4 { + 0 => { + let attrs = { + let attrs: Vec<_> = (0..(rand::random::() % 10)) + .map(|_| create_random_attribute(attr_idx)) + .collect(); + Box::leak(attrs.into_boxed_slice()) + }; + TemplateNode::Element { + tag: Box::leak(format!("tag{}", rand::random::()).into_boxed_str()), + namespace: random_ns(), + attrs, + children: { + if depth > 4 { + &[] + } else { + let children: Vec<_> = (0..(rand::random::() % 3)) + .map(|_| { + create_random_template_node( + dynamic_node_types, + template_idx, + attr_idx, + depth + 1, + ) + }) + .collect(); + Box::leak(children.into_boxed_slice()) + } + }, + } + } + 1 => TemplateNode::Text { + text: Box::leak(format!("{}", rand::random::()).into_boxed_str()), + }, + 2 => TemplateNode::DynamicText { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Text); + old_idx + }, + }, + 3 => TemplateNode::Dynamic { + id: { + let old_idx = *template_idx; + *template_idx += 1; + dynamic_node_types.push(DynamicNodeType::Other); + old_idx + }, + }, + _ => unreachable!(), + } +} + +fn generate_paths( + node: &TemplateNode<'static>, + current_path: &[u8], + node_paths: &mut Vec>, + attr_paths: &mut Vec>, +) { + match node { + TemplateNode::Element { children, attrs, .. } => { + for attr in *attrs { + match attr { + TemplateAttribute::Static { .. } => {} + TemplateAttribute::Dynamic { .. } => { + attr_paths.push(current_path.to_vec()); + } + } + } + for (i, child) in children.iter().enumerate() { + let mut current_path = current_path.to_vec(); + current_path.push(i as u8); + generate_paths(child, ¤t_path, node_paths, attr_paths); + } + } + TemplateNode::Text { .. } => {} + TemplateNode::DynamicText { .. } => { + node_paths.push(current_path.to_vec()); + } + TemplateNode::Dynamic { .. } => { + node_paths.push(current_path.to_vec()); + } + } +} + +enum DynamicNodeType { + Text, + Other, +} + +fn create_random_template(name: &'static str) -> (Template<'static>, Vec) { + let mut dynamic_node_type = Vec::new(); + let mut template_idx = 0; + let mut attr_idx = 0; + let roots = (0..(1 + rand::random::() % 5)) + .map(|_| { + create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0) + }) + .collect::>(); + assert!(!roots.is_empty()); + let roots = Box::leak(roots.into_boxed_slice()); + let mut node_paths = Vec::new(); + let mut attr_paths = Vec::new(); + for (i, root) in roots.iter().enumerate() { + generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths); + } + let node_paths = Box::leak( + node_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + let attr_paths = Box::leak( + attr_paths + .into_iter() + .map(|v| &*Box::leak(v.into_boxed_slice())) + .collect::>() + .into_boxed_slice(), + ); + ( + Template { name, roots, node_paths, attr_paths }, + dynamic_node_type, + ) +} + +fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { + let range = if depth > 3 { 1 } else { 3 }; + match rand::random::() % range { + 0 => DynamicNode::Placeholder(Default::default()), + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { + key: None, + parent: Default::default(), + template: Cell::new(Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }), + root_ids: Default::default(), + dynamic_nodes: cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + dynamic_attrs: &[], + })), + 2 => cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + ), + _ => unreachable!(), + } +} + +fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { + let value = match rand::random::() % 6 { + 0 => AttributeValue::Text(Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + )), + 1 => AttributeValue::Float(rand::random()), + 2 => AttributeValue::Int(rand::random()), + 3 => AttributeValue::Bool(rand::random()), + 4 => cx.any_value(rand::random::()), + 5 => AttributeValue::None, + // Listener(RefCell>>), + _ => unreachable!(), + }; + Attribute { + name: Box::leak(format!("attr{}", rand::random::()).into_boxed_str()), + value, + namespace: random_ns(), + mounted_element: Default::default(), + volatile: rand::random(), + } +} + +static mut TEMPLATE_COUNT: usize = 0; + +#[derive(PartialEq, Props)] +struct DepthProps { + depth: usize, + root: bool, +} + +fn create_random_element(cx: Scope) -> Element { + cx.needs_update(); + let range = if cx.props.root { 2 } else { 3 }; + let node = match rand::random::() % range { + 0 | 1 => { + let (template, dynamic_node_types) = create_random_template(Box::leak( + format!( + "{}{}", + concat!(file!(), ":", line!(), ":", column!(), ":"), + { + unsafe { + let old = TEMPLATE_COUNT; + TEMPLATE_COUNT += 1; + old + } + } + ) + .into_boxed_str(), + )); + println!("{template:#?}"); + let node = VNode { + key: None, + parent: None, + template: Cell::new(template), + root_ids: Default::default(), + dynamic_nodes: { + let dynamic_nodes: Vec<_> = dynamic_node_types + .iter() + .map(|ty| match ty { + DynamicNodeType::Text => DynamicNode::Text(VText { + value: Box::leak( + format!("{}", rand::random::()).into_boxed_str(), + ), + id: Default::default(), + }), + DynamicNodeType::Other => { + create_random_dynamic_node(cx, cx.props.depth + 1) + } + }) + .collect(); + cx.bump().alloc(dynamic_nodes) + }, + dynamic_attrs: cx.bump().alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + }; + Some(node) + } + _ => None, + }; + println!("{node:#?}"); + node +} + +// test for panics when creating random nodes and templates +#[test] +fn create() { + for _ in 0..100 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + } +} + +// test for panics when diffing random nodes +// This test will change the template every render which is not very realistic, but it helps stress the system +#[test] +fn diff() { + for _ in 0..10 { + let mut vdom = + VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); + let _ = vdom.rebuild(); + for _ in 0..10 { + let _ = vdom.render_immediate(); + } + } +} diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index a81e16235..61e37c2cb 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -129,6 +129,34 @@ mod js { root.appendChild(els[k]); } } + const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, + }; "#; extern "C" { diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 72ee1eb13..f5cd988b3 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -13,37 +13,29 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } -dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" } -dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" } -dioxus-hot-reload = { path = "../hot-reload" } - -tokio = { version = "1.23.0", features = ["full"] } +thiserror = "1.0.38" futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } futures-channel = { version = "0.3.25", features = ["sink"] } -pretty_env_logger = "0.4.0" +tokio = { version = "1.23.0", features = ["time"] } tokio-stream = { version = "0.1.11", features = ["net"] } - +tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" -tokio-util = { version = "0.7.4", features = ["full"] } - -interprocess = { version = "1.2.1", optional = true } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" } +dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" } +dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" } +dioxus-hot-reload = { path = "../hot-reload" } # warp warp = { version = "0.3.3", optional = true } # axum axum = { version = "0.6.1", optional = true, features = ["ws"] } -tower = { version = "0.4.13", optional = true } # salvo salvo = { version = "0.37.7", optional = true, features = ["ws"] } -thiserror = "1.0.38" -uuid = { version = "1.2.2", features = ["v4"] } -anyhow = "1.0.68" # actix is ... complicated? # actix-files = { version = "0.6.2", optional = true } @@ -51,6 +43,7 @@ anyhow = "1.0.68" # actix-ws = { version = "0.2.5", optional = true } [dev-dependencies] +pretty_env_logger = { version = "0.4.0" } tokio = { version = "1.23.0", features = ["full"] } dioxus = { path = "../dioxus", version = "0.3.0" } warp = "0.3.3" diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index ef76d14ff..0d5556552 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -95,25 +95,7 @@ impl Parse for BodyNode { // Transform for loops into into_iter calls if stream.peek(Token![for]) { - let _f = stream.parse::()?; - let pat = stream.parse::()?; - let _i = stream.parse::()?; - let expr = stream.parse::>()?; - - let body; - braced!(body in stream); - let mut children = vec![]; - while !body.is_empty() { - children.push(body.parse()?); - } - - return Ok(BodyNode::ForLoop(ForLoop { - for_token: _f, - pat, - in_token: _i, - expr, - body: children, - })); + return Ok(BodyNode::ForLoop(stream.parse()?)); } // Transform unterminated if statements into terminated optional if statements @@ -221,6 +203,36 @@ pub struct ForLoop { pub in_token: Token![in], pub expr: Box, pub body: Vec, + pub brace_token: token::Brace, +} + +impl Parse for ForLoop { + fn parse(input: ParseStream) -> Result { + let for_token: Token![for] = input.parse()?; + + let pat = input.parse()?; + + let in_token: Token![in] = input.parse()?; + let expr: Expr = input.call(Expr::parse_without_eager_brace)?; + + let content; + let brace_token = braced!(content in input); + + let mut children = vec![]; + + while !content.is_empty() { + children.push(content.parse()?); + } + + Ok(Self { + for_token, + pat, + in_token, + body: children, + expr: Box::new(expr), + brace_token, + }) + } } fn is_if_chain_terminated(chain: &ExprIf) -> bool {