Skip to main content

yaml_edit/
error_recovery.rs

1//! Error recovery mechanisms for YAML parsing
2//!
3//! This module provides enhanced error handling with:
4//! - Line and column information for errors
5//! - Recovery strategies to continue parsing after errors
6//! - Detailed error messages with context
7
8use crate::{lex::SyntaxKind, PositionedParseError};
9use rowan::{TextRange, TextSize};
10
11/// Error recovery strategy for the parser
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RecoveryStrategy {
14    /// Skip the current token and continue
15    SkipToken,
16    /// Skip until end of line
17    SkipToEndOfLine,
18    /// Skip until a safe synchronization point
19    SyncToSafePoint,
20    /// Insert a synthetic token to fix the parse
21    InsertToken(SyntaxKind),
22}
23
24/// Context for error recovery during parsing
25#[derive(Clone)]
26pub struct ErrorRecoveryContext {
27    /// The original text being parsed
28    text: String,
29    /// Current position in the text
30    position: usize,
31    /// Line number (1-based)
32    line: usize,
33    /// Column number (1-based)
34    column: usize,
35    /// Stack of recovery points for nested structures
36    recovery_stack: Vec<RecoveryPoint>,
37}
38
39/// A recovery point in the parse
40#[derive(Debug, Clone)]
41pub struct RecoveryPoint {
42    /// The context we're in
43    pub context: ParseContext,
44    /// Position where this context started
45    pub start_position: usize,
46    /// Line where this context started
47    pub start_line: usize,
48    /// Column where this context started
49    pub start_column: usize,
50}
51
52/// The parsing context for error recovery
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ParseContext {
55    /// Top level document
56    Document,
57    /// Inside a mapping
58    Mapping,
59    /// Inside a sequence
60    Sequence,
61    /// Inside a flow sequence
62    FlowSequence,
63    /// Inside a flow mapping
64    FlowMapping,
65    /// Inside a block scalar
66    BlockScalar,
67    /// Inside a quoted string
68    QuotedString,
69}
70
71impl ErrorRecoveryContext {
72    /// Create a new error recovery context
73    pub fn new(text: String) -> Self {
74        Self {
75            text,
76            position: 0,
77            line: 1,
78            column: 1,
79            recovery_stack: vec![RecoveryPoint {
80                context: ParseContext::Document,
81                start_position: 0,
82                start_line: 1,
83                start_column: 1,
84            }],
85        }
86    }
87
88    /// Update position and line/column tracking
89    pub fn advance(&mut self, bytes: usize) {
90        let end = (self.position + bytes).min(self.text.len());
91        let advanced_text = &self.text[self.position..end];
92
93        for ch in advanced_text.chars() {
94            if ch == '\n' {
95                self.line += 1;
96                self.column = 1;
97            } else {
98                self.column += 1;
99            }
100        }
101
102        self.position = end;
103    }
104
105    /// Get current line and column
106    pub fn current_location(&self) -> (usize, usize) {
107        (self.line, self.column)
108    }
109
110    /// Get a text range for the current position
111    pub fn current_range(&self, length: usize) -> TextRange {
112        let start = TextSize::from(self.position as u32);
113        let end = TextSize::from((self.position + length) as u32);
114        TextRange::new(start, end)
115    }
116
117    /// Push a new parsing context
118    pub fn push_context(&mut self, context: ParseContext) {
119        self.recovery_stack.push(RecoveryPoint {
120            context,
121            start_position: self.position,
122            start_line: self.line,
123            start_column: self.column,
124        });
125    }
126
127    /// Pop the current parsing context
128    pub fn pop_context(&mut self) {
129        if self.recovery_stack.len() > 1 {
130            self.recovery_stack.pop();
131        }
132    }
133
134    /// Get the current parsing context
135    pub fn current_context(&self) -> ParseContext {
136        self.recovery_stack
137            .last()
138            .map(|r| r.context)
139            .unwrap_or(ParseContext::Document)
140    }
141
142    /// Create a positioned error with current location
143    pub fn create_error(
144        &self,
145        message: String,
146        length: usize,
147        kind: crate::ParseErrorKind,
148    ) -> PositionedParseError {
149        let (line, column) = self.current_location();
150        let range = self.current_range(length);
151
152        PositionedParseError {
153            message: format!("{}:{}: {}", line, column, message),
154            range: range.into(),
155            code: None,
156            kind,
157        }
158    }
159
160    /// Determine the best recovery strategy for the current error
161    pub fn suggest_recovery(
162        &self,
163        expected: SyntaxKind,
164        found: Option<SyntaxKind>,
165    ) -> RecoveryStrategy {
166        match self.current_context() {
167            ParseContext::FlowSequence => {
168                // In flow sequence, recover to comma or closing bracket
169                match expected {
170                    SyntaxKind::RIGHT_BRACKET => {
171                        // Missing closing bracket - insert it synthetically
172                        RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACKET)
173                    }
174                    _ => match found {
175                        Some(SyntaxKind::COMMA) | Some(SyntaxKind::RIGHT_BRACKET) => {
176                            RecoveryStrategy::SkipToken
177                        }
178                        _ => {
179                            // Don't use SkipUntil for brackets that might not exist
180                            // Skip to next safe point instead
181                            RecoveryStrategy::SkipToEndOfLine
182                        }
183                    },
184                }
185            }
186            ParseContext::FlowMapping => {
187                // In flow mapping, recover to comma or closing brace
188                match expected {
189                    SyntaxKind::RIGHT_BRACE => {
190                        // Missing closing brace - insert it synthetically
191                        RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACE)
192                    }
193                    _ => match found {
194                        Some(SyntaxKind::COMMA) | Some(SyntaxKind::RIGHT_BRACE) => {
195                            RecoveryStrategy::SkipToken
196                        }
197                        _ => {
198                            // Don't use SkipUntil for braces that might not exist
199                            // Skip to next safe point instead
200                            RecoveryStrategy::SkipToEndOfLine
201                        }
202                    },
203                }
204            }
205            ParseContext::Mapping => {
206                // In mapping, skip to next key or end of mapping
207                match expected {
208                    SyntaxKind::COLON => {
209                        // Missing colon after key, try to insert it
210                        RecoveryStrategy::InsertToken(SyntaxKind::COLON)
211                    }
212                    _ => RecoveryStrategy::SkipToEndOfLine,
213                }
214            }
215            ParseContext::Sequence => {
216                // In sequence, skip to next item
217                RecoveryStrategy::SkipToEndOfLine
218            }
219            ParseContext::QuotedString => {
220                // In quoted string, look for closing quote
221                match expected {
222                    SyntaxKind::QUOTE | SyntaxKind::SINGLE_QUOTE => {
223                        RecoveryStrategy::InsertToken(expected)
224                    }
225                    _ => RecoveryStrategy::SkipToken,
226                }
227            }
228            ParseContext::BlockScalar => {
229                // In block scalar, sync to dedent
230                RecoveryStrategy::SyncToSafePoint
231            }
232            ParseContext::Document => {
233                // At document level, skip to next document marker or directive
234                RecoveryStrategy::SyncToSafePoint
235            }
236        }
237    }
238
239    /// Find the next safe synchronization point
240    pub fn find_sync_point(&self, tokens: &[(SyntaxKind, String)], current: usize) -> usize {
241        let sync_tokens = match self.current_context() {
242            ParseContext::Document => vec![
243                SyntaxKind::DOC_START,
244                SyntaxKind::DOC_END,
245                SyntaxKind::DIRECTIVE,
246            ],
247            ParseContext::Mapping | ParseContext::Sequence => {
248                vec![SyntaxKind::DASH, SyntaxKind::NEWLINE]
249            }
250            ParseContext::FlowSequence => vec![SyntaxKind::RIGHT_BRACKET, SyntaxKind::COMMA],
251            ParseContext::FlowMapping => vec![SyntaxKind::RIGHT_BRACE, SyntaxKind::COMMA],
252            _ => vec![SyntaxKind::NEWLINE],
253        };
254
255        for (i, (kind, _)) in tokens[current..].iter().enumerate() {
256            if sync_tokens.contains(kind) {
257                return current + i;
258            }
259        }
260
261        tokens.len()
262    }
263
264    /// Get context information for error messages
265    pub fn get_context_snippet(&self, range: TextRange) -> String {
266        let start = range.start().into();
267        let end = range.end().into();
268
269        // Find line boundaries
270        let line_start = self.text[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
271
272        let line_end = self.text[end..]
273            .find('\n')
274            .map(|i| end + i)
275            .unwrap_or(self.text.len());
276
277        let line = &self.text[line_start..line_end];
278        let error_start = start - line_start;
279        let error_len = (end - start).min(line_end - start);
280
281        // Create error indicator
282        let mut indicator = String::new();
283        for _ in 0..error_start {
284            indicator.push(' ');
285        }
286        for _ in 0..error_len.max(1) {
287            indicator.push('^');
288        }
289
290        format!("{}\n{}", line, indicator)
291    }
292}
293
294/// Enhanced error builder for creating detailed error messages
295pub struct ErrorBuilder {
296    message: String,
297    expected: Vec<String>,
298    found: Option<String>,
299    context: Option<String>,
300    suggestion: Option<String>,
301}
302
303impl ErrorBuilder {
304    /// Create a new error builder
305    pub fn new(message: impl Into<String>) -> Self {
306        Self {
307            message: message.into(),
308            expected: Vec::new(),
309            found: None,
310            context: None,
311            suggestion: None,
312        }
313    }
314
315    /// Add what was expected
316    pub fn expected(mut self, expected: impl Into<String>) -> Self {
317        self.expected.push(expected.into());
318        self
319    }
320
321    /// Add what was found instead
322    pub fn found(mut self, found: impl Into<String>) -> Self {
323        self.found = Some(found.into());
324        self
325    }
326
327    /// Add context information
328    pub fn context(mut self, context: impl Into<String>) -> Self {
329        self.context = Some(context.into());
330        self
331    }
332
333    /// Add a suggestion for fixing the error
334    pub fn suggestion(mut self, suggestion: impl Into<String>) -> Self {
335        self.suggestion = Some(suggestion.into());
336        self
337    }
338
339    /// Build the final error message
340    pub fn build(self) -> String {
341        let mut parts = vec![self.message];
342
343        if !self.expected.is_empty() {
344            parts.push(format!("Expected: {}", self.expected.join(" or ")));
345        }
346
347        if let Some(found) = self.found {
348            parts.push(format!("Found: {}", found));
349        }
350
351        if let Some(context) = self.context {
352            parts.push(format!("Context: {}", context));
353        }
354
355        if let Some(suggestion) = self.suggestion {
356            parts.push(format!("Suggestion: {}", suggestion));
357        }
358
359        parts.join(". ")
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_error_recovery_context() {
369        let mut ctx = ErrorRecoveryContext::new("foo: bar\nbaz: qux".to_string());
370
371        assert_eq!(ctx.current_location(), (1, 1));
372
373        ctx.advance(4); // "foo:"
374        assert_eq!(ctx.current_location(), (1, 5));
375
376        ctx.advance(5); // " bar\n"
377        assert_eq!(ctx.current_location(), (2, 1));
378    }
379
380    #[test]
381    fn test_error_builder() {
382        let error = ErrorBuilder::new("Syntax error")
383            .expected("colon")
384            .found("newline")
385            .context("in mapping")
386            .suggestion("add ':' after key")
387            .build();
388
389        assert_eq!(
390            error,
391            "Syntax error. Expected: colon. Found: newline. Context: in mapping. Suggestion: add ':' after key"
392        );
393    }
394
395    #[test]
396    fn test_context_snippet() {
397        let ctx = ErrorRecoveryContext::new("foo: bar\nbaz qux\nend".to_string());
398        let range = TextRange::new(TextSize::from(13), TextSize::from(16)); // "qux"
399
400        let snippet = ctx.get_context_snippet(range);
401        assert_eq!(snippet, "baz qux\n    ^^^");
402    }
403
404    #[test]
405    fn test_recovery_strategy() {
406        let ctx = ErrorRecoveryContext::new("test".to_string());
407
408        // Test flow sequence recovery
409        let mut ctx_flow = ctx;
410        ctx_flow.push_context(ParseContext::FlowSequence);
411        let strategy = ctx_flow.suggest_recovery(SyntaxKind::COMMA, Some(SyntaxKind::COLON));
412        assert_eq!(strategy, RecoveryStrategy::SkipToEndOfLine);
413
414        // Test mapping colon recovery
415        let mut ctx_map = ErrorRecoveryContext::new("test".to_string());
416        ctx_map.push_context(ParseContext::Mapping);
417        let strategy = ctx_map.suggest_recovery(SyntaxKind::COLON, Some(SyntaxKind::NEWLINE));
418        assert_eq!(strategy, RecoveryStrategy::InsertToken(SyntaxKind::COLON));
419    }
420}