use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorStatistics {
pub total_compilations: usize,
pub total_errors: usize,
pub errors_by_code: HashMap<String, ErrorCodeStats>,
pub errors_by_file: HashMap<PathBuf, usize>,
pub errors_by_type: HashMap<String, usize>,
pub recent_errors: Vec<ErrorRecord>,
pub started_at: SystemTime,
pub updated_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorCodeStats {
pub code: String,
pub count: usize,
pub message: String,
pub first_seen: SystemTime,
pub last_seen: SystemTime,
pub files: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorRecord {
pub code: Option<String>,
pub message: String,
pub file: PathBuf,
pub line: usize,
pub timestamp: SystemTime,
pub fixed: bool,
}
impl ErrorStatistics {
pub fn new() -> Self {
let now = SystemTime::now();
Self {
total_compilations: 0,
total_errors: 0,
errors_by_code: HashMap::new(),
errors_by_file: HashMap::new(),
errors_by_type: HashMap::new(),
recent_errors: Vec::new(),
started_at: now,
updated_at: now,
}
}
pub fn load(path: &Path) -> anyhow::Result<Self> {
let content = fs::read_to_string(path)?;
let stats: ErrorStatistics = serde_json::from_str(&content)?;
Ok(stats)
}
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
let content = serde_json::to_string_pretty(self)?;
fs::write(path, content)?;
Ok(())
}
pub fn record_compilation(&mut self) {
self.total_compilations += 1;
self.updated_at = SystemTime::now();
}
pub fn record_error(
&mut self,
code: Option<String>,
message: String,
file: PathBuf,
line: usize,
) {
self.total_errors += 1;
self.updated_at = SystemTime::now();
if let Some(ref error_code) = code {
let stats = self
.errors_by_code
.entry(error_code.clone())
.or_insert_with(|| ErrorCodeStats {
code: error_code.clone(),
count: 0,
message: message.clone(),
first_seen: SystemTime::now(),
last_seen: SystemTime::now(),
files: Vec::new(),
});
stats.count += 1;
stats.last_seen = SystemTime::now();
if !stats.files.contains(&file) {
stats.files.push(file.clone());
}
}
*self.errors_by_file.entry(file.clone()).or_insert(0) += 1;
let error_type = if code.is_some() { "error" } else { "warning" };
*self
.errors_by_type
.entry(error_type.to_string())
.or_insert(0) += 1;
self.recent_errors.push(ErrorRecord {
code,
message,
file,
line,
timestamp: SystemTime::now(),
fixed: false,
});
if self.recent_errors.len() > 100 {
self.recent_errors.remove(0);
}
}
pub fn get_top_errors(&self, limit: usize) -> Vec<(&ErrorCodeStats, usize)> {
let mut errors: Vec<_> = self
.errors_by_code
.values()
.map(|stats| (stats, stats.count))
.collect();
errors.sort_by_key(|b| std::cmp::Reverse(b.1));
errors.truncate(limit);
errors
}
pub fn get_top_files(&self, limit: usize) -> Vec<(&PathBuf, usize)> {
let mut files: Vec<_> = self.errors_by_file.iter().map(|(k, v)| (k, *v)).collect();
files.sort_by_key(|b| std::cmp::Reverse(b.1));
files.truncate(limit);
files
}
pub fn get_error_rate(&self) -> f64 {
if self.total_compilations == 0 {
0.0
} else {
self.total_errors as f64 / self.total_compilations as f64
}
}
pub fn get_summary(&self) -> StatsSummary {
let duration = self
.updated_at
.duration_since(self.started_at)
.unwrap_or_default();
StatsSummary {
total_compilations: self.total_compilations,
total_errors: self.total_errors,
unique_error_codes: self.errors_by_code.len(),
unique_files: self.errors_by_file.len(),
error_rate: self.get_error_rate(),
duration_days: duration.as_secs() / 86400,
most_common_error: self
.get_top_errors(1)
.first()
.map(|(stats, _)| stats.code.clone()),
}
}
pub fn format(&self) -> String {
use colored::*;
let mut output = String::new();
output.push_str(&format!(
"\n{}\n",
"📊 Windjammer Error Statistics".cyan().bold()
));
output.push_str(&format!("{}\n\n", "=".repeat(50)));
let summary = self.get_summary();
output.push_str(&format!("{}\n", "Summary:".yellow().bold()));
output.push_str(&format!(
" Total Compilations: {}\n",
summary.total_compilations
));
output.push_str(&format!(" Total Errors: {}\n", summary.total_errors));
output.push_str(&format!(
" Error Rate: {:.2} errors/compilation\n",
summary.error_rate
));
output.push_str(&format!(
" Tracking Period: {} days\n\n",
summary.duration_days
));
output.push_str(&format!(
"{}\n",
"Top 5 Most Common Errors:".yellow().bold()
));
let top_errors = self.get_top_errors(5);
if top_errors.is_empty() {
output.push_str(" No errors recorded yet\n");
} else {
for (i, (stats, count)) in top_errors.iter().enumerate() {
output.push_str(&format!(
" {}. {} - {} occurrences\n",
i + 1,
stats.code.green(),
count
));
output.push_str(&format!(" {}\n", stats.message.dimmed()));
}
}
output.push('\n');
output.push_str(&format!(
"{}\n",
"Top 5 Most Error-Prone Files:".yellow().bold()
));
let top_files = self.get_top_files(5);
if top_files.is_empty() {
output.push_str(" No files recorded yet\n");
} else {
for (i, (file, count)) in top_files.iter().enumerate() {
output.push_str(&format!(
" {}. {} - {} errors\n",
i + 1,
file.display(),
count
));
}
}
output.push('\n');
output.push_str(&format!("{}\n", "Recent Errors (last 5):".yellow().bold()));
let recent = self.recent_errors.iter().rev().take(5).collect::<Vec<_>>();
if recent.is_empty() {
output.push_str(" No recent errors\n");
} else {
for (i, record) in recent.iter().enumerate() {
let code_str = record
.code
.as_ref()
.map(|c| format!("[{}]", c))
.unwrap_or_default();
output.push_str(&format!(
" {}. {} {} at {}:{}\n",
i + 1,
code_str.green(),
record.message,
record.file.display(),
record.line
));
}
}
output
}
pub fn clear(&mut self) {
*self = Self::new();
}
}
impl Default for ErrorStatistics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct StatsSummary {
pub total_compilations: usize,
pub total_errors: usize,
pub unique_error_codes: usize,
pub unique_files: usize,
pub error_rate: f64,
pub duration_days: u64,
pub most_common_error: Option<String>,
}
pub fn get_stats_file_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".windjammer").join("stats.json")
}
pub fn load_or_create_stats() -> ErrorStatistics {
let path = get_stats_file_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
ErrorStatistics::load(&path).unwrap_or_else(|_| ErrorStatistics::new())
}
pub fn save_stats(stats: &ErrorStatistics) -> anyhow::Result<()> {
let path = get_stats_file_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
stats.save(&path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_statistics_creation() {
let stats = ErrorStatistics::new();
assert_eq!(stats.total_compilations, 0);
assert_eq!(stats.total_errors, 0);
}
#[test]
fn test_record_compilation() {
let mut stats = ErrorStatistics::new();
stats.record_compilation();
assert_eq!(stats.total_compilations, 1);
}
#[test]
fn test_record_error() {
let mut stats = ErrorStatistics::new();
stats.record_error(
Some("E0425".to_string()),
"Variable not found".to_string(),
PathBuf::from("test.wj"),
10,
);
assert_eq!(stats.total_errors, 1);
assert_eq!(stats.errors_by_code.len(), 1);
assert!(stats.errors_by_code.contains_key("E0425"));
}
#[test]
fn test_top_errors() {
let mut stats = ErrorStatistics::new();
for _ in 0..5 {
stats.record_error(
Some("E0425".to_string()),
"Variable not found".to_string(),
PathBuf::from("test.wj"),
10,
);
}
for _ in 0..3 {
stats.record_error(
Some("E0308".to_string()),
"Type mismatch".to_string(),
PathBuf::from("test.wj"),
20,
);
}
let top = stats.get_top_errors(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0.code, "E0425");
assert_eq!(top[0].1, 5);
}
#[test]
fn test_error_rate() {
let mut stats = ErrorStatistics::new();
stats.record_compilation();
stats.record_compilation();
stats.record_error(
Some("E0425".to_string()),
"Variable not found".to_string(),
PathBuf::from("test.wj"),
10,
);
assert_eq!(stats.get_error_rate(), 0.5); }
#[test]
fn test_recent_errors_limit() {
let mut stats = ErrorStatistics::new();
for i in 0..150 {
stats.record_error(
Some(format!("E{:04}", i)),
"Test error".to_string(),
PathBuf::from("test.wj"),
i,
);
}
assert_eq!(stats.recent_errors.len(), 100);
}
}