feat(validate): validate concept_comparison patterns
Checks that concept_comparison variant names exist in the referenced concept's enum sub-concepts and that all variants are covered (exhaustiveness checking).
This commit is contained in:
@@ -542,6 +542,133 @@ pub fn validate_species_type_invariance(file: &File, collector: &mut ErrorCollec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate concept_comparison patterns against the concept registry
|
||||||
|
///
|
||||||
|
/// Checks:
|
||||||
|
/// 1. The concept_comparison name references an existing concept
|
||||||
|
/// 2. All variant names in patterns exist in the concept's enum sub-concepts
|
||||||
|
/// 3. Patterns are exhaustive (all variants are covered)
|
||||||
|
pub fn validate_concept_comparison_patterns(
|
||||||
|
file: &File,
|
||||||
|
registry: &crate::resolve::types::ConceptRegistry,
|
||||||
|
collector: &mut ErrorCollector,
|
||||||
|
) {
|
||||||
|
for decl in &file.declarations {
|
||||||
|
if let Declaration::ConceptComparison(comp) = decl {
|
||||||
|
// Check that the concept exists
|
||||||
|
let concept = match registry.lookup_concept(&comp.name) {
|
||||||
|
| Some(c) => c,
|
||||||
|
| None => {
|
||||||
|
collector.add(ResolveError::ValidationError {
|
||||||
|
message: format!(
|
||||||
|
"Concept '{}' not found for concept_comparison",
|
||||||
|
comp.name
|
||||||
|
),
|
||||||
|
help: Some(format!(
|
||||||
|
"Add 'concept {}' before defining a concept_comparison for it.",
|
||||||
|
comp.name
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect all enum variants from all enum sub-concepts
|
||||||
|
let mut all_variants: HashSet<String> = HashSet::new();
|
||||||
|
for sc_info in concept.sub_concepts.values() {
|
||||||
|
if let SubConceptKind::Enum { variants } = &sc_info.kind {
|
||||||
|
for variant in variants {
|
||||||
|
all_variants.insert(variant.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each variant pattern references valid variants
|
||||||
|
let mut covered_variants: HashSet<String> = HashSet::new();
|
||||||
|
for pattern in &comp.variants {
|
||||||
|
// Check that the variant name is a known variant
|
||||||
|
if !all_variants.contains(&pattern.name) {
|
||||||
|
collector.add(ResolveError::ValidationError {
|
||||||
|
message: format!(
|
||||||
|
"Variant '{}' not found in concept '{}'",
|
||||||
|
pattern.name, comp.name
|
||||||
|
),
|
||||||
|
help: Some(format!(
|
||||||
|
"Available variants for '{}': {}",
|
||||||
|
comp.name,
|
||||||
|
if all_variants.is_empty() {
|
||||||
|
"(none - add sub_concept with enum variants)".to_string()
|
||||||
|
} else {
|
||||||
|
let mut sorted: Vec<_> = all_variants.iter().collect();
|
||||||
|
sorted.sort();
|
||||||
|
sorted
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
covered_variants.insert(pattern.name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check field conditions reference valid fields in the Is values
|
||||||
|
for condition in &pattern.conditions {
|
||||||
|
if let Condition::Is(values) = &condition.condition {
|
||||||
|
for value in values {
|
||||||
|
// Check that the value references a valid variant
|
||||||
|
if !all_variants.contains(value) {
|
||||||
|
collector.add(ResolveError::ValidationError {
|
||||||
|
message: format!(
|
||||||
|
"Value '{}' in condition for variant '{}' not found in concept '{}'",
|
||||||
|
value, pattern.name, comp.name
|
||||||
|
),
|
||||||
|
help: Some(format!(
|
||||||
|
"Available variants: {}",
|
||||||
|
{
|
||||||
|
let mut sorted: Vec<_> = all_variants.iter().collect();
|
||||||
|
sorted.sort();
|
||||||
|
sorted.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check exhaustiveness - all variants should be covered
|
||||||
|
let uncovered: Vec<_> = all_variants.difference(&covered_variants).collect();
|
||||||
|
if !uncovered.is_empty() {
|
||||||
|
let mut sorted: Vec<_> = uncovered.into_iter().collect();
|
||||||
|
sorted.sort();
|
||||||
|
collector.add(ResolveError::ValidationError {
|
||||||
|
message: format!(
|
||||||
|
"concept_comparison '{}' is not exhaustive: missing variants {}",
|
||||||
|
comp.name,
|
||||||
|
sorted
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("'{}'", s))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
),
|
||||||
|
help: Some(format!(
|
||||||
|
"Add patterns for all variants of concept '{}'. Missing: {}",
|
||||||
|
comp.name,
|
||||||
|
sorted
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate an entire file
|
/// Validate an entire file
|
||||||
///
|
///
|
||||||
/// Collects all validation errors and returns them together instead of failing
|
/// Collects all validation errors and returns them together instead of failing
|
||||||
@@ -552,6 +679,8 @@ pub fn validate_file(file: &File, action_registry: &HashSet<String>) -> Result<(
|
|||||||
// Type system validation
|
// Type system validation
|
||||||
validate_sub_concept_parents(file, &mut collector);
|
validate_sub_concept_parents(file, &mut collector);
|
||||||
validate_species_type_invariance(file, &mut collector);
|
validate_species_type_invariance(file, &mut collector);
|
||||||
|
let concept_registry = crate::resolve::types::ConceptRegistry::from_file(file);
|
||||||
|
validate_concept_comparison_patterns(file, &concept_registry, &mut collector);
|
||||||
|
|
||||||
for decl in &file.declarations {
|
for decl in &file.declarations {
|
||||||
match decl {
|
match decl {
|
||||||
@@ -1053,4 +1182,179 @@ mod tests {
|
|||||||
validate_species_type_invariance(&file, &mut collector);
|
validate_species_type_invariance(&file, &mut collector);
|
||||||
assert!(!collector.has_errors());
|
assert!(!collector.has_errors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Concept Comparison Validation Tests =====
|
||||||
|
|
||||||
|
use crate::resolve::types::ConceptRegistry;
|
||||||
|
|
||||||
|
fn make_concept_comparison_file() -> 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),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concept_comparison_valid_exhaustive() {
|
||||||
|
let mut file = make_concept_comparison_file();
|
||||||
|
file.declarations
|
||||||
|
.push(Declaration::ConceptComparison(ConceptComparisonDecl {
|
||||||
|
name: "Cup".to_string(),
|
||||||
|
variants: vec![
|
||||||
|
VariantPattern {
|
||||||
|
name: "Small".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Medium".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Large".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
span: Span::new(0, 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let registry = ConceptRegistry::from_file(&file);
|
||||||
|
let mut collector = ErrorCollector::new();
|
||||||
|
validate_concept_comparison_patterns(&file, ®istry, &mut collector);
|
||||||
|
assert!(!collector.has_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concept_comparison_not_exhaustive() {
|
||||||
|
let mut file = make_concept_comparison_file();
|
||||||
|
file.declarations
|
||||||
|
.push(Declaration::ConceptComparison(ConceptComparisonDecl {
|
||||||
|
name: "Cup".to_string(),
|
||||||
|
variants: vec![
|
||||||
|
VariantPattern {
|
||||||
|
name: "Small".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
// Missing Medium and Large
|
||||||
|
],
|
||||||
|
span: Span::new(0, 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let registry = ConceptRegistry::from_file(&file);
|
||||||
|
let mut collector = ErrorCollector::new();
|
||||||
|
validate_concept_comparison_patterns(&file, ®istry, &mut collector);
|
||||||
|
assert!(collector.has_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concept_comparison_unknown_variant() {
|
||||||
|
let mut file = make_concept_comparison_file();
|
||||||
|
file.declarations
|
||||||
|
.push(Declaration::ConceptComparison(ConceptComparisonDecl {
|
||||||
|
name: "Cup".to_string(),
|
||||||
|
variants: vec![
|
||||||
|
VariantPattern {
|
||||||
|
name: "Small".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Medium".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Large".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Huge".to_string(), // Not a valid variant
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
span: Span::new(0, 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let registry = ConceptRegistry::from_file(&file);
|
||||||
|
let mut collector = ErrorCollector::new();
|
||||||
|
validate_concept_comparison_patterns(&file, ®istry, &mut collector);
|
||||||
|
assert!(collector.has_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concept_comparison_missing_concept() {
|
||||||
|
let file = File {
|
||||||
|
declarations: vec![Declaration::ConceptComparison(ConceptComparisonDecl {
|
||||||
|
name: "NonExistent".to_string(),
|
||||||
|
variants: vec![],
|
||||||
|
span: Span::new(0, 100),
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
|
||||||
|
let registry = ConceptRegistry::from_file(&file);
|
||||||
|
let mut collector = ErrorCollector::new();
|
||||||
|
validate_concept_comparison_patterns(&file, ®istry, &mut collector);
|
||||||
|
assert!(collector.has_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_concept_comparison_with_field_conditions() {
|
||||||
|
let mut file = make_concept_comparison_file();
|
||||||
|
file.declarations
|
||||||
|
.push(Declaration::ConceptComparison(ConceptComparisonDecl {
|
||||||
|
name: "Cup".to_string(),
|
||||||
|
variants: vec![
|
||||||
|
VariantPattern {
|
||||||
|
name: "Small".to_string(),
|
||||||
|
conditions: vec![FieldCondition {
|
||||||
|
field_name: "material".to_string(),
|
||||||
|
condition: Condition::Is(vec!["Medium".to_string()]), // Valid variant ref
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
}],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Medium".to_string(),
|
||||||
|
conditions: vec![FieldCondition {
|
||||||
|
field_name: "material".to_string(),
|
||||||
|
condition: Condition::Any,
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
}],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
VariantPattern {
|
||||||
|
name: "Large".to_string(),
|
||||||
|
conditions: vec![],
|
||||||
|
span: Span::new(0, 10),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
span: Span::new(0, 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let registry = ConceptRegistry::from_file(&file);
|
||||||
|
let mut collector = ErrorCollector::new();
|
||||||
|
validate_concept_comparison_patterns(&file, ®istry, &mut collector);
|
||||||
|
assert!(!collector.has_errors());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user