Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial implementation of key value block attributes #49

Merged
merged 6 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 70 additions & 22 deletions src/node/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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,
Expand Down Expand Up @@ -46,7 +55,7 @@ impl AttributeValueExpr {
/// Adapted from leptos
pub fn value_literal_string(&self) -> Option<String> {
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()),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Self> {
let key = NodeName::parse(input)?;
impl ParseRecoverable for KeyedAttribute {
tachibanayui marked this conversation as resolved.
Show resolved Hide resolved
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
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::<Token![=]>()?;
let eq = input
.parse::<Token![=]>()
.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::<Expr>().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::<Expr>()
.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,
})
Expand All @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
4 changes: 2 additions & 2 deletions src/node/node_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/node/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Block> {
Expand Down
1 change: 1 addition & 0 deletions src/parser/recoverable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ impl RecoverableContext {

///
/// Result of parsing.
#[derive(Debug)]
pub enum ParsingResult<T> {
/// Fully valid ast that was parsed without errors.
Ok(T),
Expand Down
5 changes: 4 additions & 1 deletion src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
71 changes: 69 additions & 2 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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! {
<foo>{block.} </foo>
};

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! {
<foo foo={block.}> </foo>
};

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! {};
Expand Down
Loading