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
This commit is contained in:
@@ -18,6 +18,8 @@
|
|||||||
# is externally equivalent while create/update/no-op behavior is mutation-proven.
|
# is externally equivalent while create/update/no-op behavior is mutation-proven.
|
||||||
# - stylesheet_class_tokens loop-progress mutants are deterministically
|
# - stylesheet_class_tokens loop-progress mutants are deterministically
|
||||||
# non-terminating; sorted, deduplicated, boundary-aware outputs are asserted.
|
# 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 = [
|
exclude_re = [
|
||||||
"test_process_try_wait",
|
"test_process_try_wait",
|
||||||
"test_process_poll_delay",
|
"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 match guard error.kind\\(\\) == io::ErrorKind::NotFound with true in write_if_changed",
|
||||||
"replace \\+= with (?:-=|\\*=) in stylesheet_class_tokens",
|
"replace \\+= with (?:-=|\\*=) in stylesheet_class_tokens",
|
||||||
"replace 1 with 0 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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.
|
- [ ] **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.
|
- **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.
|
- **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
|
- **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
|
## 3. Elect and enforce the release license policy
|
||||||
|
|||||||
+53
-24
@@ -1868,18 +1868,10 @@ fn reject_unkeyed_loop(
|
|||||||
fn context_type_for_heml_path(path: &Path) -> Option<String> {
|
fn context_type_for_heml_path(path: &Path) -> Option<String> {
|
||||||
let stem = path.file_stem()?.to_str()?;
|
let stem = path.file_stem()?.to_str()?;
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
let mut upper_next = true;
|
for word in stem.split(['_', '-']).filter(|word| !word.is_empty()) {
|
||||||
for ch in stem.chars() {
|
let mut chars = word.chars();
|
||||||
if ch == '_' || ch == '-' {
|
out.extend(chars.next()?.to_uppercase());
|
||||||
upper_next = true;
|
out.extend(chars);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if upper_next {
|
|
||||||
out.extend(ch.to_uppercase());
|
|
||||||
upper_next = false;
|
|
||||||
} else {
|
|
||||||
out.push(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(!out.is_empty()).then_some(out)
|
(!out.is_empty()).then_some(out)
|
||||||
}
|
}
|
||||||
@@ -1966,17 +1958,21 @@ fn collect_rust_struct_facts_from_items(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn derives_hemplate(attrs: &[syn::Attribute]) -> bool {
|
fn derives_hemplate(attrs: &[syn::Attribute]) -> bool {
|
||||||
attrs.iter().any(|attr| {
|
attrs
|
||||||
attr.path()
|
.iter()
|
||||||
.segments
|
.filter(|attr| attr.path().is_ident("derive"))
|
||||||
.last()
|
.filter_map(|attr| {
|
||||||
.is_some_and(|segment| segment.ident == "derive")
|
attr.parse_args_with(
|
||||||
&& attr
|
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
|
||||||
.meta
|
)
|
||||||
.require_list()
|
.ok()
|
||||||
.ok()
|
})
|
||||||
.is_some_and(|list| list.tokens.to_string().contains("Hemplate"))
|
.flatten()
|
||||||
})
|
.any(|path| {
|
||||||
|
path.segments
|
||||||
|
.last()
|
||||||
|
.is_some_and(|segment| segment.ident == "Hemplate")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compact_tokens(tokens: &impl ToTokens) -> String {
|
fn compact_tokens(tokens: &impl ToTokens) -> String {
|
||||||
@@ -2306,7 +2302,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
crate_root.join("src/lib.rs"),
|
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();
|
.unwrap();
|
||||||
let profile = templates.join("profile.heml");
|
let profile = templates.join("profile.heml");
|
||||||
@@ -2318,6 +2314,39 @@ mod tests {
|
|||||||
.expect("Hemlate-derived context facts");
|
.expect("Hemlate-derived context facts");
|
||||||
assert_eq!(facts.context_type, "Profile");
|
assert_eq!(facts.context_type, "Profile");
|
||||||
assert_eq!(facts.self_fields[0].name, "title");
|
assert_eq!(facts.self_fields[0].name, "title");
|
||||||
|
|
||||||
|
let card_facts = template_context_facts_for_heml_source(
|
||||||
|
templates.join("profile-card.heml"),
|
||||||
|
"<main>{{ self.card_title }}</main>".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"),
|
||||||
|
"<main>{{ self.hidden }}</main>".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();
|
std::fs::write(crate_root.join("src/broken.rs"), [0xff]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
template_context_facts_for_heml_source(
|
template_context_facts_for_heml_source(
|
||||||
|
|||||||
Reference in New Issue
Block a user