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 let mut chars = advanced_text.chars().peekable();
97 while let Some(ch) = chars.next() {
98 match ch {
99 '\r' => {
100 if chars.peek() == Some(&'\n') {
102 chars.next();
103 }
104 self.line += 1;
105 self.column = 1;
106 }
107 '\n' => {
108 self.line += 1;
109 self.column = 1;
110 }
111 _ => self.column += 1,
112 }
113 }
114
115 self.position = end;
116 }
117
118 pub fn current_location(&self) -> (usize, usize) {
120 (self.line, self.column)
121 }
122
123 pub fn current_range(&self, length: usize) -> TextRange {
125 let start = TextSize::from(self.position as u32);
126 let end = TextSize::from((self.position + length) as u32);
127 TextRange::new(start, end)
128 }
129
130 pub fn push_context(&mut self, context: ParseContext) {
132 self.recovery_stack.push(RecoveryPoint {
133 context,
134 start_position: self.position,
135 start_line: self.line,
136 start_column: self.column,
137 });
138 }
139
140 pub fn pop_context(&mut self) {
142 if self.recovery_stack.len() > 1 {
143 self.recovery_stack.pop();
144 }
145 }
146
147 pub fn current_context(&self) -> ParseContext {
149 self.recovery_stack
150 .last()
151 .map_or(ParseContext::Document, |r| r.context)
152 }
153
154 pub fn create_error(
156 &self,
157 message: String,
158 length: usize,
159 kind: crate::ParseErrorKind,
160 ) -> PositionedParseError {
161 let (line, column) = self.current_location();
162 let range = self.current_range(length);
163
164 PositionedParseError {
165 message: format!("{line}:{column}: {message}"),
166 range: range.into(),
167 code: None,
168 kind,
169 }
170 }
171
172 pub fn suggest_recovery(
174 &self,
175 expected: SyntaxKind,
176 found: Option<SyntaxKind>,
177 ) -> RecoveryStrategy {
178 match self.current_context() {
179 ParseContext::FlowSequence => {
180 match expected {
182 SyntaxKind::RIGHT_BRACKET => {
183 RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACKET)
185 }
186 _ => match found {
187 Some(SyntaxKind::COMMA | SyntaxKind::RIGHT_BRACKET) => {
188 RecoveryStrategy::SkipToken
189 }
190 _ => {
191 RecoveryStrategy::SkipToEndOfLine
194 }
195 },
196 }
197 }
198 ParseContext::FlowMapping => {
199 match expected {
201 SyntaxKind::RIGHT_BRACE => {
202 RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACE)
204 }
205 _ => match found {
206 Some(SyntaxKind::COMMA | SyntaxKind::RIGHT_BRACE) => {
207 RecoveryStrategy::SkipToken
208 }
209 _ => {
210 RecoveryStrategy::SkipToEndOfLine
213 }
214 },
215 }
216 }
217 ParseContext::Mapping => {
218 match expected {
220 SyntaxKind::COLON => {
221 RecoveryStrategy::InsertToken(SyntaxKind::COLON)
223 }
224 _ => RecoveryStrategy::SkipToEndOfLine,
225 }
226 }
227 ParseContext::Sequence => {
228 RecoveryStrategy::SkipToEndOfLine
230 }
231 ParseContext::QuotedString => {
232 match expected {
234 SyntaxKind::QUOTE | SyntaxKind::SINGLE_QUOTE => {
235 RecoveryStrategy::InsertToken(expected)
236 }
237 _ => RecoveryStrategy::SkipToken,
238 }
239 }
240 ParseContext::BlockScalar => {
241 RecoveryStrategy::SyncToSafePoint
243 }
244 ParseContext::Document => {
245 RecoveryStrategy::SyncToSafePoint
247 }
248 }
249 }
250
251 pub fn find_sync_point(&self, tokens: &[(SyntaxKind, String)], current: usize) -> usize {
253 let sync_tokens = match self.current_context() {
254 ParseContext::Document => vec![
255 SyntaxKind::DOC_START,
256 SyntaxKind::DOC_END,
257 SyntaxKind::DIRECTIVE,
258 ],
259 ParseContext::Mapping | ParseContext::Sequence => {
260 vec![SyntaxKind::DASH, SyntaxKind::NEWLINE]
261 }
262 ParseContext::FlowSequence => vec![SyntaxKind::RIGHT_BRACKET, SyntaxKind::COMMA],
263 ParseContext::FlowMapping => vec![SyntaxKind::RIGHT_BRACE, SyntaxKind::COMMA],
264 _ => vec![SyntaxKind::NEWLINE],
265 };
266
267 for (i, (kind, _)) in tokens[current..].iter().enumerate() {
268 if sync_tokens.contains(kind) {
269 return current + i;
270 }
271 }
272
273 tokens.len()
274 }
275
276 pub fn get_context_snippet(&self, range: TextRange) -> String {
278 let start = range.start().into();
279 let end = range.end().into();
280
281 let line_start = self.text[..start].rfind('\n').map_or(0, |i| i + 1);
283
284 let line_end = self.text[end..]
285 .find('\n')
286 .map_or(self.text.len(), |i| end + i);
287
288 let line = &self.text[line_start..line_end];
289 let error_start = start - line_start;
290 let error_len = (end - start).min(line_end - start);
291
292 let mut indicator = String::new();
294 for _ in 0..error_start {
295 indicator.push(' ');
296 }
297 for _ in 0..error_len.max(1) {
298 indicator.push('^');
299 }
300
301 format!("{line}\n{indicator}")
302 }
303}
304
305pub struct ErrorBuilder {
307 message: String,
308 expected: Vec<String>,
309 found: Option<String>,
310 context: Option<String>,
311 suggestion: Option<String>,
312}
313
314impl ErrorBuilder {
315 pub fn new(message: impl Into<String>) -> Self {
317 Self {
318 message: message.into(),
319 expected: Vec::new(),
320 found: None,
321 context: None,
322 suggestion: None,
323 }
324 }
325
326 pub fn expected(mut self, expected: impl Into<String>) -> Self {
328 self.expected.push(expected.into());
329 self
330 }
331
332 pub fn found(mut self, found: impl Into<String>) -> Self {
334 self.found = Some(found.into());
335 self
336 }
337
338 pub fn context(mut self, context: impl Into<String>) -> Self {
340 self.context = Some(context.into());
341 self
342 }
343
344 pub fn suggestion(mut self, suggestion: impl Into<String>) -> Self {
346 self.suggestion = Some(suggestion.into());
347 self
348 }
349
350 pub fn build(self) -> String {
352 let mut parts = vec![self.message];
353
354 if !self.expected.is_empty() {
355 parts.push(format!("Expected: {}", self.expected.join(" or ")));
356 }
357
358 if let Some(found) = self.found {
359 parts.push(format!("Found: {found}"));
360 }
361
362 if let Some(context) = self.context {
363 parts.push(format!("Context: {context}"));
364 }
365
366 if let Some(suggestion) = self.suggestion {
367 parts.push(format!("Suggestion: {suggestion}"));
368 }
369
370 parts.join(". ")
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_error_recovery_context() {
380 let mut ctx = ErrorRecoveryContext::new("foo: bar\nbaz: qux".to_string());
381
382 assert_eq!(ctx.current_location(), (1, 1));
383
384 ctx.advance(4); assert_eq!(ctx.current_location(), (1, 5));
386
387 ctx.advance(5); assert_eq!(ctx.current_location(), (2, 1));
389 }
390
391 #[test]
392 fn test_error_builder() {
393 let error = ErrorBuilder::new("Syntax error")
394 .expected("colon")
395 .found("newline")
396 .context("in mapping")
397 .suggestion("add ':' after key")
398 .build();
399
400 assert_eq!(
401 error,
402 "Syntax error. Expected: colon. Found: newline. Context: in mapping. Suggestion: add ':' after key"
403 );
404 }
405
406 #[test]
407 fn test_context_snippet() {
408 let ctx = ErrorRecoveryContext::new("foo: bar\nbaz qux\nend".to_string());
409 let range = TextRange::new(TextSize::from(13), TextSize::from(16)); let snippet = ctx.get_context_snippet(range);
412 assert_eq!(snippet, "baz qux\n ^^^");
413 }
414
415 #[test]
416 fn test_recovery_strategy() {
417 let ctx = ErrorRecoveryContext::new("test".to_string());
418
419 let mut ctx_flow = ctx;
421 ctx_flow.push_context(ParseContext::FlowSequence);
422 let strategy = ctx_flow.suggest_recovery(SyntaxKind::COMMA, Some(SyntaxKind::COLON));
423 assert_eq!(strategy, RecoveryStrategy::SkipToEndOfLine);
424
425 let mut ctx_map = ErrorRecoveryContext::new("test".to_string());
427 ctx_map.push_context(ParseContext::Mapping);
428 let strategy = ctx_map.suggest_recovery(SyntaxKind::COLON, Some(SyntaxKind::NEWLINE));
429 assert_eq!(strategy, RecoveryStrategy::InsertToken(SyntaxKind::COLON));
430 }
431}