## 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
    }