variable_core/
validate.rs1#![allow(unused_assignments)]
2
3use std::collections::HashMap;
4
5use miette::{Diagnostic, SourceSpan};
6use thiserror::Error;
7
8use crate::ast::VarFile;
9use crate::lexer::Span;
10
11#[derive(Error, Debug, Diagnostic)]
12pub enum ValidationError {
13 #[error("duplicate feature name: `{name}`")]
14 DuplicateFeature {
15 name: String,
16 #[label("first defined here")]
17 first: SourceSpan,
18 #[label("duplicate defined here")]
19 duplicate: SourceSpan,
20 },
21
22 #[error("duplicate feature id `{id}`")]
23 DuplicateFeatureId {
24 id: u32,
25 #[label("first defined here")]
26 first: SourceSpan,
27 #[label("duplicate defined here")]
28 duplicate: SourceSpan,
29 },
30
31 #[error("duplicate variable name `{name}` in feature `{feature}`")]
32 DuplicateVariable {
33 feature: String,
34 name: String,
35 #[label("first defined here")]
36 first: SourceSpan,
37 #[label("duplicate defined here")]
38 duplicate: SourceSpan,
39 },
40
41 #[error("duplicate variable id `{id}` in feature `{feature}`")]
42 DuplicateVariableId {
43 feature: String,
44 id: u32,
45 #[label("first defined here")]
46 first: SourceSpan,
47 #[label("duplicate defined here")]
48 duplicate: SourceSpan,
49 },
50}
51
52fn span_to_source_span(span: &Span) -> SourceSpan {
53 SourceSpan::from(span.offset)
54}
55
56pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
57 let mut errors = Vec::new();
58
59 let mut feature_names: HashMap<&str, &Span> = HashMap::new();
61 let mut feature_ids: HashMap<u32, &Span> = HashMap::new();
62 for feature in &var_file.features {
63 if let Some(first_span) = feature_names.get(feature.name.as_str()) {
64 errors.push(ValidationError::DuplicateFeature {
65 name: feature.name.clone(),
66 first: span_to_source_span(first_span),
67 duplicate: span_to_source_span(&feature.span),
68 });
69 } else {
70 feature_names.insert(&feature.name, &feature.span);
71 }
72
73 if let Some(first_span) = feature_ids.get(&feature.id) {
74 errors.push(ValidationError::DuplicateFeatureId {
75 id: feature.id,
76 first: span_to_source_span(first_span),
77 duplicate: span_to_source_span(&feature.span),
78 });
79 } else {
80 feature_ids.insert(feature.id, &feature.span);
81 }
82 }
83
84 for feature in &var_file.features {
86 let mut var_names: HashMap<&str, &Span> = HashMap::new();
87 let mut var_ids: HashMap<u32, &Span> = HashMap::new();
88 for variable in &feature.variables {
89 if let Some(first_span) = var_names.get(variable.name.as_str()) {
90 errors.push(ValidationError::DuplicateVariable {
91 feature: feature.name.clone(),
92 name: variable.name.clone(),
93 first: span_to_source_span(first_span),
94 duplicate: span_to_source_span(&variable.span),
95 });
96 } else {
97 var_names.insert(&variable.name, &variable.span);
98 }
99
100 if let Some(first_span) = var_ids.get(&variable.id) {
101 errors.push(ValidationError::DuplicateVariableId {
102 feature: feature.name.clone(),
103 id: variable.id,
104 first: span_to_source_span(first_span),
105 duplicate: span_to_source_span(&variable.span),
106 });
107 } else {
108 var_ids.insert(variable.id, &variable.span);
109 }
110 }
111 }
112
113 if errors.is_empty() {
114 Ok(())
115 } else {
116 Err(errors)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::lexer::lex;
124 use crate::parser::parse;
125
126 fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
127 let tokens = lex(input).expect("lex failed");
128 let var_file = parse(tokens).expect("parse failed");
129 validate(&var_file)?;
130 Ok(var_file)
131 }
132
133 #[test]
134 fn valid_file_passes() {
135 let input = r#"1: Feature Checkout = {
136 1: Variable enabled Boolean = true
137 2: Variable max_items Number = 50
138}
139
1402: Feature Search = {
141 1: Variable query String = "default"
142}"#;
143 assert!(parse_and_validate(input).is_ok());
144 }
145
146 #[test]
147 fn duplicate_feature_name_error() {
148 let input = r#"1: Feature Checkout = {
149 1: Variable enabled Boolean = true
150}
151
1522: Feature Checkout = {
153 1: Variable max_items Number = 50
154}"#;
155 let err = parse_and_validate(input).unwrap_err();
156 assert_eq!(err.len(), 1);
157 match &err[0] {
158 ValidationError::DuplicateFeature { name, .. } => {
159 assert_eq!(name, "Checkout");
160 }
161 _ => panic!("expected DuplicateFeature error"),
162 }
163 }
164
165 #[test]
166 fn duplicate_variable_name_error() {
167 let input = r#"1: Feature Checkout = {
168 1: Variable enabled Boolean = true
169 2: Variable enabled Boolean = false
170}"#;
171 let err = parse_and_validate(input).unwrap_err();
172 assert_eq!(err.len(), 1);
173 match &err[0] {
174 ValidationError::DuplicateVariable { feature, name, .. } => {
175 assert_eq!(feature, "Checkout");
176 assert_eq!(name, "enabled");
177 }
178 _ => panic!("expected DuplicateVariable error"),
179 }
180 }
181
182 #[test]
183 fn error_has_correct_line_info() {
184 let input = r#"1: Feature Checkout = {
185 1: Variable enabled Boolean = true
186}
187
1882: Feature Checkout = {
189 1: Variable max_items Number = 50
190}"#;
191 let err = parse_and_validate(input).unwrap_err();
192 match &err[0] {
193 ValidationError::DuplicateFeature {
194 first, duplicate, ..
195 } => {
196 assert_eq!(first.offset(), 0);
198 assert!(duplicate.offset() > 0);
200 }
201 _ => panic!("expected DuplicateFeature error"),
202 }
203 }
204
205 #[test]
206 fn duplicate_feature_id_error() {
207 let input = r#"1: Feature Checkout = {
208 1: Variable enabled Boolean = true
209}
210
2111: Feature Search = {
212 1: Variable query String = "default"
213}"#;
214 let err = parse_and_validate(input).unwrap_err();
215 assert_eq!(err.len(), 1);
216 match &err[0] {
217 ValidationError::DuplicateFeatureId { id, .. } => {
218 assert_eq!(*id, 1);
219 }
220 _ => panic!("expected DuplicateFeatureId error"),
221 }
222 }
223
224 #[test]
225 fn duplicate_variable_id_error() {
226 let input = r#"1: Feature Checkout = {
227 1: Variable enabled Boolean = true
228 1: Variable max_items Number = 50
229}"#;
230 let err = parse_and_validate(input).unwrap_err();
231 assert_eq!(err.len(), 1);
232 match &err[0] {
233 ValidationError::DuplicateVariableId { feature, id, .. } => {
234 assert_eq!(feature, "Checkout");
235 assert_eq!(*id, 1);
236 }
237 _ => panic!("expected DuplicateVariableId error"),
238 }
239 }
240}