From 0936bb8ab5ffd59c9f89a9f84f9645884207d8bd Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Tue, 6 Feb 2024 13:02:34 +0700 Subject: [PATCH 1/5] initial implementation of block attributes --- src/node/attribute.rs | 76 +++++++++++++++++++++++++++++++++++++++++-- src/node/mod.rs | 2 +- src/visitor.rs | 18 ++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/node/attribute.rs b/src/node/attribute.rs index 5687efa..6db1825 100644 --- a/src/node/attribute.rs +++ b/src/node/attribute.rs @@ -5,7 +5,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::{Brace, Comma, Paren}, - Attribute, Expr, Lit, Pat, PatType, Token, + Attribute, Block, Expr, Lit, Pat, PatType, Token, }; use crate::{ @@ -59,10 +59,17 @@ impl AttributeValueExpr { } } +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct AttributeValueBlock { + pub token_eq: Token![=], + pub value: NodeBlock, +} + #[derive(Clone, Debug, syn_derive::ToTokens)] pub enum KeyedAttributeValue { Binding(FnBinding), Value(AttributeValueExpr), + Block(AttributeValueBlock), None, } @@ -72,6 +79,7 @@ impl KeyedAttributeValue { KeyedAttributeValue::Value(v) => Some(v), KeyedAttributeValue::None => None, KeyedAttributeValue::Binding(_) => None, + KeyedAttributeValue::Block(_) => None, } } } @@ -250,12 +258,76 @@ impl Parse for KeyedAttribute { } } +impl ParseRecoverable for KeyedAttribute { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + // TODO: Make this function actually recoverable + + let key = NodeName::parse(input) + .map_err(|e| parser.push_diagnostic(e)) + .ok()?; + + let possible_value = if input.peek(Paren) { + KeyedAttributeValue::Binding( + FnBinding::parse(input) + .map_err(|e| parser.push_diagnostic(e)) + .ok()?, + ) + } else if input.peek(Token![=]) { + let eq = input + .parse::() + .map_err(|e| parser.push_diagnostic(e)) + .ok()?; + if input.is_empty() { + parser.push_diagnostic(syn::Error::new(eq.span(), "missing attribute value")); + return None; + } + + let fork = input.fork(); + if let Some(res) = parser.parse_recoverable::(&fork) { + input.advance_to(&fork); + KeyedAttributeValue::Block(AttributeValueBlock { + token_eq: eq, + value: res, + }) + } else { + let res = fork + .parse::() + .map_err(|e| { + // if we stuck on end of input, span that is created will be call_site, so + // we need to correct it, in order to make it more + // IDE friendly. + if fork.is_empty() { + KeyedAttribute::correct_expr_error_span(e, input) + } else { + e + } + }) + .map_err(|e| parser.push_diagnostic(e)) + .ok()?; + + input.advance_to(&fork); + KeyedAttributeValue::Value(AttributeValueExpr { + token_eq: eq, + value: res, + }) + } + } else { + KeyedAttributeValue::None + }; + + Some(KeyedAttribute { + key, + possible_value, + }) + } +} + impl ParseRecoverable for NodeAttribute { fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { let node = if input.peek(Brace) { NodeAttribute::Block(parser.parse_recoverable(input)?) } else { - NodeAttribute::Attribute(parser.parse_simple(input)?) + NodeAttribute::Attribute(parser.parse_recoverable(input)?) }; Some(node) } diff --git a/src/node/mod.rs b/src/node/mod.rs index f589302..f236cd8 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -16,7 +16,7 @@ mod parser_ext; mod raw_text; pub use attribute::{ - AttributeValueExpr, FnBinding, KeyedAttribute, KeyedAttributeValue, NodeAttribute, + AttributeValueExpr, AttributeValueBlock, FnBinding, KeyedAttribute, KeyedAttributeValue, NodeAttribute, }; pub use node_name::{NodeName, NodeNameFragment}; pub use node_value::{InvalidBlock, NodeBlock}; diff --git a/src/visitor.rs b/src/visitor.rs index 0204522..73b992b 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -91,6 +91,13 @@ pub trait Visitor { ) -> bool { true } + fn visit_attribute_block( + &mut self, + _key: &mut NodeName, + _value: &mut AttributeValueBlock, + ) -> bool { + true + } } #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Ord, Copy, Eq)] @@ -344,6 +351,7 @@ where KeyedAttributeValue::None => self.visit_attribute_flag(&mut attribute.key), KeyedAttributeValue::Binding(b) => self.visit_attribute_binding(&mut attribute.key, b), KeyedAttributeValue::Value(v) => self.visit_attribute_value(&mut attribute.key, v), + KeyedAttributeValue::Block(b) => self.visit_attribute_block(&mut attribute.key, b), } } fn visit_attribute_flag(&mut self, key: &mut NodeName) -> bool { @@ -368,6 +376,16 @@ where self.visit_node_name(key); self.visit_rust_code(RustCode::Expr(&mut value.value)) } + fn visit_attribute_block( + &mut self, + key: &mut NodeName, + value: &mut AttributeValueBlock, + ) -> bool { + visit_inner!(self.visitor.visit_attribute_block(key, value)); + + self.visit_node_name(key); + self.visit_block(&mut value.value) + } fn visit_invalid_block(&mut self, block: &mut InvalidBlock) -> bool { visit_inner!(self.visitor.visit_invalid_block(block)); From d2bfad756771e0e7317bb0f83712e00d1347cd64 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:02:54 +0700 Subject: [PATCH 2/5] implement braced key value attribute. See also: [#54](https://github.com/rs-tml/rstml/issues/54#issuecomment-2247924159) --- src/node/attribute.rs | 127 ++++++++++++++++---------------------- src/node/mod.rs | 3 +- src/node/node_value.rs | 4 +- src/node/parse.rs | 2 +- src/parser/recoverable.rs | 1 + src/visitor.rs | 16 ++++- 6 files changed, 74 insertions(+), 79 deletions(-) diff --git a/src/node/attribute.rs b/src/node/attribute.rs index 6db1825..9587673 100644 --- a/src/node/attribute.rs +++ b/src/node/attribute.rs @@ -2,12 +2,14 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ parse::{discouraged::Speculative, Parse, ParseStream}, + parse_quote, punctuated::Punctuated, spanned::Spanned, token::{Brace, Comma, Paren}, - Attribute, Block, Expr, Lit, Pat, PatType, Token, + Attribute, Expr, Lit, Pat, PatType, Token, }; +use super::{parse::parse_valid_block_expr, InvalidBlock}; use crate::{ node::{NodeBlock, NodeName}, parser::recoverable::{ParseRecoverable, RecoverableContext}, @@ -16,8 +18,15 @@ use crate::{ #[derive(Clone, Debug, syn_derive::ToTokens)] pub struct AttributeValueExpr { pub token_eq: Token![=], - pub value: Expr, + pub value: KVAttributeValue, } + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub enum KVAttributeValue { + Expr(Expr), + Braced(InvalidBlock), +} + impl AttributeValueExpr { /// /// Returns string representation of inner value, @@ -46,7 +55,7 @@ impl AttributeValueExpr { /// Adapted from leptos pub fn value_literal_string(&self) -> Option { match &self.value { - Expr::Lit(l) => match &l.lit { + KVAttributeValue::Expr(Expr::Lit(l)) => match &l.lit { Lit::Str(s) => Some(s.value()), Lit::Char(c) => Some(c.value().to_string()), Lit::Int(i) => Some(i.base10_digits().to_string()), @@ -69,7 +78,6 @@ pub struct AttributeValueBlock { pub enum KeyedAttributeValue { Binding(FnBinding), Value(AttributeValueExpr), - Block(AttributeValueBlock), None, } @@ -79,7 +87,6 @@ impl KeyedAttributeValue { KeyedAttributeValue::Value(v) => Some(v), KeyedAttributeValue::None => None, KeyedAttributeValue::Binding(_) => None, - KeyedAttributeValue::Block(_) => None, } } } @@ -129,7 +136,13 @@ impl KeyedAttribute { } pub fn value(&self) -> Option<&Expr> { - self.possible_value.to_value().map(|v| &v.value) + self.possible_value + .to_value() + .map(|v| match &v.value { + KVAttributeValue::Expr(expr) => Some(expr), + KVAttributeValue::Braced(_) => None, + }) + .flatten() } // Checks if error is about eof. @@ -220,48 +233,8 @@ pub enum NodeAttribute { Attribute(KeyedAttribute), } -// Use custom parse to correct error. -impl Parse for KeyedAttribute { - fn parse(input: ParseStream) -> syn::Result { - let key = NodeName::parse(input)?; - let possible_value = if input.peek(Paren) { - KeyedAttributeValue::Binding(FnBinding::parse(input)?) - } else if input.peek(Token![=]) { - let eq = input.parse::()?; - if input.is_empty() { - return Err(syn::Error::new(eq.span(), "missing attribute value")); - } - - let fork = input.fork(); - let res = fork.parse::().map_err(|e| { - // if we stuck on end of input, span that is created will be call_site, so we - // need to correct it, in order to make it more IDE friendly. - if fork.is_empty() { - KeyedAttribute::correct_expr_error_span(e, input) - } else { - e - } - })?; - - input.advance_to(&fork); - KeyedAttributeValue::Value(AttributeValueExpr { - token_eq: eq, - value: res, - }) - } else { - KeyedAttributeValue::None - }; - Ok(KeyedAttribute { - key, - possible_value, - }) - } -} - impl ParseRecoverable for KeyedAttribute { fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { - // TODO: Make this function actually recoverable - let key = NodeName::parse(input) .map_err(|e| parser.push_diagnostic(e)) .ok()?; @@ -283,34 +256,42 @@ impl ParseRecoverable for KeyedAttribute { } let fork = input.fork(); - if let Some(res) = parser.parse_recoverable::(&fork) { - input.advance_to(&fork); - KeyedAttributeValue::Block(AttributeValueBlock { - token_eq: eq, - value: res, - }) - } else { - let res = fork - .parse::() - .map_err(|e| { - // if we stuck on end of input, span that is created will be call_site, so - // we need to correct it, in order to make it more - // IDE friendly. - if fork.is_empty() { - KeyedAttribute::correct_expr_error_span(e, input) - } else { - e - } - }) - .map_err(|e| parser.push_diagnostic(e)) - .ok()?; - input.advance_to(&fork); - KeyedAttributeValue::Value(AttributeValueExpr { - token_eq: eq, - value: res, - }) - } + let rs = match parse_valid_block_expr(parser, &fork) { + Ok(vbl) => { + input.advance_to(&fork); + KVAttributeValue::Expr(parse_quote!(#vbl)) + } + + Err(_) if input.fork().peek(Brace) => { + let ivb = parser.parse_simple(input)?; + KVAttributeValue::Braced(ivb) + } + Err(_) => { + let res = fork + .parse::() + .map_err(|e| { + // if we stuck on end of input, span that is created will be call_site, + // so we need to correct it, in order to + // make it more IDE friendly. + if fork.is_empty() { + KeyedAttribute::correct_expr_error_span(e, input) + } else { + e + } + }) + .map_err(|e| parser.push_diagnostic(e)) + .ok()?; + + input.advance_to(&fork); + KVAttributeValue::Expr(res) + } + }; + + KeyedAttributeValue::Value(AttributeValueExpr { + token_eq: eq, + value: rs, + }) } else { KeyedAttributeValue::None }; diff --git a/src/node/mod.rs b/src/node/mod.rs index f236cd8..ba96560 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -16,7 +16,8 @@ mod parser_ext; mod raw_text; pub use attribute::{ - AttributeValueExpr, AttributeValueBlock, FnBinding, KeyedAttribute, KeyedAttributeValue, NodeAttribute, + AttributeValueBlock, AttributeValueExpr, FnBinding, KVAttributeValue, KeyedAttribute, + KeyedAttributeValue, NodeAttribute, }; pub use node_name::{NodeName, NodeNameFragment}; pub use node_value::{InvalidBlock, NodeBlock}; diff --git a/src/node/node_value.rs b/src/node/node_value.rs index aefd6db..f908d18 100644 --- a/src/node/node_value.rs +++ b/src/node/node_value.rs @@ -8,9 +8,9 @@ use syn::{token::Brace, Block}; #[derive(Clone, Debug, syn_derive::ToTokens, syn_derive::Parse)] pub struct InvalidBlock { #[syn(braced)] - brace: Brace, + pub brace: Brace, #[syn(in = brace)] - body: TokenStream, + pub body: TokenStream, } /// Block node. diff --git a/src/node/parse.rs b/src/node/parse.rs index 7a23bb8..f0172a0 100644 --- a/src/node/parse.rs +++ b/src/node/parse.rs @@ -358,7 +358,7 @@ fn block_transform(input: ParseStream, transform_fn: &TransformBlockFn) -> syn:: } #[allow(clippy::needless_pass_by_ref_mut)] -fn parse_valid_block_expr( +pub(crate) fn parse_valid_block_expr( parser: &mut RecoverableContext, input: syn::parse::ParseStream, ) -> syn::Result { diff --git a/src/parser/recoverable.rs b/src/parser/recoverable.rs index dd7d64b..bbfbbfe 100644 --- a/src/parser/recoverable.rs +++ b/src/parser/recoverable.rs @@ -191,6 +191,7 @@ impl RecoverableContext { /// /// Result of parsing. +#[derive(Debug)] pub enum ParsingResult { /// Fully valid ast that was parsed without errors. Ok(T), diff --git a/src/visitor.rs b/src/visitor.rs index 73b992b..d2c4c2c 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -351,7 +351,6 @@ where KeyedAttributeValue::None => self.visit_attribute_flag(&mut attribute.key), KeyedAttributeValue::Binding(b) => self.visit_attribute_binding(&mut attribute.key, b), KeyedAttributeValue::Value(v) => self.visit_attribute_value(&mut attribute.key, v), - KeyedAttributeValue::Block(b) => self.visit_attribute_block(&mut attribute.key, b), } } fn visit_attribute_flag(&mut self, key: &mut NodeName) -> bool { @@ -374,7 +373,10 @@ where visit_inner!(self.visitor.visit_attribute_value(key, value)); self.visit_node_name(key); - self.visit_rust_code(RustCode::Expr(&mut value.value)) + match &mut value.value { + KVAttributeValue::Expr(expr) => self.visit_rust_code(RustCode::Expr(expr)), + KVAttributeValue::Braced(braced) => self.visit_invalid_block(braced), + } } fn visit_attribute_block( &mut self, @@ -550,6 +552,16 @@ mod tests { assert_eq!(node_names, vec!["div", "span", "span", "foo"]); } + #[test] + fn asd() { + let a = quote! { + + }; + + let a = crate::parse2(a); + dbg!(a); + } + #[test] fn collect_rust_blocks() { #[derive(Default)] From 89b0ba80ec17146cf38c2b41aade197804fb6ba9 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:03:45 +0700 Subject: [PATCH 3/5] remove scratch --- src/visitor.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/visitor.rs b/src/visitor.rs index d2c4c2c..ca56410 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -552,16 +552,6 @@ mod tests { assert_eq!(node_names, vec!["div", "span", "span", "foo"]); } - #[test] - fn asd() { - let a = quote! { - - }; - - let a = crate::parse2(a); - dbg!(a); - } - #[test] fn collect_rust_blocks() { #[derive(Default)] From 725e2ec63faf8e4bea04f4fd8779c222d1d68951 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Fri, 26 Jul 2024 04:18:31 +0700 Subject: [PATCH 4/5] rename KVAttributeValue::Braced -> KVAttributeValue::InvalidBraced --- src/node/attribute.rs | 6 +++--- src/visitor.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/attribute.rs b/src/node/attribute.rs index 9587673..f4c6b11 100644 --- a/src/node/attribute.rs +++ b/src/node/attribute.rs @@ -24,7 +24,7 @@ pub struct AttributeValueExpr { #[derive(Clone, Debug, syn_derive::ToTokens)] pub enum KVAttributeValue { Expr(Expr), - Braced(InvalidBlock), + InvalidBraced(InvalidBlock), } impl AttributeValueExpr { @@ -140,7 +140,7 @@ impl KeyedAttribute { .to_value() .map(|v| match &v.value { KVAttributeValue::Expr(expr) => Some(expr), - KVAttributeValue::Braced(_) => None, + KVAttributeValue::InvalidBraced(_) => None, }) .flatten() } @@ -265,7 +265,7 @@ impl ParseRecoverable for KeyedAttribute { Err(_) if input.fork().peek(Brace) => { let ivb = parser.parse_simple(input)?; - KVAttributeValue::Braced(ivb) + KVAttributeValue::InvalidBraced(ivb) } Err(_) => { let res = fork diff --git a/src/visitor.rs b/src/visitor.rs index ca56410..a80531f 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -375,7 +375,7 @@ where self.visit_node_name(key); match &mut value.value { KVAttributeValue::Expr(expr) => self.visit_rust_code(RustCode::Expr(expr)), - KVAttributeValue::Braced(braced) => self.visit_invalid_block(braced), + KVAttributeValue::InvalidBraced(braced) => self.visit_invalid_block(braced), } } fn visit_attribute_block( From 187e7df61fd4415661f480800b2e1e22fe98210f Mon Sep 17 00:00:00 2001 From: Vladimir Motylenko Date: Sat, 27 Jul 2024 17:34:50 +0300 Subject: [PATCH 5/5] Fix: use config and diagnostc from invalid_block in attribute place. Also add tests and remove unused structure. --- src/node/attribute.rs | 9 ++---- src/node/mod.rs | 4 +-- src/visitor.rs | 17 ----------- tests/test.rs | 71 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/node/attribute.rs b/src/node/attribute.rs index f4c6b11..2334d8e 100644 --- a/src/node/attribute.rs +++ b/src/node/attribute.rs @@ -68,12 +68,6 @@ impl AttributeValueExpr { } } -#[derive(Clone, Debug, syn_derive::ToTokens)] -pub struct AttributeValueBlock { - pub token_eq: Token![=], - pub value: NodeBlock, -} - #[derive(Clone, Debug, syn_derive::ToTokens)] pub enum KeyedAttributeValue { Binding(FnBinding), @@ -263,7 +257,8 @@ impl ParseRecoverable for KeyedAttribute { KVAttributeValue::Expr(parse_quote!(#vbl)) } - Err(_) if input.fork().peek(Brace) => { + Err(err) if input.fork().peek(Brace) && parser.config().recover_block => { + parser.push_diagnostic(err); let ivb = parser.parse_simple(input)?; KVAttributeValue::InvalidBraced(ivb) } diff --git a/src/node/mod.rs b/src/node/mod.rs index ba96560..949d449 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -16,8 +16,8 @@ mod parser_ext; mod raw_text; pub use attribute::{ - AttributeValueBlock, AttributeValueExpr, FnBinding, KVAttributeValue, KeyedAttribute, - KeyedAttributeValue, NodeAttribute, + AttributeValueExpr, FnBinding, KVAttributeValue, KeyedAttribute, KeyedAttributeValue, + NodeAttribute, }; pub use node_name::{NodeName, NodeNameFragment}; pub use node_value::{InvalidBlock, NodeBlock}; diff --git a/src/visitor.rs b/src/visitor.rs index a80531f..f6d4226 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -91,13 +91,6 @@ pub trait Visitor { ) -> bool { true } - fn visit_attribute_block( - &mut self, - _key: &mut NodeName, - _value: &mut AttributeValueBlock, - ) -> bool { - true - } } #[derive(Debug, Default, Clone, PartialEq, PartialOrd, Ord, Copy, Eq)] @@ -378,16 +371,6 @@ where KVAttributeValue::InvalidBraced(braced) => self.visit_invalid_block(braced), } } - fn visit_attribute_block( - &mut self, - key: &mut NodeName, - value: &mut AttributeValueBlock, - ) -> bool { - visit_inner!(self.visitor.visit_attribute_block(key, value)); - - self.visit_node_name(key); - self.visit_block(&mut value.value) - } fn visit_invalid_block(&mut self, block: &mut InvalidBlock) -> bool { visit_inner!(self.visitor.visit_invalid_block(block)); diff --git a/tests/test.rs b/tests/test.rs index 79aaf32..d8e70da 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -5,8 +5,8 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use rstml::{ node::{ - CustomNode, KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeElement, - NodeName, NodeType, + CustomNode, KVAttributeValue, KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, + NodeElement, NodeName, NodeType, }, parse2, recoverable::{ParseRecoverable, RecoverableContext}, @@ -974,6 +974,73 @@ fn test_single_element_with_different_attributes() -> Result<()> { Ok(()) } +#[test] +fn test_invalid_blocks() -> Result<()> { + // test that invalid blocks can be parsed in recoverable mode + // usefull for IDEs + let tokens = quote! { + {block.} + }; + + let config = ParserConfig::new().recover_block(true); + let (nodes, diagnostics) = Parser::new(config) + .parse_recoverable(tokens.clone()) + .split_vec(); + + let Node::Block(block) = get_element_child(&nodes, 0, 0) else { + panic!("expected block") + }; + + assert_eq!(block.to_token_stream().to_string(), "{ block . }"); + assert_eq!(diagnostics.len(), 1); + let dbg_diag = format!("{:?}", diagnostics[0]); + assert!(dbg_diag.contains("unexpected end of input, expected identifier or integer")); + // same should not work if recover_block = false + let config = ParserConfig::new(); + let (nodes, diagnostics) = Parser::new(config).parse_recoverable(tokens).split_vec(); + let node = get_element(&nodes, 0); + assert!(node.children.is_empty()); + // TODO: Cleanup errors + assert!(diagnostics.len() > 1); + Ok(()) +} + +#[test] +fn test_invalid_blocks_in_attr() -> Result<()> { + // test that invalid blocks can be parsed in recoverable mode + // usefull for IDEs + let tokens = quote! { + + }; + + let config = ParserConfig::new().recover_block(true); + let (nodes, diagnostics) = Parser::new(config) + .parse_recoverable(tokens.clone()) + .split_vec(); + + let attr = get_element_attribute(&nodes, 0, 0); + let KeyedAttributeValue::Value(eq_val) = &attr.possible_value else { + panic!("expected value") + }; + + let KVAttributeValue::InvalidBraced(block) = &eq_val.value else { + panic!("expected invalid block") + }; + + assert_eq!(block.to_token_stream().to_string(), "{ block . }"); + + assert_eq!(diagnostics.len(), 1); + let dbg_diag = format!("{:?}", diagnostics[0]); + assert!(dbg_diag.contains("unexpected end of input, expected identifier or integer")); + // same should not work if recover_block = false + let config = ParserConfig::new(); + let (nodes, diagnostics) = Parser::new(config).parse_recoverable(tokens).split_vec(); + let node = get_element(&nodes, 0); + assert!(node.attributes().is_empty()); + assert_eq!(diagnostics.len(), 1); + Ok(()) +} + #[test] fn test_empty_input() -> Result<()> { let tokens = quote! {};