use crate::ast::Span;
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum LexError {
#[error("unexpected character '{ch}' at line {}, column {}", span.line, span.column)]
UnexpectedChar {
ch: char,
span: Span,
},
#[error("unterminated string literal starting at line {}, column {}", span.line, span.column)]
UnterminatedString {
span: Span,
},
#[error("invalid version number '{text}' at line {}, column {}", span.line, span.column)]
InvalidVersion {
text: String,
span: Span,
},
#[error("invalid escape sequence '\\{ch}' at line {}, column {}", span.line, span.column)]
InvalidEscape {
ch: char,
span: Span,
},
}
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ParseError {
#[error("expected {expected}, found {found} at line {}, column {}", span.line, span.column)]
UnexpectedToken {
expected: String,
found: String,
span: Span,
},
#[error("missing required exegesis block at line {}, column {}", span.line, span.column)]
MissingExegesis {
span: Span,
},
#[error("{message} at line {}, column {}", span.line, span.column)]
InvalidStatement {
message: String,
span: Span,
},
#[error("invalid declaration type '{found}' at line {}, column {} (expected module, use, pub, fun, gen, gene, trait, rule, constraint, system, evo, evolves, docs, or exegesis)", span.line, span.column)]
InvalidDeclaration {
found: String,
span: Span,
},
#[error("unexpected end of file at line {}, column {}: {context}", span.line, span.column)]
UnexpectedEof {
context: String,
span: Span,
},
#[error("lexer error: {0}")]
LexerError(#[from] LexError),
}
impl ParseError {
pub fn span(&self) -> Span {
match self {
ParseError::UnexpectedToken { span, .. } => *span,
ParseError::MissingExegesis { span } => *span,
ParseError::InvalidStatement { span, .. } => *span,
ParseError::InvalidDeclaration { span, .. } => *span,
ParseError::UnexpectedEof { span, .. } => *span,
ParseError::LexerError(lex_err) => match lex_err {
LexError::UnexpectedChar { span, .. } => *span,
LexError::UnterminatedString { span } => *span,
LexError::InvalidVersion { span, .. } => *span,
LexError::InvalidEscape { span, .. } => *span,
},
}
}
}
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ValidationError {
#[error("invalid identifier '{name}': {reason}")]
InvalidIdentifier {
name: String,
reason: String,
},
#[error("unresolved reference to '{reference}' at line {}, column {}", span.line, span.column)]
UnresolvedReference {
reference: String,
span: Span,
},
#[error("invalid version '{version}': {reason}")]
InvalidVersion {
version: String,
reason: String,
},
#[error("duplicate {kind} '{name}'")]
DuplicateDefinition {
kind: String,
name: String,
},
#[error("evolution references non-existent parent version '{parent}' for '{name}'")]
InvalidEvolutionLineage {
name: String,
parent: String,
},
#[error("type error at line {}, column {}: {message}", span.line, span.column)]
TypeError {
message: String,
expected: Option<String>,
actual: Option<String>,
span: Span,
},
}
#[derive(Debug, Clone, Default)]
pub struct ValidationErrors {
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
impl ValidationErrors {
pub fn new() -> Self {
Self::default()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty()
}
pub fn add_error(&mut self, error: ValidationError) {
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: ValidationWarning) {
self.warnings.push(warning);
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationWarning {
ShortExegesis {
length: usize,
span: Span,
},
NamingConvention {
name: String,
suggestion: String,
},
DeprecatedFeature {
feature: String,
alternative: String,
},
}
impl std::fmt::Display for ValidationWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationWarning::ShortExegesis { length, span } => {
write!(
f,
"exegesis is unusually short ({} chars) at line {}, column {}",
length, span.line, span.column
)
}
ValidationWarning::NamingConvention { name, suggestion } => {
write!(
f,
"identifier '{}' doesn't follow naming convention; consider: {}",
name, suggestion
)
}
ValidationWarning::DeprecatedFeature {
feature,
alternative,
} => {
write!(
f,
"deprecated feature '{}'; use '{}' instead",
feature, alternative
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lex_error_display() {
let error = LexError::UnexpectedChar {
ch: '$',
span: Span::new(10, 11, 2, 5),
};
let msg = error.to_string();
assert!(msg.contains("$"));
assert!(msg.contains("line 2"));
assert!(msg.contains("column 5"));
}
#[test]
fn test_parse_error_display() {
let error = ParseError::UnexpectedToken {
expected: "identifier".to_string(),
found: "'gene'".to_string(),
span: Span::new(0, 4, 1, 1),
};
let msg = error.to_string();
assert!(msg.contains("expected identifier"));
assert!(msg.contains("'gene'"));
}
#[test]
fn test_validation_errors_collection() {
let mut errors = ValidationErrors::new();
assert!(errors.is_empty());
errors.add_error(ValidationError::InvalidIdentifier {
name: "invalid".to_string(),
reason: "Invalid identifier".to_string(),
});
assert!(!errors.has_warnings());
errors.add_warning(ValidationWarning::ShortExegesis {
length: 10,
span: Span::default(),
});
assert!(errors.has_warnings());
}
}