From 2e39fb17a139cbceb87a736d4a47404df1b6e309 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 17 Jul 2026 05:45:03 +0200 Subject: [PATCH] fix(build): recognize exact Hemlate derives Parse derive paths structurally so qualified Hemlate derives provide template context while similarly named derives fail closed. Prove hyphen/underscore template type mapping, empty and non-UTF-8 paths, Cargo-root lookup, Rust source failures, and derived field facts through the public inspection entry point. req: diagnostics/006 req: surface/008 req: test/021 --- .cargo/mutants.toml | 3 ++ PLAN.md | 2 +- hemx-build/src/lib.rs | 77 +++++++++++++++++++++++++++++-------------- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index fcbf22a..c45e8db 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -18,6 +18,8 @@ # is externally equivalent while create/update/no-op behavior is mutation-proven. # - stylesheet_class_tokens loop-progress mutants are deterministically # non-terminating; sorted, deduplicated, boundary-aware outputs are asserted. +# - context path words are filtered non-empty before extracting their first char; +# `?` and `unwrap` are equivalent under that local iterator invariant. exclude_re = [ "test_process_try_wait", "test_process_poll_delay", @@ -38,4 +40,5 @@ exclude_re = [ "replace match guard error.kind\\(\\) == io::ErrorKind::NotFound with true in write_if_changed", "replace \\+= with (?:-=|\\*=) in stylesheet_class_tokens", "replace 1 with 0 in stylesheet_class_tokens", + "replace chars.next\\(\\)\\? with chars.next\\(\\).unwrap\\(\\) in context_type_for_heml_path", ] diff --git a/PLAN.md b/PLAN.md index 0c29ab6..d54f814 100644 --- a/PLAN.md +++ b/PLAN.md @@ -21,7 +21,7 @@ - [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages, propagates mutest failure, and mutation-tests `hemx-axum`, `hemx-core`, `hemx-js`, and the full `hemx-test` package cleanly; full package closure remains. - **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. - **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The current `hemx-build` frontier now mutation-proves resource extraction, canonical generated contracts, `AppBuilder::run`/input collection, and all 66 stylesheet discovery/scanner mutants, including exact sorting, deduplication, selector boundaries, invalid stylesheet names, and generated CSS resources; the remaining package frontier starts at template-context/Rust-fact diagnostics. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. +- **Blocked by:** none; broad survivors currently remain in `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts. The current `hemx-build` frontier now also mutation-proves all 39 template-context path, Cargo-root, Rust-source, struct-field, and exact `Hemplate` derive-recognition mutants; qualified derives work while similarly named derives fail closed instead of granting context authority. The remaining package frontier starts at loop-local context inference and placement/attribute diagnostics. The complete 470-mutant `hemx-axum` package gate now passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. - **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 ## 3. Elect and enforce the release license policy diff --git a/hemx-build/src/lib.rs b/hemx-build/src/lib.rs index 90c0408..7e0c63c 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -1868,18 +1868,10 @@ fn reject_unkeyed_loop( fn context_type_for_heml_path(path: &Path) -> Option { let stem = path.file_stem()?.to_str()?; let mut out = String::new(); - let mut upper_next = true; - for ch in stem.chars() { - if ch == '_' || ch == '-' { - upper_next = true; - continue; - } - if upper_next { - out.extend(ch.to_uppercase()); - upper_next = false; - } else { - out.push(ch); - } + for word in stem.split(['_', '-']).filter(|word| !word.is_empty()) { + let mut chars = word.chars(); + out.extend(chars.next()?.to_uppercase()); + out.extend(chars); } (!out.is_empty()).then_some(out) } @@ -1966,17 +1958,21 @@ fn collect_rust_struct_facts_from_items( } fn derives_hemplate(attrs: &[syn::Attribute]) -> bool { - attrs.iter().any(|attr| { - attr.path() - .segments - .last() - .is_some_and(|segment| segment.ident == "derive") - && attr - .meta - .require_list() - .ok() - .is_some_and(|list| list.tokens.to_string().contains("Hemplate")) - }) + attrs + .iter() + .filter(|attr| attr.path().is_ident("derive")) + .filter_map(|attr| { + attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) + .ok() + }) + .flatten() + .any(|path| { + path.segments + .last() + .is_some_and(|segment| segment.ident == "Hemplate") + }) } fn compact_tokens(tokens: &impl ToTokens) -> String { @@ -2306,7 +2302,7 @@ mod tests { .unwrap(); std::fs::write( crate_root.join("src/lib.rs"), - "#[derive(Hemplate)] struct Profile { title: String }\nstruct Plain { title: String }", + "#[derive(Hemplate)] struct Profile { title: String }\n#[derive(Clone, hemplate::Hemplate)] struct ProfileCard { card_title: String }\n#[derive(NotHemplate)] struct ProfileDetails { hidden: String }\n#[derive(HemplateExtra)] struct Extra { hidden: String }\nstruct Plain { title: String }", ) .unwrap(); let profile = templates.join("profile.heml"); @@ -2318,6 +2314,39 @@ mod tests { .expect("Hemlate-derived context facts"); assert_eq!(facts.context_type, "Profile"); assert_eq!(facts.self_fields[0].name, "title"); + + let card_facts = template_context_facts_for_heml_source( + templates.join("profile-card.heml"), + "
{{ self.card_title }}
".to_owned(), + ) + .unwrap() + .expect("qualified Hemlate derive"); + assert_eq!(card_facts.context_type, "ProfileCard"); + assert_eq!(card_facts.self_fields[0].name, "card_title"); + assert_eq!( + template_context_facts_for_heml_source( + templates.join("profile_details.heml"), + "
{{ self.hidden }}
".to_owned(), + ) + .unwrap(), + None, + "derive names containing Hemlate must not grant context authority" + ); + assert_eq!( + context_type_for_heml_path(Path::new("--profile__card--.heml")), + Some("ProfileCard".into()) + ); + assert_eq!(context_type_for_heml_path(Path::new("---.heml")), None); + assert_eq!(context_type_for_heml_path(Path::new("")), None); + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + assert_eq!( + context_type_for_heml_path(Path::new(std::ffi::OsStr::from_bytes(b"\xff.heml"))), + None + ); + } + std::fs::write(crate_root.join("src/broken.rs"), [0xff]).unwrap(); assert_eq!( template_context_facts_for_heml_source(