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