Feat: reject invalid keys

This commit is contained in:
Jonathan Kelley 2024-03-07 16:03:00 -08:00
parent 6faa51a4a9
commit ae352f8958
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
6 changed files with 40 additions and 23 deletions

View file

@ -6,7 +6,7 @@ fn basic_syntax_is_a_template() -> Element {
let var = 123; let var = 123;
rsx! { rsx! {
div { key: "12345", class: "asd", class: "{asd}", class: if true { div { key: "{asd}", class: "asd", class: "{asd}", class: if true {
"{asd}" "{asd}"
}, class: if false { }, class: if false {
"{asd}" "{asd}"

View file

@ -141,6 +141,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
fields: vec![], fields: vec![],
children: vec![], children: vec![],
manual_props: None, manual_props: None,
key: None,
brace: Default::default(), brace: Default::default(),
}); });

View file

@ -27,6 +27,7 @@ use syn::{
pub struct Component { pub struct Component {
pub name: syn::Path, pub name: syn::Path,
pub prop_gen_args: Option<AngleBracketedGenericArguments>, pub prop_gen_args: Option<AngleBracketedGenericArguments>,
pub key: Option<IfmtInput>,
pub fields: Vec<ComponentField>, pub fields: Vec<ComponentField>,
pub children: Vec<BodyNode>, pub children: Vec<BodyNode>,
pub manual_props: Option<Expr>, pub manual_props: Option<Expr>,
@ -47,6 +48,7 @@ impl Parse for Component {
let mut fields = Vec::new(); let mut fields = Vec::new();
let mut children = Vec::new(); let mut children = Vec::new();
let mut manual_props = None; let mut manual_props = None;
let mut key = None;
while !content.is_empty() { while !content.is_empty() {
// if we splat into a component then we're merging properties // if we splat into a component then we're merging properties
@ -56,14 +58,26 @@ impl Parse for Component {
} else if } else if
// Named fields // Named fields
(content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:])) (content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]))
// shorthand struct initialization // shorthand struct initialization
// Not a div {}, mod::Component {}, or web-component {} // Not a div {}, mod::Component {}, or web-component {}
|| (content.peek(Ident) || (content.peek(Ident)
&& !content.peek2(Brace) && !content.peek2(Brace)
&& !content.peek2(Token![:]) && !content.peek2(Token![:])
&& !content.peek2(Token![-])) && !content.peek2(Token![-]))
{ {
fields.push(content.parse()?); // If it
if content.fork().parse::<Ident>()? == "key" {
_ = content.parse::<Ident>()?;
_ = content.parse::<Token![:]>()?;
let _key: IfmtInput = content.parse()?;
if _key.is_static() {
invalid_key!(_key);
}
key = Some(_key);
} else {
fields.push(content.parse()?);
}
} else { } else {
children.push(content.parse()?); children.push(content.parse()?);
} }
@ -80,6 +94,7 @@ impl Parse for Component {
children, children,
manual_props, manual_props,
brace, brace,
key,
}) })
} }
} }
@ -137,15 +152,7 @@ impl Component {
} }
pub fn key(&self) -> Option<&IfmtInput> { pub fn key(&self) -> Option<&IfmtInput> {
match self self.key.as_ref()
.fields
.iter()
.find(|f| f.name == "key")
.map(|f| &f.content)
{
Some(ContentField::Formatted(fmt)) => Some(fmt),
_ => None,
}
} }
fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 { fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 {
@ -169,10 +176,7 @@ impl Component {
None => quote! { fc_to_builder(#name) }, None => quote! { fc_to_builder(#name) },
}; };
for field in &self.fields { for field in &self.fields {
match field.name.to_string().as_str() { toks.append_all(quote! {#field})
"key" => {}
_ => toks.append_all(quote! {#field}),
}
} }
if !self.children.is_empty() { if !self.children.is_empty() {
let renderer: TemplateRenderer = TemplateRenderer { let renderer: TemplateRenderer = TemplateRenderer {
@ -218,10 +222,6 @@ impl ContentField {
return Ok(ContentField::OnHandlerRaw(input.parse()?)); return Ok(ContentField::OnHandlerRaw(input.parse()?));
} }
if *name == "key" {
return Ok(ContentField::Formatted(input.parse()?));
}
if input.peek(LitStr) { if input.peek(LitStr) {
let forked = input.fork(); let forked = input.fork();
let t: LitStr = forked.parse()?; let t: LitStr = forked.parse()?;

View file

@ -169,7 +169,13 @@ impl Parse for Element {
}, },
})); }));
} else if name_str == "key" { } else if name_str == "key" {
key = Some(content.parse()?); let _key: IfmtInput = content.parse()?;
if _key.is_static() {
invalid_key!(_key);
}
key = Some(_key);
} else { } else {
let value = content.parse::<ElementAttrValue>()?; let value = content.parse::<ElementAttrValue>()?;
attributes.push(attribute::AttributeType::Named(ElementAttrNamed { attributes.push(attribute::AttributeType::Named(ElementAttrNamed {

View file

@ -24,3 +24,13 @@ macro_rules! invalid_component_path {
return Err(Error::new($span, "Invalid component path syntax")); return Err(Error::new($span, "Invalid component path syntax"));
}; };
} }
macro_rules! invalid_key {
($_key:ident) => {
let val = $_key.to_static().unwrap();
return Err(syn::Error::new(
$_key.span(),
format!("Element keys must be a dynamic value. Considering using `key: {{{val}}}` instead.\nStatic keys will result in every element using the same key which will cause rendering issues or panics."),
));
};
}

View file

@ -179,8 +179,8 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
let mut context = DynamicContext::default(); let mut context = DynamicContext::default();
let key = match self.roots.first() { let key = match self.roots.first() {
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), Some(BodyNode::Element(el)) if self.roots.len() >= 1 => el.key.clone(),
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), Some(BodyNode::Component(comp)) if self.roots.len() >= 1 => comp.key().cloned(),
_ => None, _ => None,
}; };