use anyhow::Result;
use rayon::prelude::*;
use std::collections::HashMap;
use std::fs;
use crate::languages::{LanguageInfo, LanguageRegistry};
use crate::scanner::FileInfo;
#[derive(Debug)]
pub struct LanguageStats {
pub language: String,
pub file_count: usize,
pub total_bytes: u64,
pub percentage: f64,
pub files: Vec<String>,
pub color: String,
}
#[derive(Debug)]
pub struct Analysis {
pub stats: Vec<LanguageStats>,
pub total_files: usize,
pub total_bytes: u64,
pub unknown_files: Vec<String>,
}
pub struct LanguageDetector {
registry: LanguageRegistry,
}
impl LanguageDetector {
pub fn new() -> Result<Self> {
Ok(Self {
registry: LanguageRegistry::new()
.unwrap_or_else(|_| LanguageRegistry::from_default()),
})
}
pub fn analyze_files(&self, files: &[FileInfo]) -> Result<Analysis> {
let mut language_stats: HashMap<String, LanguageStats> = HashMap::new();
let mut unknown_files = Vec::new();
let mut total_bytes = 0u64;
let results: Vec<_> = files
.par_iter()
.filter(|file| !file.is_binary)
.map(|file| self.detect_language(file))
.collect();
for (file, detected_language) in results {
total_bytes += file.size;
if let Some(lang_info) = detected_language {
let stats = language_stats
.entry(lang_info.name.clone())
.or_insert_with(|| LanguageStats {
language: lang_info.name.clone(),
file_count: 0,
total_bytes: 0,
percentage: 0.0,
files: Vec::new(),
color: lang_info.color.clone(),
});
stats.file_count += 1;
stats.total_bytes += file.size;
stats.files.push(file.path.to_string_lossy().to_string());
} else {
unknown_files.push(file.path.to_string_lossy().to_string());
}
}
let total_files = files.len() - files.iter().filter(|f| f.is_binary).count();
let mut stats: Vec<LanguageStats> = language_stats.into_values().collect();
for stat in &mut stats {
stat.percentage = (stat.file_count as f64 / total_files as f64) * 100.0;
}
stats.sort_by(|a, b| b.file_count.cmp(&a.file_count));
Ok(Analysis {
stats,
total_files,
total_bytes,
unknown_files,
})
}
fn detect_language<'a>(&self, file: &'a FileInfo) -> (&'a FileInfo, Option<&LanguageInfo>) {
if let Some(ref extension) = file.extension {
let candidate_languages = self.registry.get_languages_by_extension(extension);
if candidate_languages.len() == 1 {
return (file, Some(candidate_languages[0]));
} else if candidate_languages.len() > 1 {
if let Ok(content) = fs::read_to_string(&file.path) {
let filename = file.path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if let Some(lang_info) = self.registry.detect_by_content_among_candidates(&content, filename, &candidate_languages) {
return (file, Some(lang_info));
}
return (file, Some(candidate_languages[0]));
}
return (file, Some(candidate_languages[0]));
}
}
if let Ok(content) = fs::read_to_string(&file.path) {
let filename = file.path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if let Some(lang_info) = self.registry.detect_by_content(&content, filename) {
return (file, Some(lang_info));
}
}
if file.extension.is_none() {
if let Some(filename) = file.path.file_name().and_then(|n| n.to_str()) {
let special_files = [
("Dockerfile", "Dockerfile"),
("Makefile", "Makefile"),
("Rakefile", "Ruby"),
("Gemfile", "Ruby"),
("Podfile", "Ruby"),
("CMakeLists.txt", "CMake"),
];
for (pattern, language) in &special_files {
if filename.eq_ignore_ascii_case(pattern) {
if let Some(lang_info) = self.registry.get_language_by_name(language) {
return (file, Some(lang_info));
}
}
}
}
}
(file, None)
}
}