variable_core/
validate.rs1use std::collections::HashMap;
2
3use miette::{Diagnostic, SourceSpan};
4use thiserror::Error;
5
6use crate::ast::VarFile;
7use crate::lexer::Span;
8
9#[derive(Error, Debug, Diagnostic)]
10pub enum ValidationError {
11 #[error("duplicate feature name: `{name}`")]
12 DuplicateFeature {
13 name: String,
14 #[label("first defined here")]
15 first: SourceSpan,
16 #[label("duplicate defined here")]
17 duplicate: SourceSpan,
18 },
19
20 #[error("duplicate variable name `{name}` in feature `{feature}`")]
21 DuplicateVariable {
22 feature: String,
23 name: String,
24 #[label("first defined here")]
25 first: SourceSpan,
26 #[label("duplicate defined here")]
27 duplicate: SourceSpan,
28 },
29}
30
31fn span_to_source_span(span: &Span) -> SourceSpan {
32 SourceSpan::from(span.offset)
33}
34
35pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
36 let mut errors = Vec::new();
37
38 let mut feature_names: HashMap<&str, &Span> = HashMap::new();
40 for feature in &var_file.features {
41 if let Some(first_span) = feature_names.get(feature.name.as_str()) {
42 errors.push(ValidationError::DuplicateFeature {
43 name: feature.name.clone(),
44 first: span_to_source_span(first_span),
45 duplicate: span_to_source_span(&feature.span),
46 });
47 } else {
48 feature_names.insert(&feature.name, &feature.span);
49 }
50 }
51
52 for feature in &var_file.features {
54 let mut var_names: HashMap<&str, &Span> = HashMap::new();
55 for variable in &feature.variables {
56 if let Some(first_span) = var_names.get(variable.name.as_str()) {
57 errors.push(ValidationError::DuplicateVariable {
58 feature: feature.name.clone(),
59 name: variable.name.clone(),
60 first: span_to_source_span(first_span),
61 duplicate: span_to_source_span(&variable.span),
62 });
63 } else {
64 var_names.insert(&variable.name, &variable.span);
65 }
66 }
67 }
68
69 if errors.is_empty() {
70 Ok(())
71 } else {
72 Err(errors)
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::lexer::lex;
80 use crate::parser::parse;
81
82 fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
83 let tokens = lex(input).expect("lex failed");
84 let var_file = parse(tokens).expect("parse failed");
85 validate(&var_file)?;
86 Ok(var_file)
87 }
88
89 #[test]
90 fn valid_file_passes() {
91 let input = r#"Feature Checkout {
92 Variable enabled Boolean = true
93 Variable max_items Number = 50
94}
95
96Feature Search {
97 Variable query String = "default"
98}"#;
99 assert!(parse_and_validate(input).is_ok());
100 }
101
102 #[test]
103 fn duplicate_feature_name_error() {
104 let input = r#"Feature Checkout {
105 Variable enabled Boolean = true
106}
107
108Feature Checkout {
109 Variable max_items Number = 50
110}"#;
111 let err = parse_and_validate(input).unwrap_err();
112 assert_eq!(err.len(), 1);
113 match &err[0] {
114 ValidationError::DuplicateFeature { name, .. } => {
115 assert_eq!(name, "Checkout");
116 }
117 _ => panic!("expected DuplicateFeature error"),
118 }
119 }
120
121 #[test]
122 fn duplicate_variable_name_error() {
123 let input = r#"Feature Checkout {
124 Variable enabled Boolean = true
125 Variable enabled Boolean = false
126}"#;
127 let err = parse_and_validate(input).unwrap_err();
128 assert_eq!(err.len(), 1);
129 match &err[0] {
130 ValidationError::DuplicateVariable {
131 feature, name, ..
132 } => {
133 assert_eq!(feature, "Checkout");
134 assert_eq!(name, "enabled");
135 }
136 _ => panic!("expected DuplicateVariable error"),
137 }
138 }
139
140 #[test]
141 fn error_has_correct_line_info() {
142 let input = r#"Feature Checkout {
143 Variable enabled Boolean = true
144}
145
146Feature Checkout {
147 Variable max_items Number = 50
148}"#;
149 let err = parse_and_validate(input).unwrap_err();
150 match &err[0] {
151 ValidationError::DuplicateFeature {
152 first, duplicate, ..
153 } => {
154 assert_eq!(first.offset(), 0);
156 assert!(duplicate.offset() > 0);
158 }
159 _ => panic!("expected DuplicateFeature error"),
160 }
161 }
162}