Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/frontend/diagnostics.rs
Line
Count
Source
1
//! Enhanced error diagnostics with source code display and suggestions
2
3
use crate::frontend::error_recovery::{ParseError, ErrorSeverity};
4
use crate::frontend::ast::Span;
5
use std::fmt;
6
7
/// Enhanced diagnostic information with source context
8
#[derive(Debug, Clone)]
9
pub struct Diagnostic {
10
    pub error: ParseError,
11
    pub source_code: String,
12
    pub filename: Option<String>,
13
    pub suggestions: Vec<Suggestion>,
14
}
15
16
/// A suggestion for fixing an error
17
#[derive(Debug, Clone)]
18
pub struct Suggestion {
19
    pub message: String,
20
    pub replacement: Option<String>,
21
    pub span: Span,
22
}
23
24
impl Diagnostic {
25
0
    pub fn new(error: ParseError, source_code: String) -> Self {
26
0
        Self {
27
0
            error,
28
0
            source_code,
29
0
            filename: None,
30
0
            suggestions: Vec::new(),
31
0
        }
32
0
    }
33
34
0
    pub fn with_filename(mut self, filename: String) -> Self {
35
0
        self.filename = Some(filename);
36
0
        self
37
0
    }
38
39
0
    pub fn add_suggestion(&mut self, suggestion: Suggestion) {
40
0
        self.suggestions.push(suggestion);
41
0
    }
42
43
    /// Extract the relevant source lines with context
44
0
    fn get_source_context(&self) -> (Vec<String>, usize, usize, usize) {
45
0
        let lines: Vec<String> = self.source_code.lines().map(std::string::ToString::to_string).collect();
46
        
47
        // Find line and column from byte offset
48
0
        let mut current_pos = 0;
49
0
        let mut line_num = 0;
50
0
        let mut col_start = 0;
51
        
52
0
        for (i, line) in lines.iter().enumerate() {
53
0
            let line_len = line.len() + 1; // +1 for newline
54
0
            if current_pos + line_len > self.error.span.start {
55
0
                line_num = i;
56
0
                col_start = self.error.span.start - current_pos;
57
0
                break;
58
0
            }
59
0
            current_pos += line_len;
60
        }
61
        
62
        // Calculate error span width
63
0
        let col_end = col_start + (self.error.span.end - self.error.span.start);
64
        
65
        // Get context lines (2 before, 2 after)
66
0
        let context_start = line_num.saturating_sub(2);
67
0
        let context_end = (line_num + 3).min(lines.len());
68
0
        let context_lines = lines[context_start..context_end].to_vec();
69
        
70
0
        (context_lines, line_num - context_start, col_start, col_end)
71
0
    }
72
73
    /// Generate colored output for terminal display
74
0
    pub fn format_colored(&self) -> String {
75
0
        let mut output = String::new();
76
        
77
        // Header with severity and location
78
0
        let severity_color = match self.error.severity {
79
0
            ErrorSeverity::Error => "\x1b[31m",   // Red
80
0
            ErrorSeverity::Warning => "\x1b[33m", // Yellow
81
0
            ErrorSeverity::Info => "\x1b[34m",    // Blue
82
0
            ErrorSeverity::Hint => "\x1b[36m",    // Cyan
83
        };
84
0
        let reset = "\x1b[0m";
85
0
        let bold = "\x1b[1m";
86
        
87
        // File and location header
88
0
        if let Some(ref filename) = self.filename {
89
0
            output.push_str(&format!(
90
0
                "{bold}{severity_color}error[{:?}]{reset}: {}\n",
91
0
                self.error.error_code,
92
0
                self.error.message
93
0
            ));
94
0
            output.push_str(&format!(
95
0
                "  {bold}-->{reset} {}:{}:{}\n",
96
0
                filename,
97
0
                self.error.span.start / 100 + 1, // Rough line estimate
98
0
                self.error.span.start % 100 + 1  // Rough column estimate
99
0
            ));
100
0
        } else {
101
0
            output.push_str(&format!(
102
0
                "{bold}{severity_color}error[{:?}]{reset}: {}\n",
103
0
                self.error.error_code,
104
0
                self.error.message
105
0
            ));
106
0
        }
107
        
108
        // Source code context with error highlighting
109
0
        let (context_lines, error_line_idx, col_start, col_end) = self.get_source_context();
110
0
        let line_num_start = (self.error.span.start / 100 + 1).saturating_sub(error_line_idx);
111
        
112
0
        for (i, line) in context_lines.iter().enumerate() {
113
0
            let line_num = line_num_start + i;
114
0
            let is_error_line = i == error_line_idx;
115
            
116
            // Line number and content
117
0
            if is_error_line {
118
0
                output.push_str(&format!(
119
0
                    "{bold}{line_num:4} |{reset} {line}\n"
120
0
                ));
121
                
122
                // Error underline
123
0
                let spaces = " ".repeat(col_start);
124
0
                let arrows = "^".repeat((col_end - col_start).max(1));
125
0
                output.push_str(&format!(
126
0
                    "     | {spaces}{severity_color}{arrows}\n"
127
0
                ));
128
                
129
                // Error message under the line
130
0
                if let Some(ref hint) = self.error.recovery_hint {
131
0
                    output.push_str(&format!(
132
0
                        "     {} {}{}{reset} {}\n",
133
0
                        "|",
134
0
                        " ".repeat(col_start),
135
0
                        severity_color,
136
0
                        hint
137
0
                    ));
138
0
                }
139
0
            } else {
140
0
                output.push_str(&format!("{line_num:4} | {line}\n"));
141
0
            }
142
        }
143
        
144
        // Suggestions
145
0
        if !self.suggestions.is_empty() {
146
0
            output.push_str(&format!("\n{bold}help{reset}: "));
147
0
            for suggestion in &self.suggestions {
148
0
                output.push_str(&format!("{}\n", suggestion.message));
149
0
                if let Some(ref replacement) = suggestion.replacement {
150
0
                    output.push_str(&format!("      suggested fix: `{replacement}`\n"));
151
0
                }
152
            }
153
0
        }
154
        
155
0
        output.push_str(reset);
156
0
        output
157
0
    }
158
}
159
160
impl fmt::Display for Diagnostic {
161
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162
0
        write!(f, "{}", self.format_colored())
163
0
    }
