diff --git a/Cargo.lock b/Cargo.lock index 1d2b8a57f3..2bf83e0937 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6311,6 +6311,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", + "rustversion", "syn 2.0.108", ] diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 594748ef02..e5069de76a 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -16,6 +16,7 @@ keywords = ["dom", "ui", "gui", "react"] proc-macro2 = { workspace = true, features = ["span-locations"] } proc-macro2-diagnostics = { workspace = true } quote = { workspace = true } +rustversion.workspace = true syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] } [features] diff --git a/packages/rsx/src/diagnostics.rs b/packages/rsx/src/diagnostics.rs index 9671ef7364..7f0ff179a3 100644 --- a/packages/rsx/src/diagnostics.rs +++ b/packages/rsx/src/diagnostics.rs @@ -53,3 +53,29 @@ impl ToTokens for Diagnostics { } } } + +// TODO: Ideally this would be integrated into the existing diagnostics struct, but currently all fields are public so adding +// new fields would be a breaking change. Diagnostics also doesn't expose the message directly so we can't just modify +// the expansion +pub(crate) mod new_diagnostics { + use proc_macro2_diagnostics::SpanDiagnosticExt; + use std::fmt::Display; + + use proc_macro2::{Span, TokenStream as TokenStream2}; + use quote::quote_spanned; + + pub(crate) fn warning_diagnostic(span: Span, message: impl Display) -> TokenStream2 { + let note = message.to_string(); + // If we are compiling on nightly, use diagnostics directly which supports proper warnings through new span apis + if rustversion::cfg!(nightly) { + return span.warning(note).emit_as_item_tokens(); + } + quote_spanned! { span => + const _: () = { + #[deprecated(note = #note)] + struct Warning; + _ = Warning; + }; + } + } +} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index ad5e67d7b2..b432837c07 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -176,6 +176,14 @@ impl BodyNode { _ => panic!("Element name not available for this node"), } } + + pub(crate) fn key(&self) -> Option<&AttributeValue> { + match self { + Self::Element(el) => el.key(), + Self::Component(comp) => comp.get_key(), + _ => None, + } + } } #[cfg(test)] diff --git a/packages/rsx/src/template_body.rs b/packages/rsx/src/template_body.rs index f17dff9069..d2ddda1bd2 100644 --- a/packages/rsx/src/template_body.rs +++ b/packages/rsx/src/template_body.rs @@ -109,6 +109,8 @@ impl ToTokens for TemplateBody { None => quote! { None }, }; + let key_warnings = self.check_for_duplicate_keys(); + let roots = node.quote_roots(); // Print paths is easy - just print the paths @@ -138,6 +140,8 @@ impl ToTokens for TemplateBody { dioxus_core::Element::Ok({ #diagnostics + #key_warnings + // Components pull in the dynamic literal pool and template in debug mode, so they need to be defined before dynamic nodes #[cfg(debug_assertions)] fn __original_template() -> &'static dioxus_core::internal::HotReloadedTemplate { @@ -273,11 +277,7 @@ impl TemplateBody { } pub fn implicit_key(&self) -> Option<&AttributeValue> { - match self.roots.first() { - Some(BodyNode::Element(el)) => el.key(), - Some(BodyNode::Component(comp)) => comp.get_key(), - _ => None, - } + self.roots.first().and_then(BodyNode::key) } /// Ensure only one key and that the key is not a static str @@ -304,6 +304,22 @@ impl TemplateBody { } } + fn check_for_duplicate_keys(&self) -> TokenStream2 { + let mut warnings = TokenStream2::new(); + + // Make sure there are not multiple keys or keys on nodes other than the first in the block + for root in self.roots.iter().skip(1) { + if let Some(key) = root.key() { + warnings.extend(new_diagnostics::warning_diagnostic( + key.span(), + "Keys are only allowed on the first node in the block.", + )); + } + } + + warnings + } + pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode { let mut node = self.roots.get(path[0] as usize).unwrap(); for idx in path.iter().skip(1) {