feat(validate): check parent concept exists for sub_concepts

Added validation that sub_concept declarations reference an existing
parent concept. Produces clear error message with guidance to add
the missing concept declaration.
This commit is contained in:
2026-02-14 14:07:39 +00:00
parent 0cd00a9e73
commit 0dae430841

View File

@@ -410,6 +410,40 @@ pub fn validate_schedule_composition(schedule: &Schedule, collector: &mut ErrorC
}
}
/// Validate that all sub_concepts reference existing parent concepts
pub fn validate_sub_concept_parents(file: &File, collector: &mut ErrorCollector) {
// Collect all concept names
let concept_names: HashSet<String> = file
.declarations
.iter()
.filter_map(|decl| {
if let Declaration::Concept(c) = decl {
Some(c.name.clone())
} else {
None
}
})
.collect();
// Check each sub_concept references an existing parent
for decl in &file.declarations {
if let Declaration::SubConcept(sc) = decl {
if !concept_names.contains(&sc.parent_concept) {
collector.add(ResolveError::ValidationError {
message: format!(
"Parent concept '{}' not found for sub_concept '{}.{}'",
sc.parent_concept, sc.parent_concept, sc.name
),
help: Some(format!(
"Add 'concept {}' before defining sub_concepts for it.",
sc.parent_concept
)),
});
}
}
}
}
/// Validate an entire file
///
/// Collects all validation errors and returns them together instead of failing
@@ -417,6 +451,9 @@ pub fn validate_schedule_composition(schedule: &Schedule, collector: &mut ErrorC
pub fn validate_file(file: &File, action_registry: &HashSet<String>) -> Result<()> {
let mut collector = ErrorCollector::new();
// Type system validation
validate_sub_concept_parents(file, &mut collector);
for decl in &file.declarations {
match decl {
| Declaration::Character(c) => {
@@ -652,4 +689,106 @@ mod tests {
validate_behavior_tree_actions(&tree, &registry, &mut collector);
assert!(collector.has_errors());
}
#[test]
fn test_sub_concept_with_valid_parent() {
let file = File {
declarations: vec![
Declaration::Concept(ConceptDecl {
name: "Cup".to_string(),
span: Span::new(0, 10),
}),
Declaration::SubConcept(SubConceptDecl {
name: "Type".to_string(),
parent_concept: "Cup".to_string(),
kind: SubConceptKind::Enum {
variants: vec![
"Small".to_string(),
"Medium".to_string(),
"Large".to_string(),
],
},
span: Span::new(20, 60),
}),
],
};
let mut collector = ErrorCollector::new();
validate_sub_concept_parents(&file, &mut collector);
assert!(!collector.has_errors());
}
#[test]
fn test_sub_concept_with_missing_parent() {
let file = File {
declarations: vec![Declaration::SubConcept(SubConceptDecl {
name: "Type".to_string(),
parent_concept: "Cup".to_string(),
kind: SubConceptKind::Enum {
variants: vec!["Small".to_string()],
},
span: Span::new(0, 40),
})],
};
let mut collector = ErrorCollector::new();
validate_sub_concept_parents(&file, &mut collector);
assert!(collector.has_errors());
}
#[test]
fn test_multiple_sub_concepts_same_valid_parent() {
let file = File {
declarations: vec![
Declaration::Concept(ConceptDecl {
name: "Cup".to_string(),
span: Span::new(0, 10),
}),
Declaration::SubConcept(SubConceptDecl {
name: "Type".to_string(),
parent_concept: "Cup".to_string(),
kind: SubConceptKind::Enum {
variants: vec!["Mug".to_string()],
},
span: Span::new(20, 40),
}),
Declaration::SubConcept(SubConceptDecl {
name: "Material".to_string(),
parent_concept: "Cup".to_string(),
kind: SubConceptKind::Enum {
variants: vec!["Glass".to_string()],
},
span: Span::new(50, 70),
}),
],
};
let mut collector = ErrorCollector::new();
validate_sub_concept_parents(&file, &mut collector);
assert!(!collector.has_errors());
}
#[test]
fn test_sub_concept_wrong_parent_name() {
let file = File {
declarations: vec![
Declaration::Concept(ConceptDecl {
name: "Plate".to_string(),
span: Span::new(0, 10),
}),
Declaration::SubConcept(SubConceptDecl {
name: "Type".to_string(),
parent_concept: "Cup".to_string(),
kind: SubConceptKind::Enum {
variants: vec!["Small".to_string()],
},
span: Span::new(20, 40),
}),
],
};
let mut collector = ErrorCollector::new();
validate_sub_concept_parents(&file, &mut collector);
assert!(collector.has_errors());
}
}