shape_ast/error/
formatting.rs1use super::types::{ErrorCode, ShapeError};
7
8impl ShapeError {
9 pub fn format_with_source(&self) -> String {
11 let mut output = String::new();
12
13 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 output.push_str("error");
24
25 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 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 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 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 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) = ¬e.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) = ¬e_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 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 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") {
168 Some(ErrorCode::E0100)
169 } else if message.starts_with("Undefined variable")
170 || message.starts_with("Undefined variables")
171 {
172 Some(ErrorCode::E0101)
173 } else {
174 Some(ErrorCode::SemanticError)
175 }
176 }
177 ShapeError::RuntimeError { .. } => Some(ErrorCode::RuntimeError),
178 ShapeError::DataError { .. } => Some(ErrorCode::DataError),
179 ShapeError::ModuleError { .. } => Some(ErrorCode::ModuleError),
180 _ => None,
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::error::SourceLocation;
189
190 #[test]
191 fn semantic_error_code_is_not_unknown() {
192 let err = ShapeError::SemanticError {
193 message: "boom".to_string(),
194 location: Some(SourceLocation::new(2, 4)),
195 };
196
197 let rendered = err.format_with_source();
198 assert!(rendered.starts_with("error[SEMANTIC]: boom"));
199 assert!(!rendered.contains("UNKNOWN"));
200 }
201
202 #[test]
203 fn format_with_source_uses_message_without_redundant_prefix() {
204 let err = ShapeError::RuntimeError {
205 message: "division by zero".to_string(),
206 location: Some(SourceLocation::new(1, 1).with_source_line("1 / 0".to_string())),
207 };
208
209 let rendered = err.format_with_source();
210 assert!(rendered.contains("error[RUNTIME]: division by zero"));
211 assert!(!rendered.contains("Runtime error: Runtime error:"));
212 }
213
214 #[test]
219 fn semantic_error_code_undefined_variable_maps_to_e0101() {
220 let single = ShapeError::SemanticError {
221 message: "Undefined variable: 'zzz_undefined'".to_string(),
222 location: None,
223 };
224 assert!(matches!(single.error_code(), Some(ErrorCode::E0101)));
225
226 let multi = ShapeError::SemanticError {
227 message: "Undefined variables: 'h', 'i'".to_string(),
228 location: None,
229 };
230 assert!(matches!(multi.error_code(), Some(ErrorCode::E0101)));
231
232 let other = ShapeError::SemanticError {
234 message: "some other semantic error".to_string(),
235 location: None,
236 };
237 assert!(matches!(other.error_code(), Some(ErrorCode::SemanticError)));
238 }
239}