diff --git a/src/node/attribute.rs b/src/node/attribute.rs index 5687efa..2334d8e 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, 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), + InvalidBraced(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()), @@ -121,7 +130,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::InvalidBraced(_) => None, + }) + .flatten() } // Checks if error is about eof. @@ -212,38 +227,71 @@ 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)?; +impl ParseRecoverable for KeyedAttribute { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + 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)?) + KeyedAttributeValue::Binding( + FnBinding::parse(input) + .map_err(|e| parser.push_diagnostic(e)) + .ok()?, + ) } else if input.peek(Token![=]) { - let eq = input.parse::()?; + let eq = input + .parse::() + .map_err(|e| parser.push_diagnostic(e)) + .ok()?; if input.is_empty() { - return Err(syn::Error::new(eq.span(), "missing attribute value")); + parser.push_diagnostic(syn::Error::new(eq.span(), "missing attribute value")); + return None; } 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 + + let rs = match parse_valid_block_expr(parser, &fork) { + Ok(vbl) => { + input.advance_to(&fork); + KVAttributeValue::Expr(parse_quote!(#vbl)) + } + + Err(err) if input.fork().peek(Brace) && parser.config().recover_block => { + parser.push_diagnostic(err); + let ivb = parser.parse_simple(input)?; + KVAttributeValue::InvalidBraced(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) + } + }; - input.advance_to(&fork); KeyedAttributeValue::Value(AttributeValueExpr { token_eq: eq, - value: res, + value: rs, }) } else { KeyedAttributeValue::None }; - Ok(KeyedAttribute { + + Some(KeyedAttribute { key, possible_value, }) @@ -255,7 +303,7 @@ impl ParseRecoverable for NodeAttribute { 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..949d449 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, FnBinding, 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/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 0204522..f6d4226 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -366,7 +366,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::InvalidBraced(braced) => self.visit_invalid_block(braced), + } } fn visit_invalid_block(&mut self, block: &mut InvalidBlock) -> bool { 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! {};