1use crate::models::Error;
11use std::fmt;
12
13#[derive(Debug, Clone)]
15pub struct Diagnostic {
16 pub error: String,
18
19 pub file: Option<String>,
21
22 pub line: Option<usize>,
24
25 pub column: Option<usize>,
27
28 pub category: ErrorCategory,
30
31 pub note: Option<String>,
33
34 pub help: Option<String>,
36
37 pub snippet: Option<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum ErrorCategory {
43 Syntax,
45
46 UnsupportedFeature,
48
49 Validation,
51
52 Transpilation,
54
55 Io,
57
58 Internal,
60}
61
62impl Diagnostic {
63 pub fn from_error(error: &Error, file: Option<String>) -> Self {
65 let (category, note, help) = Self::categorize_error(error);
66
67 let (line, column) = (None, None);
70
71 Self {
72 error: error.to_string(),
73 file,
74 line,
75 column,
76 category,
77 note,
78 help,
79 snippet: None,
80 }
81 }
82
83 fn categorize_error(error: &Error) -> (ErrorCategory, Option<String>, Option<String>) {
85 match error {
86 Error::Parse(_) => (
87 ErrorCategory::Syntax,
88 Some("Rash uses a subset of Rust syntax for transpilation to shell scripts.".to_string()),
89 Some("Ensure your code uses supported Rust syntax. See docs/user-guide.md for details.".to_string()),
90 ),
91
92 Error::Validation(msg) if msg.contains("Only functions") => (
93 ErrorCategory::UnsupportedFeature,
94 Some("Rash only supports function definitions at the top level.".to_string()),
95 Some("Remove struct, trait, impl, or other definitions. Only 'fn' declarations are allowed.".to_string()),
96 ),
97
98 Error::Validation(msg) if msg.contains("Unsupported") => (
99 ErrorCategory::UnsupportedFeature,
100 Some("This Rust feature is not supported for shell script transpilation.".to_string()),
101 Some("Check the user guide for supported features, or file an issue for feature requests.".to_string()),
102 ),
103
104 Error::Validation(msg) => (
105 ErrorCategory::Validation,
106 Some(format!("Validation failed: {}", msg)),
107 Some("Review the error message and ensure your code follows Rash constraints.".to_string()),
108 ),
109
110 Error::IrGeneration(msg) => (
111 ErrorCategory::Transpilation,
112 Some(format!("Failed to generate intermediate representation: {}", msg)),
113 Some("This is likely a transpiler bug. Please report this issue.".to_string()),
114 ),
115
116 Error::Io(_) => (
117 ErrorCategory::Io,
118 Some("Failed to read or write files.".to_string()),
119 Some("Check file paths and permissions.".to_string()),
120 ),
121
122 Error::Unsupported(feature) => (
123 ErrorCategory::UnsupportedFeature,
124 Some(format!("The feature '{}' is not yet supported for transpilation.", feature)),
125 Some("See docs/user-guide.md for supported features, or use a workaround.".to_string()),
126 ),
127
128 _ => (
129 ErrorCategory::Internal,
130 Some("An internal error occurred during transpilation.".to_string()),
131 Some("This may be a bug. Please report this with a minimal reproduction.".to_string()),
132 ),
133 }
134 }
135
136 pub fn quality_score(&self) -> f32 {
138 let mut score = 0.0;
139
140 score += 1.0;
142
143 if self.file.is_some() {
145 score += 1.0;
146 }
147 if self.line.is_some() {
148 score += 0.25;
149 }
150 if self.column.is_some() {
151 score += 0.25;
152 }
153
154 if self.snippet.is_some() {
156 score += 1.0;
157 }
158
159 if self.note.is_some() {
161 score += 2.5;
162 }
163
164 if self.help.is_some() {
166 score += 2.5;
167 }
168
169 score / 8.5 }
171}
172
173impl fmt::Display for Diagnostic {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(f, "error")?;
177
178 if let Some(file) = &self.file {
179 write!(f, " in {}", file)?;
180 if let Some(line) = self.line {
181 write!(f, ":{}", line)?;
182 if let Some(col) = self.column {
183 write!(f, ":{}", col)?;
184 }
185 }
186 }
187
188 writeln!(f, ": {}", self.error)?;
189
190 if let Some(snippet) = &self.snippet {
192 writeln!(f)?;
193 writeln!(f, "{}", snippet)?;
194 if let Some(col) = self.column {
195 writeln!(f, "{}^", " ".repeat(col.saturating_sub(1)))?;
197 }
198 }
199
200 if let Some(note) = &self.note {
202 writeln!(f)?;
203 writeln!(f, "note: {}", note)?;
204 }
205
206 if let Some(help) = &self.help {
208 writeln!(f)?;
209 writeln!(f, "help: {}", help)?;
210 }
211
212 Ok(())
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_diagnostic_quality_score() {
222 let mut diag = Diagnostic {
223 error: "test error".to_string(),
224 file: None,
225 line: None,
226 column: None,
227 category: ErrorCategory::Syntax,
228 note: None,
229 help: None,
230 snippet: None,
231 };
232
233 assert!(diag.quality_score() < 0.7); diag.file = Some("test.rs".to_string());
238 diag.line = Some(10);
239 diag.column = Some(5);
240 assert!(diag.quality_score() < 0.7); diag.note = Some("Explanation".to_string());
244 diag.help = Some("Suggestion".to_string());
245 assert!(diag.quality_score() >= 0.7); }
247
248 #[test]
249 fn test_unsupported_feature_diagnostic() {
250 let error = Error::Validation("Only functions are allowed in Rash code".to_string());
251 let diag = Diagnostic::from_error(&error, Some("example.rs".to_string()));
252
253 assert_eq!(diag.category, ErrorCategory::UnsupportedFeature);
254 assert!(diag.note.is_some());
255 assert!(diag.help.is_some());
256
257 assert!(
259 diag.quality_score() >= 0.7,
260 "Quality score {} should be ≥0.7",
261 diag.quality_score()
262 );
263 }
264
265 #[test]
266 fn test_diagnostic_display() {
267 let diag = Diagnostic {
268 error: "unexpected token".to_string(),
269 file: Some("main.rs".to_string()),
270 line: Some(5),
271 column: Some(10),
272 category: ErrorCategory::Syntax,
273 note: Some("Expected a semicolon here".to_string()),
274 help: Some("Add ';' after the statement".to_string()),
275 snippet: None,
276 };
277
278 let output = format!("{}", diag);
279 assert!(output.contains("error in main.rs:5:10"));
280 assert!(output.contains("note: Expected a semicolon"));
281 assert!(output.contains("help: Add ';'"));
282 }
283
284 #[test]
287 fn test_diagnostic_display_no_file() {
288 let diag = Diagnostic {
289 error: "parse error".to_string(),
290 file: None,
291 line: None,
292 column: None,
293 category: ErrorCategory::Syntax,
294 note: None,
295 help: None,
296 snippet: None,
297 };
298
299 let output = format!("{}", diag);
300 assert!(output.contains("error: parse error"));
301 assert!(!output.contains("in "));
302 }
303
304 #[test]
305 fn test_diagnostic_display_file_only() {
306 let diag = Diagnostic {
307 error: "file error".to_string(),
308 file: Some("test.rs".to_string()),
309 line: None,
310 column: None,
311 category: ErrorCategory::Io,
312 note: None,
313 help: None,
314 snippet: None,
315 };
316
317 let output = format!("{}", diag);
318 assert!(output.contains("error in test.rs"));
319 assert!(!output.contains(":0")); }
321
322 #[test]
323 fn test_diagnostic_display_file_and_line() {
324 let diag = Diagnostic {
325 error: "line error".to_string(),
326 file: Some("test.rs".to_string()),
327 line: Some(42),
328 column: None,
329 category: ErrorCategory::Validation,
330 note: None,
331 help: None,
332 snippet: None,
333 };
334
335 let output = format!("{}", diag);
336 assert!(output.contains("error in test.rs:42"));
337 }
338
339 #[test]
340 fn test_diagnostic_display_with_snippet() {
341 let diag = Diagnostic {
342 error: "syntax error".to_string(),
343 file: Some("test.rs".to_string()),
344 line: Some(5),
345 column: Some(10),
346 category: ErrorCategory::Syntax,
347 note: None,
348 help: None,
349 snippet: Some("let x = foo(".to_string()),
350 };
351
352 let output = format!("{}", diag);
353 assert!(output.contains("let x = foo("));
354 assert!(output.contains("^")); }
356
357 #[test]
358 fn test_diagnostic_display_snippet_column_0() {
359 let diag = Diagnostic {
360 error: "syntax error".to_string(),
361 file: Some("test.rs".to_string()),
362 line: Some(5),
363 column: Some(0),
364 category: ErrorCategory::Syntax,
365 note: None,
366 help: None,
367 snippet: Some("bad code".to_string()),
368 };
369
370 let output = format!("{}", diag);
371 assert!(output.contains("bad code"));
372 assert!(output.contains("^")); }
374
375 #[test]
376 fn test_quality_score_with_snippet() {
377 let diag = Diagnostic {
378 error: "test error".to_string(),
379 file: Some("test.rs".to_string()),
380 line: Some(10),
381 column: Some(5),
382 category: ErrorCategory::Syntax,
383 note: Some("Explanation".to_string()),
384 help: Some("Suggestion".to_string()),
385 snippet: Some("let x = bad;".to_string()),
386 };
387
388 let score = diag.quality_score();
390 assert!(
391 score > 0.9,
392 "Score with snippet should be >0.9, got {}",
393 score
394 );
395 }
396
397 #[test]
398 fn test_categorize_parse_error() {
399 let error = Error::Parse(syn::Error::new(proc_macro2::Span::call_site(), "test"));
400 let diag = Diagnostic::from_error(&error, None);
401
402 assert_eq!(diag.category, ErrorCategory::Syntax);
403 assert!(diag.note.is_some());
404 assert!(diag.help.is_some());
405 }
406
407 #[test]
408 fn test_categorize_validation_unsupported() {
409 let error = Error::Validation("Unsupported expression type".to_string());
410 let diag = Diagnostic::from_error(&error, None);
411
412 assert_eq!(diag.category, ErrorCategory::UnsupportedFeature);
413 }
414
415 #[test]
416 fn test_categorize_validation_generic() {
417 let error = Error::Validation("Some validation issue".to_string());
418 let diag = Diagnostic::from_error(&error, None);
419
420 assert_eq!(diag.category, ErrorCategory::Validation);
421 }
422
423 #[test]
424 fn test_categorize_ir_generation() {
425 let error = Error::IrGeneration("Failed to generate IR".to_string());
426 let diag = Diagnostic::from_error(&error, None);
427
428 assert_eq!(diag.category, ErrorCategory::Transpilation);
429 assert!(diag
430 .note
431 .as_ref()
432 .unwrap()
433 .contains("intermediate representation"));
434 }
435
436 #[test]
437 fn test_categorize_io_error() {
438 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
439 let error = Error::Io(io_err);
440 let diag = Diagnostic::from_error(&error, None);
441
442 assert_eq!(diag.category, ErrorCategory::Io);
443 assert!(diag.help.as_ref().unwrap().contains("permissions"));
444 }
445
446 #[test]
447 fn test_categorize_unsupported() {
448 let error = Error::Unsupported("async functions".to_string());
449 let diag = Diagnostic::from_error(&error, None);
450
451 assert_eq!(diag.category, ErrorCategory::UnsupportedFeature);
452 assert!(diag.note.as_ref().unwrap().contains("async functions"));
453 }
454
455 #[test]
456 fn test_categorize_internal_error() {
457 let error = Error::Internal("unexpected state".to_string());
458 let diag = Diagnostic::from_error(&error, None);
459
460 assert_eq!(diag.category, ErrorCategory::Internal);
461 assert!(diag.help.as_ref().unwrap().contains("bug"));
462 }
463
464 #[test]
465 fn test_error_category_equality() {
466 assert_eq!(ErrorCategory::Syntax, ErrorCategory::Syntax);
467 assert_ne!(ErrorCategory::Syntax, ErrorCategory::Io);
468 assert_eq!(
469 ErrorCategory::UnsupportedFeature,
470 ErrorCategory::UnsupportedFeature
471 );
472 assert_eq!(ErrorCategory::Validation, ErrorCategory::Validation);
473 assert_eq!(ErrorCategory::Transpilation, ErrorCategory::Transpilation);
474 assert_eq!(ErrorCategory::Internal, ErrorCategory::Internal);
475 }
476
477 #[test]
478 fn test_diagnostic_clone() {
479 let diag = Diagnostic {
480 error: "test".to_string(),
481 file: Some("test.rs".to_string()),
482 line: Some(1),
483 column: Some(1),
484 category: ErrorCategory::Syntax,
485 note: Some("note".to_string()),
486 help: Some("help".to_string()),
487 snippet: Some("code".to_string()),
488 };
489
490 let cloned = diag.clone();
491 assert_eq!(diag.error, cloned.error);
492 assert_eq!(diag.file, cloned.file);
493 assert_eq!(diag.category, cloned.category);
494 }
495
496 #[test]
497 fn test_error_category_debug() {
498 let cat = ErrorCategory::Syntax;
499 let debug_str = format!("{:?}", cat);
500 assert_eq!(debug_str, "Syntax");
501 }
502
503 #[test]
504 fn test_diagnostic_debug() {
505 let diag = Diagnostic {
506 error: "test".to_string(),
507 file: None,
508 line: None,
509 column: None,
510 category: ErrorCategory::Syntax,
511 note: None,
512 help: None,
513 snippet: None,
514 };
515
516 let debug_str = format!("{:?}", diag);
517 assert!(debug_str.contains("Diagnostic"));
518 assert!(debug_str.contains("test"));
519 }
520}