use super::types::{ErrorCode, ShapeError};
impl ShapeError {
pub fn format_with_source(&self) -> String {
let mut output = String::new();
let location = match self {
ShapeError::ParseError { location, .. }
| ShapeError::LexError { location, .. }
| ShapeError::SemanticError { location, .. }
| ShapeError::RuntimeError { location, .. } => location.as_ref(),
_ => None,
};
output.push_str("error");
if let Some(code) = self.error_code() {
output.push_str(&format!("[{}]", code.as_str()));
}
output.push_str(": ");
output.push_str(&self.diagnostic_message());
output.push('\n');
if let Some(loc) = location {
let file_str = loc.file.as_deref().unwrap_or("<input>");
output.push_str(&format!(
" --> {}:{}:{}
",
file_str, loc.line, loc.column
));
if let Some(source) = &loc.source_line {
let line_num = loc.line.to_string();
let padding = " ".repeat(line_num.len());
output.push_str(&format!(" {} |\n", padding));
output.push_str(&format!(
" {} | {}
",
line_num, source
));
let mut pointer = format!(
" {} | {}",
padding,
" ".repeat(loc.column.saturating_sub(1))
);
pointer.push('^');
if let Some(len) = loc.length {
pointer.push_str(&"~".repeat(len.saturating_sub(1)));
}
output.push_str(&pointer);
output.push('\n');
}
}
if let Some(loc) = location {
for note in &loc.notes {
output.push_str(" |\n");
output.push_str(&format!(
" = note: {}
",
note.message
));
if let Some(note_loc) = ¬e.location {
let file_str = note_loc.file.as_deref().unwrap_or("<input>");
output.push_str(&format!(
" --> {}:{}:{}
",
file_str, note_loc.line, note_loc.column
));
if let Some(source) = ¬e_loc.source_line {
let line_num = note_loc.line.to_string();
let padding = " ".repeat(line_num.len());
output.push_str(&format!(" {} |\n", padding));
output.push_str(&format!(
" {} | {}
",
line_num, source
));
output.push_str(&format!(
" {} | {}-
",
padding,
" ".repeat(note_loc.column.saturating_sub(1))
));
}
}
}
}
if let Some(loc) = location {
for hint in &loc.hints {
output.push_str(" |\n");
output.push_str(&format!(
" = help: {}
",
hint
));
}
}
output
}
fn diagnostic_message(&self) -> String {
match self {
ShapeError::ParseError { message, .. }
| ShapeError::LexError { message, .. }
| ShapeError::SemanticError { message, .. }
| ShapeError::RuntimeError { message, .. }
| ShapeError::ConfigError { message }
| ShapeError::CacheError { message } => message.clone(),
ShapeError::TypeError(message) | ShapeError::VMError(message) => message.clone(),
ShapeError::PatternError { message, .. }
| ShapeError::SimulationError { message, .. }
| ShapeError::DataProviderError { message, .. }
| ShapeError::TestError { message, .. }
| ShapeError::StreamError { message, .. }
| ShapeError::AlignmentError { message, .. } => message.clone(),
ShapeError::DataError { message, .. } | ShapeError::ModuleError { message, .. } => {
message.clone()
}
_ => self.to_string(),
}
}
pub fn error_code(&self) -> Option<ErrorCode> {
match self {
ShapeError::ParseError { message, .. } => {
if message.contains("unexpected") {
Some(ErrorCode::E0001)
} else if message.contains("unterminated") {
Some(ErrorCode::E0002)
} else if message.contains("semicolon") {
Some(ErrorCode::E0004)
} else if message.contains("bracket") || message.contains("paren") {
Some(ErrorCode::E0005)
} else {
Some(ErrorCode::ParseError)
}
}
ShapeError::TypeError(_) => Some(ErrorCode::TypeError),
ShapeError::SemanticError { message, .. } => {
if message.contains("type mismatch") {
Some(ErrorCode::E0100)
} else {
Some(ErrorCode::SemanticError)
}
}
ShapeError::RuntimeError { .. } => Some(ErrorCode::RuntimeError),
ShapeError::DataError { .. } => Some(ErrorCode::DataError),
ShapeError::ModuleError { .. } => Some(ErrorCode::ModuleError),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::SourceLocation;
#[test]
fn semantic_error_code_is_not_unknown() {
let err = ShapeError::SemanticError {
message: "boom".to_string(),
location: Some(SourceLocation::new(2, 4)),
};
let rendered = err.format_with_source();
assert!(rendered.starts_with("error[SEMANTIC]: boom"));
assert!(!rendered.contains("UNKNOWN"));
}
#[test]
fn format_with_source_uses_message_without_redundant_prefix() {
let err = ShapeError::RuntimeError {
message: "division by zero".to_string(),
location: Some(SourceLocation::new(1, 1).with_source_line("1 / 0".to_string())),
};
let rendered = err.format_with_source();
assert!(rendered.contains("error[RUNTIME]: division by zero"));
assert!(!rendered.contains("Runtime error: Runtime error:"));
}
}