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
This commit is contained in:
slhx agent
2026-06-12 14:47:22 +02:00
parent 3b5de85319
commit d2049c559a
+19 -6
View File
@@ -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,
} = &current.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#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row">{+ todo.title +}</li></template>"#,
"data-hemx-slot=\"todo_row\"",
"ui::todo_row",
),
(
"slot_data_key",
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-key="todo.id">{+ todo.title +}</li></template>"#,
"data-hemx-slot=\"todo_row\"",
"ui::todo_row",
),
(
"handle",
r#"<template h-for="todo in &self.todos"><button data-hemx-handle="delete">Delete</button></template>"#,
"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);
}