1use miette::{Diagnostic, NamedSource, SourceSpan};
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum ParseError {
6 #[error("Syntax error: {0}")]
7 Syntax(#[from] pest::error::Error<crate::Rule>),
8
9 #[error("Invalid integer at {location}: {value}")]
10 InvalidInt { value: String, location: String },
11
12 #[error("Unknown re-import policy: {0}")]
13 UnknownPolicy(String),
14
15 #[error("Unknown map target type '{0}' (expected one of: span, event, event_range)")]
16 UnknownTargetType(String),
17
18 #[error("Unexpected rule {rule} at {location}")]
19 UnexpectedRule { rule: String, location: String },
20
21 #[error("Invalid month at {location}: {value} (expected 1-12)")]
22 InvalidMonth { value: u32, location: String },
23
24 #[error("Invalid day at {location}: {value} (expected 1-31)")]
25 InvalidDay { value: u32, location: String },
26}
27
28#[derive(Debug, Error, Diagnostic)]
33#[error("{message}")]
34#[diagnostic(
35 code(tdsl::parse_error),
36 help("DSL 仕様書 docs/dsl-spec.md を確認してください")
37)]
38pub struct ParseDiagnostic {
39 message: String,
40 #[source_code]
41 src: NamedSource<String>,
42 #[label("ここに問題があります")]
43 span: Option<SourceSpan>,
44}
45
46impl ParseDiagnostic {
47 pub fn from_parse_error(err: &ParseError, src: &str, filename: &str) -> Self {
54 let message = match err {
55 ParseError::Syntax(pest_err) => {
58 format!("構文エラー: {}", pest_err.variant.message())
59 }
60 other => other.to_string(),
61 };
62 let named_src = NamedSource::new(filename, src.to_owned());
63
64 let span = Self::extract_span(err, src);
65
66 ParseDiagnostic {
67 message,
68 src: named_src,
69 span,
70 }
71 }
72
73 fn extract_span(err: &ParseError, src: &str) -> Option<SourceSpan> {
74 match err {
75 ParseError::Syntax(pest_err) => {
76 use pest::error::InputLocation;
77 match pest_err.location {
78 InputLocation::Pos(offset) => {
79 let offset = offset.min(src.len().saturating_sub(1));
81 Some(SourceSpan::from((offset, 1usize)))
82 }
83 InputLocation::Span((start, end)) => {
84 let start = start.min(src.len());
85 let end = end.min(src.len());
86 let len = end.saturating_sub(start).max(1);
87 Some(SourceSpan::from((start, len)))
88 }
89 }
90 }
91 ParseError::InvalidInt { location, .. }
92 | ParseError::UnexpectedRule { location, .. }
93 | ParseError::InvalidMonth { location, .. }
94 | ParseError::InvalidDay { location, .. } => parse_byte_range_to_span(location, src),
95 ParseError::UnknownPolicy(_) | ParseError::UnknownTargetType(_) => None,
96 }
97 }
98
99 pub fn span(&self) -> Option<SourceSpan> {
101 self.span
102 }
103}
104
105fn parse_byte_range_to_span(location: &str, src: &str) -> Option<SourceSpan> {
107 let (start_str, end_str) = location.split_once(':')?;
108 let start_byte: usize = start_str.trim().parse().ok()?;
109 let end_byte: usize = end_str.trim().parse().ok()?;
110 let start = start_byte.min(src.len());
111 let end = end_byte.min(src.len());
112 let len = end.saturating_sub(start).max(1);
113 Some(SourceSpan::from((start, len)))
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct ParseErrorLoc {
120 pub line: u32,
122 pub col: u32,
124 pub end_line: u32,
126 pub end_col: u32,
128}
129
130impl ParseError {
131 pub fn source_location(&self, src: &str) -> Option<ParseErrorLoc> {
139 match self {
140 ParseError::Syntax(e) => {
141 use pest::error::LineColLocation;
142 match e.line_col {
143 LineColLocation::Pos((line, col)) => Some(ParseErrorLoc {
144 line: line as u32,
145 col: col as u32,
146 end_line: line as u32,
147 end_col: col as u32,
148 }),
149 LineColLocation::Span((sl, sc), (el, ec)) => Some(ParseErrorLoc {
150 line: sl as u32,
151 col: sc as u32,
152 end_line: el as u32,
153 end_col: ec as u32,
154 }),
155 }
156 }
157 ParseError::InvalidInt { location, .. }
158 | ParseError::UnexpectedRule { location, .. }
159 | ParseError::InvalidMonth { location, .. }
160 | ParseError::InvalidDay { location, .. } => byte_range_to_loc(location, src),
161 ParseError::UnknownPolicy(_) | ParseError::UnknownTargetType(_) => None,
162 }
163 }
164}
165
166fn byte_range_to_loc(location: &str, src: &str) -> Option<ParseErrorLoc> {
168 let (start_str, end_str) = location.split_once(':')?;
169 let start_byte: usize = start_str.trim().parse().ok()?;
170 let end_byte: usize = end_str.trim().parse().ok()?;
171
172 let (start_line, start_col) = byte_offset_to_line_col(src, start_byte);
173 let (end_line, end_col) = byte_offset_to_line_col(src, end_byte);
174
175 Some(ParseErrorLoc {
176 line: start_line,
177 col: start_col,
178 end_line,
179 end_col,
180 })
181}
182
183pub fn byte_offset_to_line_col(src: &str, offset: usize) -> (u32, u32) {
188 let offset = offset.min(src.len());
190 let before = &src[..offset];
191 let line = (before.chars().filter(|&c| c == '\n').count() + 1) as u32;
192 let col = (before.rfind('\n').map_or(offset, |pos| offset - pos - 1) + 1) as u32;
193 (line, col)
194}