1use std::fmt;
8use std::path::PathBuf;
9
10use pest::error::{ErrorVariant, LineColLocation};
11use thiserror::Error;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct FileParseError {
18 pub format: &'static str,
20 pub message: Box<str>,
22 pub line: Option<usize>,
24 pub column: Option<usize>,
26}
27
28impl fmt::Display for FileParseError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.write_str(&self.message)
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct ParseDiagnostic {
39 pub message: Box<str>,
41 pub line: usize,
43 pub column: usize,
45}
46
47impl ParseDiagnostic {
48 pub(crate) fn from_pest<R: pest::RuleType>(error: &pest::error::Error<R>) -> Self {
49 let (line, column) = match error.line_col {
50 LineColLocation::Pos((line, column)) => (line, column),
51 LineColLocation::Span((line, column), _) => (line, column),
52 };
53 let message = match &error.variant {
54 ErrorVariant::ParsingError { positives, .. } if !positives.is_empty() => format!(
55 "expected {}",
56 positives
57 .iter()
58 .map(|rule| format!("{rule:?}").replace('_', " "))
59 .collect::<Vec<_>>()
60 .join(", ")
61 ),
62 ErrorVariant::CustomError { message } => message.clone(),
63 _ => error.to_string(),
64 };
65 Self {
66 message: message.into_boxed_str(),
67 line,
68 column,
69 }
70 }
71}
72
73impl fmt::Display for ParseDiagnostic {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.write_str(&self.message)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Error)]
81#[error("parse error in {display_name}: {diagnostic}")]
82#[non_exhaustive]
83pub struct ParseError {
84 pub display_name: String,
86 pub source_text: Box<str>,
88 pub start_line: usize,
90 pub diagnostic: ParseDiagnostic,
92}
93
94impl ParseError {
95 fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
96 self.display_name = display_name.into();
97 self
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Error)]
103#[error("config error in {path}: {details}")]
104#[non_exhaustive]
105pub struct ConfigError {
106 pub path: PathBuf,
108 pub details: FileParseError,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Error)]
114#[error("spec error in {path}: {details}")]
115#[non_exhaustive]
116pub struct SpecError {
117 pub path: PathBuf,
119 pub details: FileParseError,
121}
122
123#[derive(Debug, Error)]
126#[non_exhaustive]
127pub enum Error {
128 #[error("{0}")]
130 Parse(#[from] ParseError),
131
132 #[error("{0}")]
134 Config(#[from] ConfigError),
135
136 #[error("{0}")]
138 Spec(#[from] SpecError),
139
140 #[error("I/O error: {0}")]
142 Io(#[from] std::io::Error),
143
144 #[error("formatter error: {0}")]
147 Formatter(String),
148
149 #[error(
152 "line {line_no} is {width} characters wide, exceeding the configured limit of {limit}"
153 )]
154 LayoutTooWide {
155 line_no: usize,
157 width: usize,
159 limit: usize,
161 },
162}
163
164pub type Result<T> = std::result::Result<T, Error>;
166
167impl Error {
168 pub fn with_display_name(self, display_name: impl Into<String>) -> Self {
170 match self {
171 Self::Parse(parse) => Self::Parse(parse.with_display_name(display_name)),
172 other => other,
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn parse_diagnostic_display_shows_message() {
183 let diag = ParseDiagnostic {
184 message: "expected argument part".into(),
185 line: 5,
186 column: 10,
187 };
188 assert_eq!(diag.to_string(), "expected argument part");
189 }
190
191 #[test]
192 fn parse_diagnostic_from_pest_parsing_error() {
193 let source = "if(\n";
194 let err = crate::parser::parse(source).unwrap_err();
195 if let Error::Parse(ParseError { diagnostic, .. }) = err {
196 assert!(diagnostic.line >= 1);
197 assert!(diagnostic.column >= 1);
198 assert!(!diagnostic.message.is_empty());
199 } else {
200 panic!("expected Parse, got {err:?}");
201 }
202 }
203
204 #[test]
205 fn error_parse_display() {
206 let err = Error::Parse(ParseError {
207 display_name: "test.cmake".to_owned(),
208 source_text: "if(\n".into(),
209 start_line: 1,
210 diagnostic: ParseDiagnostic {
211 message: "expected argument part".into(),
212 line: 1,
213 column: 4,
214 },
215 });
216 let msg = err.to_string();
217 assert!(msg.contains("test.cmake"));
218 assert!(msg.contains("expected argument part"));
219 }
220
221 #[test]
222 fn error_config_display() {
223 let err = Error::Config(ConfigError {
224 path: std::path::PathBuf::from("bad.yaml"),
225 details: FileParseError {
226 format: "YAML",
227 message: "unexpected key".into(),
228 line: Some(3),
229 column: Some(1),
230 },
231 });
232 let msg = err.to_string();
233 assert!(msg.contains("bad.yaml"));
234 assert!(msg.contains("unexpected key"));
235 }
236
237 #[test]
238 fn error_spec_display() {
239 let err = Error::Spec(SpecError {
240 path: std::path::PathBuf::from("commands.yaml"),
241 details: FileParseError {
242 format: "YAML",
243 message: "invalid nargs".into(),
244 line: None,
245 column: None,
246 },
247 });
248 let msg = err.to_string();
249 assert!(msg.contains("commands.yaml"));
250 assert!(msg.contains("invalid nargs"));
251 }
252
253 #[test]
254 fn error_io_display() {
255 let err = Error::Io(std::io::Error::new(
256 std::io::ErrorKind::NotFound,
257 "file not found",
258 ));
259 assert!(err.to_string().contains("file not found"));
260 }
261
262 #[test]
263 fn error_formatter_display() {
264 let err = Error::Formatter("something went wrong".to_owned());
265 assert!(err.to_string().contains("something went wrong"));
266 }
267
268 #[test]
269 fn error_layout_too_wide_display() {
270 let err = Error::LayoutTooWide {
271 line_no: 42,
272 width: 120,
273 limit: 80,
274 };
275 let msg = err.to_string();
276 assert!(msg.contains("42"));
277 assert!(msg.contains("120"));
278 assert!(msg.contains("80"));
279 }
280
281 #[test]
282 fn with_display_name_updates_parse() {
283 let err = Error::Parse(ParseError {
284 display_name: "original".to_owned(),
285 source_text: "set(\n".into(),
286 start_line: 1,
287 diagnostic: ParseDiagnostic {
288 message: "test".into(),
289 line: 1,
290 column: 5,
291 },
292 });
293 let renamed = err.with_display_name("renamed.cmake");
294 match renamed {
295 Error::Parse(ParseError { display_name, .. }) => {
296 assert_eq!(display_name, "renamed.cmake");
297 }
298 _ => panic!("expected Parse"),
299 }
300 }
301
302 #[test]
303 fn with_display_name_passes_through_non_parse_errors() {
304 let err = Error::Formatter("test".to_owned());
305 let result = err.with_display_name("ignored");
306 match result {
307 Error::Formatter(msg) => assert_eq!(msg, "test"),
308 _ => panic!("expected Formatter to pass through"),
309 }
310 }
311
312 #[test]
313 fn io_error_converts_from_std() {
314 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
315 let err: Error = io_err.into();
316 match err {
317 Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied),
318 _ => panic!("expected Io variant"),
319 }
320 }
321}