Skip to main content

rust_yaml/
error.rs

1//! Error types for YAML processing
2
3use crate::Position;
4use std::fmt;
5
6/// Result type alias for YAML operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Context information for error reporting
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ErrorContext {
12    /// The problematic line content
13    pub line_content: String,
14    /// Position within the line where the error occurred
15    pub column_position: usize,
16    /// Optional suggestion for fixing the error
17    pub suggestion: Option<String>,
18    /// Additional context lines (before and after)
19    pub surrounding_lines: Vec<(usize, String)>,
20}
21
22impl ErrorContext {
23    /// Create a new error context
24    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    /// Add a suggestion for fixing the error
34    pub fn with_suggestion(mut self, suggestion: String) -> Self {
35        self.suggestion = Some(suggestion);
36        self
37    }
38
39    /// Add surrounding lines for context
40    pub fn with_surrounding_lines(mut self, lines: Vec<(usize, String)>) -> Self {
41        self.surrounding_lines = lines;
42        self
43    }
44
45    /// Create error context from input text and position.
46    ///
47    /// Only the `context_lines`-line window around `position` is materialized.
48    /// The byte index in `position` locates the error line directly, so the
49    /// cost is proportional to that window — not to the size of the whole
50    /// input, which previously made every error an O(n) scan + allocation of
51    /// the entire line list (#27).
52    pub fn from_input(input: &str, position: &Position, context_lines: usize) -> Self {
53        let line_index = position.line.saturating_sub(1);
54
55        // Walk back from the error's byte offset to the start of the window's
56        // first line, scanning at most `context_lines + 1` lines.
57        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        // `lines()` over the suffix yields the window in document order and
68        // preserves the original `input.lines()` semantics exactly (CRLF
69        // handling, and no trailing empty line after a final newline).
70        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        // 1-based document line number of the window's first line.
81        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/// Comprehensive error type for YAML processing
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum Error {
101    /// Parsing errors with position information
102    Parse {
103        /// Position where error occurred
104        position: Position,
105        /// Error message
106        message: String,
107        /// Additional context for better error reporting
108        context: Option<ErrorContext>,
109    },
110
111    /// Scanning errors during tokenization
112    Scan {
113        /// Position where error occurred
114        position: Position,
115        /// Error message
116        message: String,
117        /// Additional context for better error reporting
118        context: Option<ErrorContext>,
119    },
120
121    /// Construction errors when building objects
122    Construction {
123        /// Position where error occurred
124        position: Position,
125        /// Error message
126        message: String,
127        /// Additional context for better error reporting
128        context: Option<ErrorContext>,
129    },
130
131    /// Emission errors during output generation
132    Emission {
133        /// Error message
134        message: String,
135    },
136
137    /// IO errors (simplified for clonability)
138    Io {
139        /// Error kind
140        kind: std::io::ErrorKind,
141        /// Error message
142        message: String,
143    },
144
145    /// UTF-8 encoding errors
146    Utf8 {
147        /// Error message
148        message: String,
149    },
150
151    /// Type conversion errors
152    Type {
153        /// Expected type
154        expected: String,
155        /// Found type
156        found: String,
157        /// Position where error occurred
158        position: Position,
159        /// Additional context for better error reporting
160        context: Option<ErrorContext>,
161    },
162
163    /// Value errors for invalid values
164    Value {
165        /// Position where error occurred
166        position: Position,
167        /// Error message
168        message: String,
169        /// Additional context for better error reporting
170        context: Option<ErrorContext>,
171    },
172
173    /// Configuration errors
174    Config {
175        /// Error message
176        message: String,
177    },
178
179    /// Multiple related errors
180    Multiple {
181        /// List of related errors
182        errors: Vec<Error>,
183        /// Context message
184        message: String,
185    },
186
187    /// Resource limit exceeded
188    LimitExceeded {
189        /// Error message describing which limit was exceeded
190        message: String,
191    },
192
193    /// Indentation errors
194    Indentation {
195        /// Position where error occurred
196        position: Position,
197        /// Expected indentation level
198        expected: usize,
199        /// Found indentation level
200        found: usize,
201        /// Additional context
202        context: Option<ErrorContext>,
203    },
204
205    /// Invalid character or sequence
206    InvalidCharacter {
207        /// Position where error occurred
208        position: Position,
209        /// The invalid character
210        character: char,
211        /// Context where it was found
212        context_description: String,
213        /// Additional context
214        context: Option<ErrorContext>,
215    },
216
217    /// Unclosed delimiter (quote, bracket, etc.)
218    UnclosedDelimiter {
219        /// Position where delimiter started
220        start_position: Position,
221        /// Position where EOF or mismatch was found
222        current_position: Position,
223        /// Type of delimiter
224        delimiter_type: String,
225        /// Additional context
226        context: Option<ErrorContext>,
227    },
228}
229
230impl Error {
231    /// Create a new parse error
232    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    /// Create a new parse error with context
241    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    /// Create a new scan error
254    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    /// Create a new scan error with context
263    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    /// Create a new construction error
276    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    /// Create a new construction error with context
285    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    /// Create a new emission error
298    pub fn emission(message: impl Into<String>) -> Self {
299        Self::Emission {
300            message: message.into(),
301        }
302    }
303
304    /// Create a new limit exceeded error
305    pub fn limit_exceeded(message: impl Into<String>) -> Self {
306        Self::LimitExceeded {
307            message: message.into(),
308        }
309    }
310
311    /// Create a new type error
312    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    /// Create a new type error with context
326    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    /// Create a new value error
341    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    /// Create a new value error with context
350    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    /// Create a new configuration error
363    pub fn config_error(message: impl Into<String>) -> Self {
364        Self::Config {
365            message: message.into(),
366        }
367    }
368
369    /// Legacy alias for config_error
370    pub fn config(message: impl Into<String>) -> Self {
371        Self::Config {
372            message: message.into(),
373        }
374    }
375
376    /// Create a multiple error with related errors
377    pub fn multiple(errors: Vec<Self>, message: impl Into<String>) -> Self {
378        Self::Multiple {
379            errors,
380            message: message.into(),
381        }
382    }
383
384    /// Create an indentation error
385    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    /// Create an indentation error with context
395    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    /// Create an invalid character error
410    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    /// Create an invalid character error with context
424    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    /// Create an unclosed delimiter error
439    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    /// Create an unclosed delimiter error with context
453    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    /// Get the position associated with this error, if any
468    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    /// Get the context associated with this error, if any
490    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    /// Format error with enhanced context display
534    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        // Write the main error message
542        writeln!(
543            f,
544            "Error at line {}, column {}: {}",
545            position.line, position.column, message
546        )?;
547
548        // Add context if available
549        if let Some(ctx) = context {
550            writeln!(f)?;
551
552            // Show surrounding lines for context
553            for (line_num, line_content) in &ctx.surrounding_lines {
554                writeln!(f, "{:4} | {}", line_num, line_content)?;
555            }
556
557            // Show the problematic line with pointer
558            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            // Show suggestion if available
566            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    // Characterization tests for `ErrorContext::from_input` (#27): they pin
715    // the extracted line + surrounding context so the O(n)->O(context-window)
716    // refactor stays behavior-identical.
717
718    #[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        // Position at the start of "line three" (line 3).
722        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(); // line 1, index 0
736        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        // One line past the content (line 3 of a 2-line document).
749        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        // Start of "bbb" (line 2).
759        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}