use std::collections::HashMap; use std::env; use std::fs; use std::path::{Path, PathBuf}; use anyhow::Result; use ignore::WalkBuilder; const CYAN: &str = "\x1b[36m"; const GREEN: &str = "\x1b[32m"; const YELLOW: &str = "\x1b[33m"; const RESET: &str = "\x1b[0m"; const SEPARATOR: &str = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; pub fn run(path: Option) -> Result<()> { let path = path.unwrap_or_else(|| env::current_dir().unwrap()); let mut languages: HashMap<&str, (usize, usize)> = HashMap::new(); let mut livewire_php_lines: usize = 0; let mut livewire_blade_lines: usize = 0; let mut livewire_files: usize = 0; let mut test_lines: usize = 0; let mut test_files: usize = 0; for entry in WalkBuilder::new(&path).build() { let entry = entry?; if !entry.file_type().map_or(false, |ft| ft.is_file()) { continue; } let file_path = entry.path(); let filename = file_path.file_name().unwrap_or_default().to_string_lossy(); if is_lock_file(&filename) { continue; } // Livewire SFC: ⚡*.blade.php (excluding test files) if filename.starts_with('⚡') && filename.ends_with(".blade.php") && !filename.ends_with(".test.php") { let Ok(content) = fs::read_to_string(file_path) else { continue; }; livewire_files += 1; let mut in_php = true; for line in content.lines() { if in_php && line.trim() == "?>" { in_php = false; } else if in_php { livewire_php_lines += 1; } else { livewire_blade_lines += 1; } } continue; } // PHP tests: *.test.php or .php in tests/ directory let in_tests_dir = file_path.components().any(|c| c.as_os_str() == "tests"); if filename.ends_with(".test.php") || (in_tests_dir && filename.ends_with(".php")) { let Ok(content) = fs::read_to_string(file_path) else { continue; }; test_lines += content.lines().count(); test_files += 1; continue; } // Blade templates: *.blade.php (not Livewire SFC) if filename.ends_with(".blade.php") { let Ok(content) = fs::read_to_string(file_path) else { continue; }; let entry = languages.entry("Blade").or_insert((0, 0)); entry.0 += content.lines().count(); entry.1 += 1; continue; } if let Some(lang) = detect_language(file_path) { let Ok(content) = fs::read_to_string(file_path) else { continue; }; let entry = languages.entry(lang).or_insert((0, 0)); entry.0 += content.lines().count(); entry.1 += 1; } } let mut sorted: Vec<_> = languages.into_iter().collect(); sorted.sort_by(|a, b| b.1 .0.cmp(&a.1 .0)); println!("{CYAN}📊 Statistiques du code{RESET}"); println!("{SEPARATOR}"); let mut total_lines: usize = 0; let mut total_files: usize = 0; if livewire_files > 0 { print_line( GREEN, "Livewire SFC (PHP)", livewire_php_lines, Some(livewire_files), ); print_line(GREEN, "Livewire SFC (Blade)", livewire_blade_lines, None); total_lines += livewire_php_lines + livewire_blade_lines; total_files += livewire_files; } for (lang, (lines, files)) in &sorted { print_line(GREEN, lang, *lines, Some(*files)); total_lines += lines; total_files += files; } if test_files > 0 { print_line(YELLOW, "Tests", test_lines, Some(test_files)); } println!("{SEPARATOR}"); print_line(CYAN, "Total (hors tests)", total_lines, Some(total_files)); print_line( CYAN, "Total", total_lines + test_lines, Some(total_files + test_files), ); Ok(()) } fn print_line(color: &str, name: &str, lines: usize, files: Option) { match files { Some(f) => println!( "{color}{:<25}{RESET} {:>6} lignes ({f} fichiers)", name, lines ), None => println!("{color}{:<25}{RESET} {:>6} lignes", name, lines), } } fn is_lock_file(filename: &str) -> bool { matches!( filename, "composer.lock" | "package-lock.json" | "yarn.lock" | "pnpm-lock.yaml" | "Cargo.lock" | "Gemfile.lock" | "poetry.lock" | "flake.lock" ) } fn detect_language(path: &Path) -> Option<&'static str> { let filename = path.file_name()?.to_str()?; match filename { "Dockerfile" | "Containerfile" => return Some("Docker"), "Makefile" | "GNUmakefile" => return Some("Makefile"), _ => {} } let ext = path.extension()?.to_str()?; match ext { "php" => Some("PHP"), "js" | "mjs" | "cjs" => Some("JavaScript"), "ts" | "mts" | "cts" => Some("TypeScript"), "jsx" => Some("JSX"), "tsx" => Some("TSX"), "rs" => Some("Rust"), "py" | "pyw" => Some("Python"), "rb" => Some("Ruby"), "go" => Some("Go"), "java" => Some("Java"), "kt" | "kts" => Some("Kotlin"), "swift" => Some("Swift"), "c" | "h" => Some("C"), "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some("C++"), "cs" => Some("C#"), "hs" | "lhs" => Some("Haskell"), "ex" | "exs" => Some("Elixir"), "erl" | "hrl" => Some("Erlang"), "dart" => Some("Dart"), "lua" => Some("Lua"), "r" | "R" => Some("R"), "scala" | "sc" => Some("Scala"), "zig" => Some("Zig"), "sh" | "bash" | "zsh" | "fish" => Some("Shell"), "sql" => Some("SQL"), "vue" => Some("Vue"), "svelte" => Some("Svelte"), _ => None, } }