Skip to main content

shape_ast/error/
formatting.rs

1//! Error formatting and display utilities
2//!
3//! This module provides functionality for formatting errors with
4//! rich context, source code display, and error codes.
5
6use super::types::{ErrorCode, ShapeError};
7
8impl ShapeError {
9    /// Format the error with source location, hints, and notes (Rust-style)
10    pub fn format_with_source(&self) -> String {
11        let mut output = String::new();
12
13        // Get location if available
14        let location = match self {
15            ShapeError::ParseError { location, .. }
16            | ShapeError::LexError { location, .. }
17            | ShapeError::SemanticError { location, .. }
18            | ShapeError::RuntimeError { location, .. } => location.as_ref(),
19            _ => None,
20        };
21
22        // Error header with location
23        output.push_str("error");
24
25        // Add error code if we can infer it from the error type
26        if let Some(code) = self.error_code() {
27            output.push_str(&format!("[{}]", code.as_str()));
28        }
29        output.push_str(": ");
30        output.push_str(&self.diagnostic_message());
31        output.push('\n');
32
33        // Location arrow
34        if let Some(loc) = location {
35            let file_str = loc.file.as_deref().unwrap_or("<input>");
36            output.push_str(&format!(
37                "  --> {}:{}:{}
38",
39                file_str, loc.line, loc.column
40            ));
41
42            // Source line with line number gutter
43            if let Some(source) = &loc.source_line {
44                let line_num = loc.line.to_string();
45                let padding = " ".repeat(line_num.len());
46
47                output.push_str(&format!("   {} |\n", padding));
48                output.push_str(&format!(
49                    " {} | {}
50",
51                    line_num, source
52                ));
53
54                // Error pointer
55                let mut pointer = format!(
56                    "   {} | {}",
57                    padding,
58                    " ".repeat(loc.column.saturating_sub(1))
59                );
60                pointer.push('^');
61                if let Some(len) = loc.length {
62                    pointer.push_str(&"~".repeat(len.saturating_sub(1)));
63                }
64                output.push_str(&pointer);
65                output.push('\n');
66            }
67        }
68
69        // Display notes
70        if let Some(loc) = location {
71            for note in &loc.notes {
72                output.push_str("   |\n");
73                output.push_str(&format!(
74                    "   = note: {}
75",
76                    note.message
77                ));
78
79                if let Some(note_loc) = &note.location {
80                    let file_str = note_loc.file.as_deref().unwrap_or("<input>");
81                    output.push_str(&format!(
82                        "  --> {}:{}:{}
83",
84                        file_str, note_loc.line, note_loc.column
85                    ));
86
87                    if let Some(source) = &note_loc.source_line {
88                        let line_num = note_loc.line.to_string();
89                        let padding = " ".repeat(line_num.len());
90                        output.push_str(&format!("   {} |\n", padding));
91                        output.push_str(&format!(
92                            " {} | {}
93",
94                            line_num, source
95                        ));
96                        output.push_str(&format!(
97                            "   {} | {}-
98",
99                            padding,
100                            " ".repeat(note_loc.column.saturating_sub(1))
101                        ));
102                    }
103                }
104            }
105        }
106
107        // Display hints
108        if let Some(loc) = location {
109            for hint in &loc.hints {
110                output.push_str("   |\n");
111                output.push_str(&format!(
112                    "   = help: {}
113",
114                    hint
115                ));
116            }
117        }
118
119        output
120    }
121
122    fn diagnostic_message(&self) -> String {
123        match self {
124            ShapeError::ParseError { message, .. }
125            | ShapeError::LexError { message, .. }
126            | ShapeError::SemanticError { message, .. }
127            | ShapeError::RuntimeError { message, .. }
128            | ShapeError::ConfigError { message }
129            | ShapeError::CacheError { message } => message.clone(),
130            ShapeError::TypeError(message) | ShapeError::VMError(message) => message.clone(),
131            ShapeError::PatternError { message, .. }
132            | ShapeError::SimulationError { message, .. }
133            | ShapeError::DataProviderError { message, .. }
134            | ShapeError::TestError { message, .. }
135            | ShapeError::StreamError { message, .. }
136            | ShapeError::AlignmentError { message, .. } => message.clone(),
137            ShapeError::DataError { message, .. } | ShapeError::ModuleError { message, .. } => {
138                message.clone()
139            }
140            _ => self.to_string(),
141        }
142    }
143
144    /// Try to determine an error code based on the error type and message
145    pub fn error_code(&self) -> Option<ErrorCode> {
146        match self {
147            ShapeError::ParseError { message, .. } => {
148                if message.contains("unexpected") {
149                    Some(ErrorCode::E0001)
150                } else if message.contains("unterminated") {
151                    Some(ErrorCode::E0002)
152                } else if message.contains("semicolon") {
153                    Some(ErrorCode::E0004)
154                } else if message.contains("bracket") || message.contains("paren") {
155                    Some(ErrorCode::E0005)
156                } else {
157                    Some(ErrorCode::ParseError)
158                }
159            }
160            ShapeError::TypeError(_) => Some(ErrorCode::TypeError),
161            ShapeError::SemanticError { message, .. } => {
162                if message.contains("type mismatch") {
163                    Some(ErrorCode::E0100)
164                } else {
165                    Some(ErrorCode::SemanticError)
166                }
167            }
168            ShapeError::RuntimeError { .. } => Some(ErrorCode::RuntimeError),
169            ShapeError::DataError { .. } => Some(ErrorCode::DataError),
170            ShapeError::ModuleError { .. } => Some(ErrorCode::ModuleError),
171            _ => None,
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::error::SourceLocation;
180
181    #[test]
182    fn semantic_error_code_is_not_unknown() {
183        let err = ShapeError::SemanticError {
184            message: "boom".to_string(),
185            location: Some(SourceLocation::new(2, 4)),
186        };
187
188        let rendered = err.format_with_source();
189        assert!(rendered.starts_with("error[SEMANTIC]: boom"));
190        assert!(!rendered.contains("UNKNOWN"));
191    }
192
193    #[test]
194    fn format_with_source_uses_message_without_redundant_prefix() {
195        let err = ShapeError::RuntimeError {
196            message: "division by zero".to_string(),
197            location: Some(SourceLocation::new(1, 1).with_source_line("1 / 0".to_string())),
198        };
199
200        let rendered = err.format_with_source();
201        assert!(rendered.contains("error[RUNTIME]: division by zero"));
202        assert!(!rendered.contains("Runtime error: Runtime error:"));
203    }
204}