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:
+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