164
}
165
166
/// Common error patterns and their suggestions
167
0
pub fn suggest_for_error(error: &ParseError) -> Vec<Suggestion> {
168
0
    let mut suggestions = Vec::new();
169
    
170
    // Common typo suggestions
171
0
    if error.message.contains("unexpected") {
172
0
        if let Some(ref _found) = error.found {
173
0
            // Token-specific suggestions would go here
174
0
            suggestions.push(Suggestion {
175
0
                message: "Check for typos or missing operators".to_string(),
176
0
                replacement: None,
177
0
                span: error.span,
178
0
            });
179
0
        }
180
0
    }
181
    
182
    // Missing semicolon suggestion
183
0
    if error.message.contains("expected") && error.message.contains("semicolon") {
184
0
        suggestions.push(Suggestion {
185
0
            message: "Add a semicolon at the end of the statement".to_string(),
186
0
            replacement: Some(";".to_string()),
187
0
            span: Span {
188
0
                start: error.span.end,
189
0
                end: error.span.end,
190
0
            },
191
0
        });
192
0
    }
193
    
194
    // Unclosed delimiter suggestions
195
0
    if error.message.contains("unclosed") || error.message.contains("unmatched") {
196
0
        if error.message.contains("paren") {
197
0
            suggestions.push(Suggestion {
198
0
                message: "Add closing parenthesis ')'".to_string(),
199
0
                replacement: Some(")".to_string()),
200
0
                span: Span {
201
0
                    start: error.span.end,
202
0
                    end: error.span.end,
203
0
                },
204
0
            });
205
0
        } else if error.message.contains("brace") {
206
0
            suggestions.push(Suggestion {
207
0
                message: "Add closing brace '}'".to_string(),
208
0
                replacement: Some("}".to_string()),
209
0
                span: Span {
210
0
                    start: error.span.end,
211
0
                    end: error.span.end,
212
0
                },
213
0
            });
214
0
        } else if error.message.contains("bracket") {
215
0
            suggestions.push(Suggestion {
216
0
                message: "Add closing bracket ']'".to_string(),
217
0
                replacement: Some("]".to_string()),
218
0
                span: Span {
219
0
                    start: error.span.end,
220
0
                    end: error.span.end,
221
0
                },
222
0
            });
223
0
        }
224
0
    }
225
    
226
0
    suggestions
227
0
}
228
229
#[cfg(test)]
230
mod tests {
231
    use super::*;
232
233
    #[test]
234
    fn test_diagnostic_display() {
235
        let error = ParseError::new(
236
            "Unexpected token".to_string(),
237
            Span { start: 10, end: 15 },
238
        );
239
        
240
        let source = "let x = 10\nlet y = @invalid\nlet z = 30".to_string();
241
        let diag = Diagnostic::new(error, source);
242
        
243
        let output = format!("{diag}");
244
        assert!(output.contains("Unexpected token"));
245
    }
246
247
    #[test]
248
    fn test_suggestions() {
249
        let mut error = ParseError::new(
250
            "unexpected '='".to_string(),
251
            Span { start: 5, end: 6 },
252
        );
253
        error.found = Some(crate::frontend::lexer::Token::Equal);
254
        
255
        let suggestions = suggest_for_error(&error);
256
        assert!(!suggestions.is_empty());
257
    }
258
}