use rowan::{TextRange, TextSize};
use super::codes::ErrorCode;
use super::context::ParseContext;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Severity {
#[default]
Error,
Warning,
Hint,
}
impl Severity {
pub fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Hint => "hint",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedInfo {
pub message: String,
pub range: TextRange,
}
impl RelatedInfo {
pub fn new(message: impl Into<String>, range: TextRange) -> Self {
Self {
message: message.into(),
range,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyntaxError {
pub message: String,
pub range: TextRange,
pub code: ErrorCode,
pub severity: Severity,
pub hint: Option<String>,
pub related: Vec<RelatedInfo>,
}
impl SyntaxError {
pub fn new(message: impl Into<String>, range: TextRange, code: ErrorCode) -> Self {
Self {
message: message.into(),
range,
code,
severity: Severity::Error,
hint: None,
related: vec![],
}
}
pub fn at_offset(message: impl Into<String>, offset: TextSize, code: ErrorCode) -> Self {
Self::new(message, TextRange::empty(offset), code)
}
pub fn builder(code: ErrorCode) -> SyntaxErrorBuilder {
SyntaxErrorBuilder::new(code)
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_related(mut self, info: RelatedInfo) -> Self {
self.related.push(info);
self
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn has_hint(&self) -> bool {
self.hint.is_some()
}
pub fn has_related(&self) -> bool {
!self.related.is_empty()
}
pub fn format(&self) -> String {
let mut result = format!("{}: {}", self.code, self.message);
if let Some(hint) = &self.hint {
result.push_str(&format!("\n hint: {}", hint));
}
result
}
}
pub struct SyntaxErrorBuilder {
code: ErrorCode,
message: Option<String>,
range: Option<TextRange>,
severity: Severity,
hint: Option<String>,
related: Vec<RelatedInfo>,
}
impl SyntaxErrorBuilder {
pub fn new(code: ErrorCode) -> Self {
Self {
code,
message: None,
range: None,
severity: Severity::Error,
hint: None,
related: vec![],
}
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn range(mut self, range: TextRange) -> Self {
self.range = Some(range);
self
}
pub fn at_offset(mut self, offset: TextSize) -> Self {
self.range = Some(TextRange::empty(offset));
self
}
pub fn severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn related(mut self, message: impl Into<String>, range: TextRange) -> Self {
self.related.push(RelatedInfo::new(message, range));
self
}
pub fn build(self) -> SyntaxError {
SyntaxError {
message: self
.message
.unwrap_or_else(|| self.code.default_message().to_string()),
range: self
.range
.unwrap_or_else(|| TextRange::empty(TextSize::new(0))),
code: self.code,
severity: self.severity,
hint: self.hint,
related: self.related,
}
}
}
#[allow(dead_code)] pub fn format_context_error(found: &str, context: ParseContext, code: ErrorCode) -> SyntaxError {
let message = format!(
"unexpected {} {}—expected {}",
found,
context.description(),
context.expected_description()
);
SyntaxError::builder(code).message(message).build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syntax_error_new() {
let err = SyntaxError::new(
"expected ';'",
TextRange::new(TextSize::new(10), TextSize::new(11)),
ErrorCode::E0201,
);
assert_eq!(err.message, "expected ';'");
assert_eq!(err.code, ErrorCode::E0201);
assert_eq!(err.severity, Severity::Error);
assert!(err.hint.is_none());
assert!(err.related.is_empty());
}
#[test]
fn test_syntax_error_with_hint() {
let err = SyntaxError::new(
"expected ';'",
TextRange::empty(TextSize::new(10)),
ErrorCode::E0201,
)
.with_hint("add ';' at the end of the statement");
assert!(err.has_hint());
assert_eq!(
err.hint.as_ref().unwrap(),
"add ';' at the end of the statement"
);
}
#[test]
fn test_syntax_error_with_related() {
let err = SyntaxError::new(
"unclosed brace",
TextRange::empty(TextSize::new(50)),
ErrorCode::E0202,
)
.with_related(RelatedInfo::new(
"opening brace here",
TextRange::new(TextSize::new(10), TextSize::new(11)),
));
assert!(err.has_related());
assert_eq!(err.related.len(), 1);
assert_eq!(err.related[0].message, "opening brace here");
}
#[test]
fn test_syntax_error_builder() {
let err = SyntaxError::builder(ErrorCode::E0201)
.message("expected ';' after part definition")
.range(TextRange::new(TextSize::new(10), TextSize::new(15)))
.hint("add ';' at the end")
.severity(Severity::Error)
.related(
"definition started here",
TextRange::empty(TextSize::new(0)),
)
.build();
assert_eq!(err.message, "expected ';' after part definition");
assert_eq!(err.code, ErrorCode::E0201);
assert!(err.has_hint());
assert!(err.has_related());
}
#[test]
fn test_syntax_error_builder_defaults() {
let err = SyntaxError::builder(ErrorCode::E0201).build();
assert_eq!(err.message, "missing semicolon");
assert_eq!(err.severity, Severity::Error);
}
#[test]
fn test_severity() {
assert!(Severity::Error.is_error());
assert!(!Severity::Warning.is_error());
assert!(!Severity::Hint.is_error());
assert_eq!(Severity::Error.as_str(), "error");
assert_eq!(Severity::Warning.as_str(), "warning");
assert_eq!(Severity::Hint.as_str(), "hint");
}
#[test]
fn test_format_error() {
let err = SyntaxError::new(
"expected ';'",
TextRange::empty(TextSize::new(10)),
ErrorCode::E0201,
)
.with_hint("add semicolon");
let formatted = err.format();
assert!(formatted.contains("E0201"));
assert!(formatted.contains("expected ';'"));
assert!(formatted.contains("hint"));
assert!(formatted.contains("add semicolon"));
}
#[test]
fn test_format_context_error() {
let err = format_context_error("'}'", ParseContext::ActionBody, ErrorCode::E0901);
assert!(err.message.contains("'}'"));
assert!(err.message.contains("in action body"));
assert!(err.message.contains("expected"));
}
}