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") {
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}