#![allow(unused_assignments)]
use std::collections::{HashMap, HashSet};
use miette::{Diagnostic, SourceSpan};
use thiserror::Error;
use crate::ast::{VarFile, VarType};
use crate::lexer::Span;
#[derive(Error, Debug, Diagnostic)]
pub enum ValidationError {
#[error("duplicate feature name: `{name}`")]
DuplicateFeature {
name: String,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate feature id `{id}`")]
DuplicateFeatureId {
id: u32,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate variable name `{name}` in feature `{feature}`")]
DuplicateVariable {
feature: String,
name: String,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate variable id `{id}` in feature `{feature}`")]
DuplicateVariableId {
feature: String,
id: u32,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate struct name: `{name}`")]
DuplicateStruct {
name: String,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate struct id `{id}`")]
DuplicateStructId {
id: u32,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate field name `{name}` in struct `{struct_name}`")]
DuplicateField {
struct_name: String,
name: String,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("duplicate field id `{id}` in struct `{struct_name}`")]
DuplicateFieldId {
struct_name: String,
id: u32,
#[label("first defined here")]
first: SourceSpan,
#[label("duplicate defined here")]
duplicate: SourceSpan,
},
#[error("unknown struct type `{type_name}` for variable `{variable}` in feature `{feature}`")]
UnknownStructType {
feature: String,
variable: String,
type_name: String,
#[label("used here")]
span: SourceSpan,
},
#[error("unknown field `{field}` in struct literal for type `{struct_name}`")]
UnknownStructField {
struct_name: String,
field: String,
#[label("used here")]
span: SourceSpan,
},
}
fn span_to_source_span(span: &Span) -> SourceSpan {
SourceSpan::from(span.offset)
}
pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
let mut struct_names_set: HashSet<&str> = HashSet::new();
let mut struct_names: HashMap<&str, &Span> = HashMap::new();
let mut struct_ids: HashMap<u32, &Span> = HashMap::new();
for struct_def in &var_file.structs {
struct_names_set.insert(&struct_def.name);
if let Some(first_span) = struct_names.get(struct_def.name.as_str()) {
errors.push(ValidationError::DuplicateStruct {
name: struct_def.name.clone(),
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&struct_def.span),
});
} else {
struct_names.insert(&struct_def.name, &struct_def.span);
}
if let Some(first_span) = struct_ids.get(&struct_def.id) {
errors.push(ValidationError::DuplicateStructId {
id: struct_def.id,
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&struct_def.span),
});
} else {
struct_ids.insert(struct_def.id, &struct_def.span);
}
let mut field_names: HashMap<&str, &Span> = HashMap::new();
let mut field_ids: HashMap<u32, &Span> = HashMap::new();
for field in &struct_def.fields {
if let Some(first_span) = field_names.get(field.name.as_str()) {
errors.push(ValidationError::DuplicateField {
struct_name: struct_def.name.clone(),
name: field.name.clone(),
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&field.span),
});
} else {
field_names.insert(&field.name, &field.span);
}
if let Some(first_span) = field_ids.get(&field.id) {
errors.push(ValidationError::DuplicateFieldId {
struct_name: struct_def.name.clone(),
id: field.id,
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&field.span),
});
} else {
field_ids.insert(field.id, &field.span);
}
}
}
let struct_field_names: HashMap<&str, HashSet<&str>> = var_file
.structs
.iter()
.map(|s| {
let fields: HashSet<&str> = s.fields.iter().map(|f| f.name.as_str()).collect();
(s.name.as_str(), fields)
})
.collect();
let mut feature_names: HashMap<&str, &Span> = HashMap::new();
let mut feature_ids: HashMap<u32, &Span> = HashMap::new();
for feature in &var_file.features {
if let Some(first_span) = feature_names.get(feature.name.as_str()) {
errors.push(ValidationError::DuplicateFeature {
name: feature.name.clone(),
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&feature.span),
});
} else {
feature_names.insert(&feature.name, &feature.span);
}
if let Some(first_span) = feature_ids.get(&feature.id) {
errors.push(ValidationError::DuplicateFeatureId {
id: feature.id,
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&feature.span),
});
} else {
feature_ids.insert(feature.id, &feature.span);
}
}
for feature in &var_file.features {
let mut var_names: HashMap<&str, &Span> = HashMap::new();
let mut var_ids: HashMap<u32, &Span> = HashMap::new();
for variable in &feature.variables {
if let Some(first_span) = var_names.get(variable.name.as_str()) {
errors.push(ValidationError::DuplicateVariable {
feature: feature.name.clone(),
name: variable.name.clone(),
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&variable.span),
});
} else {
var_names.insert(&variable.name, &variable.span);
}
if let Some(first_span) = var_ids.get(&variable.id) {
errors.push(ValidationError::DuplicateVariableId {
feature: feature.name.clone(),
id: variable.id,
first: span_to_source_span(first_span),
duplicate: span_to_source_span(&variable.span),
});
} else {
var_ids.insert(variable.id, &variable.span);
}
if let VarType::Struct(ref struct_name) = variable.var_type {
if !struct_names_set.contains(struct_name.as_str()) {
errors.push(ValidationError::UnknownStructType {
feature: feature.name.clone(),
variable: variable.name.clone(),
type_name: struct_name.clone(),
span: span_to_source_span(&variable.span),
});
}
if let crate::ast::Value::Struct { fields, .. } = &variable.default {
if let Some(valid_fields) = struct_field_names.get(struct_name.as_str()) {
for field_name in fields.keys() {
if !valid_fields.contains(field_name.as_str()) {
errors.push(ValidationError::UnknownStructField {
struct_name: struct_name.clone(),
field: field_name.clone(),
span: span_to_source_span(&variable.span),
});
}
}
}
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
use crate::parser::parse;
fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
let tokens = lex(input).expect("lex failed");
let var_file = parse(tokens).expect("parse failed");
validate(&var_file)?;
Ok(var_file)
}
#[test]
fn valid_file_passes() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
}
2: Feature Search = {
1: query String = "default"
}"#;
assert!(parse_and_validate(input).is_ok());
}
#[test]
fn duplicate_feature_name_error() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
}
2: Feature Checkout = {
1: max_items Integer = 50
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateFeature { name, .. } => {
assert_eq!(name, "Checkout");
}
_ => panic!("expected DuplicateFeature error"),
}
}
#[test]
fn duplicate_variable_name_error() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
2: enabled Boolean = false
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateVariable { feature, name, .. } => {
assert_eq!(feature, "Checkout");
assert_eq!(name, "enabled");
}
_ => panic!("expected DuplicateVariable error"),
}
}
#[test]
fn error_has_correct_line_info() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
}
2: Feature Checkout = {
1: max_items Integer = 50
}"#;
let err = parse_and_validate(input).unwrap_err();
match &err[0] {
ValidationError::DuplicateFeature {
first, duplicate, ..
} => {
assert_eq!(first.offset(), 0);
assert!(duplicate.offset() > 0);
}
_ => panic!("expected DuplicateFeature error"),
}
}
#[test]
fn duplicate_feature_id_error() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
}
1: Feature Search = {
1: query String = "default"
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateFeatureId { id, .. } => {
assert_eq!(*id, 1);
}
_ => panic!("expected DuplicateFeatureId error"),
}
}
#[test]
fn duplicate_variable_id_error() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
1: max_items Integer = 50
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateVariableId { feature, id, .. } => {
assert_eq!(feature, "Checkout");
assert_eq!(*id, 1);
}
_ => panic!("expected DuplicateVariableId error"),
}
}
#[test]
fn valid_file_with_struct_passes() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
2: font_size Integer = 14
}
1: Feature Dashboard = {
1: enabled Boolean = true
2: theme Theme = Theme {}
}"#;
assert!(parse_and_validate(input).is_ok());
}
#[test]
fn duplicate_struct_name_error() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
}
2: Struct Theme = {
1: font_size Integer = 14
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateStruct { name, .. } => {
assert_eq!(name, "Theme");
}
_ => panic!("expected DuplicateStruct error"),
}
}
#[test]
fn duplicate_struct_id_error() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
}
1: Struct Config = {
1: retries Integer = 3
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateStructId { id, .. } => {
assert_eq!(*id, 1);
}
_ => panic!("expected DuplicateStructId error"),
}
}
#[test]
fn duplicate_field_name_error() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
2: dark_mode Boolean = true
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateField {
struct_name, name, ..
} => {
assert_eq!(struct_name, "Theme");
assert_eq!(name, "dark_mode");
}
_ => panic!("expected DuplicateField error"),
}
}
#[test]
fn duplicate_field_id_error() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
1: font_size Integer = 14
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::DuplicateFieldId {
struct_name, id, ..
} => {
assert_eq!(struct_name, "Theme");
assert_eq!(*id, 1);
}
_ => panic!("expected DuplicateFieldId error"),
}
}
#[test]
fn unknown_struct_type_error() {
let input = r#"1: Feature Dashboard = {
1: theme UnknownType = UnknownType {}
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::UnknownStructType {
feature,
variable,
type_name,
..
} => {
assert_eq!(feature, "Dashboard");
assert_eq!(variable, "theme");
assert_eq!(type_name, "UnknownType");
}
_ => panic!("expected UnknownStructType error"),
}
}
#[test]
fn unknown_struct_field_in_literal_error() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
}
1: Feature Dashboard = {
1: theme Theme = Theme { nonexistent = true }
}"#;
let err = parse_and_validate(input).unwrap_err();
assert_eq!(err.len(), 1);
match &err[0] {
ValidationError::UnknownStructField {
struct_name, field, ..
} => {
assert_eq!(struct_name, "Theme");
assert_eq!(field, "nonexistent");
}
_ => panic!("expected UnknownStructField error"),
}
}
}