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    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        // Add context lines before
58        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        // Add context lines after
66        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/// Comprehensive error type for YAML processing
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum Error {
85    /// Parsing errors with position information
86    Parse {
87        /// Position where error occurred
88        position: Position,
89        /// Error message
90        message: String,
91        /// Additional context for better error reporting
92        context: Option<ErrorContext>,
93    },
94
95    /// Scanning errors during tokenization
96    Scan {
97        /// Position where error occurred
98        position: Position,
99        /// Error message
100        message: String,
101        /// Additional context for better error reporting
102        context: Option<ErrorContext>,
103    },
104
105    /// Construction errors when building objects
106    Construction {
107        /// Position where error occurred
108        position: Position,
109        /// Error message
110        message: String,
111        /// Additional context for better error reporting
112        context: Option<ErrorContext>,
113    },
114
115    /// Emission errors during output generation
116    Emission {
117        /// Error message
118        message: String,
119    },
120
121    /// IO errors (simplified for clonability)
122    Io {
123        /// Error kind
124        kind: std::io::ErrorKind,
125        /// Error message
126        message: String,
127    },
128
129    /// UTF-8 encoding errors
130    Utf8 {
131        /// Error message
132        message: String,
133    },
134
135    /// Type conversion errors
136    Type {
137        /// Expected type
138        expected: String,
139        /// Found type
140        found: String,
141        /// Position where error occurred
142        position: Position,
143        /// Additional context for better error reporting
144        context: Option<ErrorContext>,
145    },
146
147    /// Value errors for invalid values
148    Value {
149        /// Position where error occurred
150        position: Position,
151        /// Error message
152        message: String,
153        /// Additional context for better error reporting
154        context: Option<ErrorContext>,
155    },
156
157    /// Configuration errors
158    Config {
159        /// Error message
160        message: String,
161    },
162
163    /// Multiple related errors
164    Multiple {
165        /// List of related errors
166        errors: Vec<Error>,
167        /// Context message
168        message: String,
169    },
170
171    /// Resource limit exceeded
172    LimitExceeded {
173        /// Error message describing which limit was exceeded
174        message: String,
175    },
176
177    /// Indentation errors
178    Indentation {
179        /// Position where error occurred
180        position: Position,
181        /// Expected indentation level
182        expected: usize,
183        /// Found indentation level
184        found: usize,
185        /// Additional context
186        context: Option<ErrorContext>,
187    },
188
189    /// Invalid character or sequence
190    InvalidCharacter {
191        /// Position where error occurred
192        position: Position,
193        /// The invalid character
194        character: char,
195        /// Context where it was found
196        context_description: String,
197        /// Additional context
198        context: Option<ErrorContext>,
199    },
200
201    /// Unclosed delimiter (quote, bracket, etc.)
202    UnclosedDelimiter {
203        /// Position where delimiter started
204        start_position: Position,
205        /// Position where EOF or mismatch was found
206        current_position: Position,
207        /// Type of delimiter
208        delimiter_type: String,
209        /// Additional context
210        context: Option<ErrorContext>,
211    },
212}
213
214impl Error {
215    /// Create a new parse error
216    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    /// Create a new parse error with context
225    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    /// Create a new scan error
238    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    /// Create a new scan error with context
247    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    /// Create a new construction error
260    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    /// Create a new construction error with context
269    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    /// Create a new emission error
282    pub fn emission(message: impl Into<String>) -> Self {
283        Self::Emission {
284            message: message.into(),
285        }
286    }
287
288    /// Create a new limit exceeded error
289    pub fn limit_exceeded(message: impl Into<String>) -> Self {
290        Self::LimitExceeded {
291            message: message.into(),
292        }
293    }
294
295    /// Create a new type error
296    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    /// Create a new type error with context
310    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    /// Create a new value error
325    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    /// Create a new value error with context
334    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    /// Create a new configuration error
347    pub fn config_error(message: impl Into<String>) -> Self {
348        Self::Config {
349            message: message.into(),
350        }
351    }
352
353    /// Legacy alias for config_error
354    pub fn config(message: impl Into<String>) -> Self {
355        Self::Config {
356            message: message.into(),
357        }
358    }
359
360    /// Create a multiple error with related errors
361    pub fn multiple(errors: Vec<Self>, message: impl Into<String>) -> Self {
362        Self::Multiple {
363            errors,
364            message: message.into(),
365        }
366    }
367
368    /// Create an indentation error
369    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    /// Create an indentation error with context
379    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    /// Create an invalid character error
394    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    /// Create an invalid character error with context
408    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    /// Create an unclosed delimiter error
423    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    /// Create an unclosed delimiter error with context
437    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    /// Get the position associated with this error, if any
452    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    /// Get the context associated with this error, if any
474    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    /// Format error with enhanced context display
518    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        // Write the main error message
526        writeln!(
527            f,
528            "Error at line {}, column {}: {}",
529            position.line, position.column, message
530        )?;
531
532        // Add context if available
533        if let Some(ctx) = context {
534            writeln!(f)?;
535
536            // Show surrounding lines for context
537            for (line_num, line_content) in &ctx.surrounding_lines {
538                writeln!(f, "{:4} | {}", line_num, line_content)?;
539            }
540
541            // Show the problematic line with pointer
542            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            // Show suggestion if available
550            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}