1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ParseError {
8 pub message: String,
10 pub line: usize,
12 pub column: usize,
14 pub context: Option<String>,
16}
17
18impl ParseError {
19 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 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 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 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 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}