use inillucent_base::{DbError, PrimaryCode};
use crate::lexer::{LexError, LexErrorKind, Span};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParseErrorKind {
Lex(LexErrorKind),
Unexpected {
found: String,
expected: Vec<&'static str>,
},
UnexpectedEnd {
expected: Vec<&'static str>,
},
Unsupported(&'static str),
Refused(String),
RefusedNotBuilt {
said: String,
feature: &'static str,
},
LimitExceeded(&'static str),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseError {
pub kind: ParseErrorKind,
pub span: Span,
}
impl ParseError {
pub fn new(kind: ParseErrorKind, span: Span) -> ParseError {
ParseError { kind, span }
}
pub fn offset(&self) -> u32 {
self.span.start
}
pub fn message(&self) -> String {
match &self.kind {
ParseErrorKind::Lex(kind) => kind.message().to_string(),
ParseErrorKind::Unexpected { found, expected } => {
let _ = expected;
format!(r#"near "{found}": syntax error"#)
}
ParseErrorKind::UnexpectedEnd { expected } => {
if expected.is_empty() {
"incomplete input".to_string()
} else {
format!("incomplete input, expected {}", join_expected(expected))
}
}
ParseErrorKind::Unsupported(what) => format!("unsupported: {what}"),
ParseErrorKind::Refused(message) => message.clone(),
ParseErrorKind::RefusedNotBuilt { said, .. } => said.clone(),
ParseErrorKind::LimitExceeded(
what @ ("too many terms in compound SELECT"
| "too many columns in result set"
| "string or blob too big"),
) => (*what).to_string(),
ParseErrorKind::LimitExceeded(what) => format!("{what} exceeded"),
}
}
pub fn code(&self) -> PrimaryCode {
match &self.kind {
ParseErrorKind::LimitExceeded("string or blob too big") => PrimaryCode::TooBig,
_ => PrimaryCode::Error,
}
}
}
impl From<LexError> for ParseError {
fn from(error: LexError) -> ParseError {
ParseError {
kind: ParseErrorKind::Lex(error.kind),
span: Span::at(error.offset as usize),
}
}
}
pub fn lex_failure(source: &[u8], error: LexError) -> ParseError {
if error.kind == LexErrorKind::UnterminatedComment {
return ParseError::from(error);
}
let text = crate::lexer::illegal_token_text(source, error);
ParseError::new(
ParseErrorKind::Refused(format!(
"unrecognized token: \"{}\"",
String::from_utf8_lossy(text)
)),
Span::at(error.offset as usize),
)
}
impl From<ParseError> for DbError {
fn from(error: ParseError) -> DbError {
DbError::primary(error.code())
.with_message(error.message())
.with_sql_offset(error.offset())
}
}
fn join_expected(expected: &[&'static str]) -> String {
match expected {
[] => String::new(),
[only] => (*only).to_string(),
[first, second] => format!("{first} or {second}"),
_ => {
let head: Vec<&str> = expected
.get(..expected.len().saturating_sub(1))
.unwrap_or(&[])
.to_vec();
let tail = expected.last().copied().unwrap_or("");
format!("{}, or {tail}", head.join(", "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_offset_reaches_the_engine_error() {
let error = ParseError::new(
ParseErrorKind::Unexpected {
found: "FROM".to_string(),
expected: vec!["an expression"],
},
Span::new(7, 11),
);
let db: DbError = error.into();
assert_eq!(db.sql_offset(), Some(7));
assert_eq!(db.code(), PrimaryCode::Error);
}
#[test]
fn the_expected_set_reads_as_a_sentence() {
assert_eq!(join_expected(&["a"]), "a");
assert_eq!(join_expected(&["a", "b"]), "a or b");
assert_eq!(join_expected(&["a", "b", "c"]), "a, b, or c");
}
#[test]
fn a_lex_failure_keeps_its_offset() {
let error: ParseError = LexError {
kind: LexErrorKind::UnterminatedQuote,
offset: 12,
}
.into();
assert_eq!(error.offset(), 12);
}
}