1use crate::Span;
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParseError {
9 pub kind: ParseErrorKind,
11 pub span: Span,
13 pub context: Option<String>,
15 pub hint: Option<String>,
17}
18
19impl ParseError {
20 #[must_use]
22 pub const fn new(kind: ParseErrorKind, span: Span) -> Self {
23 Self {
24 kind,
25 span,
26 context: None,
27 hint: None,
28 }
29 }
30
31 #[must_use]
33 pub fn with_context(mut self, context: impl Into<String>) -> Self {
34 self.context = Some(context.into());
35 self
36 }
37
38 #[must_use]
40 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
41 self.hint = Some(hint.into());
42 self
43 }
44
45 #[must_use]
47 pub const fn span(&self) -> (usize, usize) {
48 (self.span.start, self.span.end)
49 }
50
51 #[must_use]
53 pub const fn kind_code(&self) -> u32 {
54 match &self.kind {
55 ParseErrorKind::UnexpectedChar(_) => 1,
56 ParseErrorKind::UnexpectedEof => 2,
57 ParseErrorKind::Expected(_) => 3,
58 ParseErrorKind::InvalidDate(_) => 4,
59 ParseErrorKind::InvalidNumber(_) => 5,
60 ParseErrorKind::InvalidAccount(_) => 6,
61 ParseErrorKind::InvalidCurrency(_) => 7,
62 ParseErrorKind::UnclosedString => 8,
63 ParseErrorKind::InvalidEscape(_) => 9,
64 ParseErrorKind::MissingField(_) => 10,
65 ParseErrorKind::IndentationError => 11,
66 ParseErrorKind::SyntaxError(_) => 12,
67 ParseErrorKind::MissingNewline => 13,
68 ParseErrorKind::MissingAccount => 14,
69 ParseErrorKind::InvalidDateValue(_) => 15,
70 ParseErrorKind::MissingAmount => 16,
71 ParseErrorKind::MissingCurrency => 17,
72 ParseErrorKind::InvalidAccountFormat(_) => 18,
73 ParseErrorKind::MissingDirective => 19,
74 }
75 }
76
77 #[must_use]
79 pub fn message(&self) -> String {
80 format!("{}", self.kind)
81 }
82
83 #[must_use]
85 pub const fn label(&self) -> &str {
86 match &self.kind {
87 ParseErrorKind::UnexpectedChar(_) => "unexpected character",
88 ParseErrorKind::UnexpectedEof => "unexpected end of file",
89 ParseErrorKind::Expected(_) => "expected different token",
90 ParseErrorKind::InvalidDate(_) => "invalid date",
91 ParseErrorKind::InvalidNumber(_) => "invalid number",
92 ParseErrorKind::InvalidAccount(_) => "invalid account",
93 ParseErrorKind::InvalidCurrency(_) => "invalid currency",
94 ParseErrorKind::UnclosedString => "unclosed string",
95 ParseErrorKind::InvalidEscape(_) => "invalid escape",
96 ParseErrorKind::MissingField(_) => "missing field",
97 ParseErrorKind::IndentationError => "indentation error",
98 ParseErrorKind::SyntaxError(_) => "parse error",
99 ParseErrorKind::MissingNewline => "syntax error",
100 ParseErrorKind::MissingAccount => "expected account name",
101 ParseErrorKind::InvalidDateValue(_) => "invalid date value",
102 ParseErrorKind::MissingAmount => "expected amount",
103 ParseErrorKind::MissingCurrency => "expected currency",
104 ParseErrorKind::InvalidAccountFormat(_) => "invalid account format",
105 ParseErrorKind::MissingDirective => "expected directive",
106 }
107 }
108}
109
110impl fmt::Display for ParseError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "{}", self.kind)?;
113 if let Some(ctx) = &self.context {
114 write!(f, " ({ctx})")?;
115 }
116 Ok(())
117 }
118}
119
120impl std::error::Error for ParseError {}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum ParseErrorKind {
125 UnexpectedChar(char),
127 UnexpectedEof,
129 Expected(String),
131 InvalidDate(String),
133 InvalidNumber(String),
135 InvalidAccount(String),
137 InvalidCurrency(String),
139 UnclosedString,
141 InvalidEscape(char),
143 MissingField(String),
145 IndentationError,
147 SyntaxError(String),
149 MissingNewline,
151 MissingAccount,
153 InvalidDateValue(String),
155 MissingAmount,
157 MissingCurrency,
159 InvalidAccountFormat(String),
161 MissingDirective,
163}
164
165impl fmt::Display for ParseErrorKind {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 match self {
168 Self::UnexpectedChar(c) => write!(f, "syntax error: unexpected '{c}'"),
169 Self::UnexpectedEof => write!(f, "unexpected end of file"),
170 Self::Expected(what) => write!(f, "expected {what}"),
171 Self::InvalidDate(s) => write!(f, "invalid date '{s}'"),
172 Self::InvalidNumber(s) => write!(f, "invalid number '{s}'"),
173 Self::InvalidAccount(s) => write!(f, "invalid account '{s}'"),
174 Self::InvalidCurrency(s) => write!(f, "invalid currency '{s}'"),
175 Self::UnclosedString => write!(f, "unclosed string literal"),
176 Self::InvalidEscape(c) => write!(f, "invalid escape sequence '\\{c}'"),
177 Self::MissingField(field) => write!(f, "missing required field: {field}"),
178 Self::IndentationError => write!(f, "indentation error"),
179 Self::SyntaxError(msg) => write!(f, "parse error: {msg}"),
180 Self::MissingNewline => write!(f, "syntax error: missing final newline"),
181 Self::MissingAccount => write!(f, "expected account name"),
182 Self::InvalidDateValue(msg) => write!(f, "invalid date: {msg}"),
183 Self::MissingAmount => write!(f, "expected amount in posting"),
184 Self::MissingCurrency => write!(f, "expected currency after number"),
185 Self::InvalidAccountFormat(s) => {
186 write!(f, "invalid account '{s}': must contain ':'")
187 }
188 Self::MissingDirective => write!(f, "expected directive after date"),
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_parse_error_new() {
199 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5));
200 assert_eq!(err.span(), (0, 5));
201 assert!(err.context.is_none());
202 assert!(err.hint.is_none());
203 }
204
205 #[test]
206 fn test_parse_error_with_context() {
207 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
208 .with_context("in transaction");
209 assert_eq!(err.context, Some("in transaction".to_string()));
210 }
211
212 #[test]
213 fn test_parse_error_with_hint() {
214 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
215 .with_hint("add more input");
216 assert_eq!(err.hint, Some("add more input".to_string()));
217 }
218
219 #[test]
220 fn test_parse_error_display_with_context() {
221 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
222 .with_context("parsing header");
223 let display = format!("{err}");
224 assert!(display.contains("unexpected end of file"));
225 assert!(display.contains("parsing header"));
226 }
227
228 #[test]
229 fn test_kind_codes() {
230 let kinds = [
232 (ParseErrorKind::UnexpectedChar('x'), 1),
233 (ParseErrorKind::UnexpectedEof, 2),
234 (ParseErrorKind::Expected("foo".to_string()), 3),
235 (ParseErrorKind::InvalidDate("bad".to_string()), 4),
236 (ParseErrorKind::InvalidNumber("nan".to_string()), 5),
237 (ParseErrorKind::InvalidAccount("bad".to_string()), 6),
238 (ParseErrorKind::InvalidCurrency("???".to_string()), 7),
239 (ParseErrorKind::UnclosedString, 8),
240 (ParseErrorKind::InvalidEscape('n'), 9),
241 (ParseErrorKind::MissingField("name".to_string()), 10),
242 (ParseErrorKind::IndentationError, 11),
243 (ParseErrorKind::SyntaxError("oops".to_string()), 12),
244 (ParseErrorKind::MissingNewline, 13),
245 (ParseErrorKind::MissingAccount, 14),
246 (ParseErrorKind::InvalidDateValue("month 13".to_string()), 15),
247 (ParseErrorKind::MissingAmount, 16),
248 (ParseErrorKind::MissingCurrency, 17),
249 (
250 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
251 18,
252 ),
253 (ParseErrorKind::MissingDirective, 19),
254 ];
255
256 for (kind, expected_code) in kinds {
257 let err = ParseError::new(kind, Span::new(0, 1));
258 assert_eq!(err.kind_code(), expected_code);
259 }
260 }
261
262 #[test]
263 fn test_error_labels() {
264 let kinds = [
266 ParseErrorKind::UnexpectedChar('x'),
267 ParseErrorKind::UnexpectedEof,
268 ParseErrorKind::Expected("foo".to_string()),
269 ParseErrorKind::InvalidDate("bad".to_string()),
270 ParseErrorKind::InvalidNumber("nan".to_string()),
271 ParseErrorKind::InvalidAccount("bad".to_string()),
272 ParseErrorKind::InvalidCurrency("???".to_string()),
273 ParseErrorKind::UnclosedString,
274 ParseErrorKind::InvalidEscape('n'),
275 ParseErrorKind::MissingField("name".to_string()),
276 ParseErrorKind::IndentationError,
277 ParseErrorKind::SyntaxError("oops".to_string()),
278 ParseErrorKind::MissingNewline,
279 ParseErrorKind::MissingAccount,
280 ParseErrorKind::InvalidDateValue("month 13".to_string()),
281 ParseErrorKind::MissingAmount,
282 ParseErrorKind::MissingCurrency,
283 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
284 ParseErrorKind::MissingDirective,
285 ];
286
287 for kind in kinds {
288 let err = ParseError::new(kind, Span::new(0, 1));
289 assert!(!err.label().is_empty());
290 }
291 }
292
293 #[test]
294 fn test_error_messages() {
295 let test_cases = [
297 (ParseErrorKind::UnexpectedChar('$'), "unexpected '$'"),
298 (ParseErrorKind::UnexpectedEof, "unexpected end of file"),
299 (
300 ParseErrorKind::Expected("number".to_string()),
301 "expected number",
302 ),
303 (
304 ParseErrorKind::InvalidDate("2024-13-01".to_string()),
305 "invalid date '2024-13-01'",
306 ),
307 (
308 ParseErrorKind::InvalidNumber("abc".to_string()),
309 "invalid number 'abc'",
310 ),
311 (
312 ParseErrorKind::InvalidAccount("bad".to_string()),
313 "invalid account 'bad'",
314 ),
315 (
316 ParseErrorKind::InvalidCurrency("???".to_string()),
317 "invalid currency '???'",
318 ),
319 (ParseErrorKind::UnclosedString, "unclosed string literal"),
320 (
321 ParseErrorKind::InvalidEscape('x'),
322 "invalid escape sequence '\\x'",
323 ),
324 (
325 ParseErrorKind::MissingField("date".to_string()),
326 "missing required field: date",
327 ),
328 (ParseErrorKind::IndentationError, "indentation error"),
329 (
330 ParseErrorKind::SyntaxError("bad token".to_string()),
331 "parse error: bad token",
332 ),
333 (ParseErrorKind::MissingNewline, "missing final newline"),
334 (ParseErrorKind::MissingAccount, "expected account name"),
335 (
336 ParseErrorKind::InvalidDateValue("month 13".to_string()),
337 "invalid date: month 13",
338 ),
339 (ParseErrorKind::MissingAmount, "expected amount in posting"),
340 (
341 ParseErrorKind::MissingCurrency,
342 "expected currency after number",
343 ),
344 (
345 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
346 "must contain ':'",
347 ),
348 (
349 ParseErrorKind::MissingDirective,
350 "expected directive after date",
351 ),
352 ];
353
354 for (kind, expected_substring) in test_cases {
355 let msg = format!("{kind}");
356 assert!(
357 msg.contains(expected_substring),
358 "Expected '{expected_substring}' in '{msg}'"
359 );
360 }
361 }
362
363 #[test]
364 fn test_parse_error_is_error_trait() {
365 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 1));
366 let _: &dyn std::error::Error = &err;
368 }
369}