use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Syntax error: {0}")]
Syntax(#[from] pest::error::Error<crate::Rule>),
#[error("Invalid integer at {location}: {value}")]
InvalidInt { value: String, location: String },
#[error("Unknown re-import policy: {0}")]
UnknownPolicy(String),
#[error("Unknown map target type '{0}' (expected one of: span, event, event_range)")]
UnknownTargetType(String),
#[error("Unexpected rule {rule} at {location}")]
UnexpectedRule { rule: String, location: String },
#[error("Invalid month at {location}: {value} (expected 1-12)")]
InvalidMonth { value: u32, location: String },
#[error("Invalid day at {location}: {value} (expected 1-31)")]
InvalidDay { value: u32, location: String },
}
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
#[diagnostic(
code(tdsl::parse_error),
help("DSL 仕様書 docs/dsl-spec.md を確認してください")
)]
pub struct ParseDiagnostic {
message: String,
#[source_code]
src: NamedSource<String>,
#[label("ここに問題があります")]
span: Option<SourceSpan>,
}
impl ParseDiagnostic {
pub fn from_parse_error(err: &ParseError, src: &str, filename: &str) -> Self {
let message = match err {
ParseError::Syntax(pest_err) => {
format!("構文エラー: {}", pest_err.variant.message())
}
other => other.to_string(),
};
let named_src = NamedSource::new(filename, src.to_owned());
let span = Self::extract_span(err, src);
ParseDiagnostic {
message,
src: named_src,
span,
}
}
fn extract_span(err: &ParseError, src: &str) -> Option<SourceSpan> {
match err {
ParseError::Syntax(pest_err) => {
use pest::error::InputLocation;
match pest_err.location {
InputLocation::Pos(offset) => {
let offset = offset.min(src.len().saturating_sub(1));
Some(SourceSpan::from((offset, 1usize)))
}
InputLocation::Span((start, end)) => {
let start = start.min(src.len());
let end = end.min(src.len());
let len = end.saturating_sub(start).max(1);
Some(SourceSpan::from((start, len)))
}
}
}
ParseError::InvalidInt { location, .. }
| ParseError::UnexpectedRule { location, .. }
| ParseError::InvalidMonth { location, .. }
| ParseError::InvalidDay { location, .. } => parse_byte_range_to_span(location, src),
ParseError::UnknownPolicy(_) | ParseError::UnknownTargetType(_) => None,
}
}
pub fn span(&self) -> Option<SourceSpan> {
self.span
}
}
fn parse_byte_range_to_span(location: &str, src: &str) -> Option<SourceSpan> {
let (start_str, end_str) = location.split_once(':')?;
let start_byte: usize = start_str.trim().parse().ok()?;
let end_byte: usize = end_str.trim().parse().ok()?;
let start = start_byte.min(src.len());
let end = end_byte.min(src.len());
let len = end.saturating_sub(start).max(1);
Some(SourceSpan::from((start, len)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseErrorLoc {
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
}
impl ParseError {
pub fn source_location(&self, src: &str) -> Option<ParseErrorLoc> {
match self {
ParseError::Syntax(e) => {
use pest::error::LineColLocation;
match e.line_col {
LineColLocation::Pos((line, col)) => Some(ParseErrorLoc {
line: line as u32,
col: col as u32,
end_line: line as u32,
end_col: col as u32,
}),
LineColLocation::Span((sl, sc), (el, ec)) => Some(ParseErrorLoc {
line: sl as u32,
col: sc as u32,
end_line: el as u32,
end_col: ec as u32,
}),
}
}
ParseError::InvalidInt { location, .. }
| ParseError::UnexpectedRule { location, .. }
| ParseError::InvalidMonth { location, .. }
| ParseError::InvalidDay { location, .. } => byte_range_to_loc(location, src),
ParseError::UnknownPolicy(_) | ParseError::UnknownTargetType(_) => None,
}
}
}
fn byte_range_to_loc(location: &str, src: &str) -> Option<ParseErrorLoc> {
let (start_str, end_str) = location.split_once(':')?;
let start_byte: usize = start_str.trim().parse().ok()?;
let end_byte: usize = end_str.trim().parse().ok()?;
let (start_line, start_col) = byte_offset_to_line_col(src, start_byte);
let (end_line, end_col) = byte_offset_to_line_col(src, end_byte);
Some(ParseErrorLoc {
line: start_line,
col: start_col,
end_line,
end_col,
})
}
pub fn byte_offset_to_line_col(src: &str, offset: usize) -> (u32, u32) {
let offset = offset.min(src.len());
let before = &src[..offset];
let line = (before.chars().filter(|&c| c == '\n').count() + 1) as u32;
let col = (before.rfind('\n').map_or(offset, |pos| offset - pos - 1) + 1) as u32;
(line, col)
}