Skip to main content

variable_core/
validate.rs

1#![allow(unused_assignments)]
2
3use std::collections::{HashMap, HashSet};
4
5use miette::{Diagnostic, SourceSpan};
6use thiserror::Error;
7
8use crate::ast::{VarFile, VarType};
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    #[error("duplicate struct name: `{name}`")]
52    DuplicateStruct {
53        name: String,
54        #[label("first defined here")]
55        first: SourceSpan,
56        #[label("duplicate defined here")]
57        duplicate: SourceSpan,
58    },
59
60    #[error("duplicate struct id `{id}`")]
61    DuplicateStructId {
62        id: u32,
63        #[label("first defined here")]
64        first: SourceSpan,
65        #[label("duplicate defined here")]
66        duplicate: SourceSpan,
67    },
68
69    #[error("duplicate field name `{name}` in struct `{struct_name}`")]
70    DuplicateField {
71        struct_name: String,
72        name: String,
73        #[label("first defined here")]
74        first: SourceSpan,
75        #[label("duplicate defined here")]
76        duplicate: SourceSpan,
77    },
78
79    #[error("duplicate field id `{id}` in struct `{struct_name}`")]
80    DuplicateFieldId {
81        struct_name: String,
82        id: u32,
83        #[label("first defined here")]
84        first: SourceSpan,
85        #[label("duplicate defined here")]
86        duplicate: SourceSpan,
87    },
88
89    #[error("unknown struct type `{type_name}` for variable `{variable}` in feature `{feature}`")]
90    UnknownStructType {
91        feature: String,
92        variable: String,
93        type_name: String,
94        #[label("used here")]
95        span: SourceSpan,
96    },
97
98    #[error("unknown field `{field}` in struct literal for type `{struct_name}`")]
99    UnknownStructField {
100        struct_name: String,
101        field: String,
102        #[label("used here")]
103        span: SourceSpan,
104    },
105}
106
107fn span_to_source_span(span: &Span) -> SourceSpan {
108    SourceSpan::from(span.offset)
109}
110
111pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
112    let mut errors = Vec::new();
113
114    // Collect known struct names for type reference validation
115    let mut struct_names_set: HashSet<&str> = HashSet::new();
116
117    // Check for duplicate struct names and IDs
118    let mut struct_names: HashMap<&str, &Span> = HashMap::new();
119    let mut struct_ids: HashMap<u32, &Span> = HashMap::new();
120    for struct_def in &var_file.structs {
121        struct_names_set.insert(&struct_def.name);
122
123        if let Some(first_span) = struct_names.get(struct_def.name.as_str()) {
124            errors.push(ValidationError::DuplicateStruct {
125                name: struct_def.name.clone(),
126                first: span_to_source_span(first_span),
127                duplicate: span_to_source_span(&struct_def.span),
128            });
129        } else {
130            struct_names.insert(&struct_def.name, &struct_def.span);
131        }
132
133        if let Some(first_span) = struct_ids.get(&struct_def.id) {
134            errors.push(ValidationError::DuplicateStructId {
135                id: struct_def.id,
136                first: span_to_source_span(first_span),
137                duplicate: span_to_source_span(&struct_def.span),
138            });
139        } else {
140            struct_ids.insert(struct_def.id, &struct_def.span);
141        }
142
143        // Check for duplicate field names and IDs within struct
144        let mut field_names: HashMap<&str, &Span> = HashMap::new();
145        let mut field_ids: HashMap<u32, &Span> = HashMap::new();
146        for field in &struct_def.fields {
147            if let Some(first_span) = field_names.get(field.name.as_str()) {
148                errors.push(ValidationError::DuplicateField {
149                    struct_name: struct_def.name.clone(),
150                    name: field.name.clone(),
151                    first: span_to_source_span(first_span),
152                    duplicate: span_to_source_span(&field.span),
153                });
154            } else {
155                field_names.insert(&field.name, &field.span);
156            }
157
158            if let Some(first_span) = field_ids.get(&field.id) {
159                errors.push(ValidationError::DuplicateFieldId {
160                    struct_name: struct_def.name.clone(),
161                    id: field.id,
162                    first: span_to_source_span(first_span),
163                    duplicate: span_to_source_span(&field.span),
164                });
165            } else {
166                field_ids.insert(field.id, &field.span);
167            }
168        }
169    }
170
171    // Build a map of struct field names for literal validation
172    let struct_field_names: HashMap<&str, HashSet<&str>> = var_file
173        .structs
174        .iter()
175        .map(|s| {
176            let fields: HashSet<&str> = s.fields.iter().map(|f| f.name.as_str()).collect();
177            (s.name.as_str(), fields)
178        })
179        .collect();
180
181    // Check for duplicate feature names and IDs
182    let mut feature_names: HashMap<&str, &Span> = HashMap::new();
183    let mut feature_ids: HashMap<u32, &Span> = HashMap::new();
184    for feature in &var_file.features {
185        if let Some(first_span) = feature_names.get(feature.name.as_str()) {
186            errors.push(ValidationError::DuplicateFeature {
187                name: feature.name.clone(),
188                first: span_to_source_span(first_span),
189                duplicate: span_to_source_span(&feature.span),
190            });
191        } else {
192            feature_names.insert(&feature.name, &feature.span);
193        }
194
195        if let Some(first_span) = feature_ids.get(&feature.id) {
196            errors.push(ValidationError::DuplicateFeatureId {
197                id: feature.id,
198                first: span_to_source_span(first_span),
199                duplicate: span_to_source_span(&feature.span),
200            });
201        } else {
202            feature_ids.insert(feature.id, &feature.span);
203        }
204    }
205
206    // Check for duplicate variable names and IDs within each feature,
207    // and validate struct type references
208    for feature in &var_file.features {
209        let mut var_names: HashMap<&str, &Span> = HashMap::new();
210        let mut var_ids: HashMap<u32, &Span> = HashMap::new();
211        for variable in &feature.variables {
212            if let Some(first_span) = var_names.get(variable.name.as_str()) {
213                errors.push(ValidationError::DuplicateVariable {
214                    feature: feature.name.clone(),
215                    name: variable.name.clone(),
216                    first: span_to_source_span(first_span),
217                    duplicate: span_to_source_span(&variable.span),
218                });
219            } else {
220                var_names.insert(&variable.name, &variable.span);
221            }
222
223            if let Some(first_span) = var_ids.get(&variable.id) {
224                errors.push(ValidationError::DuplicateVariableId {
225                    feature: feature.name.clone(),
226                    id: variable.id,
227                    first: span_to_source_span(first_span),
228                    duplicate: span_to_source_span(&variable.span),
229                });
230            } else {
231                var_ids.insert(variable.id, &variable.span);
232            }
233
234            // Validate struct type references resolve to a defined struct
235            if let VarType::Struct(ref struct_name) = variable.var_type {
236                if !struct_names_set.contains(struct_name.as_str()) {
237                    errors.push(ValidationError::UnknownStructType {
238                        feature: feature.name.clone(),
239                        variable: variable.name.clone(),
240                        type_name: struct_name.clone(),
241                        span: span_to_source_span(&variable.span),
242                    });
243                }
244
245                // Validate struct literal fields exist in the struct definition
246                if let crate::ast::Value::Struct { fields, .. } = &variable.default {
247                    if let Some(valid_fields) = struct_field_names.get(struct_name.as_str()) {
248                        for field_name in fields.keys() {
249                            if !valid_fields.contains(field_name.as_str()) {
250                                errors.push(ValidationError::UnknownStructField {
251                                    struct_name: struct_name.clone(),
252                                    field: field_name.clone(),
253                                    span: span_to_source_span(&variable.span),
254                                });
255                            }
256                        }
257                    }
258                }
259            }
260        }
261    }
262
263    if errors.is_empty() {
264        Ok(())
265    } else {
266        Err(errors)
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::lexer::lex;
274    use crate::parser::parse;
275
276    fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
277        let tokens = lex(input).expect("lex failed");
278        let var_file = parse(tokens).expect("parse failed");
279        validate(&var_file)?;
280        Ok(var_file)
281    }
282
283    #[test]
284    fn valid_file_passes() {
285        let input = r#"1: Feature Checkout = {
286    1: enabled Boolean = true
287    2: max_items Integer = 50
288}
289
2902: Feature Search = {
291    1: query String = "default"
292}"#;
293        assert!(parse_and_validate(input).is_ok());
294    }
295
296    #[test]
297    fn duplicate_feature_name_error() {
298        let input = r#"1: Feature Checkout = {
299    1: enabled Boolean = true
300}
301
3022: Feature Checkout = {
303    1: max_items Integer = 50
304}"#;
305        let err = parse_and_validate(input).unwrap_err();
306        assert_eq!(err.len(), 1);
307        match &err[0] {
308            ValidationError::DuplicateFeature { name, .. } => {
309                assert_eq!(name, "Checkout");
310            }
311            _ => panic!("expected DuplicateFeature error"),
312        }
313    }
314
315    #[test]
316    fn duplicate_variable_name_error() {
317        let input = r#"1: Feature Checkout = {
318    1: enabled Boolean = true
319    2: enabled Boolean = false
320}"#;
321        let err = parse_and_validate(input).unwrap_err();
322        assert_eq!(err.len(), 1);
323        match &err[0] {
324            ValidationError::DuplicateVariable { feature, name, .. } => {
325                assert_eq!(feature, "Checkout");
326                assert_eq!(name, "enabled");
327            }
328            _ => panic!("expected DuplicateVariable error"),
329        }
330    }
331
332    #[test]
333    fn error_has_correct_line_info() {
334        let input = r#"1: Feature Checkout = {
335    1: enabled Boolean = true
336}
337
3382: Feature Checkout = {
339    1: max_items Integer = 50
340}"#;
341        let err = parse_and_validate(input).unwrap_err();
342        match &err[0] {
343            ValidationError::DuplicateFeature {
344                first, duplicate, ..
345            } => {
346                // First "Feature" is at offset 0
347                assert_eq!(first.offset(), 0);
348                // Duplicate "Feature" is on line 5
349                assert!(duplicate.offset() > 0);
350            }
351            _ => panic!("expected DuplicateFeature error"),
352        }
353    }
354
355    #[test]
356    fn duplicate_feature_id_error() {
357        let input = r#"1: Feature Checkout = {
358    1: enabled Boolean = true
359}
360
3611: Feature Search = {
362    1: query String = "default"
363}"#;
364        let err = parse_and_validate(input).unwrap_err();
365        assert_eq!(err.len(), 1);
366        match &err[0] {
367            ValidationError::DuplicateFeatureId { id, .. } => {
368                assert_eq!(*id, 1);
369            }
370            _ => panic!("expected DuplicateFeatureId error"),
371        }
372    }
373
374    #[test]
375    fn duplicate_variable_id_error() {
376        let input = r#"1: Feature Checkout = {
377    1: enabled Boolean = true
378    1: max_items Integer = 50
379}"#;
380        let err = parse_and_validate(input).unwrap_err();
381        assert_eq!(err.len(), 1);
382        match &err[0] {
383            ValidationError::DuplicateVariableId { feature, id, .. } => {
384                assert_eq!(feature, "Checkout");
385                assert_eq!(*id, 1);
386            }
387            _ => panic!("expected DuplicateVariableId error"),
388        }
389    }
390
391    #[test]
392    fn valid_file_with_struct_passes() {
393        let input = r#"1: Struct Theme = {
394    1: dark_mode Boolean = false
395    2: font_size Integer = 14
396}
397
3981: Feature Dashboard = {
399    1: enabled Boolean = true
400    2: theme Theme = Theme {}
401}"#;
402        assert!(parse_and_validate(input).is_ok());
403    }
404
405    #[test]
406    fn duplicate_struct_name_error() {
407        let input = r#"1: Struct Theme = {
408    1: dark_mode Boolean = false
409}
410
4112: Struct Theme = {
412    1: font_size Integer = 14
413}"#;
414        let err = parse_and_validate(input).unwrap_err();
415        assert_eq!(err.len(), 1);
416        match &err[0] {
417            ValidationError::DuplicateStruct { name, .. } => {
418                assert_eq!(name, "Theme");
419            }
420            _ => panic!("expected DuplicateStruct error"),
421        }
422    }
423
424    #[test]
425    fn duplicate_struct_id_error() {
426        let input = r#"1: Struct Theme = {
427    1: dark_mode Boolean = false
428}
429
4301: Struct Config = {
431    1: retries Integer = 3
432}"#;
433        let err = parse_and_validate(input).unwrap_err();
434        assert_eq!(err.len(), 1);
435        match &err[0] {
436            ValidationError::DuplicateStructId { id, .. } => {
437                assert_eq!(*id, 1);
438            }
439            _ => panic!("expected DuplicateStructId error"),
440        }
441    }
442
443    #[test]
444    fn duplicate_field_name_error() {
445        let input = r#"1: Struct Theme = {
446    1: dark_mode Boolean = false
447    2: dark_mode Boolean = true
448}"#;
449        let err = parse_and_validate(input).unwrap_err();
450        assert_eq!(err.len(), 1);
451        match &err[0] {
452            ValidationError::DuplicateField {
453                struct_name, name, ..
454            } => {
455                assert_eq!(struct_name, "Theme");
456                assert_eq!(name, "dark_mode");
457            }
458            _ => panic!("expected DuplicateField error"),
459        }
460    }
461
462    #[test]
463    fn duplicate_field_id_error() {
464        let input = r#"1: Struct Theme = {
465    1: dark_mode Boolean = false
466    1: font_size Integer = 14
467}"#;
468        let err = parse_and_validate(input).unwrap_err();
469        assert_eq!(err.len(), 1);
470        match &err[0] {
471            ValidationError::DuplicateFieldId {
472                struct_name, id, ..
473            } => {
474                assert_eq!(struct_name, "Theme");
475                assert_eq!(*id, 1);
476            }
477            _ => panic!("expected DuplicateFieldId error"),
478        }
479    }
480
481    #[test]
482    fn unknown_struct_type_error() {
483        let input = r#"1: Feature Dashboard = {
484    1: theme UnknownType = UnknownType {}
485}"#;
486        let err = parse_and_validate(input).unwrap_err();
487        assert_eq!(err.len(), 1);
488        match &err[0] {
489            ValidationError::UnknownStructType {
490                feature,
491                variable,
492                type_name,
493                ..
494            } => {
495                assert_eq!(feature, "Dashboard");
496                assert_eq!(variable, "theme");
497                assert_eq!(type_name, "UnknownType");
498            }
499            _ => panic!("expected UnknownStructType error"),
500        }
501    }
502
503    #[test]
504    fn unknown_struct_field_in_literal_error() {
505        let input = r#"1: Struct Theme = {
506    1: dark_mode Boolean = false
507}
508
5091: Feature Dashboard = {
510    1: theme Theme = Theme { nonexistent = true }
511}"#;
512        let err = parse_and_validate(input).unwrap_err();
513        assert_eq!(err.len(), 1);
514        match &err[0] {
515            ValidationError::UnknownStructField {
516                struct_name, field, ..
517            } => {
518                assert_eq!(struct_name, "Theme");
519                assert_eq!(field, "nonexistent");
520            }
521            _ => panic!("expected UnknownStructField error"),
522        }
523    }
524}