diff --git a/Cargo.lock b/Cargo.lock index bdce8fc..4775351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "fs_extra", "geoutils", "gpx", + "ignore", "notify", "pulldown-cmark", "regex", diff --git a/Cargo.toml b/Cargo.toml index 90291ec..32eac2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ fs_extra = "1" notify = "7" axum = "0.8" tower-http = { version = "0.6", features = ["fs"] } +ignore = "0.4" diff --git a/src/cli.rs b/src/cli.rs index 7da04b8..ff639bb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,6 +40,11 @@ pub enum Commands { Deploy { site: Site }, /// Analyser un export CSV d'index EDF et comparer les tarifs Edf { csv: PathBuf }, + /// Compter les lignes de code + Cloc { + /// Répertoire à analyser + path: Option, + }, } #[derive(Subcommand)] diff --git a/src/commands/cloc.rs b/src/commands/cloc.rs new file mode 100644 index 0000000..542acc6 --- /dev/null +++ b/src/commands/cloc.rs @@ -0,0 +1,197 @@ +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, + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 23a24a1..85fe73c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,3 +3,4 @@ pub mod dev; pub mod deploy; pub mod edf; pub mod site; +pub mod cloc; diff --git a/src/main.rs b/src/main.rs index 4dfba80..6ef6be1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,5 +14,6 @@ async fn main() -> Result<()> { Commands::Dev { site } => commands::dev::run(site).await, Commands::Deploy { site } => commands::deploy::run(site).await, Commands::Edf { csv } => commands::edf::run(&csv), + Commands::Cloc { path } => commands::cloc::run(path), } }