use std::fmt;
pub type ParseResult<T> = std::result::Result<T, ParseError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseErrorKind {
PestParse,
InvalidSyntax { command: String },
UnknownCommand { name: String },
Structural { rule: String },
Validation { command: String },
}
#[derive(Debug, Clone, Default)]
pub struct SpanContext<'a> {
pub line: usize,
pub col_start: Option<usize>,
pub col_end: Option<usize>,
pub source_line: Option<String>,
pub source: Option<&'a str>,
#[cfg(feature = "proc-macro-api")]
pub compiler_span: Option<proc_macro2::Span>,
pub step_index: Option<usize>,
}
impl<'a> SpanContext<'a> {
pub fn line_only(line: usize) -> Self {
Self {
line,
..Self::default()
}
}
pub fn full(line: usize, col_start: usize, col_end: usize, source_line: String) -> Self {
Self {
line,
col_start: Some(col_start),
col_end: Some(col_end),
source_line: Some(source_line),
source: None,
#[cfg(feature = "proc-macro-api")]
compiler_span: None,
step_index: None,
}
}
pub fn with_source(mut self, source: &'a str) -> Self {
if self.source_line.is_none() {
self.source_line = source
.lines()
.nth(self.line.saturating_sub(1))
.map(str::to_string);
}
self.source = Some(source);
self
}
#[cfg(feature = "proc-macro-api")]
pub fn from_compiler_span(
line: usize,
col_start: Option<usize>,
col_end: Option<usize>,
span: proc_macro2::Span,
) -> Self {
Self {
line,
col_start,
col_end,
source_line: None,
source: None,
compiler_span: Some(span),
step_index: None,
}
}
#[cfg(feature = "proc-macro-api")]
pub fn compiler_span(&self) -> Option<&proc_macro2::Span> {
self.compiler_span.as_ref()
}
pub fn with_step(mut self, step_index: usize) -> Self {
self.step_index = Some(step_index);
self
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
kind: ParseErrorKind,
line: usize,
detail: Box<ErrorDetail>,
}
#[derive(Debug, Clone)]
struct ErrorDetail {
col_start: Option<usize>,
col_end: Option<usize>,
source_line: Option<String>,
step_index: Option<usize>,
found: Option<String>,
expected: Vec<String>,
hint: Option<String>,
message: String,
}
impl ParseError {
fn new(
kind: ParseErrorKind,
line: usize,
ctx: &SpanContext,
found: Option<String>,
expected: Vec<String>,
hint: Option<String>,
message: String,
) -> Self {
Self {
kind,
line,
detail: Box::new(ErrorDetail {
col_start: ctx.col_start,
col_end: ctx.col_end,
source_line: ctx.source_line.clone(),
step_index: ctx.step_index,
found,
expected,
hint,
message,
}),
}
}
pub fn pest(
message: String,
expected: Vec<String>,
hint: Option<String>,
ctx: &SpanContext,
) -> Self {
Self::new(
ParseErrorKind::PestParse,
ctx.line,
ctx,
None,
expected,
hint,
message,
)
}
pub fn invalid_syntax(
command: &str,
message: String,
found: Option<String>,
expected: Vec<String>,
hint: Option<String>,
ctx: &SpanContext,
) -> Self {
Self::new(
ParseErrorKind::InvalidSyntax {
command: command.to_string(),
},
ctx.line,
ctx,
found,
expected,
hint,
message,
)
}
pub fn unknown_command(
name: &str,
message: String,
hint: Option<String>,
ctx: &SpanContext,
) -> Self {
Self::new(
ParseErrorKind::UnknownCommand {
name: name.to_string(),
},
ctx.line,
ctx,
Some(name.to_string()),
Vec::new(),
hint,
message,
)
}
pub fn structural(rule: &str, message: String, ctx: &SpanContext) -> Self {
Self::new(
ParseErrorKind::Structural {
rule: rule.to_string(),
},
ctx.line,
ctx,
None,
Vec::new(),
None,
message,
)
}
pub fn validation(command: &str, message: String, ctx: &SpanContext) -> Self {
Self::new(
ParseErrorKind::Validation {
command: command.to_string(),
},
ctx.line,
ctx,
None,
Vec::new(),
None,
message,
)
}
pub fn with_span(mut self, ctx: &SpanContext) -> Self {
if self.line == 0 {
self.line = ctx.line;
}
let detail = &mut self.detail;
if detail.col_start.is_none() {
detail.col_start = ctx.col_start;
}
if detail.col_end.is_none() {
detail.col_end = ctx.col_end;
}
if detail.source_line.is_none() {
detail.source_line = ctx.source_line.clone();
}
if detail.step_index.is_none() {
detail.step_index = ctx.step_index;
}
self
}
pub fn with_step(mut self, step_index: usize) -> Self {
self.detail.step_index = Some(step_index);
self
}
pub fn kind(&self) -> &ParseErrorKind {
&self.kind
}
pub fn line(&self) -> usize {
self.line
}
pub fn col_start(&self) -> Option<usize> {
self.detail.col_start
}
pub fn col_end(&self) -> Option<usize> {
self.detail.col_end
}
pub fn source_line(&self) -> Option<&str> {
self.detail.source_line.as_deref()
}
pub fn step_index(&self) -> Option<usize> {
self.detail.step_index
}
pub fn found(&self) -> Option<&str> {
self.detail.found.as_deref()
}
pub fn expected(&self) -> &[String] {
&self.detail.expected
}
pub fn hint(&self) -> Option<&str> {
self.detail.hint.as_deref()
}
fn caret_block(&self) -> Option<String> {
let (start, end) = match (self.detail.col_start, self.detail.col_end) {
(Some(s), Some(e)) => (s, e),
_ => return None,
};
let line_text = self.detail.source_line.as_deref()?;
if self.line == 0 || start == 0 {
return None;
}
let width = end.saturating_sub(start).saturating_add(1).max(1);
let pad = " ".repeat(start.saturating_sub(1));
let carets = "^".repeat(width.min(80));
Some(format!(
"\n --> line {line}, col {start}-{end}\n {line} | {line_text}\n | {pad}{carets}",
line = self.line,
))
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.detail.message)?;
if let Some(block) = self.caret_block() {
if !self.detail.message.contains("--> line") {
write!(f, "{block}")?;
}
} else if self.line > 0 && !self.detail.message.contains(&format!("line {}", self.line)) {
write!(f, "\n --> line {}", self.line)?;
}
Ok(())
}
}
impl std::error::Error for ParseError {}