Skip to main content

elo_rust/parser/
error.rs

1//! Parse error types
2
3use std::fmt;
4
5/// Parse error with location information and optional source context
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ParseError {
8    /// Error message
9    pub message: String,
10    /// Line number (1-based)
11    pub line: usize,
12    /// Column number (1-based)
13    pub column: usize,
14    /// Optional source context showing the problematic line
15    pub context: Option<String>,
16}
17
18impl ParseError {
19    /// Create a new parse error
20    pub fn new(message: impl Into<String>, line: usize, column: usize) -> Self {
21        ParseError {
22            message: message.into(),
23            line,
24            column,
25            context: None,
26        }
27    }
28
29    /// Create a parse error with context from an input string
30    pub fn with_context(message: impl Into<String>, input: &str, position: usize) -> Self {
31        let (line, column) = Self::position_to_line_col(input, position);
32        let context = Self::extract_context(input, line, column);
33        ParseError {
34            message: message.into(),
35            line,
36            column,
37            context,
38        }
39    }
40
41    /// Create a parse error with explicit context
42    pub fn with_explicit_context(
43        message: impl Into<String>,
44        line: usize,
45        column: usize,
46        context: impl Into<String>,
47    ) -> Self {
48        ParseError {
49            message: message.into(),
50            line,
51            column,
52            context: Some(context.into()),
53        }
54    }
55
56    /// Convert a string position to line and column numbers
57    fn position_to_line_col(input: &str, position: usize) -> (usize, usize) {
58        let mut line = 1;
59        let mut column = 1;
60
61        for (idx, ch) in input.chars().enumerate() {
62            if idx >= position {
63                break;
64            }
65            if ch == '\n' {
66                line += 1;
67                column = 1;
68            } else {
69                column += 1;
70            }
71        }
72
73        (line, column)
74    }
75
76    /// Extract source context for an error (the problematic line with a caret pointer)
77    fn extract_context(input: &str, line: usize, column: usize) -> Option<String> {
78        let lines: Vec<&str> = input.lines().collect();
79        if line == 0 || line > lines.len() {
80            return None;
81        }
82
83        let error_line = lines[line - 1];
84        let pointer = " ".repeat(column.saturating_sub(1)) + "^";
85
86        Some(format!(
87            "  {} |\n  {} | {}\n  {} | {}",
88            line, "|", error_line, "|", pointer
89        ))
90    }
91}
92
93impl fmt::Display for ParseError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(
96            f,
97            "Parse error at line {}, column {}: {}",
98            self.line, self.column, self.message
99        )?;
100
101        if let Some(context) = &self.context {
102            write!(f, "\n{}", context)?;
103        }
104
105        Ok(())
106    }
107}
108
109impl std::error::Error for ParseError {}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_parse_error_creation() {
117        let err = ParseError::new("test error", 1, 5);
118        assert_eq!(err.message, "test error");
119        assert_eq!(err.line, 1);
120        assert_eq!(err.column, 5);
121        assert_eq!(err.context, None);
122    }
123
124    #[test]
125    fn test_parse_error_display() {
126        let err = ParseError::new("unexpected token", 2, 10);
127        assert_eq!(
128            err.to_string(),
129            "Parse error at line 2, column 10: unexpected token"
130        );
131    }
132
133    #[test]
134    fn test_parse_error_with_context() {
135        let input = "age >= 18";
136        let err = ParseError::with_context("unexpected character", input, 5);
137        assert_eq!(err.line, 1);
138        assert_eq!(err.column, 6);
139        assert!(err.context.is_some());
140        let context = err.context.unwrap();
141        assert!(context.contains("age >= 18"));
142        assert!(context.contains("^"));
143    }
144
145    #[test]
146    fn test_parse_error_with_explicit_context() {
147        let err =
148            ParseError::with_explicit_context("invalid syntax", 1, 3, "  | age >= 18\n  | ^^");
149        assert_eq!(err.message, "invalid syntax");
150        assert!(err.context.is_some());
151    }
152
153    #[test]
154    fn test_parse_error_multiline_context() {
155        let input = "let x = 1\nin y = 2";
156        let err = ParseError::with_context("unexpected token", input, 15);
157        assert!(err.context.is_some());
158    }
159
160    #[test]
161    fn test_position_to_line_col_first_line() {
162        let input = "hello world";
163        let (line, col) = ParseError::position_to_line_col(input, 5);
164        assert_eq!(line, 1);
165        assert_eq!(col, 6);
166    }
167
168    #[test]
169    fn test_position_to_line_col_multiple_lines() {
170        let input = "hello\nworld\ntest";
171        let (line, col) = ParseError::position_to_line_col(input, 12);
172        assert_eq!(line, 3);
173        assert_eq!(col, 1);
174    }
175
176    #[test]
177    fn test_extract_context_basic() {
178        let input = "age >= 18";
179        let context = ParseError::extract_context(input, 1, 5);
180        assert!(context.is_some());
181        let ctx = context.unwrap();
182        assert!(ctx.contains("age >= 18"));
183        assert!(ctx.contains("^"));
184    }
185
186    #[test]
187    fn test_extract_context_multiple_lines() {
188        let input = "age >= 18\nname == 'John'";
189        let context = ParseError::extract_context(input, 2, 8);
190        assert!(context.is_some());
191        let ctx = context.unwrap();
192        assert!(ctx.contains("name == 'John'"));
193    }
194
195    #[test]
196    fn test_extract_context_invalid_line() {
197        let input = "age >= 18";
198        let context = ParseError::extract_context(input, 10, 1);
199        assert!(context.is_none());
200    }
201}