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 ParseErrorKind::InvalidPoptag(_) => 20,
75 ParseErrorKind::UnclosedPushtag(_) => 21,
76 ParseErrorKind::InvalidPopmeta(_) => 22,
77 ParseErrorKind::UnclosedPushmeta(_) => 23,
78 ParseErrorKind::DeprecatedPipeSymbol => 24,
79 }
80 }
81
82 #[must_use]
84 pub fn message(&self) -> String {
85 format!("{}", self.kind)
86 }
87
88 #[must_use]
90 pub const fn label(&self) -> &str {
91 match &self.kind {
92 ParseErrorKind::UnexpectedChar(_) => "unexpected character",
93 ParseErrorKind::UnexpectedEof => "unexpected end of file",
94 ParseErrorKind::Expected(_) => "expected different token",
95 ParseErrorKind::InvalidDate(_) => "invalid date",
96 ParseErrorKind::InvalidNumber(_) => "invalid number",
97 ParseErrorKind::InvalidAccount(_) => "invalid account",
98 ParseErrorKind::InvalidCurrency(_) => "invalid currency",
99 ParseErrorKind::UnclosedString => "unclosed string",
100 ParseErrorKind::InvalidEscape(_) => "invalid escape",
101 ParseErrorKind::MissingField(_) => "missing field",
102 ParseErrorKind::IndentationError => "indentation error",
103 ParseErrorKind::SyntaxError(_) => "parse error",
104 ParseErrorKind::MissingNewline => "syntax error",
105 ParseErrorKind::MissingAccount => "expected account name",
106 ParseErrorKind::InvalidDateValue(_) => "invalid date value",
107 ParseErrorKind::MissingAmount => "expected amount",
108 ParseErrorKind::MissingCurrency => "expected currency",
109 ParseErrorKind::InvalidAccountFormat(_) => "invalid account format",
110 ParseErrorKind::MissingDirective => "expected directive",
111 ParseErrorKind::InvalidPoptag(_) => "invalid poptag",
112 ParseErrorKind::UnclosedPushtag(_) => "unclosed pushtag",
113 ParseErrorKind::InvalidPopmeta(_) => "invalid popmeta",
114 ParseErrorKind::UnclosedPushmeta(_) => "unclosed pushmeta",
115 ParseErrorKind::DeprecatedPipeSymbol => "deprecated pipe symbol",
116 }
117 }
118}
119
120impl fmt::Display for ParseError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "{}", self.kind)?;
123 if let Some(ctx) = &self.context {
124 write!(f, " ({ctx})")?;
125 }
126 Ok(())
127 }
128}
129
130impl std::error::Error for ParseError {}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ParseErrorKind {
135 UnexpectedChar(char),
137 UnexpectedEof,
139 Expected(String),
141 InvalidDate(String),
143 InvalidNumber(String),
145 InvalidAccount(String),
147 InvalidCurrency(String),
149 UnclosedString,
151 InvalidEscape(char),
153 MissingField(String),
155 IndentationError,
157 SyntaxError(String),
159 MissingNewline,
161 MissingAccount,
163 InvalidDateValue(String),
165 MissingAmount,
167 MissingCurrency,
169 InvalidAccountFormat(String),
171 MissingDirective,
173 InvalidPoptag(String),
175 UnclosedPushtag(String),
177 InvalidPopmeta(String),
179 UnclosedPushmeta(String),
181 DeprecatedPipeSymbol,
183}
184
185impl fmt::Display for ParseErrorKind {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::UnexpectedChar(c) => write!(f, "syntax error: unexpected '{c}'"),
189 Self::UnexpectedEof => write!(f, "unexpected end of file"),
190 Self::Expected(what) => write!(f, "expected {what}"),
191 Self::InvalidDate(s) => write!(f, "invalid date '{s}'"),
192 Self::InvalidNumber(s) => write!(f, "invalid number '{s}'"),
193 Self::InvalidAccount(s) => write!(f, "invalid account '{s}'"),
194 Self::InvalidCurrency(s) => write!(f, "invalid currency '{s}'"),
195 Self::UnclosedString => write!(f, "unclosed string literal"),
196 Self::InvalidEscape(c) => write!(f, "invalid escape sequence '\\{c}'"),
197 Self::MissingField(field) => write!(f, "missing required field: {field}"),
198 Self::IndentationError => write!(f, "indentation error"),
199 Self::SyntaxError(msg) => write!(f, "parse error: {msg}"),
200 Self::MissingNewline => write!(f, "syntax error: missing final newline"),
201 Self::MissingAccount => write!(f, "expected account name"),
202 Self::InvalidDateValue(msg) => write!(f, "invalid date: {msg}"),
203 Self::MissingAmount => write!(f, "expected amount in posting"),
204 Self::MissingCurrency => write!(f, "expected currency after number"),
205 Self::InvalidAccountFormat(s) => {
206 write!(f, "invalid account '{s}': must contain ':'")
207 }
208 Self::MissingDirective => write!(f, "expected directive after date"),
209 Self::InvalidPoptag(tag) => {
210 write!(f, "poptag attempted on tag '{tag}' which was never pushed")
211 }
212 Self::UnclosedPushtag(tag) => {
213 write!(f, "pushtag '{tag}' was never popped")
214 }
215 Self::InvalidPopmeta(key) => {
216 write!(f, "popmeta attempted on key '{key}' which was never pushed")
217 }
218 Self::UnclosedPushmeta(key) => {
219 write!(f, "pushmeta '{key}' was never popped")
220 }
221 Self::DeprecatedPipeSymbol => {
222 write!(f, "Pipe symbol is deprecated")
223 }
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_parse_error_new() {
234 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5));
235 assert_eq!(err.span(), (0, 5));
236 assert!(err.context.is_none());
237 assert!(err.hint.is_none());
238 }
239
240 #[test]
241 fn test_parse_error_with_context() {
242 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
243 .with_context("in transaction");
244 assert_eq!(err.context, Some("in transaction".to_string()));
245 }
246
247 #[test]
248 fn test_parse_error_with_hint() {
249 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
250 .with_hint("add more input");
251 assert_eq!(err.hint, Some("add more input".to_string()));
252 }
253
254 #[test]
255 fn test_parse_error_display_with_context() {
256 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 5))
257 .with_context("parsing header");
258 let display = format!("{err}");
259 assert!(display.contains("unexpected end of file"));
260 assert!(display.contains("parsing header"));
261 }
262
263 #[test]
264 fn test_kind_codes() {
265 let kinds = [
267 (ParseErrorKind::UnexpectedChar('x'), 1),
268 (ParseErrorKind::UnexpectedEof, 2),
269 (ParseErrorKind::Expected("foo".to_string()), 3),
270 (ParseErrorKind::InvalidDate("bad".to_string()), 4),
271 (ParseErrorKind::InvalidNumber("nan".to_string()), 5),
272 (ParseErrorKind::InvalidAccount("bad".to_string()), 6),
273 (ParseErrorKind::InvalidCurrency("???".to_string()), 7),
274 (ParseErrorKind::UnclosedString, 8),
275 (ParseErrorKind::InvalidEscape('n'), 9),
276 (ParseErrorKind::MissingField("name".to_string()), 10),
277 (ParseErrorKind::IndentationError, 11),
278 (ParseErrorKind::SyntaxError("oops".to_string()), 12),
279 (ParseErrorKind::MissingNewline, 13),
280 (ParseErrorKind::MissingAccount, 14),
281 (ParseErrorKind::InvalidDateValue("month 13".to_string()), 15),
282 (ParseErrorKind::MissingAmount, 16),
283 (ParseErrorKind::MissingCurrency, 17),
284 (
285 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
286 18,
287 ),
288 (ParseErrorKind::MissingDirective, 19),
289 ];
290
291 for (kind, expected_code) in kinds {
292 let err = ParseError::new(kind, Span::new(0, 1));
293 assert_eq!(err.kind_code(), expected_code);
294 }
295 }
296
297 #[test]
298 fn test_error_labels() {
299 let kinds = [
301 ParseErrorKind::UnexpectedChar('x'),
302 ParseErrorKind::UnexpectedEof,
303 ParseErrorKind::Expected("foo".to_string()),
304 ParseErrorKind::InvalidDate("bad".to_string()),
305 ParseErrorKind::InvalidNumber("nan".to_string()),
306 ParseErrorKind::InvalidAccount("bad".to_string()),
307 ParseErrorKind::InvalidCurrency("???".to_string()),
308 ParseErrorKind::UnclosedString,
309 ParseErrorKind::InvalidEscape('n'),
310 ParseErrorKind::MissingField("name".to_string()),
311 ParseErrorKind::IndentationError,
312 ParseErrorKind::SyntaxError("oops".to_string()),
313 ParseErrorKind::MissingNewline,
314 ParseErrorKind::MissingAccount,
315 ParseErrorKind::InvalidDateValue("month 13".to_string()),
316 ParseErrorKind::MissingAmount,
317 ParseErrorKind::MissingCurrency,
318 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
319 ParseErrorKind::MissingDirective,
320 ];
321
322 for kind in kinds {
323 let err = ParseError::new(kind, Span::new(0, 1));
324 assert!(!err.label().is_empty());
325 }
326 }
327
328 #[test]
329 fn test_error_messages() {
330 let test_cases = [
332 (ParseErrorKind::UnexpectedChar('$'), "unexpected '$'"),
333 (ParseErrorKind::UnexpectedEof, "unexpected end of file"),
334 (
335 ParseErrorKind::Expected("number".to_string()),
336 "expected number",
337 ),
338 (
339 ParseErrorKind::InvalidDate("2024-13-01".to_string()),
340 "invalid date '2024-13-01'",
341 ),
342 (
343 ParseErrorKind::InvalidNumber("abc".to_string()),
344 "invalid number 'abc'",
345 ),
346 (
347 ParseErrorKind::InvalidAccount("bad".to_string()),
348 "invalid account 'bad'",
349 ),
350 (
351 ParseErrorKind::InvalidCurrency("???".to_string()),
352 "invalid currency '???'",
353 ),
354 (ParseErrorKind::UnclosedString, "unclosed string literal"),
355 (
356 ParseErrorKind::InvalidEscape('x'),
357 "invalid escape sequence '\\x'",
358 ),
359 (
360 ParseErrorKind::MissingField("date".to_string()),
361 "missing required field: date",
362 ),
363 (ParseErrorKind::IndentationError, "indentation error"),
364 (
365 ParseErrorKind::SyntaxError("bad token".to_string()),
366 "parse error: bad token",
367 ),
368 (ParseErrorKind::MissingNewline, "missing final newline"),
369 (ParseErrorKind::MissingAccount, "expected account name"),
370 (
371 ParseErrorKind::InvalidDateValue("month 13".to_string()),
372 "invalid date: month 13",
373 ),
374 (ParseErrorKind::MissingAmount, "expected amount in posting"),
375 (
376 ParseErrorKind::MissingCurrency,
377 "expected currency after number",
378 ),
379 (
380 ParseErrorKind::InvalidAccountFormat("Assets".to_string()),
381 "must contain ':'",
382 ),
383 (
384 ParseErrorKind::MissingDirective,
385 "expected directive after date",
386 ),
387 ];
388
389 for (kind, expected_substring) in test_cases {
390 let msg = format!("{kind}");
391 assert!(
392 msg.contains(expected_substring),
393 "Expected '{expected_substring}' in '{msg}'"
394 );
395 }
396 }
397
398 #[test]
399 fn test_parse_error_is_error_trait() {
400 let err = ParseError::new(ParseErrorKind::UnexpectedEof, Span::new(0, 1));
401 let _: &dyn std::error::Error = &err;
403 }
404}