add basic cloc function

This commit is contained in:
Thibaud Dauce
2026-02-18 17:49:07 +01:00
parent 6d4e682c60
commit cf89569702
6 changed files with 206 additions and 0 deletions

1
Cargo.lock generated
View File

@@ -1007,6 +1007,7 @@ dependencies = [
"fs_extra", "fs_extra",
"geoutils", "geoutils",
"gpx", "gpx",
"ignore",
"notify", "notify",
"pulldown-cmark", "pulldown-cmark",
"regex", "regex",

View File

@@ -22,3 +22,4 @@ fs_extra = "1"
notify = "7" notify = "7"
axum = "0.8" axum = "0.8"
tower-http = { version = "0.6", features = ["fs"] } tower-http = { version = "0.6", features = ["fs"] }
ignore = "0.4"

View File

@@ -40,6 +40,11 @@ pub enum Commands {
Deploy { site: Site }, Deploy { site: Site },
/// Analyser un export CSV d'index EDF et comparer les tarifs /// Analyser un export CSV d'index EDF et comparer les tarifs
Edf { csv: PathBuf }, Edf { csv: PathBuf },
/// Compter les lignes de code
Cloc {
/// Répertoire à analyser
path: Option<PathBuf>,
},
} }
#[derive(Subcommand)] #[derive(Subcommand)]

197
src/commands/cloc.rs Normal file
View File

@@ -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<PathBuf>) -> 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<usize>) {
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,
}
}

View File

@@ -3,3 +3,4 @@ pub mod dev;
pub mod deploy; pub mod deploy;
pub mod edf; pub mod edf;
pub mod site; pub mod site;
pub mod cloc;

View File

@@ -14,5 +14,6 @@ async fn main() -> Result<()> {
Commands::Dev { site } => commands::dev::run(site).await, Commands::Dev { site } => commands::dev::run(site).await,
Commands::Deploy { site } => commands::deploy::run(site).await, Commands::Deploy { site } => commands::deploy::run(site).await,
Commands::Edf { csv } => commands::edf::run(&csv), Commands::Edf { csv } => commands::edf::run(&csv),
Commands::Cloc { path } => commands::cloc::run(path),
} }
} }