From 0315044f5396c880cb5980628f7d31b34ec5e44a Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 1 Jun 2026 21:01:29 +0200 Subject: [PATCH] fix(build): reject empty slhx guards Treat empty static data-slhx-on lists and empty data-slhx-confirm messages as build errors, so authors do not silently disable dispatch or confirmation guards with blank attributes. req: convention/002 req: convention/004 req: diagnostics/002 --- REQUIREMENTS.md | 2 +- slhx-build/src/lib.rs | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 7329310..a58121e 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -870,7 +870,7 @@ All forms support returning `impl IntoEffect` and compose through tuples. 003 `data-slhx-debounce` and `data-slhx-throttle` support simple millisecond values. No trigger mini-language in core. ### req: convention/004 -004 `data-slhx-confirm` dispatches a native `confirm()` before handler dispatch. Custom confirm UI belongs to integration crates. +004 `data-slhx-confirm` dispatches a native `confirm()` before handler dispatch. Static empty confirmation messages are build errors because they silently disable the guard in browsers. Custom confirm UI belongs to integration crates. ### req: convention/005 005 `data-slhx-every` dispatches a handle at a fixed interval while the element remains in the document. Duplicate timers per root are avoided. diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index 031bfe3..2e24ded 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -1015,9 +1015,7 @@ fn known_slhx_attr(name: &str) -> bool { fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> { for attr in attrs.iter().filter(|attr| attr.origin == AttributeOrigin::Static) { - let Some(value) = attr.value.as_deref() else { - continue; - }; + let value = attr.value.as_deref().unwrap_or(""); match attr.name.as_str() { "data-slhx-policy" if !valid_policy(value) => { return Err(invalid_slhx_value( @@ -1027,7 +1025,7 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i "expected one of `latest`, `queue`, `drop`, or `parallel`", )); } - "data-slhx-on" if !event_tokens(value).all(valid_runtime_event) => { + "data-slhx-on" if !valid_event_list(value) => { return Err(invalid_slhx_value( path, &attr.name, @@ -1035,6 +1033,14 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i "expected runtime-supported events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, or `drop`", )); } + "data-slhx-confirm" if value.trim().is_empty() => { + return Err(invalid_slhx_value( + path, + &attr.name, + value, + "expected a non-empty confirmation message", + )); + } "data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every" if !valid_duration(value) => { @@ -1055,6 +1061,11 @@ fn valid_policy(value: &str) -> bool { matches!(value.trim(), "latest" | "queue" | "drop" | "parallel") } +fn valid_event_list(value: &str) -> bool { + let mut events = event_tokens(value).peekable(); + events.peek().is_some() && events.all(valid_runtime_event) +} + fn valid_runtime_event(value: &str) -> bool { matches!(value, "click" | "submit" | "input" | "change" | "dragstart" | "dragover" | "drop") } @@ -1529,7 +1540,7 @@ fn main() {{ #[test] fn rejects_invalid_static_slhx_convention_values() { - // req: convention/003 req: convention/005 req: convention/006 req: diagnostics/002 + // req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: diagnostics/002 for (case, template, attr, guidance) in [ ( "policy", @@ -1555,6 +1566,18 @@ fn main() {{ "data-slhx-on", "click", ), + ( + "empty-event", + r#""#, + "data-slhx-on", + "click", + ), + ( + "empty-confirm", + r#""#, + "data-slhx-confirm", + "non-empty", + ), ] { let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test")); let templates = base.join("templates");