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}
225
226impl LnmpError {
227 pub fn with_context(self, _context: ErrorContext) -> Self {
229 self
232 }
233
234 pub fn format_with_source(&self, source: &str) -> String {
236 let (line, column) = self.position();
237
238 let error_msg = format!("{}", self);
239 let mut output = String::new();
240
241 output.push_str(&format!("Error: {}\n", error_msg));
242 output.push_str(" |\n");
243
244 let lines: Vec<&str> = source.lines().collect();
246 let start = line.saturating_sub(2);
247 let end = (line + 1).min(lines.len());
248
249 for line_num in start..end {
250 let actual_line = line_num + 1; let line_content = lines.get(line_num).unwrap_or(&"");
252 output.push_str(&format!("{} | {}\n", actual_line, line_content));
253
254 if line_num + 1 == line {
256 let spaces = " ".repeat(column.saturating_sub(1));
257 output.push_str(&format!(" | {}^ here\n", spaces));
258 }
259 }
260
261 output.push_str(" |\n");
262 output
263 }
264
265 pub fn position(&self) -> (usize, usize) {
267 match self {
268 LnmpError::InvalidCharacter { line, column, .. } => (*line, *column),
269 LnmpError::UnterminatedString { line, column } => (*line, *column),
270 LnmpError::UnexpectedToken { line, column, .. } => (*line, *column),
271 LnmpError::InvalidFieldId { line, column, .. } => (*line, *column),
272 LnmpError::InvalidValue { line, column, .. } => (*line, *column),
273 LnmpError::UnexpectedEof { line, column } => (*line, *column),
274 LnmpError::InvalidEscapeSequence { line, column, .. } => (*line, *column),
275 LnmpError::StrictModeViolation { line, column, .. } => (*line, *column),
276 LnmpError::TypeHintMismatch { line, column, .. } => (*line, *column),
277 LnmpError::InvalidTypeHint { line, column, .. } => (*line, *column),
278 LnmpError::ChecksumMismatch { line, column, .. } => (*line, *column),
279 LnmpError::InvalidChecksum { line, column, .. } => (*line, *column),
280 LnmpError::NestingTooDeep { line, column, .. } => (*line, *column),
281 LnmpError::InvalidNestedStructure { line, column, .. } => (*line, *column),
282 LnmpError::DuplicateFieldId { line, column, .. } => (*line, *column),
283 LnmpError::UnclosedNestedStructure { line, column, .. } => (*line, *column),
284 }
285 }
286}
287
288impl std::fmt::Display for LnmpError {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 match self {
291 LnmpError::InvalidCharacter { char, line, column } => write!(
292 f,
293 "Invalid character '{}' at line {}, column {}",
294 char, line, column
295 ),
296 LnmpError::UnterminatedString { line, column } => write!(
297 f,
298 "Unterminated string at line {}, column {}",
299 line, column
300 ),
301 LnmpError::UnexpectedToken {
302 expected,
303 found,
304 line,
305 column,
306 } => write!(
307 f,
308 "Unexpected token at line {}, column {}: expected {}, found {:?}",
309 line, column, expected, found
310 ),
311 LnmpError::InvalidFieldId {
312 value,
313 line,
314 column,
315 } => write!(
316 f,
317 "Invalid field ID at line {}, column {}: '{}'",
318 line, column, value
319 ),
320 LnmpError::InvalidValue {
321 field_id,
322 reason,
323 line,
324 column,
325 } => write!(
326 f,
327 "Invalid value for field {} at line {}, column {}: {}",
328 field_id, line, column, reason
329 ),
330 LnmpError::UnexpectedEof { line, column } => write!(
331 f,
332 "Unexpected end of file at line {}, column {}",
333 line, column
334 ),
335 LnmpError::InvalidEscapeSequence {
336 sequence,
337 line,
338 column,
339 } => write!(
340 f,
341 "Invalid escape sequence '{}' at line {}, column {}",
342 sequence, line, column
343 ),
344 LnmpError::StrictModeViolation {
345 reason,
346 line,
347 column,
348 } => write!(
349 f,
350 "Strict mode violation at line {}, column {}: {}",
351 line, column, reason
352 ),
353 LnmpError::TypeHintMismatch {
354 field_id,
355 expected_type,
356 actual_value,
357 line,
358 column,
359 } => write!(
360 f,
361 "Type hint mismatch for field {} at line {}, column {}: expected type '{}', got {}",
362 field_id, line, column, expected_type, actual_value
363 ),
364 LnmpError::InvalidTypeHint { hint, line, column } => write!(
365 f,
366 "Invalid type hint '{}' at line {}, column {}",
367 hint, line, column
368 ),
369 LnmpError::ChecksumMismatch {
370 field_id,
371 expected,
372 found,
373 line,
374 column,
375 } => write!(
376 f,
377 "Checksum mismatch for field {} at line {}, column {}: expected {}, found {}",
378 field_id, line, column, expected, found
379 ),
380 LnmpError::NestingTooDeep {
381 max_depth,
382 actual_depth,
383 line,
384 column,
385 } => write!(
386 f,
387 "Nesting too deep (NestingTooDeep). Maximum nesting depth exceeded at line {}, column {}: maximum depth is {}, but reached {}",
388 line, column, max_depth, actual_depth
389 ),
390 LnmpError::InvalidNestedStructure {
391 reason,
392 line,
393 column,
394 } => write!(
395 f,
396 "Invalid nested structure at line {}, column {}: {}",
397 line, column, reason
398 ),
399 LnmpError::InvalidChecksum { field_id, reason, line, column } => write!(
400 f,
401 "Invalid checksum for field {} at line {}, column {}: {}",
402 field_id, line, column, reason
403 ),
404 LnmpError::UnclosedNestedStructure {
405 structure_type,
406 opened_at_line,
407 opened_at_column,
408 line,
409 column,
410 } => write!(
411 f,
412 "Unclosed {} at line {}, column {} (opened at line {}, column {})",
413 structure_type, line, column, opened_at_line, opened_at_column
414 ),
415 LnmpError::DuplicateFieldId { field_id, line, column } => write!(
416 f,
417 "DuplicateFieldId: Field ID {} appears multiple times at line {}, column {}",
418 field_id, line, column
419 ),
420 }
421 }
422}
423
424impl std::error::Error for LnmpError {}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_error_context_creation() {
432 let ctx = ErrorContext::new(1, 5);
433 assert_eq!(ctx.line, 1);
434 assert_eq!(ctx.column, 5);
435 assert!(ctx.source_snippet.is_empty());
436 assert!(ctx.source_file.is_none());
437 }
438
439 #[test]
440 fn test_error_context_with_snippet() {
441 let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
442 let ctx = ErrorContext::with_snippet(2, 15, source);
443 assert_eq!(ctx.line, 2);
444 assert_eq!(ctx.column, 15);
445 assert!(ctx.source_snippet.contains("F12:i=14532"));
446 }
447
448 #[test]
449 fn test_error_context_with_file() {
450 let source = "F7:b=1\nF12:i=14532";
451 let ctx = ErrorContext::with_file(1, 5, source, "test.lnmp".to_string());
452 assert_eq!(ctx.line, 1);
453 assert_eq!(ctx.source_file, Some("test.lnmp".to_string()));
454 }
455
456 #[test]
457 fn test_invalid_character_display() {
458 let error = LnmpError::InvalidCharacter {
459 char: '@',
460 line: 1,
461 column: 5,
462 };
463 let msg = format!("{}", error);
464 assert!(msg.contains("line 1"));
465 assert!(msg.contains("column 5"));
466 assert!(msg.contains("'@'"));
467 }
468
469 #[test]
470 fn test_unterminated_string_display() {
471 let error = LnmpError::UnterminatedString {
472 line: 1,
473 column: 10,
474 };
475 let msg = format!("{}", error);
476 assert!(msg.contains("line 1"));
477 assert!(msg.contains("column 10"));
478 assert!(msg.contains("Unterminated string"));
479 }
480
481 #[test]
482 fn test_unexpected_token_display() {
483 let error = LnmpError::UnexpectedToken {
484 expected: "equals sign".to_string(),
485 found: Token::Semicolon,
486 line: 1,
487 column: 5,
488 };
489 let msg = format!("{}", error);
490 assert!(msg.contains("line 1"));
491 assert!(msg.contains("column 5"));
492 assert!(msg.contains("expected equals sign"));
493 }
494
495 #[test]
496 fn test_invalid_field_id_display() {
497 let error = LnmpError::InvalidFieldId {
498 value: "99999".to_string(),
499 line: 2,
500 column: 3,
501 };
502 let msg = format!("{}", error);
503 assert!(msg.contains("line 2"));
504 assert!(msg.contains("column 3"));
505 assert!(msg.contains("99999"));
506 }
507
508 #[test]
509 fn test_invalid_value_display() {
510 let error = LnmpError::InvalidValue {
511 field_id: 12,
512 reason: "not a valid integer".to_string(),
513 line: 3,
514 column: 10,
515 };
516 let msg = format!("{}", error);
517 assert!(msg.contains("field 12"));
518 assert!(msg.contains("line 3"));
519 assert!(msg.contains("column 10"));
520 assert!(msg.contains("not a valid integer"));
521 }
522
523 #[test]
524 fn test_unexpected_eof_display() {
525 let error = LnmpError::UnexpectedEof { line: 5, column: 1 };
526 let msg = format!("{}", error);
527 assert!(msg.contains("line 5"));
528 assert!(msg.contains("column 1"));
529 assert!(msg.contains("end of file"));
530 }
531
532 #[test]
533 fn test_invalid_escape_sequence_display() {
534 let error = LnmpError::InvalidEscapeSequence {
535 sequence: "\\x".to_string(),
536 line: 1,
537 column: 15,
538 };
539 let msg = format!("{}", error);
540 assert!(msg.contains("line 1"));
541 assert!(msg.contains("column 15"));
542 assert!(msg.contains("\\x"));
543 }
544
545 #[test]
546 fn test_checksum_mismatch_display() {
547 let error = LnmpError::ChecksumMismatch {
548 field_id: 12,
549 expected: "36AAE667".to_string(),
550 found: "DEADBEEF".to_string(),
551 line: 2,
552 column: 15,
553 };
554 let msg = format!("{}", error);
555 assert!(msg.contains("field 12"));
556 assert!(msg.contains("line 2"));
557 assert!(msg.contains("column 15"));
558 assert!(msg.contains("36AAE667"));
559 assert!(msg.contains("DEADBEEF"));
560 }
561
562 #[test]
563 fn test_nesting_too_deep_display() {
564 let error = LnmpError::NestingTooDeep {
565 max_depth: 10,
566 actual_depth: 15,
567 line: 5,
568 column: 20,
569 };
570 let msg = format!("{}", error);
571 assert!(msg.contains("line 5"));
572 assert!(msg.contains("column 20"));
573 assert!(msg.contains("10"));
574 assert!(msg.contains("15"));
575 assert!(msg.contains("Nesting too deep"));
576 }
577
578 #[test]
579 fn test_invalid_nested_structure_display() {
580 let error = LnmpError::InvalidNestedStructure {
581 reason: "mismatched braces".to_string(),
582 line: 3,
583 column: 12,
584 };
585 let msg = format!("{}", error);
586 assert!(msg.contains("line 3"));
587 assert!(msg.contains("column 12"));
588 assert!(msg.contains("mismatched braces"));
589 }
590
591 #[test]
592 fn test_unclosed_nested_structure_display() {
593 let error = LnmpError::UnclosedNestedStructure {
594 structure_type: "record".to_string(),
595 opened_at_line: 1,
596 opened_at_column: 5,
597 line: 3,
598 column: 1,
599 };
600 let msg = format!("{}", error);
601 assert!(msg.contains("record"));
602 assert!(msg.contains("line 3"));
603 assert!(msg.contains("column 1"));
604 assert!(msg.contains("line 1"));
605 assert!(msg.contains("column 5"));
606 }
607
608 #[test]
609 fn test_format_with_source() {
610 let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
611 let error = LnmpError::ChecksumMismatch {
612 field_id: 12,
613 expected: "36AAE667".to_string(),
614 found: "DEADBEEF".to_string(),
615 line: 2,
616 column: 15,
617 };
618 let formatted = error.format_with_source(source);
619 assert!(formatted.contains("Error:"));
620 assert!(formatted.contains("F12:i=14532#DEADBEEF"));
621 assert!(formatted.contains("^ here"));
622 }
623
624 #[test]
625 fn test_error_position() {
626 let error = LnmpError::NestingTooDeep {
627 max_depth: 10,
628 actual_depth: 15,
629 line: 5,
630 column: 20,
631 };
632 let (line, column) = error.position();
633 assert_eq!(line, 5);
634 assert_eq!(column, 20);
635 }
636
637 #[test]
638 fn test_error_equality() {
639 let error1 = LnmpError::UnexpectedEof { line: 1, column: 1 };
640 let error2 = LnmpError::UnexpectedEof { line: 1, column: 1 };
641 let error3 = LnmpError::UnexpectedEof { line: 2, column: 1 };
642
643 assert_eq!(error1, error2);
644 assert_ne!(error1, error3);
645 }
646
647 #[test]
648 fn test_error_clone() {
649 let error = LnmpError::InvalidFieldId {
650 value: "test".to_string(),
651 line: 1,
652 column: 1,
653 };
654 let cloned = error.clone();
655 assert_eq!(error, cloned);
656 }
657
658 #[test]
659 fn test_error_context_equality() {
660 let ctx1 = ErrorContext::new(1, 5);
661 let ctx2 = ErrorContext::new(1, 5);
662 let ctx3 = ErrorContext::new(2, 5);
663
664 assert_eq!(ctx1, ctx2);
665 assert_ne!(ctx1, ctx3);
666 }
667}