1use crate::lexer::Token;
4
5#[derive(Debug, Clone, PartialEq)]
7pub struct ErrorContext {
8 pub line: usize,
10 pub column: usize,
12 pub source_snippet: String,
14 pub source_file: Option<String>,
16}
17
18impl ErrorContext {
19 pub fn new(line: usize, column: usize) -> Self {
21 Self {
22 line,
23 column,
24 source_snippet: String::new(),
25 source_file: None,
26 }
27 }
28
29 pub fn with_snippet(line: usize, column: usize, source: &str) -> Self {
31 let snippet = Self::extract_snippet(source, line);
32 Self {
33 line,
34 column,
35 source_snippet: snippet,
36 source_file: None,
37 }
38 }
39
40 pub fn with_file(line: usize, column: usize, source: &str, file: String) -> Self {
42 let snippet = Self::extract_snippet(source, line);
43 Self {
44 line,
45 column,
46 source_snippet: snippet,
47 source_file: Some(file),
48 }
49 }
50
51 fn extract_snippet(source: &str, error_line: usize) -> String {
53 let lines: Vec<&str> = source.lines().collect();
54 let start = error_line.saturating_sub(2);
55 let end = (error_line + 1).min(lines.len());
56
57 lines[start..end].join("\n")
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum LnmpError {
64 InvalidCharacter {
66 char: char,
68 line: usize,
70 column: usize,
72 },
73 UnterminatedString {
75 line: usize,
77 column: usize,
79 },
80 UnexpectedToken {
82 expected: String,
84 found: Token,
86 line: usize,
88 column: usize,
90 },
91 InvalidFieldId {
93 value: String,
95 line: usize,
97 column: usize,
99 },
100 InvalidValue {
102 field_id: u16,
104 reason: String,
106 line: usize,
108 column: usize,
110 },
111 InvalidChecksum {
113 field_id: u16,
115 reason: String,
117 line: usize,
119 column: usize,
121 },
122 UnexpectedEof {
124 line: usize,
126 column: usize,
128 },
129 InvalidEscapeSequence {
131 sequence: String,
133 line: usize,
135 column: usize,
137 },
138 StrictModeViolation {
140 reason: String,
142 line: usize,
144 column: usize,
146 },
147 TypeHintMismatch {
149 field_id: u16,
151 expected_type: String,
153 actual_value: String,
155 line: usize,
157 column: usize,
159 },
160 InvalidTypeHint {
162 hint: String,
164 line: usize,
166 column: usize,
168 },
169 ChecksumMismatch {
171 field_id: u16,
173 expected: String,
175 found: String,
177 line: usize,
179 column: usize,
181 },
182 NestingTooDeep {
184 max_depth: usize,
186 actual_depth: usize,
188 line: usize,
190 column: usize,
192 },
193 InvalidNestedStructure {
195 reason: String,
197 line: usize,
199 column: usize,
201 },
202 DuplicateFieldId {
204 field_id: u16,
206 line: usize,
208 column: usize,
210 },
211 UnclosedNestedStructure {
213 structure_type: String,
215 opened_at_line: usize,
217 opened_at_column: usize,
219 line: usize,
221 column: usize,
223 },
224 ValidationError(String),
226}
227
228impl LnmpError {
229 pub fn with_context(self, _context: ErrorContext) -> Self {
231 self
234 }
235
236 pub fn format_with_source(&self, source: &str) -> String {
238 let (line, column) = self.position();
239
240 let error_msg = format!("{}", self);
241 let mut output = String::new();
242
243 output.push_str(&format!("Error: {}\n", error_msg));
244 output.push_str(" |\n");
245
246 let lines: Vec<&str> = source.lines().collect();
248 let start = line.saturating_sub(2);
249 let end = (line + 1).min(lines.len());
250
251 for line_num in start..end {
252 let actual_line = line_num + 1; let line_content = lines.get(line_num).unwrap_or(&"");
254 output.push_str(&format!("{} | {}\n", actual_line, line_content));
255
256 if line_num + 1 == line {
258 let spaces = " ".repeat(column.saturating_sub(1));
259 output.push_str(&format!(" | {}^ here\n", spaces));
260 }
261 }
262
263 output.push_str(" |\n");
264 output
265 }
266
267 pub fn position(&self) -> (usize, usize) {
269 match self {
270 LnmpError::InvalidCharacter { line, column, .. } => (*line, *column),
271 LnmpError::UnterminatedString { line, column } => (*line, *column),
272 LnmpError::UnexpectedToken { line, column, .. } => (*line, *column),
273 LnmpError::InvalidFieldId { line, column, .. } => (*line, *column),
274 LnmpError::InvalidValue { line, column, .. } => (*line, *column),
275 LnmpError::UnexpectedEof { line, column } => (*line, *column),
276 LnmpError::InvalidEscapeSequence { line, column, .. } => (*line, *column),
277 LnmpError::StrictModeViolation { line, column, .. } => (*line, *column),
278 LnmpError::TypeHintMismatch { line, column, .. } => (*line, *column),
279 LnmpError::InvalidTypeHint { line, column, .. } => (*line, *column),
280 LnmpError::ChecksumMismatch { line, column, .. } => (*line, *column),
281 LnmpError::InvalidChecksum { line, column, .. } => (*line, *column),
282 LnmpError::NestingTooDeep { line, column, .. } => (*line, *column),
283 LnmpError::InvalidNestedStructure { line, column, .. } => (*line, *column),
284 LnmpError::DuplicateFieldId { line, column, .. } => (*line, *column),
285 LnmpError::UnclosedNestedStructure { line, column, .. } => (*line, *column),
286 LnmpError::ValidationError(_) => (0, 0), }
288 }
289}
290
291impl std::fmt::Display for LnmpError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 LnmpError::InvalidCharacter { char, line, column } => write!(
295 f,
296 "Invalid character '{}' at line {}, column {}",
297 char, line, column
298 ),
299 LnmpError::UnterminatedString { line, column } => write!(
300 f,
301 "Unterminated string at line {}, column {}",
302 line, column
303 ),
304 LnmpError::UnexpectedToken {
305 expected,
306 found,
307 line,
308 column,
309 } => write!(
310 f,
311 "Unexpected token at line {}, column {}: expected {}, found {:?}",
312 line, column, expected, found
313 ),
314 LnmpError::InvalidFieldId {
315 value,
316 line,
317 column,
318 } => write!(
319 f,
320 "Invalid field ID at line {}, column {}: '{}'",
321 line, column, value
322 ),
323 LnmpError::InvalidValue {
324 field_id,
325 reason,
326 line,
327 column,
328 } => write!(
329 f,
330 "Invalid value for field {} at line {}, column {}: {}",
331 field_id, line, column, reason
332 ),
333 LnmpError::UnexpectedEof { line, column } => write!(
334 f,
335 "Unexpected end of file at line {}, column {}",
336 line, column
337 ),
338 LnmpError::InvalidEscapeSequence {
339 sequence,
340 line,
341 column,
342 } => write!(
343 f,
344 "Invalid escape sequence '{}' at line {}, column {}",
345 sequence, line, column
346 ),
347 LnmpError::StrictModeViolation {
348 reason,
349 line,
350 column,
351 } => write!(
352 f,
353 "Strict mode violation at line {}, column {}: {}",
354 line, column, reason
355 ),
356 LnmpError::TypeHintMismatch {
357 field_id,
358 expected_type,
359 actual_value,
360 line,
361 column,
362 } => write!(
363 f,
364 "Type hint mismatch for field {} at line {}, column {}: expected type '{}', got {}",
365 field_id, line, column, expected_type, actual_value
366 ),
367 LnmpError::InvalidTypeHint { hint, line, column } => write!(
368 f,
369 "Invalid type hint '{}' at line {}, column {}",
370 hint, line, column
371 ),
372 LnmpError::ChecksumMismatch {
373 field_id,
374 expected,
375 found,
376 line,
377 column,
378 } => write!(
379 f,
380 "Checksum mismatch for field {} at line {}, column {}: expected {}, found {}",
381 field_id, line, column, expected, found
382 ),
383 LnmpError::NestingTooDeep {
384 max_depth,
385 actual_depth,
386 line,
387 column,
388 } => write!(
389 f,
390 "Nesting too deep (NestingTooDeep). Maximum nesting depth exceeded at line {}, column {}: maximum depth is {}, but reached {}",
391 line, column, max_depth, actual_depth
392 ),
393 LnmpError::InvalidNestedStructure {
394 reason,
395 line,
396 column,
397 } => write!(
398 f,
399 "Invalid nested structure at line {}, column {}: {}",
400 line, column, reason
401 ),
402 LnmpError::InvalidChecksum { field_id, reason, line, column } => write!(
403 f,
404 "Invalid checksum for field {} at line {}, column {}: {}",
405 field_id, line, column, reason
406 ),
407 LnmpError::UnclosedNestedStructure {
408 structure_type,
409 opened_at_line,
410 opened_at_column,
411 line,
412 column,
413 } => write!(
414 f,
415 "Unclosed {} at line {}, column {} (opened at line {}, column {})",
416 structure_type, line, column, opened_at_line, opened_at_column
417 ),
418 LnmpError::DuplicateFieldId { field_id, line, column } => write!(
419 f,
420 "DuplicateFieldId: Field ID {} appears multiple times at line {}, column {}",
421 field_id, line, column
422 ),
423 LnmpError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
424 }
425 }
426}
427
428impl std::error::Error for LnmpError {}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_error_context_creation() {
436 let ctx = ErrorContext::new(1, 5);
437 assert_eq!(ctx.line, 1);
438 assert_eq!(ctx.column, 5);
439 assert!(ctx.source_snippet.is_empty());
440 assert!(ctx.source_file.is_none());
441 }
442
443 #[test]
444 fn test_error_context_with_snippet() {
445 let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
446 let ctx = ErrorContext::with_snippet(2, 15, source);
447 assert_eq!(ctx.line, 2);
448 assert_eq!(ctx.column, 15);
449 assert!(ctx.source_snippet.contains("F12:i=14532"));
450 }
451
452 #[test]
453 fn test_error_context_with_file() {
454 let source = "F7:b=1\nF12:i=14532";
455 let ctx = ErrorContext::with_file(1, 5, source, "test.lnmp".to_string());
456 assert_eq!(ctx.line, 1);
457 assert_eq!(ctx.source_file, Some("test.lnmp".to_string()));
458 }
459
460 #[test]
461 fn test_invalid_character_display() {
462 let error = LnmpError::InvalidCharacter {
463 char: '@',
464 line: 1,
465 column: 5,
466 };
467 let msg = format!("{}", error);
468 assert!(msg.contains("line 1"));
469 assert!(msg.contains("column 5"));
470 assert!(msg.contains("'@'"));
471 }
472
473 #[test]
474 fn test_unterminated_string_display() {
475 let error = LnmpError::UnterminatedString {
476 line: 1,
477 column: 10,
478 };
479 let msg = format!("{}", error);
480 assert!(msg.contains("line 1"));
481 assert!(msg.contains("column 10"));
482 assert!(msg.contains("Unterminated string"));
483 }
484
485 #[test]
486 fn test_unexpected_token_display() {
487 let error = LnmpError::UnexpectedToken {
488 expected: "equals sign".to_string(),
489 found: Token::Semicolon,
490 line: 1,
491 column: 5,
492 };
493 let msg = format!("{}", error);
494 assert!(msg.contains("line 1"));
495 assert!(msg.contains("column 5"));
496 assert!(msg.contains("expected equals sign"));
497 }
498
499 #[test]
500 fn test_invalid_field_id_display() {
501 let error = LnmpError::InvalidFieldId {
502 value: "99999".to_string(),
503 line: 2,
504 column: 3,
505 };
506 let msg = format!("{}", error);
507 assert!(msg.contains("line 2"));
508 assert!(msg.contains("column 3"));
509 assert!(msg.contains("99999"));
510 }
511
512 #[test]
513 fn test_invalid_value_display() {
514 let error = LnmpError::InvalidValue {
515 field_id: 12,
516 reason: "not a valid integer".to_string(),
517 line: 3,
518 column: 10,
519 };
520 let msg = format!("{}", error);
521 assert!(msg.contains("field 12"));
522 assert!(msg.contains("line 3"));
523 assert!(msg.contains("column 10"));
524 assert!(msg.contains("not a valid integer"));
525 }
526
527 #[test]
528 fn test_unexpected_eof_display() {
529 let error = LnmpError::UnexpectedEof { line: 5, column: 1 };
530 let msg = format!("{}", error);
531 assert!(msg.contains("line 5"));
532 assert!(msg.contains("column 1"));
533 assert!(msg.contains("end of file"));
534 }
535
536 #[test]
537 fn test_invalid_escape_sequence_display() {
538 let error = LnmpError::InvalidEscapeSequence {
539 sequence: "\\x".to_string(),
540 line: 1,
541 column: 15,
542 };
543 let msg = format!("{}", error);
544 assert!(msg.contains("line 1"));
545 assert!(msg.contains("column 15"));
546 assert!(msg.contains("\\x"));
547 }
548
549 #[test]
550 fn test_checksum_mismatch_display() {
551 let error = LnmpError::ChecksumMismatch {
552 field_id: 12,
553 expected: "36AAE667".to_string(),
554 found: "DEADBEEF".to_string(),
555 line: 2,
556 column: 15,
557 };
558 let msg = format!("{}", error);
559 assert!(msg.contains("field 12"));
560 assert!(msg.contains("line 2"));
561 assert!(msg.contains("column 15"));
562 assert!(msg.contains("36AAE667"));
563 assert!(msg.contains("DEADBEEF"));
564 }
565
566 #[test]
567 fn test_nesting_too_deep_display() {
568 let error = LnmpError::NestingTooDeep {
569 max_depth: 10,
570 actual_depth: 15,
571 line: 5,
572 column: 20,
573 };
574 let msg = format!("{}", error);
575 assert!(msg.contains("line 5"));
576 assert!(msg.contains("column 20"));
577 assert!(msg.contains("10"));
578 assert!(msg.contains("15"));
579 assert!(msg.contains("Nesting too deep"));
580 }
581
582 #[test]
583 fn test_invalid_nested_structure_display() {
584 let error = LnmpError::InvalidNestedStructure {
585 reason: "mismatched braces".to_string(),
586 line: 3,
587 column: 12,
588 };
589 let msg = format!("{}", error);
590 assert!(msg.contains("line 3"));
591 assert!(msg.contains("column 12"));
592 assert!(msg.contains("mismatched braces"));
593 }
594
595 #[test]
596 fn test_unclosed_nested_structure_display() {
597 let error = LnmpError::UnclosedNestedStructure {
598 structure_type: "record".to_string(),
599 opened_at_line: 1,
600 opened_at_column: 5,
601 line: 3,
602 column: 1,
603 };
604 let msg = format!("{}", error);
605 assert!(msg.contains("record"));
606 assert!(msg.contains("line 3"));
607 assert!(msg.contains("column 1"));
608 assert!(msg.contains("line 1"));
609 assert!(msg.contains("column 5"));
610 }
611
612 #[test]
613 fn test_format_with_source() {
614 let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
615 let error = LnmpError::ChecksumMismatch {
616 field_id: 12,
617 expected: "36AAE667".to_string(),
618 found: "DEADBEEF".to_string(),
619 line: 2,
620 column: 15,
621 };
622 let formatted = error.format_with_source(source);
623 assert!(formatted.contains("Error:"));
624 assert!(formatted.contains("F12:i=14532#DEADBEEF"));
625 assert!(formatted.contains("^ here"));
626 }
627
628 #[test]
629 fn test_error_position() {
630 let error = LnmpError::NestingTooDeep {
631 max_depth: 10,
632 actual_depth: 15,
633 line: 5,
634 column: 20,
635 };
636 let (line, column) = error.position();
637 assert_eq!(line, 5);
638 assert_eq!(column, 20);
639 }
640
641 #[test]
642 fn test_error_equality() {
643 let error1 = LnmpError::UnexpectedEof { line: 1, column: 1 };
644 let error2 = LnmpError::UnexpectedEof { line: 1, column: 1 };
645 let error3 = LnmpError::UnexpectedEof { line: 2, column: 1 };
646
647 assert_eq!(error1, error2);
648 assert_ne!(error1, error3);
649 }
650
651 #[test]
652 fn test_error_clone() {
653 let error = LnmpError::InvalidFieldId {
654 value: "test".to_string(),
655 line: 1,
656 column: 1,
657 };
658 let cloned = error.clone();
659 assert_eq!(error, cloned);
660 }
661
662 #[test]
663 fn test_error_context_equality() {
664 let ctx1 = ErrorContext::new(1, 5);
665 let ctx2 = ErrorContext::new(1, 5);
666 let ctx3 = ErrorContext::new(2, 5);
667
668 assert_eq!(ctx1, ctx2);
669 assert_ne!(ctx1, ctx3);
670 }
671}