From d335137200c8c28cfcb197dedd25809ee7df8911 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Nov 2025 09:03:08 -0600 Subject: [PATCH 1/3] add a warning for keys after the first node which do nothing --- packages/rsx/src/node.rs | 8 ++++++++ packages/rsx/src/template_body.rs | 7 +++++++ 2 files changed, 15 insertions(+) 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..74c8a82e49 100644 --- a/packages/rsx/src/template_body.rs +++ b/packages/rsx/src/template_body.rs @@ -302,6 +302,13 @@ impl TemplateBody { self.diagnostics.push(diagnostic); } + + // 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() { + self.diagnostics.push(key.span().warning("Keys are only allowed on the first node in the block.")); + } + } } pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode { From 0d14794afb7b717480150684c187d93aef0cf0fc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Nov 2025 09:52:26 -0600 Subject: [PATCH 2/3] warnings on stable --- packages/rsx/src/diagnostics.rs | 21 +++++++++++++++++++++ packages/rsx/src/template_body.rs | 21 +++++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/rsx/src/diagnostics.rs b/packages/rsx/src/diagnostics.rs index 9671ef7364..598ee4e274 100644 --- a/packages/rsx/src/diagnostics.rs +++ b/packages/rsx/src/diagnostics.rs @@ -53,3 +53,24 @@ 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 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(); + quote_spanned! { span => + const _: () = { + #[deprecated(note = #note)] + struct Warning; + _ = Warning; + }; + } + } +} diff --git a/packages/rsx/src/template_body.rs b/packages/rsx/src/template_body.rs index 74c8a82e49..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 @@ -302,13 +302,22 @@ impl TemplateBody { self.diagnostics.push(diagnostic); } + } + + 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() { - self.diagnostics.push(key.span().warning("Keys are only allowed on the first node in the block.")); + 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 { From c31f2360c48eb2db42246e824e446352f857177c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Nov 2025 13:41:29 -0600 Subject: [PATCH 3/3] better warnings on nightly --- Cargo.lock | 1 + packages/rsx/Cargo.toml | 1 + packages/rsx/src/diagnostics.rs | 5 +++++ 3 files changed, 7 insertions(+) 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 598ee4e274..7f0ff179a3 100644 --- a/packages/rsx/src/diagnostics.rs +++ b/packages/rsx/src/diagnostics.rs @@ -58,6 +58,7 @@ impl ToTokens for Diagnostics { // 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}; @@ -65,6 +66,10 @@ pub(crate) mod new_diagnostics { 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)]