1use crate::{lex::SyntaxKind, PositionedParseError};
9use rowan::{TextRange, TextSize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RecoveryStrategy {
14 SkipToken,
16 SkipToEndOfLine,
18 SyncToSafePoint,
20 InsertToken(SyntaxKind),
22}
23
24#[derive(Clone)]
26pub struct ErrorRecoveryContext {
27 text: String,
29 position: usize,
31 line: usize,
33 column: usize,
35 recovery_stack: Vec<RecoveryPoint>,
37}
38
39#[derive(Debug, Clone)]
41pub struct RecoveryPoint {
42 pub context: ParseContext,
44 pub start_position: usize,
46 pub start_line: usize,
48 pub start_column: usize,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ParseContext {
55 Document,
57 Mapping,
59 Sequence,
61 FlowSequence,
63 FlowMapping,
65 BlockScalar,
67 QuotedString,
69}
70
71impl ErrorRecoveryContext {
72 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 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 pub fn current_location(&self) -> (usize, usize) {
107 (self.line, self.column)
108 }
109
110 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 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 pub fn pop_context(&mut self) {
129 if self.recovery_stack.len() > 1 {
130 self.recovery_stack.pop();
131 }
132 }
133
134 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 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 pub fn suggest_recovery(
162 &self,
163 expected: SyntaxKind,
164 found: Option<SyntaxKind>,
165 ) -> RecoveryStrategy {
166 match self.current_context() {
167 ParseContext::FlowSequence => {
168 match expected {
170 SyntaxKind::RIGHT_BRACKET => {
171 RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACKET)
173 }
174 _ => match found {
175 Some(SyntaxKind::COMMA) | Some(SyntaxKind::RIGHT_BRACKET) => {
176 RecoveryStrategy::SkipToken
177 }
178 _ => {
179 RecoveryStrategy::SkipToEndOfLine
182 }
183 },
184 }
185 }
186 ParseContext::FlowMapping => {
187 match expected {
189 SyntaxKind::RIGHT_BRACE => {
190 RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACE)
192 }
193 _ => match found {
194 Some(SyntaxKind::COMMA) | Some(SyntaxKind::RIGHT_BRACE) => {
195 RecoveryStrategy::SkipToken
196 }
197 _ => {
198 RecoveryStrategy::SkipToEndOfLine
201 }
202 },
203 }
204 }
205 ParseContext::Mapping => {
206 match expected {
208 SyntaxKind::COLON => {
209 RecoveryStrategy::InsertToken(SyntaxKind::COLON)
211 }
212 _ => RecoveryStrategy::SkipToEndOfLine,
213 }
214 }
215 ParseContext::Sequence => {
216 RecoveryStrategy::SkipToEndOfLine
218 }
219 ParseContext::QuotedString => {
220 match expected {
222 SyntaxKind::QUOTE | SyntaxKind::SINGLE_QUOTE => {
223 RecoveryStrategy::InsertToken(expected)
224 }
225 _ => RecoveryStrategy::SkipToken,
226 }
227 }
228 ParseContext::BlockScalar => {
229 RecoveryStrategy::SyncToSafePoint
231 }
232 ParseContext::Document => {
233 RecoveryStrategy::SyncToSafePoint
235 }
236 }
237 }
238
239 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 pub fn get_context_snippet(&self, range: TextRange) -> String {
266 let start = range.start().into();
267 let end = range.end().into();
268
269 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 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
294pub 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 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 pub fn expected(mut self, expected: impl Into<String>) -> Self {
317 self.expected.push(expected.into());
318 self
319 }
320
321 pub fn found(mut self, found: impl Into<String>) -> Self {
323 self.found = Some(found.into());
324 self
325 }
326
327 pub fn context(mut self, context: impl Into<String>) -> Self {
329 self.context = Some(context.into());
330 self
331 }
332
333 pub fn suggestion(mut self, suggestion: impl Into<String>) -> Self {
335 self.suggestion = Some(suggestion.into());
336 self
337 }
338
339 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); assert_eq!(ctx.current_location(), (1, 5));
375
376 ctx.advance(5); 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)); 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 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 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}