sherlock-io 0.1.0

SherlockIO - Lightning-fast language detection CLI tool
Documentation
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;

        // Process files in parallel for better performance
        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());
            }
        }

        // Calculate percentages
        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;
        }

        // Sort by file count (descending)
        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>) {
        // First, try detection by file extension
        if let Some(ref extension) = file.extension {
            if let Some(lang_info) = self.registry.get_language_by_extension(extension) {
                return (file, Some(lang_info));
            }
        }

        // If extension-based detection fails, try content-based detection
        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));
            }
        }

        // Special cases for files without extensions
        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)
    }
}