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 {
47 let lines: Vec<&str> = input.lines().collect();
48 let line_index = position.line.saturating_sub(1);
49
50 let line_content = lines
51 .get(line_index)
52 .map(|s| s.to_string())
53 .unwrap_or_else(|| "<EOF>".to_string());
54
55 let mut surrounding_lines = Vec::new();
56
57 let start = line_index.saturating_sub(context_lines);
59 for i in start..line_index {
60 if let Some(line) = lines.get(i) {
61 surrounding_lines.push((i + 1, line.to_string()));
62 }
63 }
64
65 let end = (line_index + context_lines + 1).min(lines.len());
67 for i in (line_index + 1)..end {
68 if let Some(line) = lines.get(i) {
69 surrounding_lines.push((i + 1, line.to_string()));
70 }
71 }
72
73 Self {
74 line_content,
75 column_position: position.column,
76 suggestion: None,
77 surrounding_lines,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum Error {
85 Parse {
87 position: Position,
89 message: String,
91 context: Option<ErrorContext>,
93 },
94
95 Scan {
97 position: Position,
99 message: String,
101 context: Option<ErrorContext>,
103 },
104
105 Construction {
107 position: Position,
109 message: String,
111 context: Option<ErrorContext>,
113 },
114
115 Emission {
117 message: String,
119 },
120
121 Io {
123 kind: std::io::ErrorKind,
125 message: String,
127 },
128
129 Utf8 {
131 message: String,
133 },
134
135 Type {
137 expected: String,
139 found: String,
141 position: Position,
143 context: Option<ErrorContext>,
145 },
146
147 Value {
149 position: Position,
151 message: String,
153 context: Option<ErrorContext>,
155 },
156
157 Config {
159 message: String,
161 },
162
163 Multiple {
165 errors: Vec<Error>,
167 message: String,
169 },
170
171 LimitExceeded {
173 message: String,
175 },
176
177 Indentation {
179 position: Position,
181 expected: usize,
183 found: usize,
185 context: Option<ErrorContext>,
187 },
188
189 InvalidCharacter {
191 position: Position,
193 character: char,
195 context_description: String,
197 context: Option<ErrorContext>,
199 },
200
201 UnclosedDelimiter {
203 start_position: Position,
205 current_position: Position,
207 delimiter_type: String,
209 context: Option<ErrorContext>,
211 },
212}
213
214impl Error {
215 pub fn parse(position: Position, message: impl Into<String>) -> Self {
217 Self::Parse {
218 position,
219 message: message.into(),
220 context: None,
221 }
222 }
223
224 pub fn parse_with_context(
226 position: Position,
227 message: impl Into<String>,
228 context: ErrorContext,
229 ) -> Self {
230 Self::Parse {
231 position,
232 message: message.into(),
233 context: Some(context),
234 }
235 }
236
237 pub fn scan(position: Position, message: impl Into<String>) -> Self {
239 Self::Scan {
240 position,
241 message: message.into(),
242 context: None,
243 }
244 }
245
246 pub fn scan_with_context(
248 position: Position,
249 message: impl Into<String>,
250 context: ErrorContext,
251 ) -> Self {
252 Self::Scan {
253 position,
254 message: message.into(),
255 context: Some(context),
256 }
257 }
258
259 pub fn construction(position: Position, message: impl Into<String>) -> Self {
261 Self::Construction {
262 position,
263 message: message.into(),
264 context: None,
265 }
266 }
267
268 pub fn construction_with_context(
270 position: Position,
271 message: impl Into<String>,
272 context: ErrorContext,
273 ) -> Self {
274 Self::Construction {
275 position,
276 message: message.into(),
277 context: Some(context),
278 }
279 }
280
281 pub fn emission(message: impl Into<String>) -> Self {
283 Self::Emission {
284 message: message.into(),
285 }
286 }
287
288 pub fn limit_exceeded(message: impl Into<String>) -> Self {
290 Self::LimitExceeded {
291 message: message.into(),
292 }
293 }
294
295 pub fn type_error(
297 position: Position,
298 expected: impl Into<String>,
299 found: impl Into<String>,
300 ) -> Self {
301 Self::Type {
302 expected: expected.into(),
303 found: found.into(),
304 position,
305 context: None,
306 }
307 }
308
309 pub fn type_error_with_context(
311 position: Position,
312 expected: impl Into<String>,
313 found: impl Into<String>,
314 context: ErrorContext,
315 ) -> Self {
316 Self::Type {
317 expected: expected.into(),
318 found: found.into(),
319 position,
320 context: Some(context),
321 }
322 }
323
324 pub fn value_error(position: Position, message: impl Into<String>) -> Self {
326 Self::Value {
327 position,
328 message: message.into(),
329 context: None,
330 }
331 }
332
333 pub fn value_error_with_context(
335 position: Position,
336 message: impl Into<String>,
337 context: ErrorContext,
338 ) -> Self {
339 Self::Value {
340 position,
341 message: message.into(),
342 context: Some(context),
343 }
344 }
345
346 pub fn config_error(message: impl Into<String>) -> Self {
348 Self::Config {
349 message: message.into(),
350 }
351 }
352
353 pub fn config(message: impl Into<String>) -> Self {
355 Self::Config {
356 message: message.into(),
357 }
358 }
359
360 pub fn multiple(errors: Vec<Self>, message: impl Into<String>) -> Self {
362 Self::Multiple {
363 errors,
364 message: message.into(),
365 }
366 }
367
368 pub const fn indentation(position: Position, expected: usize, found: usize) -> Self {
370 Self::Indentation {
371 position,
372 expected,
373 found,
374 context: None,
375 }
376 }
377
378 pub const fn indentation_with_context(
380 position: Position,
381 expected: usize,
382 found: usize,
383 context: ErrorContext,
384 ) -> Self {
385 Self::Indentation {
386 position,
387 expected,
388 found,
389 context: Some(context),
390 }
391 }
392
393 pub fn invalid_character(
395 position: Position,
396 character: char,
397 context_description: impl Into<String>,
398 ) -> Self {
399 Self::InvalidCharacter {
400 position,
401 character,
402 context_description: context_description.into(),
403 context: None,
404 }
405 }
406
407 pub fn invalid_character_with_context(
409 position: Position,
410 character: char,
411 context_description: impl Into<String>,
412 context: ErrorContext,
413 ) -> Self {
414 Self::InvalidCharacter {
415 position,
416 character,
417 context_description: context_description.into(),
418 context: Some(context),
419 }
420 }
421
422 pub fn unclosed_delimiter(
424 start_position: Position,
425 current_position: Position,
426 delimiter_type: impl Into<String>,
427 ) -> Self {
428 Self::UnclosedDelimiter {
429 start_position,
430 current_position,
431 delimiter_type: delimiter_type.into(),
432 context: None,
433 }
434 }
435
436 pub fn unclosed_delimiter_with_context(
438 start_position: Position,
439 current_position: Position,
440 delimiter_type: impl Into<String>,
441 context: ErrorContext,
442 ) -> Self {
443 Self::UnclosedDelimiter {
444 start_position,
445 current_position,
446 delimiter_type: delimiter_type.into(),
447 context: Some(context),
448 }
449 }
450
451 pub const fn position(&self) -> Option<&Position> {
453 match self {
454 Self::Parse { position, .. }
455 | Self::Scan { position, .. }
456 | Self::Construction { position, .. }
457 | Self::Type { position, .. }
458 | Self::Value { position, .. }
459 | Self::Indentation { position, .. }
460 | Self::InvalidCharacter { position, .. } => Some(position),
461 Self::UnclosedDelimiter {
462 current_position, ..
463 } => Some(current_position),
464 Self::Emission { .. }
465 | Self::Io { .. }
466 | Self::Utf8 { .. }
467 | Self::Config { .. }
468 | Self::Multiple { .. }
469 | Self::LimitExceeded { .. } => None,
470 }
471 }
472
473 pub const fn context(&self) -> Option<&ErrorContext> {
475 match self {
476 Self::Parse { context, .. }
477 | Self::Scan { context, .. }
478 | Self::Construction { context, .. }
479 | Self::Type { context, .. }
480 | Self::Value { context, .. }
481 | Self::Indentation { context, .. }
482 | Self::InvalidCharacter { context, .. }
483 | Self::UnclosedDelimiter { context, .. } => context.as_ref(),
484 _ => None,
485 }
486 }
487}
488
489impl From<std::io::Error> for Error {
490 fn from(err: std::io::Error) -> Self {
491 Self::Io {
492 kind: err.kind(),
493 message: err.to_string(),
494 }
495 }
496}
497
498impl From<std::str::Utf8Error> for Error {
499 fn from(err: std::str::Utf8Error) -> Self {
500 Self::Utf8 {
501 message: err.to_string(),
502 }
503 }
504}
505
506impl From<std::string::FromUtf8Error> for Error {
507 fn from(err: std::string::FromUtf8Error) -> Self {
508 Self::Utf8 {
509 message: err.to_string(),
510 }
511 }
512}
513
514impl std::error::Error for Error {}
515
516impl Error {
517 fn format_with_context(
519 &self,
520 f: &mut fmt::Formatter<'_>,
521 position: &Position,
522 message: &str,
523 context: Option<&ErrorContext>,
524 ) -> fmt::Result {
525 writeln!(
527 f,
528 "Error at line {}, column {}: {}",
529 position.line, position.column, message
530 )?;
531
532 if let Some(ctx) = context {
534 writeln!(f)?;
535
536 for (line_num, line_content) in &ctx.surrounding_lines {
538 writeln!(f, "{:4} | {}", line_num, line_content)?;
539 }
540
541 writeln!(f, "{:4} | {}", position.line, ctx.line_content)?;
543 write!(f, " | ")?;
544 for _ in 0..ctx.column_position.saturating_sub(1) {
545 write!(f, " ")?;
546 }
547 writeln!(f, "^ here")?;
548
549 if let Some(suggestion) = &ctx.suggestion {
551 writeln!(f)?;
552 writeln!(f, "Suggestion: {}", suggestion)?;
553 }
554 }
555
556 Ok(())
557 }
558}
559
560impl fmt::Display for Error {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 match self {
563 Self::Parse {
564 position,
565 message,
566 context,
567 } => self.format_with_context(f, position, message, context.as_ref()),
568 Self::Scan {
569 position,
570 message,
571 context,
572 } => self.format_with_context(
573 f,
574 position,
575 &format!("Scan error: {}", message),
576 context.as_ref(),
577 ),
578 Self::Construction {
579 position,
580 message,
581 context,
582 } => self.format_with_context(
583 f,
584 position,
585 &format!("Construction error: {}", message),
586 context.as_ref(),
587 ),
588 Self::Type {
589 expected,
590 found,
591 position,
592 context,
593 } => {
594 let msg = format!("Type error: expected {}, found {}", expected, found);
595 self.format_with_context(f, position, &msg, context.as_ref())
596 }
597 Self::Value {
598 position,
599 message,
600 context,
601 } => self.format_with_context(
602 f,
603 position,
604 &format!("Value error: {}", message),
605 context.as_ref(),
606 ),
607 Self::Indentation {
608 position,
609 expected,
610 found,
611 context,
612 } => {
613 let msg = format!(
614 "Indentation error: expected {} spaces, found {}",
615 expected, found
616 );
617 self.format_with_context(f, position, &msg, context.as_ref())
618 }
619 Self::InvalidCharacter {
620 position,
621 character,
622 context_description,
623 context,
624 } => {
625 let msg = format!(
626 "Invalid character '{}' in {}",
627 character, context_description
628 );
629 self.format_with_context(f, position, &msg, context.as_ref())
630 }
631 Self::UnclosedDelimiter {
632 start_position,
633 current_position,
634 delimiter_type,
635 context,
636 } => {
637 let msg = format!(
638 "Unclosed {} starting at line {}, column {}",
639 delimiter_type, start_position.line, start_position.column
640 );
641 self.format_with_context(f, current_position, &msg, context.as_ref())
642 }
643 Self::Multiple { errors, message } => {
644 writeln!(f, "Multiple errors: {}", message)?;
645 for (i, error) in errors.iter().enumerate() {
646 writeln!(f, " {}. {}", i + 1, error)?;
647 }
648 Ok(())
649 }
650 Self::Emission { message } => {
651 write!(f, "Emission error: {}", message)
652 }
653 Self::Io { kind, message } => {
654 write!(f, "IO error ({:?}): {}", kind, message)
655 }
656 Self::Utf8 { message } => {
657 write!(f, "UTF-8 error: {}", message)
658 }
659 Self::Config { message } => {
660 write!(f, "Configuration error: {}", message)
661 }
662 Self::LimitExceeded { message } => {
663 write!(f, "Resource limit exceeded: {}", message)
664 }
665 }
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn test_error_creation() {
675 let pos = Position::new();
676
677 let parse_err = Error::parse(pos.clone(), "unexpected token");
678 assert!(matches!(parse_err, Error::Parse { .. }));
679 assert_eq!(parse_err.position(), Some(&pos));
680
681 let config_err = Error::config("invalid setting");
682 assert!(matches!(config_err, Error::Config { .. }));
683 assert_eq!(config_err.position(), None);
684 }
685
686 #[test]
687 fn test_error_display() {
688 let mut pos = Position::new();
689 pos.line = 5;
690 pos.column = 12;
691 let err = Error::parse(pos, "unexpected character");
692 let display = format!("{}", err);
693 assert!(display.contains("line 5"));
694 assert!(display.contains("column 12"));
695 assert!(display.contains("unexpected character"));
696 }
697}