use crate::line_index::LineIndex;
use crate::{lex::SyntaxKind, PositionedParseError};
use rowan::{TextRange, TextSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryStrategy {
SkipToken,
SkipToEndOfLine,
SyncToSafePoint,
InsertToken(SyntaxKind),
}
#[derive(Clone)]
pub struct ErrorRecoveryContext {
index: LineIndex<String>,
position: usize,
recovery_stack: Vec<RecoveryPoint>,
}
#[derive(Debug, Clone)]
pub struct RecoveryPoint {
pub context: ParseContext,
pub start_position: usize,
pub start_line: usize,
pub start_column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseContext {
Document,
Mapping,
Sequence,
FlowSequence,
FlowMapping,
BlockScalar,
QuotedString,
}
impl ErrorRecoveryContext {
pub fn new(text: String) -> Self {
Self {
index: LineIndex::new(text),
position: 0,
recovery_stack: vec![RecoveryPoint {
context: ParseContext::Document,
start_position: 0,
start_line: 1,
start_column: 1,
}],
}
}
pub fn advance(&mut self, bytes: usize) {
self.position = (self.position + bytes).min(self.index.text().len());
}
pub fn current_location(&self) -> (usize, usize) {
let lc = self.index.line_column(self.position);
(lc.line, lc.column)
}
pub fn current_range(&self, length: usize) -> TextRange {
let start = TextSize::from(self.position as u32);
let end = TextSize::from((self.position + length) as u32);
TextRange::new(start, end)
}
pub fn push_context(&mut self, context: ParseContext) {
let (start_line, start_column) = self.current_location();
self.recovery_stack.push(RecoveryPoint {
context,
start_position: self.position,
start_line,
start_column,
});
}
pub fn pop_context(&mut self) {
if self.recovery_stack.len() > 1 {
self.recovery_stack.pop();
}
}
pub fn current_context(&self) -> ParseContext {
self.recovery_stack
.last()
.map_or(ParseContext::Document, |r| r.context)
}
pub fn create_error(
&self,
message: String,
length: usize,
kind: crate::ParseErrorKind,
) -> PositionedParseError {
let range = self.current_range(length);
PositionedParseError {
message,
range: range.into(),
code: None,
kind,
}
}
pub fn suggest_recovery(
&self,
expected: SyntaxKind,
found: Option<SyntaxKind>,
) -> RecoveryStrategy {
match self.current_context() {
ParseContext::FlowSequence => {
match expected {
SyntaxKind::RIGHT_BRACKET => {
RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACKET)
}
_ => match found {
Some(SyntaxKind::COMMA | SyntaxKind::RIGHT_BRACKET) => {
RecoveryStrategy::SkipToken
}
_ => {
RecoveryStrategy::SkipToEndOfLine
}
},
}
}
ParseContext::FlowMapping => {
match expected {
SyntaxKind::RIGHT_BRACE => {
RecoveryStrategy::InsertToken(SyntaxKind::RIGHT_BRACE)
}
_ => match found {
Some(SyntaxKind::COMMA | SyntaxKind::RIGHT_BRACE) => {
RecoveryStrategy::SkipToken
}
_ => {
RecoveryStrategy::SkipToEndOfLine
}
},
}
}
ParseContext::Mapping => {
match expected {
SyntaxKind::COLON => {
RecoveryStrategy::InsertToken(SyntaxKind::COLON)
}
_ => RecoveryStrategy::SkipToEndOfLine,
}
}
ParseContext::Sequence => {
RecoveryStrategy::SkipToEndOfLine
}
ParseContext::QuotedString => {
match expected {
SyntaxKind::QUOTE | SyntaxKind::SINGLE_QUOTE => {
RecoveryStrategy::InsertToken(expected)
}
_ => RecoveryStrategy::SkipToken,
}
}
ParseContext::BlockScalar => {
RecoveryStrategy::SyncToSafePoint
}
ParseContext::Document => {
RecoveryStrategy::SyncToSafePoint
}
}
}
pub(crate) fn is_sync_point(&self, kind: SyntaxKind) -> bool {
match self.current_context() {
ParseContext::Document => matches!(
kind,
SyntaxKind::DOC_START | SyntaxKind::DOC_END | SyntaxKind::DIRECTIVE
),
ParseContext::Mapping | ParseContext::Sequence => {
matches!(kind, SyntaxKind::DASH | SyntaxKind::NEWLINE)
}
ParseContext::FlowSequence => {
matches!(kind, SyntaxKind::RIGHT_BRACKET | SyntaxKind::COMMA)
}
ParseContext::FlowMapping => {
matches!(kind, SyntaxKind::RIGHT_BRACE | SyntaxKind::COMMA)
}
_ => kind == SyntaxKind::NEWLINE,
}
}
pub fn find_sync_point(&self, tokens: &[(SyntaxKind, String)], current: usize) -> usize {
tokens[current..]
.iter()
.position(|(kind, _)| self.is_sync_point(*kind))
.map_or(tokens.len(), |i| current + i)
}
pub fn get_context_snippet(&self, range: TextRange) -> String {
let start = range.start().into();
let end = range.end().into();
let text = self.index.text();
let line_start = self.index.line_start(start);
let line_end = text[end..]
.find(['\n', '\r'])
.map_or(text.len(), |i| end + i);
let line = &text[line_start..line_end];
let error_start = self.index.column(start);
let error_len = text[start..end.min(line_end)].chars().count();
let indicator = format!(
"{}{}",
" ".repeat(error_start),
"^".repeat(error_len.max(1))
);
format!("{line}\n{indicator}")
}
}
pub struct ErrorBuilder {
message: String,
expected: Vec<String>,
found: Option<String>,
context: Option<String>,
suggestion: Option<String>,
}
impl ErrorBuilder {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
expected: Vec::new(),
found: None,
context: None,
suggestion: None,
}
}
pub fn expected(mut self, expected: impl Into<String>) -> Self {
self.expected.push(expected.into());
self
}
pub fn found(mut self, found: impl Into<String>) -> Self {
self.found = Some(found.into());
self
}
pub fn context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
pub fn suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn build(self) -> String {
let mut parts = vec![self.message];
if !self.expected.is_empty() {
parts.push(format!("Expected: {}", self.expected.join(" or ")));
}
if let Some(found) = self.found {
parts.push(format!("Found: {found}"));
}
if let Some(context) = self.context {
parts.push(format!("Context: {context}"));
}
if let Some(suggestion) = self.suggestion {
parts.push(format!("Suggestion: {suggestion}"));
}
parts.join(". ")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_recovery_context() {
let mut ctx = ErrorRecoveryContext::new("foo: bar\nbaz: qux".to_string());
assert_eq!(ctx.current_location(), (1, 1));
ctx.advance(4); assert_eq!(ctx.current_location(), (1, 5));
ctx.advance(5); assert_eq!(ctx.current_location(), (2, 1));
}
#[test]
fn test_error_builder() {
let error = ErrorBuilder::new("Syntax error")
.expected("colon")
.found("newline")
.context("in mapping")
.suggestion("add ':' after key")
.build();
assert_eq!(
error,
"Syntax error. Expected: colon. Found: newline. Context: in mapping. Suggestion: add ':' after key"
);
}
#[test]
fn test_context_snippet() {
let ctx = ErrorRecoveryContext::new("foo: bar\nbaz qux\nend".to_string());
let range = TextRange::new(TextSize::from(13), TextSize::from(16));
let snippet = ctx.get_context_snippet(range);
assert_eq!(snippet, "baz qux\n ^^^");
}
#[test]
fn test_context_snippet_cr_line_breaks() {
let ctx = ErrorRecoveryContext::new("foo: bar\rbaz qux\r\nend".to_string());
let range = TextRange::new(TextSize::from(13), TextSize::from(16));
let snippet = ctx.get_context_snippet(range);
assert_eq!(snippet, "baz qux\n ^^^");
}
#[test]
fn test_context_snippet_counts_chars() {
let ctx = ErrorRecoveryContext::new("k: \u{e9}\u{e9} bad\n".to_string());
let snippets = [(3, 7), (8, 11)].map(|(start, end)| {
ctx.get_context_snippet(TextRange::new(TextSize::from(start), TextSize::from(end)))
});
assert_eq!(
snippets,
[
"k: \u{e9}\u{e9} bad\n ^^",
"k: \u{e9}\u{e9} bad\n ^^^"
]
);
}
#[test]
fn test_current_location_line_breaks_and_bom() {
let mut ctx = ErrorRecoveryContext::new("\u{FEFF}a\rb\r\nc".to_string());
ctx.advance(3);
let mut locations = vec![ctx.current_location()];
for _ in 0..6 {
ctx.advance(1);
locations.push(ctx.current_location());
}
assert_eq!(
locations,
vec![(1, 1), (1, 2), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2)]
);
}
#[test]
fn test_recovery_strategy() {
let ctx = ErrorRecoveryContext::new("test".to_string());
let mut ctx_flow = ctx;
ctx_flow.push_context(ParseContext::FlowSequence);
let strategy = ctx_flow.suggest_recovery(SyntaxKind::COMMA, Some(SyntaxKind::COLON));
assert_eq!(strategy, RecoveryStrategy::SkipToEndOfLine);
let mut ctx_map = ErrorRecoveryContext::new("test".to_string());
ctx_map.push_context(ParseContext::Mapping);
let strategy = ctx_map.suggest_recovery(SyntaxKind::COLON, Some(SyntaxKind::NEWLINE));
assert_eq!(strategy, RecoveryStrategy::InsertToken(SyntaxKind::COLON));
}
}