From d2049c559a49ebd8c4eedb1e4b8a958ca972378f Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 14:47:22 +0200 Subject: [PATCH] fix(build): explain missing keyed loop facts Improve the unkeyed h-for diagnostic so authors see the exact loop, stable h-key shape, generated helper consequence, and why dynamic child data-key is not enough. req: scope/001 req: diagnostics/002 --- hemx-build/src/lib.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/hemx-build/src/lib.rs b/hemx-build/src/lib.rs index 6f92df3..e77d80a 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -1543,11 +1543,16 @@ fn reject_unkeyed_loop( let Some(current) = surface.scopes.get(scope.0 as usize) else { return Ok(()); }; - if matches!(current.kind, ScopeKind::For { key_expr: None, .. }) { + if let ScopeKind::For { + pattern, + expr, + key_expr: None, + } = ¤t.kind + { return Err(io::Error::new( io::ErrorKind::InvalidData, format!( - "{}: data-hemx-{kind}=\"{name}\" is inside an h-for without h-key; add a stable h-key=\"item.id\" to the loop that owns this generated target", + "{}: data-hemx-{kind}=\"{name}\" is inside h-for=\"{pattern} in {expr}\" without h-key; add a stable h-key=\"{pattern}.id\" to that h-for so generated keyed helpers such as ui::{name}.replace(row) can target this partial. Dynamic +data-key on the child is rendered HTML, not the template fact hemx uses for generated targets.", path.display() ), )); @@ -2052,22 +2057,25 @@ fn main() {{ #[test] fn rejects_hemx_resources_inside_unkeyed_for() { - // req: scope/001 - for (case, template, resource) in [ + // req: scope/001 req: diagnostics/002 + for (case, template, resource, helper) in [ ( "slot", r#""#, "data-hemx-slot=\"todo_row\"", + "ui::todo_row", ), ( "slot_data_key", r#""#, "data-hemx-slot=\"todo_row\"", + "ui::todo_row", ), ( "handle", r#""#, "data-hemx-handle=\"delete\"", + "ui::delete", ), ] { let base = test_dir(&format!("hemx-build-unkeyed-for-{case}-test")); @@ -2082,8 +2090,13 @@ fn main() {{ .out_dir(&out) .run() .unwrap_err(); - assert!(err.to_string().contains("inside an h-for without h-key")); - assert!(err.to_string().contains(resource)); + let err = err.to_string(); + assert!(err.contains("inside h-for=\"todo in &self.todos\" without h-key")); + assert!(err.contains(resource)); + assert!(err.contains("h-key=\"todo.id\"")); + assert!(err.contains("generated keyed helpers")); + assert!(err.contains(helper)); + assert!(err.contains("Dynamic +data-key on the child")); let _ = std::fs::remove_dir_all(&base); }