fix(build): fail closed on invalid loop locals
Recognize only one valid Rust identifier bound directly to a self vector field as a template local; reject malformed and non-vector bindings, preserve unknown element locals without fabricated fields, and deduplicate repeated locals. req: diagnostics/006 req: surface/008 req: test/021
This commit is contained in:
@@ -64,7 +64,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
||||
- Use `cargo run -p hemx-xtask -- app new PATH` for the generic page/form/keyed-row/notice starter, and `cargo run -p hemx-xtask -- app new --mobile PATH` for the phone-first starter with host capabilities, recovery truth, and release-kit commands; do not treat it as a mobile framework or store-submission bot. req: ceremony/005 req: ceremony/006 req: ceremony/007
|
||||
- The public component-reuse explanation lives in `docs/recipes/reusable-partials.md`; do not grow a client component framework to explain partial composition.
|
||||
- The stable public `.heml` authoring surface lives in `docs/hemplate-syntax.md`; Hemlate examples must use that real hemplate syntax, not Vue/Handlebars sketches. hemx-build consumes hemplate Surface facts and must not grow an independent `.heml` parser or CSS-path identity model. Plain CSS/SCSS owns appearance; generated class constants are ergonomic references, not a styling framework or behavior selector system. Generated resources, form/handle metadata, atoms, and event constants come from hemx-build facts, not hand-written app plumbing. Forms remain HTML-shaped, checked against user-authored Rust domain types, parsed through `FormValue`, and manipulated through generated form/control ids rather than selectors. Proc-macros stay local/side-effect-free while build.rs owns global codegen and hard build failures. No-op global codegen must preserve generated artifact timestamps so downstream Rust compilation remains fresh only when canonical output changes. req: boundary/001 req: boundary/002 req: boundary/003 req: boundary/004 req: surface/001 req: surface/002 req: surface/003 req: surface/004 req: surface/005 req: surface/006 req: surface/007 req: surface/008 req: surface/009 req: surface/010 req: codegen/001 req: codegen/003 req: codegen/004 req: codegen/005 req: codegen/006 req: form/001 req: form/004 req: form/007 req: form/008 req: form_effects/001 req: form_effects/002 req: form_effects/003 req: build/001 req: build/002 req: build/003 req: build/004 req: build/005 req: build/006 req: build/007 req: build/008 req: build/009 req: style/001 req: style/002 req: style/003 req: style/004 req: style/005 req: style/006
|
||||
- Optional `.heml` editor overlays must share authority with `hemx-build` diagnostics and `docs/hemplate-syntax.md`; `hemx-lsp` owns editor protocol glue for diagnostics/completion/hover and derive-known template facts, while VS Code/Cursor/Neovim keep normal HTML/tree-sitter tooling. Do not create a second template language, selector model, formatter, Rust type system, or custom editor framework. Compiler diagnostics with directive/target metadata select that source attribute instead of line 0 column 0. Cross-file template/handler references visible to build validation must fail at `cargo check` with useful spans; global completeness checks stay component-scoped unless caught at mount/tests. req: diagnostics/004 req: diagnostics/005 req: diagnostics/006 req: diagnostics/007 req: diagnostics/008 req: diag/009 req: diag/010 req: invariant/004 req: invariant/006 req: check/001 req: check/003
|
||||
- Optional `.heml` editor overlays must share authority with `hemx-build` diagnostics and `docs/hemplate-syntax.md`; `hemx-lsp` owns editor protocol glue for diagnostics/completion/hover and derive-known template facts, while VS Code/Cursor/Neovim keep normal HTML/tree-sitter tooling. Simple `h-for` completion facts cover one Rust identifier bound directly to a `self` vector field; malformed bindings and non-vector fields must not fabricate locals. Do not create a second template language, selector model, formatter, Rust type system, or custom editor framework. Compiler diagnostics with directive/target metadata select that source attribute instead of line 0 column 0. Cross-file template/handler references visible to build validation must fail at `cargo check` with useful spans; global completeness checks stay component-scoped unless caught at mount/tests. req: diagnostics/004 req: diagnostics/005 req: diagnostics/006 req: diagnostics/007 req: diagnostics/008 req: diag/009 req: diag/010 req: invariant/004 req: invariant/006 req: check/001 req: check/003
|
||||
- JS runtime changes must preserve root-scoped lookup, delegated listeners, canonical hemx `EffectBatch` application, fail-closed request handling, transactional/recoverable failure behavior, root-scoped error outlets, and tiny pending/failure/trigger-timing conventions without selectors, handler-name parsing, VDOM, expressions, or per-node listeners. Runtime `.d.ts` types are developer convenience only, not core tooling authority. req: invariant/002 req: runtime/001 req: runtime/002 req: runtime/003 req: runtime/005 req: runtime/006 req: failure/001 req: failure/002 req: failure/003 req: failure/004 req: failure/005 req: failure/006 req: convention/001 req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: convention/007 req: convention/008 req: convention/009 req: convention/010 req: convention/011 req: convention/012 req: convention/013 req: convention/014 req: convention/015 req: convention/016 req: convention/017 req: ts/001
|
||||
- Opaque island JavaScript is a leaf adapter for high-frequency local behavior only; client-local handlers keep the server-handler shape while `hemx-wasm` owns concrete opt-in syntax. Use native events/generated helpers at the boundary and do not introduce a component runtime, client state graph, VDOM, selector interop, or second UI model. req: canonical_authoring/017 req: client_local/001 req: client_local/003 req: client_local/004 req: interop/001 req: interop/002 req: interop/003 req: interop/006 req: interop/007 req: interop/008 req: interop/009 req: interop/010 req: interop/011 req: interop/012
|
||||
- Host capability adapters must stay at the `hemx-host` boundary: typed capabilities use fire/request/stream/schedule shapes; adapters may call host APIs and return host events, but they must not mutate DOM or own app/domain state. req: host/001 req: host/002
|
||||
|
||||
@@ -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 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.
|
||||
- **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 31 loop-local inference mutants: only one Rust identifier bound directly to a `self` vector field yields facts, malformed/non-vector bindings fail closed, unknown element types preserve an empty local, and repeated locals deduplicate. The remaining package frontier starts at 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
|
||||
|
||||
+1
-1
@@ -650,7 +650,7 @@ what a valid business email is. [north_star]
|
||||
|
||||
005 `.heml` editor startup for VS Code, Cursor, and Neovim must preserve normal HTML or tree-sitter HTML highlighting while using repo-owned `hemx-build` diagnostics through `hemx-lsp` as the shared authority for hemplate-specific feedback. [north_star]
|
||||
|
||||
006 `hemx-lsp` completion and hover for `.heml` Rust-shaped expressions uses hemx-owned compiler/build facts for derive-known template context fields and simple `h-for` locals. Missing or stale facts fall back to syntax/document completions. [north_star]
|
||||
006 `hemx-lsp` completion and hover for `.heml` Rust-shaped expressions uses hemx-owned compiler/build facts for derive-known template context fields and simple `h-for` locals. A simple local is one Rust identifier bound directly to a `self` vector field; malformed bindings and non-vector fields do not produce local facts. Missing or stale facts fall back to syntax/document completions. [north_star]
|
||||
|
||||
008 `hemx-lsp` must not proxy rust-analyzer or own a second Rust type system. [north_star]
|
||||
|
||||
|
||||
+103
-40
@@ -1994,37 +1994,36 @@ fn loop_locals_for_surface(
|
||||
) -> Vec<TemplateLocalFact> {
|
||||
let mut locals = Vec::new();
|
||||
for node in &surface.nodes {
|
||||
for attr in &node.attrs {
|
||||
if attr.name != "h-for" {
|
||||
continue;
|
||||
}
|
||||
let Some(value) = &attr.value else {
|
||||
continue;
|
||||
};
|
||||
let Some((local, field)) = h_for_local_and_self_field(value) else {
|
||||
continue;
|
||||
};
|
||||
let Some(self_field) = self_fields.iter().find(|candidate| candidate.name == field)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(type_name) = vec_element_type(&self_field.type_name) else {
|
||||
continue;
|
||||
};
|
||||
let fields = structs
|
||||
.get(&type_name)
|
||||
.map(|fact| fact.fields.clone())
|
||||
.unwrap_or_default();
|
||||
if !locals
|
||||
.iter()
|
||||
.any(|existing: &TemplateLocalFact| existing.name == local)
|
||||
{
|
||||
locals.push(TemplateLocalFact {
|
||||
name: local,
|
||||
type_name,
|
||||
fields,
|
||||
});
|
||||
}
|
||||
let Some(value) = node
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| attr.name == "h-for")
|
||||
.and_then(|attr| attr.value.as_deref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some((local, field)) = h_for_local_and_self_field(value) else {
|
||||
continue;
|
||||
};
|
||||
let Some(self_field) = self_fields.iter().find(|candidate| candidate.name == field) else {
|
||||
continue;
|
||||
};
|
||||
let Some(type_name) = vec_element_type(&self_field.type_name) else {
|
||||
continue;
|
||||
};
|
||||
let fields = structs
|
||||
.get(&type_name)
|
||||
.map(|fact| fact.fields.clone())
|
||||
.unwrap_or_default();
|
||||
if !locals
|
||||
.iter()
|
||||
.any(|existing: &TemplateLocalFact| existing.name == local)
|
||||
{
|
||||
locals.push(TemplateLocalFact {
|
||||
name: local,
|
||||
type_name,
|
||||
fields,
|
||||
});
|
||||
}
|
||||
}
|
||||
locals
|
||||
@@ -2032,17 +2031,14 @@ fn loop_locals_for_surface(
|
||||
|
||||
fn h_for_local_and_self_field(value: &str) -> Option<(String, String)> {
|
||||
let (local, expr) = value.split_once(" in ")?;
|
||||
let local = local.trim();
|
||||
if local.is_empty() || local.contains(['(', ',', ' ']) {
|
||||
return None;
|
||||
}
|
||||
let local = rust_ident(local.trim())?;
|
||||
let expr = expr.trim().strip_prefix('&').unwrap_or(expr.trim()).trim();
|
||||
let field = expr
|
||||
.strip_prefix("self.")?
|
||||
.split(['.', '(', '['])
|
||||
.next()?
|
||||
.trim();
|
||||
(!field.is_empty()).then(|| (local.to_owned(), field.to_owned()))
|
||||
.next()
|
||||
.filter(|field| !field.is_empty())?;
|
||||
Some((local, field.to_owned()))
|
||||
}
|
||||
|
||||
fn vec_element_type(type_name: &str) -> Option<String> {
|
||||
@@ -3500,12 +3496,28 @@ fn main() {{
|
||||
pub struct ExercisePlan { pub name: String, pub kg: f32 }
|
||||
|
||||
#[derive(Hemplate)]
|
||||
pub struct Workout { pub plan: Vec<ExercisePlan>, pub progress: String }
|
||||
pub struct Workout {
|
||||
pub plan: Vec<ExercisePlan>,
|
||||
pub alternate: std::vec::Vec<ExercisePlan>,
|
||||
pub mysteries: Vec<MissingFact>,
|
||||
pub progress: String,
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.expect("write lib");
|
||||
let template = dir.join("workout.heml");
|
||||
let source = r#"<main><p>{+ self.progress +}</p><li h-for="exercise in &self.plan">{+ exercise.name +}</li></main>"#;
|
||||
let source = r#"
|
||||
<main title="ghost in self.plan">
|
||||
<p h-for="malformed">{+ self.progress +}</p>
|
||||
<p h-for="(tuple, item) in self.plan">ignored</p>
|
||||
<p h-for="unknown in self.unknown">ignored</p>
|
||||
<p h-for="scalar in self.progress">ignored</p>
|
||||
<li h-for="exercise in &self.plan">{+ exercise.name +}</li>
|
||||
<li h-for="exercise in self.alternate">{+ exercise.kg +}</li>
|
||||
<li h-for="mystery in self.mysteries">{+ mystery.anything +}</li>
|
||||
<li h-for="later in self.plan[0]">{+ later.name +}</li>
|
||||
</main>
|
||||
"#;
|
||||
std::fs::write(&template, source).expect("write heml");
|
||||
|
||||
let facts = template_context_facts_for_heml_source(&template, source)
|
||||
@@ -3527,6 +3539,57 @@ fn main() {{
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.name == "name" && field.type_name == "String"));
|
||||
assert_eq!(
|
||||
facts
|
||||
.locals
|
||||
.iter()
|
||||
.filter(|local| local.name == "exercise")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let mystery = facts
|
||||
.locals
|
||||
.iter()
|
||||
.find(|local| local.name == "mystery")
|
||||
.expect("unknown element type still exposes the local");
|
||||
assert_eq!(mystery.type_name, "MissingFact");
|
||||
assert!(mystery.fields.is_empty());
|
||||
assert!(facts.locals.iter().any(|local| {
|
||||
local.name == "later"
|
||||
&& local.type_name == "ExercisePlan"
|
||||
&& local.fields.iter().any(|field| field.name == "kg")
|
||||
}));
|
||||
for rejected in ["ghost", "tuple", "item", "unknown", "scalar"] {
|
||||
assert!(
|
||||
facts.locals.iter().all(|local| local.name != rejected),
|
||||
"unexpected loop local {rejected}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
h_for_local_and_self_field(" item in &self.plan "),
|
||||
Some(("item".into(), "plan".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
h_for_local_and_self_field("item in self.plan.iter()"),
|
||||
Some(("item".into(), "plan".into()))
|
||||
);
|
||||
for invalid in [
|
||||
"item:self.plan",
|
||||
" in self.plan",
|
||||
"1item in self.plan",
|
||||
"item-name in self.plan",
|
||||
"(item, index) in self.plan",
|
||||
"item in plan",
|
||||
"item in &plan",
|
||||
"item in self.",
|
||||
] {
|
||||
assert_eq!(
|
||||
h_for_local_and_self_field(invalid),
|
||||
None,
|
||||
"accepted invalid h-for {invalid:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user