#[cfg(feature = "cli")]
use crate::error::suggest_fix;
use crate::error_codes;
use crate::fuzzy_matcher::levenshtein_distance;
use crate::source_map::{Location, SourceMap};
use crate::syntax_highlighter::SyntaxHighlighter;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CargoMessage {
pub reason: String,
pub message: Option<RustcDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RustcDiagnostic {
pub message: String,
pub level: String,
pub spans: Vec<RustcSpan>,
pub code: Option<RustcCode>,
pub children: Vec<RustcDiagnostic>,
pub rendered: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RustcSpan {
pub file_name: String,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
pub is_primary: bool,
pub label: Option<String>,
pub suggested_replacement: Option<String>,
pub text: Option<Vec<RustcSpanText>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RustcSpanText {
pub text: String,
pub highlight_start: usize,
pub highlight_end: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RustcCode {
pub code: String,
pub explanation: Option<String>,
}
#[derive(Debug, Clone)]
pub struct WindjammerDiagnostic {
pub message: String,
pub level: DiagnosticLevel,
pub location: Location,
pub spans: Vec<DiagnosticSpan>,
pub code: Option<String>,
pub help: Vec<String>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticLevel {
Error,
Warning,
Note,
Help,
}
#[derive(Debug, Clone)]
pub struct DiagnosticSpan {
pub location: Location,
pub label: Option<String>,
pub is_primary: bool,
}
pub struct ErrorMapper {
source_map: SourceMap,
}
impl ErrorMapper {
pub fn new(source_map: SourceMap) -> Self {
Self { source_map }
}
pub fn map_rustc_output(&self, json_output: &str) -> Vec<WindjammerDiagnostic> {
let mut diagnostics = Vec::new();
for line in json_output.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(cargo_msg) = serde_json::from_str::<CargoMessage>(line) {
if cargo_msg.reason == "compiler-message" {
if let Some(rustc_diag) = cargo_msg.message {
if rustc_diag.level == "error" || rustc_diag.level == "warning" {
if let Some(wj_diag) = self.map_diagnostic(&rustc_diag) {
diagnostics.push(wj_diag);
}
}
}
}
}
}
diagnostics
}
fn map_diagnostic(&self, rustc_diag: &RustcDiagnostic) -> Option<WindjammerDiagnostic> {
let primary_span = rustc_diag.spans.iter().find(|s| s.is_primary)?;
let rust_location = Location {
file: PathBuf::from(&primary_span.file_name),
line: primary_span.line_start,
column: primary_span.column_start,
};
let wj_location = self
.source_map
.map_rust_to_windjammer(&rust_location)
.unwrap_or_else(|| {
let wj_file = self
.source_map
.mappings_for_rust_file(&rust_location.file)
.first()
.map(|m| m.wj_file.clone())
.unwrap_or_else(|| {
let mut wj_path = rust_location.file.clone();
wj_path.set_extension("wj");
wj_path
});
Location {
file: wj_file,
line: rust_location.line,
column: rust_location.column,
}
});
let message =
self.translate_message_with_context(&rustc_diag.message, primary_span.label.as_deref());
let spans = rustc_diag
.spans
.iter()
.filter_map(|span| self.map_span(span))
.collect();
let mut help = Vec::new();
let mut notes = Vec::new();
for child in &rustc_diag.children {
match child.level.as_str() {
"help" => help.push(child.message.clone()),
"note" => notes.push(child.message.clone()),
_ => {}
}
}
if message.contains("No field `") && !notes.is_empty() {
if let Some(field_name) = self.extract_between(&message, "No field `", "`") {
for note in ¬es {
if note.contains("has fields") {
let fields = Self::parse_struct_fields_from_note(note);
if let Some(suggestion) = Self::fuzzy_match_field(&field_name, &fields) {
help.push(format!("did you mean `{}`?", suggestion));
break;
}
}
}
}
}
let wj_code = rustc_diag.code.as_ref().and_then(|c| {
let registry = error_codes::get_registry();
registry.map_rust_code(&c.code).map(|wj| wj.code.clone())
});
#[cfg(feature = "cli")]
if help.is_empty() {
if let Some(code) = rustc_diag.code.as_ref().map(|c| c.code.as_str()) {
if let Some(suggestion) = suggest_fix(code, &message) {
help.push(suggestion);
}
}
}
Some(WindjammerDiagnostic {
message,
level: match rustc_diag.level.as_str() {
"error" => DiagnosticLevel::Error,
"warning" => DiagnosticLevel::Warning,
"note" => DiagnosticLevel::Note,
"help" => DiagnosticLevel::Help,
_ => DiagnosticLevel::Error,
},
location: wj_location,
spans,
code: wj_code.or_else(|| rustc_diag.code.as_ref().map(|c| c.code.clone())),
help,
notes,
})
}
fn map_span(&self, span: &RustcSpan) -> Option<DiagnosticSpan> {
let rust_location = Location {
file: PathBuf::from(&span.file_name),
line: span.line_start,
column: span.column_start,
};
let wj_location = self.source_map.map_rust_to_windjammer(&rust_location)?;
Some(DiagnosticSpan {
location: wj_location,
label: span.label.clone(),
is_primary: span.is_primary,
})
}
#[allow(dead_code)]
fn translate_message(&self, rust_msg: &str) -> String {
self.translate_message_with_context(rust_msg, None)
}
fn translate_message_with_context(&self, rust_msg: &str, span_label: Option<&str>) -> String {
if rust_msg.contains("mismatched types") {
let context = span_label.unwrap_or(rust_msg);
return self.translate_type_mismatch(context);
}
if rust_msg.contains("cannot find type") {
return self.translate_type_not_found(rust_msg);
}
if rust_msg.contains("cannot find value") || rust_msg.contains("cannot find function") {
return self.translate_value_not_found(rust_msg);
}
if rust_msg.contains("no field") && rust_msg.contains("on type") {
return self.translate_missing_field(rust_msg, span_label);
}
if rust_msg.contains("cannot move out of") {
if let Some(name) = self.extract_between(rust_msg, "cannot move out of `", "`") {
return format!(
"Cannot move `{}` because it is borrowed. Consider using .clone() to create a copy.",
name
);
}
return "Ownership error: Cannot move value because it is borrowed. Consider using .clone()".to_string();
}
if rust_msg.contains("cannot borrow") && rust_msg.contains("as mutable") {
return "Cannot modify: This value is not declared as mutable. Add `mut`: let mut x = ...".to_string();
}
if rust_msg.contains("use of moved value") {
if let Some(name) = self.extract_between(rust_msg, "use of moved value: `", "`") {
return format!(
"Cannot use `{}` because it was already moved. Consider cloning before the move: {}.clone()",
name, name
);
}
return "Ownership error: This value was already used and cannot be used again. Consider cloning: value.clone()".to_string();
}
if rust_msg.contains("trait bounds were not satisfied") {
return self.translate_trait_bounds(rust_msg);
}
if rust_msg.contains("the trait") && rust_msg.contains("is not implemented") {
return self.translate_trait_not_implemented(rust_msg);
}
if rust_msg.contains("lifetime") {
return self.translate_lifetime_error(rust_msg);
}
if rust_msg.contains("expected") && rust_msg.contains("found") {
return self.translate_syntax_error(rust_msg);
}
if rust_msg.contains("unresolved import") {
return "Import error: Module or item not found".to_string();
}
rust_msg.to_string()
}
fn translate_type_mismatch(&self, rust_msg: &str) -> String {
if let (Some(expected), Some(found)) = (
self.extract_between(rust_msg, "expected `", "`"),
self.extract_between(rust_msg, "found `", "`"),
) {
let expected_wj = self.rust_type_to_windjammer(&expected);
let found_wj = self.rust_type_to_windjammer(&found);
return format!(
"Type mismatch: expected {}, found {}",
expected_wj, found_wj
);
}
"Type mismatch: The types don't match".to_string()
}
fn translate_type_not_found(&self, rust_msg: &str) -> String {
if let Some(type_name) = self.extract_between(rust_msg, "cannot find type `", "`") {
let wj_type = self.rust_type_to_windjammer(&type_name);
return format!("Type not found: {}", wj_type);
}
"Type not found".to_string()
}
fn translate_value_not_found(&self, rust_msg: &str) -> String {
if rust_msg.contains("cannot find function") {
if let Some(func_name) = self.extract_between(rust_msg, "function `", "`") {
return format!("Function not found: {}", func_name);
}
return "Function not found".to_string();
}
if let Some(value_name) = self.extract_between(rust_msg, "value `", "`") {
return format!("Variable not found: {}", value_name);
}
"Value not found".to_string()
}
fn translate_trait_bounds(&self, _rust_msg: &str) -> String {
"Trait constraint not satisfied: This type doesn't implement the required trait".to_string()
}
fn translate_trait_not_implemented(&self, rust_msg: &str) -> String {
let trait_name = self.extract_between(rust_msg, "trait `", "`");
let type_name = self.extract_between(rust_msg, "for `", "`");
match (trait_name, type_name) {
(Some(t), Some(ty)) => format!("`{}` doesn't implement `{}`", ty, t),
(Some(t), None) => format!("Missing trait implementation: {}", t),
_ => "Missing trait implementation".to_string(),
}
}
fn translate_missing_field(&self, rust_msg: &str, _span_label: Option<&str>) -> String {
let field_name = self.extract_between(rust_msg, "no field `", "`");
let type_name = self.extract_between(rust_msg, "on type `", "`");
match (field_name, type_name) {
(Some(f), Some(t)) => format!("No field `{}` on struct `{}`", f, t),
(Some(f), None) => format!("No field `{}` on this type", f),
_ => "Field not found".to_string(),
}
}
fn translate_lifetime_error(&self, _rust_msg: &str) -> String {
"Lifetime error: The value doesn't live long enough".to_string()
}
fn translate_syntax_error(&self, rust_msg: &str) -> String {
if let (Some(expected), Some(found)) = (
self.extract_between(rust_msg, "expected ", ","),
self.extract_between(rust_msg, "found ", "\n"),
) {
return format!(
"Syntax error: expected {}, found {}",
expected.trim(),
found.trim()
);
}
"Syntax error".to_string()
}
fn rust_type_to_windjammer(&self, rust_type: &str) -> String {
let mapped = crate::type_classification::rust_type_to_windjammer(rust_type);
if mapped != rust_type {
return mapped.to_string();
}
if let Some(stripped) = rust_type.strip_prefix('&') {
return format!("&{}", self.rust_type_to_windjammer(stripped));
}
if rust_type.starts_with("Option<") {
if let Some(inner) = self.extract_between(rust_type, "Option<", ">") {
return format!("{}?", self.rust_type_to_windjammer(&inner));
}
}
if rust_type.starts_with("Vec<") {
if let Some(inner) = self.extract_between(rust_type, "Vec<", ">") {
return format!("[{}]", self.rust_type_to_windjammer(&inner));
}
}
rust_type.to_string()
}
fn extract_between(&self, text: &str, start: &str, end: &str) -> Option<String> {
let start_idx = text.find(start)? + start.len();
let remaining = &text[start_idx..];
let end_idx = remaining.find(end)?;
Some(remaining[..end_idx].to_string())
}
fn parse_struct_fields_from_note(note: &str) -> Vec<String> {
let after_has_fields = note.split("has fields").nth(1).unwrap_or(note);
let mut fields = Vec::new();
let mut remaining = after_has_fields;
while let Some(start) = remaining.find('`') {
remaining = &remaining[start + 1..];
if let Some(end) = remaining.find('`') {
let field = remaining[..end].trim().to_string();
if !field.is_empty() && field.chars().all(|c| c.is_alphanumeric() || c == '_') {
fields.push(field);
}
remaining = &remaining[end + 1..];
} else {
break;
}
}
fields
}
fn fuzzy_match_field(typo: &str, fields: &[String]) -> Option<String> {
let mut best: Option<(String, usize)> = None;
for field in fields {
let d = levenshtein_distance(typo, field);
let max_distance = std::cmp::min(3, std::cmp::max(typo.len(), field.len()) * 3 / 10);
if d <= max_distance && d > 0 && best.as_ref().map(|(_, bd)| d < *bd).unwrap_or(true) {
best = Some((field.clone(), d));
}
}
best.map(|(s, _)| s)
}
}
impl WindjammerDiagnostic {
pub fn is_fixable(&self) -> bool {
if let Some(code) = &self.code {
matches!(code.as_str(), "E0384" | "E0308" | "E0425" | "E0596")
} else {
false
}
}
pub fn get_fix(&self) -> Option<crate::auto_fix::FixType> {
use crate::auto_fix::FixType;
if !self.is_fixable() {
return None;
}
match self.code.as_ref()?.as_str() {
"E0384" | "E0596" => {
extract_variable_from_message(&self.message).map(|var_name| FixType::AddMut {
file: self.location.file.clone(),
line: self.location.line,
variable_name: var_name,
})
}
"E0308" => {
if self.message.contains("expected int") && self.message.contains("found string") {
Some(FixType::AddParse {
file: self.location.file.clone(),
line: self.location.line,
column: self.location.column,
expression: "value".to_string(),
})
} else if self.message.contains("expected String")
&& self.message.contains("found &str")
{
Some(FixType::AddToString {
file: self.location.file.clone(),
line: self.location.line,
column: self.location.column,
expression: "value".to_string(),
})
} else {
None
}
}
_ => None,
}
}
pub fn format(&self) -> String {
use colored::*;
let mut output = String::new();
let level_str = match self.level {
DiagnosticLevel::Error => "error".red().bold(),
DiagnosticLevel::Warning => "warning".yellow().bold(),
DiagnosticLevel::Note => "note".blue().bold(),
DiagnosticLevel::Help => "help".cyan().bold(),
};
if let Some(code) = &self.code {
if code.starts_with("WJ") {
output.push_str(&format!(
"{}[{}]: {}\n",
level_str,
code.cyan().bold(),
self.message
));
output.push_str(&format!(" {} wj explain {}\n", "💡".yellow(), code));
} else {
output.push_str(&format!("{}[{}]: {}\n", level_str, code, self.message));
}
} else {
output.push_str(&format!("{}: {}\n", level_str, self.message));
}
output.push_str(&format!(
" {} {}:{}:{}\n",
"-->".cyan(),
self.location.file.display(),
self.location.line,
self.location.column
));
if let Ok(snippet) = self.read_source_snippet() {
output.push_str(&snippet);
}
for help_msg in &self.help {
output.push_str(&format!(" = {}: {}\n", "help".cyan(), help_msg));
}
for note in &self.notes {
output.push_str(&format!(" = {}: {}\n", "note".blue(), note));
}
if let Some(contextual_help) = self.get_contextual_help() {
output.push_str(&format!(
" = {}: {}\n",
"suggestion".green().bold(),
contextual_help
));
}
output
}
fn read_source_snippet(&self) -> Result<String, std::io::Error> {
use colored::*;
use std::fs;
let source = fs::read_to_string(&self.location.file)?;
let lines: Vec<&str> = source.lines().collect();
let mut output = String::new();
output.push_str(&format!(" {}\n", "|".cyan()));
let highlighter = SyntaxHighlighter::new();
let start_line = self.location.line.saturating_sub(2);
let end_line = (self.location.line + 2).min(lines.len());
for line_num in start_line..=end_line {
if line_num == 0 || line_num > lines.len() {
continue;
}
let line = lines[line_num - 1];
let is_error_line = line_num == self.location.line;
let highlighted_line = highlighter.highlight_line(line);
if is_error_line {
let pointer_color = match self.level {
DiagnosticLevel::Error => "^".red().bold(),
DiagnosticLevel::Warning => "^".yellow().bold(),
_ => "^".cyan(),
};
output.push_str(&format!(
"{:>4} {} {}\n",
line_num.to_string().cyan(),
"|".cyan(),
highlighted_line
));
output.push_str(&format!(
" {} {}{}\n",
"|".cyan(),
" ".repeat(self.location.column.saturating_sub(1)),
pointer_color
));
} else {
output.push_str(&format!(
"{:>4} {} {}\n",
line_num.to_string().cyan(),
"|".cyan(),
highlighted_line
));
}
}
output.push_str(&format!(" {}\n", "|".cyan()));
Ok(output)
}
fn get_contextual_help(&self) -> Option<String> {
let msg = &self.message.to_lowercase();
if msg.contains("type mismatch") {
if msg.contains("expected int") && msg.contains("found string") {
return Some(
"Use .parse() to convert a string to an integer, e.g., \"42\".parse()"
.to_string(),
);
}
if msg.contains("expected string") && msg.contains("found int") {
return Some(
"Use .to_string() to convert an integer to a string, e.g., 42.to_string()"
.to_string(),
);
}
if msg.contains("expected int") && msg.contains("found float") {
return Some(
"Convert to integer: use `value as int` or `value.round() as int`".to_string(),
);
}
if msg.contains("expected float") && msg.contains("found int") {
return Some(
"Convert to float: add .0 to literal (e.g., 3.0) or use `value as float`"
.to_string(),
);
}
if msg.contains("expected &") {
return Some("Add & before the value to create a reference".to_string());
}
}
if msg.contains("function not found") {
return Some(
"Check the function name spelling and ensure the module is imported".to_string(),
);
}
if msg.contains("variable not found") {
return Some(
"Check the variable name spelling and ensure it's declared before use".to_string(),
);
}
if msg.contains("ownership error")
|| msg.contains("cannot move")
|| msg.contains("cannot use")
|| msg.contains("already moved")
{
return Some(
"Consider cloning: use value.clone() to create a copy before moving".to_string(),
);
}
if msg.contains("cannot modify") {
return Some("Declare the variable as mutable: let mut x = ...".to_string());
}
if msg.contains("import error") {
return Some("Use 'use module::item' to import, or check if the module exists in your project or stdlib".to_string());
}
if msg.contains("doesn't implement") || msg.contains("trait") && msg.contains("implement") {
return Some(
"Implement the trait: add `impl TraitName for YourType { fn required_method(self) -> ReturnType { ... } }`"
.to_string(),
);
}
None
}
}
fn extract_variable_from_message(msg: &str) -> Option<String> {
if let Some(start) = msg.find("`") {
if let Some(end) = msg[start + 1..].find("`") {
return Some(msg[start + 1..start + 1 + end].to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rustc_json() {
let json = r#"{"message":"mismatched types","level":"error","spans":[{"file_name":"test.rs","line_start":10,"line_end":10,"column_start":5,"column_end":10,"is_primary":true,"label":"expected i32, found &str","text":null}],"code":{"code":"E0308"},"children":[],"rendered":null}"#;
let diag: RustcDiagnostic = serde_json::from_str(json).unwrap();
assert_eq!(diag.message, "mismatched types");
assert_eq!(diag.level, "error");
assert_eq!(diag.spans.len(), 1);
assert_eq!(diag.spans[0].line_start, 10);
}
#[test]
fn test_diagnostic_format() {
colored::control::set_override(false);
let diag = WindjammerDiagnostic {
message: "Type mismatch".to_string(),
level: DiagnosticLevel::Error,
location: Location {
file: PathBuf::from("test.wj"),
line: 10,
column: 5,
},
spans: vec![],
code: Some("E0308".to_string()),
help: vec!["Try using .parse()".to_string()],
notes: vec![],
};
let formatted = diag.format();
assert!(
formatted.contains("error[E0308]"),
"Expected 'error[E0308]' in:\n{}",
formatted
);
assert!(
formatted.contains("test.wj:10:5"),
"Expected 'test.wj:10:5' in:\n{}",
formatted
);
assert!(
formatted.contains("help: Try using .parse()"),
"Expected 'help: Try using .parse()' in:\n{}",
formatted
);
colored::control::unset_override();
}
#[test]
fn test_rust_type_to_windjammer() {
let mapper = ErrorMapper::new(SourceMap::new());
assert_eq!(mapper.rust_type_to_windjammer("i32"), "int");
assert_eq!(mapper.rust_type_to_windjammer("i64"), "int");
assert_eq!(mapper.rust_type_to_windjammer("&str"), "string");
assert_eq!(mapper.rust_type_to_windjammer("String"), "string");
assert_eq!(mapper.rust_type_to_windjammer("bool"), "bool");
assert_eq!(mapper.rust_type_to_windjammer("f64"), "float");
assert_eq!(mapper.rust_type_to_windjammer("()"), "void");
}
#[test]
fn test_rust_type_to_windjammer_complex() {
let mapper = ErrorMapper::new(SourceMap::new());
assert_eq!(mapper.rust_type_to_windjammer("&i32"), "&int");
assert_eq!(mapper.rust_type_to_windjammer("Vec<i32>"), "[int]");
assert_eq!(mapper.rust_type_to_windjammer("Option<String>"), "string?");
}
#[test]
fn test_translate_type_mismatch() {
let mapper = ErrorMapper::new(SourceMap::new());
let rust_msg = "mismatched types: expected `i32`, found `&str`";
let translated = mapper.translate_message(rust_msg);
assert!(translated.contains("Type mismatch"));
assert!(translated.contains("int"));
assert!(translated.contains("string"));
}
#[test]
fn test_translate_function_not_found() {
let mapper = ErrorMapper::new(SourceMap::new());
let rust_msg = "cannot find function `foo` in this scope";
let translated = mapper.translate_message(rust_msg);
assert!(translated.contains("Function not found"));
assert!(translated.contains("foo"));
}
#[test]
fn test_translate_ownership_error() {
let mapper = ErrorMapper::new(SourceMap::new());
let rust_msg = "use of moved value: `x`";
let translated = mapper.translate_message(rust_msg);
assert!(
translated.contains("moved") || translated.contains("Ownership"),
"Should explain move/ownership. Got: {}",
translated
);
assert!(translated.contains("x"), "Should mention the value name");
}
#[test]
fn test_contextual_help_type_mismatch() {
let diag = WindjammerDiagnostic {
message: "Type mismatch: expected int, found string".to_string(),
level: DiagnosticLevel::Error,
location: Location {
file: PathBuf::from("test.wj"),
line: 10,
column: 5,
},
spans: vec![],
code: None,
help: vec![],
notes: vec![],
};
let help = diag.get_contextual_help();
assert!(help.is_some());
assert!(help.unwrap().contains(".parse()"));
}
#[test]
fn test_contextual_help_mutability() {
let diag = WindjammerDiagnostic {
message: "Cannot modify: This value is not declared as mutable".to_string(),
level: DiagnosticLevel::Error,
location: Location {
file: PathBuf::from("test.wj"),
line: 10,
column: 5,
},
spans: vec![],
code: None,
help: vec![],
notes: vec![],
};
let help = diag.get_contextual_help();
assert!(help.is_some());
assert!(help.unwrap().contains("let mut"));
}
#[test]
fn test_extract_between() {
let mapper = ErrorMapper::new(SourceMap::new());
let text = "expected `i32`, found `&str`";
let expected = mapper.extract_between(text, "expected `", "`");
assert_eq!(expected, Some("i32".to_string()));
let found = mapper.extract_between(text, "found `", "`");
assert_eq!(found, Some("&str".to_string()));
}
}