use std::fmt;
use std::io;
use crate::parser::NomErrorNode;
use crate::parser::traceback::{LineIndex, TracebackEntry};
pub type ParseResult<T> = Result<T, Box<ParseError>>;
#[derive(Debug)]
pub enum ErrorInfo {
SyntaxError {
message: String,
},
UnexpectedInput {
remaining: String,
},
UnexpectedEof {
expected: String,
},
IoError {
error: io::Error,
},
}
#[derive(Debug, Clone)]
pub struct ParserLineSource {
pub filename: String,
pub lineno: usize,
pub text: String,
}
#[derive(Debug)]
pub struct ParseError {
pub error_info: ErrorInfo,
pub traceback: Option<TracebackEntry>,
pub source: Option<ParserLineSource>,
}
impl ParseError {
pub fn syntax(message: String) -> Box<Self> {
Box::new(ParseError {
error_info: ErrorInfo::SyntaxError { message },
traceback: None,
source: None,
})
}
pub fn syntax_with_context(
message: String,
line: usize,
column: usize,
context: String,
) -> Box<Self> {
Box::new(ParseError {
error_info: ErrorInfo::SyntaxError { message },
traceback: Some(TracebackEntry::new(line, (column, column + 1), context)),
source: None,
})
}
pub fn unexpected_input(
remaining: String,
line: usize,
column_offset: usize,
input: String,
) -> Box<Self> {
let line_index = LineIndex::new(&input);
let offset = input.len().saturating_sub(remaining.len());
let (rel_line, rel_column) = line_index.get_location_at(offset);
Box::new(ParseError {
traceback: Some(TracebackEntry::new(
line + rel_line - 1,
(
rel_column + if rel_line == 1 { column_offset } else { 0 },
rel_column + remaining.len() + if rel_line == 1 { column_offset } else { 0 },
),
"".to_string(),
)),
error_info: ErrorInfo::UnexpectedInput { remaining },
source: None,
})
}
pub fn unexpected_eof(expected: String, line: usize, column_offset: usize) -> Box<Self> {
Box::new(ParseError {
error_info: ErrorInfo::UnexpectedEof { expected },
traceback: Some(TracebackEntry::new(
line,
(column_offset, column_offset),
"".to_string(),
)),
source: None,
})
}
pub fn io(error: io::Error) -> Box<Self> {
Box::new(ParseError {
error_info: ErrorInfo::IoError { error },
traceback: None,
source: None,
})
}
pub(super) fn from_nom_error<I: core::ops::Deref<Target = str> + nom::Input>(
message: String,
original_input: I,
lineno: usize,
column: usize,
nom_error: NomErrorNode<I>,
) -> Box<Self> {
let traceback =
TracebackEntry::build_error_trace(original_input, lineno, column, &nom_error);
Box::new(ParseError {
error_info: ErrorInfo::SyntaxError { message },
traceback: Some(traceback),
source: None,
})
}
pub(crate) fn with_line_source(mut self: Box<Self>, source: ParserLineSource) -> Box<Self> {
self.source = Some(source);
self
}
pub fn position(&self) -> Option<(usize, usize)> {
self.traceback
.as_ref()
.map(|tb| (tb.lineno, tb.column_range.0))
}
pub fn line(&self) -> Option<usize> {
self.traceback.as_ref().map(|tb| tb.lineno)
}
pub fn message(&self) -> String {
match &self.error_info {
ErrorInfo::SyntaxError { message, .. } => message.clone(),
ErrorInfo::UnexpectedInput { remaining, .. } => {
format!("Unexpected input: '{}'", remaining)
}
ErrorInfo::UnexpectedEof { expected, .. } => {
format!("Unexpected end of input, expected {}", expected)
}
ErrorInfo::IoError { error, .. } => error.to_string(),
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.error_info {
ErrorInfo::SyntaxError { message } => {
write!(f, "SyntaxError: {}", message)?;
}
ErrorInfo::UnexpectedInput { remaining, .. } => {
write!(f, "UnexpectedInputError: '{}'", remaining)?;
}
ErrorInfo::UnexpectedEof { expected } => {
write!(f, "UnexpectedEofError: '{}'", expected)?;
}
ErrorInfo::IoError { error } => {
write!(f, "IOError: {}", error)?;
}
}
if let Some(source) = &self.source
&& let Some(traceback) = &self.traceback
{
let (start, end) = traceback.column_range;
write!(
f,
"\n --> {}:{}:{}",
source.filename, traceback.lineno, start
)?;
let line_index = LineIndex::new(&source.text);
let mut current_line_content = "";
let rel_lineno = traceback.lineno.saturating_sub(source.lineno) + 1;
let mut start_offset = 0;
for (i, &newline_offset) in line_index.newlines.iter().enumerate() {
if i + 1 == rel_lineno {
current_line_content = &source.text[start_offset..newline_offset];
break;
}
start_offset = newline_offset + 1;
}
if current_line_content.is_empty() && rel_lineno > line_index.newlines.len() {
current_line_content = &source.text[start_offset..];
}
write!(f, "\n │")?;
write!(
f,
"\n{: ^4}│ {}",
traceback.lineno,
current_line_content.trim_end()
)?;
let mut char_start = 0;
let mut char_end = 0;
for (char_idx, (i, _)) in current_line_content.char_indices().enumerate() {
if i >= start.saturating_sub(1) && char_start == 0 {
char_start = char_idx;
}
if i >= end.saturating_sub(1) {
char_end = char_idx;
break;
}
}
if char_end == 0 || char_end < char_start {
char_end = (char_start + (end as isize - start as isize).unsigned_abs())
.max(char_start + 1);
}
let line_char_count = current_line_content.chars().count();
if char_start >= line_char_count {
char_start = line_char_count;
char_end = line_char_count + 1;
} else {
char_end = char_end.min(line_char_count);
}
let arrow = " ".repeat(char_start + 4) + &"^".repeat((char_end - char_start).max(1));
write!(f, "\n │{}", arrow)?;
}
writeln!(f)?;
if let Some(traceback) = &self.traceback
&& !traceback.context.is_empty()
{
traceback.write_tree(f, " ", false)?;
}
Ok(())
}
}
impl std::error::Error for ParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = ParseError::syntax("test error".to_string());
let display = format!("{}", err);
assert!(display.contains("SyntaxError: test error"));
let err = ParseError::unexpected_input("bad".to_string(), 1, 0, "good bad".to_string());
let display = format!("{}", err);
assert!(display.contains("UnexpectedInputError: 'bad'"));
let err = ParseError::unexpected_eof("value".to_string(), 1, 0);
let display = format!("{}", err);
assert!(display.contains("UnexpectedEofError: 'value'"));
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = ParseError::io(io_err);
let display = format!("{}", err);
assert!(display.contains("IOError: file not found"));
}
#[test]
fn test_error_methods() {
let err = ParseError::syntax("msg".to_string());
assert_eq!(err.message(), "msg");
assert!(err.position().is_none());
assert!(err.line().is_none());
let err = ParseError::unexpected_input("bad".to_string(), 10, 5, "prefix bad".to_string());
assert!(err.message().contains("Unexpected input"));
assert_eq!(err.line(), Some(10));
assert_eq!(err.position(), Some((10, 13)));
}
#[test]
fn test_error_with_source() {
let mut err = ParseError::syntax_with_context("error".to_string(), 1, 1, "ctx".to_string());
err.source = Some(ParserLineSource {
filename: "test.koi".to_string(),
lineno: 1,
text: "line content".to_string(),
});
let display = format!("{}", err);
assert!(display.contains("test.koi:1:1"));
assert!(display.contains("line content"));
assert!(display.contains("^")); }
#[test]
fn test_error_with_non_ascii_source() {
let mut err = ParseError::syntax_with_context("error".to_string(), 1, 5, "ctx".to_string());
err.source = Some(ParserLineSource {
filename: "test.koi".to_string(),
lineno: 1,
text: "你好世界test".to_string(), });
let display = format!("{}", err);
assert!(display.contains("你好世界test"));
assert!(display.contains("^"));
}
}