1use crate::Position;
4use std::fmt;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ErrorContext {
12 pub line_content: String,
14 pub column_position: usize,
16 pub suggestion: Option<String>,
18 pub surrounding_lines: Vec<(usize, String)>,
20}
21
22impl ErrorContext {
23 pub const fn new(line_content: String, column_position: usize) -> Self {
25 Self {
26 line_content,
27 column_position,
28 suggestion: None,
29 surrounding_lines: Vec::new(),
30 }
31 }
32
33 pub fn with_suggestion(mut self, suggestion: String) -> Self {
35 self.suggestion = Some(suggestion);
36 self
37 }
38
39 pub fn with_surrounding_lines(mut self, lines: Vec<(usize, String)>) -> Self {
41 self.surrounding_lines = lines;
42 self
43 }
44
45 pub fn from_input(input: &str, position: &Position, context_lines: usize) -> Self {
53 let line_index = position.line.saturating_sub(1);
54
55 let clamped_index = position.index.min(input.len());
58 let mut window_start = input[..clamped_index].rfind('\n').map_or(0, |nl| nl + 1);
59 let lines_before = context_lines.min(line_index);
60 for _ in 0..lines_before {
61 if window_start == 0 {
62 break;
63 }
64 window_start = input[..window_start - 1].rfind('\n').map_or(0, |nl| nl + 1);
65 }
66
67 let window: Vec<&str> = input[window_start..]
71 .lines()
72 .take(lines_before + 1 + context_lines)
73 .collect();
74
75 let line_content = window
76 .get(lines_before)
77 .map(|s| (*s).to_string())
78 .unwrap_or_else(|| "<EOF>".to_string());
79
80 let first_line_number = line_index - lines_before + 1;
82 let mut surrounding_lines = Vec::new();
83 for (offset, line) in window.iter().enumerate() {
84 if offset != lines_before {
85 surrounding_lines.push((first_line_number + offset, (*line).to_string()));
86 }
87 }
88
89 Self {
90 line_content,
91 column_position: position.column,
92 suggestion: None,
93 surrounding_lines,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum Error {
101 Parse {
103 position: Position,
105 message: String,
107 context: Option<ErrorContext>,
109 },
110
111 Scan {
113 position: Position,
115 message: String,
117 context: Option<ErrorContext>,
119 },
120
121 Construction {
123 position: Position,
125 message: String,
127 context: Option<ErrorContext>,
129 },
130
131 Emission {
133 message: String,
135 },
136
137 Io {
139 kind: std::io::ErrorKind,
141 message: String,
143 },
144
145 Utf8 {
147 message: String,
149 },
150
151 Type {
153 expected: String,
155 found: String,
157 position: Position,
159 context: Option<ErrorContext>,
161 },
162
163 Value {
165 position: Position,
167 message: String,
169 context: Option<ErrorContext>,
171 },
172
173 Config {
175 message: String,
177 },
178
179 Multiple {
181 errors: Vec<Error>,
183 message: String,
185 },
186
187 LimitExceeded {
189 message: String,
191 },
192
193 Indentation {
195 position: Position,
197 expected: usize,
199 found: usize,
201 context: Option<ErrorContext>,
203 },
204
205 InvalidCharacter {
207 position: Position,
209 character: char,
211 context_description: String,
213 context: Option<ErrorContext>,
215 },
216
217 UnclosedDelimiter {
219 start_position: Position,
221 current_position: Position,
223 delimiter_type: String,
225 context: Option<ErrorContext>,
227 },
228}
229
230impl Error {
231 pub fn parse(position: Position, message: impl Into<String>) -> Self {
233 Self::Parse {
234 position,
235 message: message.into(),
236 context: None,
237 }
238 }
239
240 pub fn parse_with_context(
242 position: Position,
243 message: impl Into<String>,
244 context: ErrorContext,
245 ) -> Self {
246 Self::Parse {
247 position,
248 message: message.into(),
249 context: Some(context),
250 }
251 }
252
253 pub fn scan(position: Position, message: impl Into<String>) -> Self {
255 Self::Scan {
256 position,
257 message: message.into(),
258 context: None,
259 }
260 }
261
262 pub fn scan_with_context(
264 position: Position,
265 message: impl Into<String>,
266 context: ErrorContext,
267 ) -> Self {
268 Self::Scan {
269 position,
270 message: message.into(),
271 context: Some(context),
272 }
273 }
274
275 pub fn construction(position: Position, message: impl Into<String>) -> Self {
277 Self::Construction {
278 position,
279 message: message.into(),
280 context: None,
281 }
282 }
283
284 pub fn construction_with_context(
286 position: Position,
287 message: impl Into<String>,
288 context: ErrorContext,
289 ) -> Self {
290 Self::Construction {
291 position,
292 message: message.into(),
293 context: Some(context),
294 }
295 }
296
297 pub fn emission(message: impl Into<String>) -> Self {
299 Self::Emission {
300 message: message.into(),
301 }
302 }
303
304 pub fn limit_exceeded(message: impl Into<String>) -> Self {
306 Self::LimitExceeded {
307 message: message.into(),
308 }
309 }
310
311 pub fn type_error(
313 position: Position,
314 expected: impl Into<String>,
315 found: impl Into<String>,
316 ) -> Self {
317 Self::Type {
318 expected: expected.into(),
319 found: found.into(),
320 position,
321 context: None,
322 }
323 }
324
325 pub fn type_error_with_context(
327 position: Position,
328 expected: impl Into<String>,
329 found: impl Into<String>,
330 context: ErrorContext,
331 ) -> Self {
332 Self::Type {
333 expected: expected.into(),
334 found: found.into(),
335 position,
336 context: Some(context),
337 }
338 }
339
340 pub fn value_error(position: Position, message: impl Into<String>) -> Self {
342 Self::Value {
343 position,
344 message: message.into(),
345 context: None,
346 }
347 }
348
349 pub fn value_error_with_context(
351 position: Position,
352 message: impl Into<String>,
353 context: ErrorContext,
354 ) -> Self {
355 Self::Value {
356 position,
357 message: message.into(),
358 context: Some(context),
359 }
360 }
361
362 pub fn config_error(message: impl Into<String>) -> Self {
364 Self::Config {
365 message: message.into(),
366 }
367 }
368
369 pub fn config(message: impl Into<String>) -> Self {
371 Self::Config {
372 message: message.into(),
373 }
374 }
375
376 pub fn multiple(errors: Vec<Self>, message: impl Into<String>) -> Self {
378 Self::Multiple {
379 errors,
380 message: message.into(),
381 }
382 }
383
384 pub const fn indentation(position: Position, expected: usize, found: usize) -> Self {
386 Self::Indentation {
387 position,
388 expected,
389 found,
390 context: None,
391 }
392 }
393
394 pub const fn indentation_with_context(
396 position: Position,
397 expected: usize,
398 found: usize,
399 context: ErrorContext,
400 ) -> Self {
401 Self::Indentation {
402 position,
403 expected,
404 found,
405 context: Some(context),
406 }
407 }
408
409 pub fn invalid_character(
411 position: Position,
412 character: char,
413 context_description: impl Into<String>,
414 ) -> Self {
415 Self::InvalidCharacter {
416 position,
417 character,
418 context_description: context_description.into(),
419 context: None,
420 }
421 }
422
423 pub fn invalid_character_with_context(
425 position: Position,
426 character: char,
427 context_description: impl Into<String>,
428 context: ErrorContext,
429 ) -> Self {
430 Self::InvalidCharacter {
431 position,
432 character,
433 context_description: context_description.into(),
434 context: Some(context),
435 }
436 }
437
438 pub fn unclosed_delimiter(
440 start_position: Position,
441 current_position: Position,
442 delimiter_type: impl Into<String>,
443 ) -> Self {
444 Self::UnclosedDelimiter {
445 start_position,
446 current_position,
447 delimiter_type: delimiter_type.into(),
448 context: None,
449 }
450 }
451
452 pub fn unclosed_delimiter_with_context(
454 start_position: Position,
455 current_position: Position,
456 delimiter_type: impl Into<String>,
457 context: ErrorContext,
458 ) -> Self {
459 Self::UnclosedDelimiter {
460 start_position,
461 current_position,
462 delimiter_type: delimiter_type.into(),
463 context: Some(context),
464 }
465 }
466
467 pub const fn position(&self) -> Option<&Position> {
469 match self {
470 Self::Parse { position, .. }
471 | Self::Scan { position, .. }
472 | Self::Construction { position, .. }
473 | Self::Type { position, .. }
474 | Self::Value { position, .. }
475 | Self::Indentation { position, .. }
476 | Self::InvalidCharacter { position, .. } => Some(position),
477 Self::UnclosedDelimiter {
478 current_position, ..
479 } => Some(current_position),
480 Self::Emission { .. }
481 | Self::Io { .. }
482 | Self::Utf8 { .. }
483 | Self::Config { .. }
484 | Self::Multiple { .. }
485 | Self::LimitExceeded { .. } => None,
486 }
487 }
488
489 pub const fn context(&self) -> Option<&ErrorContext> {
491 match self {
492 Self::Parse { context, .. }
493 | Self::Scan { context, .. }
494 | Self::Construction { context, .. }
495 | Self::Type { context, .. }
496 | Self::Value { context, .. }
497 | Self::Indentation { context, .. }
498 | Self::InvalidCharacter { context, .. }
499 | Self::UnclosedDelimiter { context, .. } => context.as_ref(),
500 _ => None,
501 }
502 }
503}
504
505impl From<std::io::Error> for Error {
506 fn from(err: std::io::Error) -> Self {
507 Self::Io {
508 kind: err.kind(),
509 message: err.to_string(),
510 }
511 }
512}
513
514impl From<std::str::Utf8Error> for Error {
515 fn from(err: std::str::Utf8Error) -> Self {
516 Self::Utf8 {
517 message: err.to_string(),
518 }
519 }
520}
521
522impl From<std::string::FromUtf8Error> for Error {
523 fn from(err: std::string::FromUtf8Error) -> Self {
524 Self::Utf8 {
525 message: err.to_string(),
526 }
527 }
528}
529
530impl std::error::Error for Error {}
531
532impl Error {
533 fn format_with_context(
535 &self,
536 f: &mut fmt::Formatter<'_>,
537 position: &Position,
538 message: &str,
539 context: Option<&ErrorContext>,
540 ) -> fmt::Result {
541 writeln!(
543 f,
544 "Error at line {}, column {}: {}",
545 position.line, position.column, message
546 )?;
547
548 if let Some(ctx) = context {
550 writeln!(f)?;
551
552 for (line_num, line_content) in &ctx.surrounding_lines {
554 writeln!(f, "{:4} | {}", line_num, line_content)?;
555 }
556
557 writeln!(f, "{:4} | {}", position.line, ctx.line_content)?;
559 write!(f, " | ")?;
560 for _ in 0..ctx.column_position.saturating_sub(1) {
561 write!(f, " ")?;
562 }
563 writeln!(f, "^ here")?;
564
565 if let Some(suggestion) = &ctx.suggestion {
567 writeln!(f)?;
568 writeln!(f, "Suggestion: {}", suggestion)?;
569 }
570 }
571
572 Ok(())
573 }
574}
575
576impl fmt::Display for Error {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 match self {
579 Self::Parse {
580 position,
581 message,
582 context,
583 } => self.format_with_context(f, position, message, context.as_ref()),
584 Self::Scan {
585 position,
586 message,
587 context,
588 } => self.format_with_context(
589 f,
590 position,
591 &format!("Scan error: {}", message),
592 context.as_ref(),
593 ),
594 Self::Construction {
595 position,
596 message,
597 context,
598 } => self.format_with_context(
599 f,
600 position,
601 &format!("Construction error: {}", message),
602 context.as_ref(),
603 ),
604 Self::Type {
605 expected,
606 found,
607 position,
608 context,
609 } => {
610 let msg = format!("Type error: expected {}, found {}", expected, found);
611 self.format_with_context(f, position, &msg, context.as_ref())
612 }
613 Self::Value {
614 position,
615 message,
616 context,
617 } => self.format_with_context(
618 f,
619 position,
620 &format!("Value error: {}", message),
621 context.as_ref(),
622 ),
623 Self::Indentation {
624 position,
625 expected,
626 found,
627 context,
628 } => {
629 let msg = format!(
630 "Indentation error: expected {} spaces, found {}",
631 expected, found
632 );
633 self.format_with_context(f, position, &msg, context.as_ref())
634 }
635 Self::InvalidCharacter {
636 position,
637 character,
638 context_description,
639 context,
640 } => {
641 let msg = format!(
642 "Invalid character '{}' in {}",
643 character, context_description
644 );
645 self.format_with_context(f, position, &msg, context.as_ref())
646 }
647 Self::UnclosedDelimiter {
648 start_position,
649 current_position,
650 delimiter_type,
651 context,
652 } => {
653 let msg = format!(
654 "Unclosed {} starting at line {}, column {}",
655 delimiter_type, start_position.line, start_position.column
656 );
657 self.format_with_context(f, current_position, &msg, context.as_ref())
658 }
659 Self::Multiple { errors, message } => {
660 writeln!(f, "Multiple errors: {}", message)?;
661 for (i, error) in errors.iter().enumerate() {
662 writeln!(f, " {}. {}", i + 1, error)?;
663 }
664 Ok(())
665 }
666 Self::Emission { message } => {
667 write!(f, "Emission error: {}", message)
668 }
669 Self::Io { kind, message } => {
670 write!(f, "IO error ({:?}): {}", kind, message)
671 }
672 Self::Utf8 { message } => {
673 write!(f, "UTF-8 error: {}", message)
674 }
675 Self::Config { message } => {
676 write!(f, "Configuration error: {}", message)
677 }
678 Self::LimitExceeded { message } => {
679 write!(f, "Resource limit exceeded: {}", message)
680 }
681 }
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_error_creation() {
691 let pos = Position::new();
692
693 let parse_err = Error::parse(pos.clone(), "unexpected token");
694 assert!(matches!(parse_err, Error::Parse { .. }));
695 assert_eq!(parse_err.position(), Some(&pos));
696
697 let config_err = Error::config("invalid setting");
698 assert!(matches!(config_err, Error::Config { .. }));
699 assert_eq!(config_err.position(), None);
700 }
701
702 #[test]
703 fn test_error_display() {
704 let mut pos = Position::new();
705 pos.line = 5;
706 pos.column = 12;
707 let err = Error::parse(pos, "unexpected character");
708 let display = format!("{}", err);
709 assert!(display.contains("line 5"));
710 assert!(display.contains("column 12"));
711 assert!(display.contains("unexpected character"));
712 }
713
714 #[test]
719 fn from_input_extracts_error_line_and_context() {
720 let input = "line one\nline two\nline three\nline four\nline five\n";
721 let pos = Position::new().advance_str("line one\nline two\n");
723 let ctx = ErrorContext::from_input(input, &pos, 1);
724
725 assert_eq!(ctx.line_content, "line three");
726 assert_eq!(
727 ctx.surrounding_lines,
728 vec![(2, "line two".to_string()), (4, "line four".to_string())]
729 );
730 }
731
732 #[test]
733 fn from_input_handles_first_line_without_underflow() {
734 let input = "first\nsecond\nthird\n";
735 let pos = Position::new(); let ctx = ErrorContext::from_input(input, &pos, 2);
737
738 assert_eq!(ctx.line_content, "first");
739 assert_eq!(
740 ctx.surrounding_lines,
741 vec![(2, "second".to_string()), (3, "third".to_string())]
742 );
743 }
744
745 #[test]
746 fn from_input_reports_eof_past_last_line() {
747 let input = "alpha\nbeta\n";
748 let pos = Position::new().advance_str("alpha\nbeta\n");
750 let ctx = ErrorContext::from_input(input, &pos, 1);
751
752 assert_eq!(ctx.line_content, "<EOF>");
753 }
754
755 #[test]
756 fn from_input_handles_crlf_line_endings() {
757 let input = "aaa\r\nbbb\r\nccc\r\n";
758 let pos = Position::new().advance_str("aaa\r\n");
760 let ctx = ErrorContext::from_input(input, &pos, 1);
761
762 assert_eq!(ctx.line_content, "bbb");
763 assert_eq!(
764 ctx.surrounding_lines,
765 vec![(1, "aaa".to_string()), (3, "ccc".to_string())]
766 );
767 }
768}