Coverage Report

Created: 2025-09-05 15: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
1
    pub fn new(error: ParseError, source_code: String) -> Self {
26
1
        Self {
27
1
            error,
28
1
            source_code,
29
1
            filename: None,
30
1
            suggestions: Vec::new(),
31
1
        }
32
1
    }
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
1
    fn get_source_context(&self) -> (Vec<String>, usize, usize, usize) {
45
1
        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
1
        let mut current_pos = 0;
49
1
        let mut line_num = 0;
50
1
        let mut col_start = 0;
51
        
52
1
        for (i, line) in lines.iter().enumerate() {
53
1
            let line_len = line.len() + 1; // +1 for newline
54
1
            if current_pos + line_len > self.error.span.start {
55
1
                line_num = i;
56
1
                col_start = self.error.span.start - current_pos;
57
1
                break;
58
0
            }
59
0
            current_pos += line_len;
60
        }
61
        
62
        // Calculate error span width
63
1
        let col_end = col_start + (self.error.span.end - self.error.span.start);
64
        
65
        // Get context lines (2 before, 2 after)
66
1
        let context_start = line_num.saturating_sub(2);
67
1
        let context_end = (line_num + 3).min(lines.len());
68
1
        let context_lines = lines[context_start..context_end].to_vec();
69
        
70
1
        (context_lines, line_num - context_start, col_start, col_end)
71
1
    }
72
73
    /// Generate colored output for terminal display
74
1
    pub fn format_colored(&self) -> String {
75
1
        let mut output = String::new();
76
        
77
        // Header with severity and location
78
1
        let severity_color = match self.error.severity {
79
1
            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
1
        let reset = "\x1b[0m";
85
1
        let bold = "\x1b[1m";
86
        
87
        // File and location header
88
1
        if let Some(
ref filename0
) = 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
1
        } else {
101
1
            output.push_str(&format!(
102
1
                "{bold}{severity_color}error[{:?}]{reset}: {}\n",
103
1
                self.error.error_code,
104
1
                self.error.message
105
1
            ));
106
1
        }
107
        
108
        // Source code context with error highlighting
109
1
        let (context_lines, error_line_idx, col_start, col_end) = self.get_source_context();
110
1
        let line_num_start = (self.error.span.start / 100 + 1).saturating_sub(error_line_idx);
111
        
112
3
        for (i, line) in 
context_lines.iter()1
.
enumerate1
() {
113
3
            let line_num = line_num_start + i;
114
3
            let is_error_line = i == error_line_idx;
115
            
116
            // Line number and content
117
3
            if is_error_line {
118
1
                output.push_str(&format!(
119
1
                    "{bold}{line_num:4} |{reset} {line}\n"
120
1
                ));
121
                
122
                // Error underline
123
1
                let spaces = " ".repeat(col_start);
124
1
                let arrows = "^".repeat((col_end - col_start).max(1));
125
1
                output.push_str(&format!(
126
1
                    "     | {spaces}{severity_color}{arrows}\n"
127
1
                ));
128
                
129
                // Error message under the line
130
1
                if let Some(
ref hint0
) = 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
1
                }
139
2
            } else {
140
2
                output.push_str(&format!("{line_num:4} | {line}\n"));
141
2
            }
142
        }
143
        
144
        // Suggestions
145
1
        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
1
        }
154
        
155
1
        output.push_str(reset);
156
1
        output
157
1
    }
158
}
159
160
impl fmt::Display for Diagnostic {
161
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162
1
        write!(f, "{}", self.format_colored())
163
1
    }
164
}
165
166
/// Common error patterns and their suggestions
167
1
pub fn suggest_for_error(error: &ParseError) -> Vec<Suggestion> {
168
1
    let mut suggestions = Vec::new();
169
    
170
    // Common typo suggestions
171
1
    if error.message.contains("unexpected") {
172
1
        if let Some(ref _found) = error.found {
173
1
            // Token-specific suggestions would go here
174
1
            suggestions.push(Suggestion {
175
1
                message: "Check for typos or missing operators".to_string(),
176
1
                replacement: None,
177
1
                span: error.span,
178
1
            });
179
1
        
}0
180
0
    }
181
    
182
    // Missing semicolon suggestion
183
1
    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
1
    }
193
    
194
    // Unclosed delimiter suggestions
195
1
    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
1
    }
225
    
226
1
    suggestions
227
1
}
228
229
#[cfg(test)]
230
mod tests {
231
    use super::*;
232
233
    #[test]
234
1
    fn test_diagnostic_display() {
235
1
        let error = ParseError::new(
236
1
            "Unexpected token".to_string(),
237
1
            Span { start: 10, end: 15 },
238
        );
239
        
240
1
        let source = "let x = 10\nlet y = @invalid\nlet z = 30".to_string();
241
1
        let diag = Diagnostic::new(error, source);
242
        
243
1
        let output = format!("{diag}");
244
1
        assert!(output.contains("Unexpected token"));
245
1
    }
246
247
    #[test]
248
1
    fn test_suggestions() {
249
1
        let mut error = ParseError::new(
250
1
            "unexpected '='".to_string(),
251
1
            Span { start: 5, end: 6 },
252
        );
253
1
        error.found = Some(crate::frontend::lexer::Token::Equal);
254
        
255
1
        let suggestions = suggest_for_error(&error);
256
1
        assert!(!suggestions.is_empty());
257
1
    }
258
}