From 8f08cd5f0df3cef614d0ab7a8d44208c7a307c37 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Fri, 27 Mar 2026 19:37:05 +0200 Subject: [PATCH 1/4] deps: remove unused `walkdir` dependency Signed-off-by: Nashwan Azhari --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffc1db0..c2f3c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,6 @@ dependencies = [ "clap", "ignore", "utf8-chars", - "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 604f464..55aa7f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,3 @@ categories = ["development-tools", "development-tools::cargo-plugins"] clap = { version = "4.5", features = ["derive"] } ignore = "0.4.23" utf8-chars = "3.0.5" -walkdir = "2.5.0" From 99df98fcbd595fe970ef7ddc052a449592584590 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Fri, 27 Mar 2026 20:28:26 +0200 Subject: [PATCH 2/4] feat: add `--output-format/-o tabular/csv/yaml/json` arg This patch adds support for selecting the output format using the `--output-format/-o` arg. Options include CSV, Json, YAML, as well as the pre-existing Tabular format. Signed-off-by: Nashwan Azhari --- Cargo.lock | 115 +++++++++++++++++++++- Cargo.toml | 4 + src/cli.rs | 29 +++++- src/main.rs | 85 ++++------------ src/output.rs | 261 +++++++++++++++++++++++++++++++++++++++++++++++++ src/visitor.rs | 10 +- src/warlocs.rs | 21 +++- 7 files changed, 444 insertions(+), 81 deletions(-) create mode 100644 src/output.rs diff --git a/Cargo.lock b/Cargo.lock index c2f3c7c..94a7122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,7 +82,11 @@ name = "cargo-warloc" version = "0.1.0" dependencies = [ "clap", + "csv", "ignore", + "serde", + "serde_json", + "serde_yaml", "utf8-chars", ] @@ -157,6 +161,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "globset" version = "0.4.15" @@ -170,6 +201,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -192,12 +229,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "log" version = "0.4.25" @@ -251,6 +304,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -262,24 +321,60 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "strsim" version = "0.11.1" @@ -303,6 +398,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8-chars" version = "3.0.5" @@ -409,3 +510,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 55aa7f3..ca9e51e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,9 @@ categories = ["development-tools", "development-tools::cargo-plugins"] [dependencies] clap = { version = "4.5", features = ["derive"] } +csv = "1.4.0" ignore = "0.4.23" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +serde_yaml = "0.9.34" utf8-chars = "3.0.5" diff --git a/src/cli.rs b/src/cli.rs index 03eadea..3b821db 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,6 @@ -use clap::Parser; +use std::fmt::Display; + +use clap::{Parser, ValueEnum}; /// Wise analysis of Rust lines of code /// @@ -14,6 +16,31 @@ pub struct Cli { /// If set, will print out stats for each file separately #[arg(long)] pub by_file: bool, + /// Output format to print to standard output + #[arg(short, long, default_value_t = OutputFormat::Tabular)] + pub output_format: OutputFormat, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "lower")] +pub enum OutputFormat { + #[default] + Tabular, + Json, + Csv, + Yaml, +} + +impl Display for OutputFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Tabular => "tabular", + Self::Yaml => "yaml", + Self::Csv => "csv", + Self::Json => "json", + }; + f.write_str(s) + } } #[derive(Parser)] diff --git a/src/main.rs b/src/main.rs index 0462d41..44a1c33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ mod cli; +mod output; mod visitor; mod warlocs; -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use clap::Parser; use cli::{CargoCli, Cli}; @@ -10,82 +14,27 @@ use ignore::Walk; use visitor::Visitor; use warlocs::Warlocs; +use crate::output::{output_multiple_file_stats, output_total_stats}; + fn main() { let CargoCli::Command(args) = CargoCli::parse(); - let mut stats = Warlocs::default(); + let root_dir = PathBuf::from("."); - for rust_file in enumerate_rust_files(".") { - let file_stats = calculate_file_stats(&rust_file, &args); - stats += file_stats; - } - - println!("File count: {}", stats.file_count); - pretty_print_stats(&stats); -} - -fn calculate_file_stats(file_path: impl AsRef, args: &Cli) -> Warlocs { - let stats = Visitor::new(&file_path, args.debug).visit_file(); + let files_stats: HashMap = enumerate_rust_files(&root_dir) + .map(|p| (p.clone(), calculate_file_stats(&p, &args))) + .collect(); if args.by_file { - println!("File name: {}", file_path.as_ref().to_str().unwrap()); - pretty_print_stats(&stats); - println!(); + output_multiple_file_stats(&args.output_format, files_stats); + } else { + let total_stats = files_stats.into_values().sum(); + output_total_stats(&root_dir, &total_stats, &args.output_format); } - - stats } -fn pretty_print_stats(stats: &Warlocs) { - println!( - "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", - "Type", "Code", "Blank", "Doc comments", "Comments", "Total", - ); - println!( - "{0:-<12}-|-{1:-<12}-|-{2:-<12}-|-{3:-<12}-|-{4:-<12}-|-{5:-<12}", - "", "", "", "", "", "", - ); - - println!( - "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", - "Main", - stats.main.code, - stats.main.whitespaces, - stats.main.docs, - stats.main.comments, - stats.main.sum(), - ); - println!( - "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", - "Tests", - stats.tests.code, - stats.tests.whitespaces, - stats.tests.docs, - stats.tests.comments, - stats.tests.sum(), - ); - println!( - "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", - "Examples", - stats.examples.code, - stats.examples.whitespaces, - stats.examples.docs, - stats.examples.comments, - stats.examples.sum(), - ); - println!( - "{0:-<12}-|-{1:-<12}-|-{2:-<12}-|-{3:-<12}-|-{4:-<12}-|-{5:-<12}", - "", "", "", "", "", "", - ); - println!( - "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", - "", - stats.code(), - stats.whitespaces(), - stats.docs(), - stats.comments(), - stats.sum(), - ); +fn calculate_file_stats(file_path: impl AsRef, args: &Cli) -> Warlocs { + Visitor::new(&file_path, args.debug).visit_file() } fn enumerate_rust_files(root: impl AsRef) -> impl Iterator { diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..f2df224 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,261 @@ +//! Output routines. + +use std::{ + collections::HashMap, + io::stdout, + path::{Path, PathBuf}, +}; + +use csv::Writer as CsvWriter; +use serde::Serialize; + +use crate::{cli::OutputFormat, warlocs::Warlocs}; + +/// Prints to stdout multiple stats formatted based on the given `output_format`. +pub fn output_multiple_file_stats(output_format: &OutputFormat, stats: HashMap) { + match output_format { + OutputFormat::Tabular => { + println!("Total file count: {}", stats.len()); + for (path, warloc) in stats.iter() { + println!("\nFile path : {}", path.to_str().unwrap()); + single_stat_tabular(warloc); + } + } + OutputFormat::Json => { + let multistats = SerializableMultiFileStats::from_file_stats(stats); + println!( + "{}", + serde_json::to_string(&multistats).expect("JSON serialization should work") + ) + } + OutputFormat::Yaml => { + let multistats = SerializableMultiFileStats::from_file_stats(stats); + println!( + "{}", + serde_yaml::to_string(&multistats).expect("YAML serialization should work") + ) + } + OutputFormat::Csv => { + let multistats = SerializableMultiFileStats::from_file_stats(stats); + csv_output::output_csv(&multistats.totals, &multistats.files, stdout()); + } + } +} + +/// Prints to stdout the provided total [Warloc] stats formatted to the given `output_format`. +pub fn output_total_stats( + root_dir: impl AsRef, + stats: &Warlocs, + output_format: &OutputFormat, +) { + match output_format { + OutputFormat::Tabular => single_stat_tabular(stats), + OutputFormat::Json => { + print!( + "{}", + serde_json::to_string(&stats).expect("JSON serialization should work") + ) + } + OutputFormat::Yaml => { + print!( + "{}", + serde_yaml::to_string(&stats).expect("YAML serialization should work") + ) + } + OutputFormat::Csv => { + let mut writer = CsvWriter::from_writer(stdout()); + csv_output::write_csv_header_row(&mut writer); + csv_output::write_single_csv_value(root_dir, stats, &mut writer); + } + } +} + +/// Prints to stdout a tabular representation for a single [Warlocs] values. +fn single_stat_tabular(stats: &Warlocs) { + if stats.file_count >= 1 { + println!("File count: {}", stats.file_count); + } + println!( + "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", + "Type", "Code", "Blank", "Doc comments", "Comments", "Total", + ); + println!( + "{0:-<12}-|-{1:-<12}-|-{2:-<12}-|-{3:-<12}-|-{4:-<12}-|-{5:-<12}", + "", "", "", "", "", "", + ); + + println!( + "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", + "Main", + stats.main.code, + stats.main.whitespaces, + stats.main.docs, + stats.main.comments, + stats.main.sum(), + ); + println!( + "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", + "Tests", + stats.tests.code, + stats.tests.whitespaces, + stats.tests.docs, + stats.tests.comments, + stats.tests.sum(), + ); + println!( + "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", + "Examples", + stats.examples.code, + stats.examples.whitespaces, + stats.examples.docs, + stats.examples.comments, + stats.examples.sum(), + ); + println!( + "{0:-<12}-|-{1:-<12}-|-{2:-<12}-|-{3:-<12}-|-{4:-<12}-|-{5:-<12}", + "", "", "", "", "", "", + ); + println!( + "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", + "", + stats.code(), + stats.whitespaces(), + stats.docs(), + stats.comments(), + stats.sum(), + ); +} + +/// Simple wrapper struct representing the structure of serialized multi-file output. +#[derive(Serialize)] +struct SerializableMultiFileStats { + totals: Warlocs, + files: HashMap, +} + +impl SerializableMultiFileStats { + fn from_file_stats(files: HashMap) -> Self { + SerializableMultiFileStats { + totals: files.values().fold(Warlocs::default(), |acc, v| acc + *v), + files, + } + } +} + +/// Module defining CSV output routines. +mod csv_output { + use std::{ + collections::HashMap, + io::Write, + path::{Path, PathBuf}, + }; + + use csv::Writer as CsvWriter; + + use crate::warlocs::Warlocs; + + /// Performs CSV serialization and outputs to the provided [Write]. + pub fn output_csv(totals: &Warlocs, files: &HashMap, writer: impl Write) { + let mut csvw = csv::Writer::from_writer(writer); + + write_csv_header_row(&mut csvw); + + write_single_csv_value(PathBuf::from("Totals"), totals, &mut csvw); + + for (path, stats) in files.iter() { + write_single_csv_value(path, stats, &mut csvw); + } + } + + /// Writes out the olumn row for the CSV output of [Warlocs]. + pub fn write_csv_header_row(writer: &mut CsvWriter) { + let columns = [ + "File Path", + "Main Code", + "Main Docs", + "Main Comments", + "Main Spaces", + "Test Code", + "Test Docs", + "Test Comments", + "Test Spaces", + "Example Code", + "Example Docs", + "Example Comments", + "Example Spaces", + ]; + writer.write_record(columns).expect("Write CSV columns"); + } + + pub fn write_single_csv_value( + path: impl AsRef, + stats: &Warlocs, + writer: &mut CsvWriter, + ) { + let s = stats; + let mut row: Vec = vec![path.as_ref().to_str().unwrap().to_string()]; + row.extend( + [ + s.main.code, + s.main.docs, + s.main.comments, + s.main.whitespaces, + s.tests.code, + s.tests.docs, + s.tests.comments, + s.tests.whitespaces, + s.examples.code, + s.examples.docs, + s.examples.comments, + s.examples.whitespaces, + ] + .map(|v| v.to_string()), + ); + + writer.write_record(row).expect("Write CSV row"); + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::{ + cli::OutputFormat, + output::{output_multiple_file_stats, output_total_stats}, + warlocs::Warlocs, + }; + + macro_rules! all_output_formats { + () => { + [ + OutputFormat::Csv, + OutputFormat::Json, + OutputFormat::Tabular, + OutputFormat::Yaml, + ] + }; + } + + #[test] + fn test_output_single_no_panics() { + let val = Warlocs::default(); + + for output_format in all_output_formats!() { + output_total_stats(PathBuf::from("."), &val, &output_format); + } + } + + #[test] + fn test_output_multiple_no_panics() { + for output_format in all_output_formats!() { + let files = [ + (PathBuf::from("abc"), Warlocs::default()), + (PathBuf::from("123"), Warlocs::default()), + ] + .into_iter() + .collect(); + output_multiple_file_stats(&output_format, files); + } + } +} diff --git a/src/visitor.rs b/src/visitor.rs index f2cbd87..4cbac0b 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -541,10 +541,10 @@ mod tests { #[test] fn multiline_comment_block() { - let file = r#" /* + let file = r#" /* - comment - */ + comment + */ "#; let stats = stats(file); @@ -567,8 +567,8 @@ mod tests { fn multiline_doc_comment_block() { let file = r#" /*! - comment - */ + comment + */ "#; let stats = stats(file); diff --git a/src/warlocs.rs b/src/warlocs.rs index 0e93b72..92537a9 100644 --- a/src/warlocs.rs +++ b/src/warlocs.rs @@ -1,14 +1,19 @@ -use std::ops::{Add, AddAssign}; +use serde::Serialize; +use std::{ + iter::Sum, + ops::{Add, AddAssign}, +}; -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, Serialize)] pub struct Warlocs { + #[serde(skip_serializing_if = "is_single_file_stats")] pub file_count: u64, pub main: Locs, pub tests: Locs, pub examples: Locs, } -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, Serialize)] pub struct Locs { pub whitespaces: u64, pub code: u64, @@ -44,6 +49,12 @@ impl Locs { } } +impl Sum for Warlocs { + fn sum>(iter: I) -> Self { + iter.fold(Warlocs::default(), Warlocs::add) + } +} + impl Add for Warlocs { type Output = Self; @@ -75,3 +86,7 @@ impl Add for Locs { } } } + +fn is_single_file_stats(file_count: &u64) -> bool { + *file_count <= 1 +} From 6b31841ec51c8555d4d67063f4473098390bd8dd Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Sun, 19 Apr 2026 02:21:32 +0300 Subject: [PATCH 3/4] fix: iron out output format issues Signed-off-by: Nashwan Azhari --- Cargo.lock | 106 +++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- README.md | 16 +++--- src/main.rs | 7 +-- src/output.rs | 135 ++++++++++++++++++++++++++++--------------------- src/visitor.rs | 4 -- src/warlocs.rs | 7 --- 7 files changed, 183 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94a7122..a0524fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,12 +61,30 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bstr" version = "1.11.3" @@ -86,7 +104,7 @@ dependencies = [ "ignore", "serde", "serde_json", - "serde_yaml", + "serde_yaml_bw", "utf8-chars", ] @@ -203,9 +221,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -231,9 +249,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -263,6 +281,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.3" @@ -287,6 +314,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -319,6 +358,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saphyr-parser-bw" +version = "0.0.611" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dec0c833db75dc98957956b303fe447ffc5eb13f2325ef4c2350f7f3aa69e3" +dependencies = [ + "arraydeque", + "smallvec", + "thiserror", +] + [[package]] name = "serde" version = "1.0.228" @@ -363,18 +413,28 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yaml_bw" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "eef5e25868f1a82b8be2395c14a744924ce5f6d2154a746dbfe2fbc22a8b5033" dependencies = [ + "base64", "indexmap", "itoa", - "ryu", + "num-traits", + "regex", + "saphyr-parser-bw", "serde", - "unsafe-libyaml", + "unsafe-libyaml-norway", + "zmij", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "strsim" version = "0.11.1" @@ -392,6 +452,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.16" @@ -399,10 +479,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] -name = "unsafe-libyaml" -version = "0.2.11" +name = "unsafe-libyaml-norway" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" [[package]] name = "utf8-chars" diff --git a/Cargo.toml b/Cargo.toml index ca9e51e..55dc36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,5 @@ csv = "1.4.0" ignore = "0.4.23" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -serde_yaml = "0.9.34" +serde_yaml_bw = "2.5.5" utf8-chars = "3.0.5" diff --git a/README.md b/README.md index af3cad5..79ddc19 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,19 @@ about our project documentation being bigger than the code itself. * Understands (to a limit) Rust syntax, so is more accurate in its counts then most generic LOC counters. * Does not count ignored (by `.gitignore`) files. * Can optionally give you stats file-by-file. +* Output stats in tables, CSV, JSON, or YAML. + +Here is the output for the `cargo` repository: -Here is the output for `cargo` repository: ``` File count: 1188 -Type | Code | Blank | Doc comments | Comments | Total +Type | Code | Blank | Doc comments | Comments | Total -------------|--------------|--------------|--------------|--------------|------------- -Main | 82530 | 9682 | 12625 | 6220 | 111057 -Tests | 144421 | 20538 | 588 | 10151 | 175698 -Examples | 169 | 27 | 5 | 19 | 220 +Main | 82530 | 9682 | 12625 | 6220 | 111057 +Tests | 144421 | 20538 | 588 | 10151 | 175698 +Examples | 169 | 27 | 5 | 19 | 220 -------------|--------------|--------------|--------------|--------------|------------- - | 227120 | 30247 | 13218 | 16390 | 286975 + | 227120 | 30247 | 13218 | 16390 | 286975 ``` # Installation @@ -34,7 +36,7 @@ cargo install cargo-warloc # Usage ```shell -cargo warloc +cargo warloc [--by-file] [-o tabular|csv|json|yaml] ``` # Contributing diff --git a/src/main.rs b/src/main.rs index 44a1c33..9a37464 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod visitor; mod warlocs; use std::{ - collections::HashMap, + collections::BTreeMap, path::{Path, PathBuf}, }; @@ -21,15 +21,16 @@ fn main() { let root_dir = PathBuf::from("."); - let files_stats: HashMap = enumerate_rust_files(&root_dir) + let files_stats: BTreeMap = enumerate_rust_files(&root_dir) .map(|p| (p.clone(), calculate_file_stats(&p, &args))) .collect(); if args.by_file { output_multiple_file_stats(&args.output_format, files_stats); } else { + let file_count = files_stats.len(); let total_stats = files_stats.into_values().sum(); - output_total_stats(&root_dir, &total_stats, &args.output_format); + output_total_stats(file_count as u64, &total_stats, &args.output_format); } } diff --git a/src/output.rs b/src/output.rs index f2df224..aeb7e2e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,10 +1,6 @@ //! Output routines. -use std::{ - collections::HashMap, - io::stdout, - path::{Path, PathBuf}, -}; +use std::{collections::BTreeMap, path::PathBuf}; use csv::Writer as CsvWriter; use serde::Serialize; @@ -12,7 +8,7 @@ use serde::Serialize; use crate::{cli::OutputFormat, warlocs::Warlocs}; /// Prints to stdout multiple stats formatted based on the given `output_format`. -pub fn output_multiple_file_stats(output_format: &OutputFormat, stats: HashMap) { +pub fn output_multiple_file_stats(output_format: &OutputFormat, stats: BTreeMap) { match output_format { OutputFormat::Tabular => { println!("Total file count: {}", stats.len()); @@ -30,51 +26,66 @@ pub fn output_multiple_file_stats(output_format: &OutputFormat, stats: HashMap

{ let multistats = SerializableMultiFileStats::from_file_stats(stats); - println!( + print!( "{}", - serde_yaml::to_string(&multistats).expect("YAML serialization should work") + serde_yaml_bw::to_string(&multistats).expect("YAML serialization should work") ) } OutputFormat::Csv => { let multistats = SerializableMultiFileStats::from_file_stats(stats); - csv_output::output_csv(&multistats.totals, &multistats.files, stdout()); + let mut buff: Vec = Vec::new(); + csv_output::output_csv(&multistats.totals, &multistats.files, &mut buff); + println!( + "{}", + str::from_utf8(&buff).expect("CSV output should be UTF-8") + ) } } } /// Prints to stdout the provided total [Warloc] stats formatted to the given `output_format`. -pub fn output_total_stats( - root_dir: impl AsRef, - stats: &Warlocs, - output_format: &OutputFormat, -) { +pub fn output_total_stats(file_count: u64, stats: &Warlocs, output_format: &OutputFormat) { match output_format { - OutputFormat::Tabular => single_stat_tabular(stats), + OutputFormat::Tabular => { + println!("Total file count: {file_count}",); + single_stat_tabular(stats) + } OutputFormat::Json => { - print!( + let stats = SerializableTotalStats { file_count, stats }; + println!( "{}", serde_json::to_string(&stats).expect("JSON serialization should work") ) } OutputFormat::Yaml => { + let stats = SerializableTotalStats { file_count, stats }; print!( "{}", - serde_yaml::to_string(&stats).expect("YAML serialization should work") + serde_yaml_bw::to_string(&stats).expect("YAML serialization should work") ) } OutputFormat::Csv => { - let mut writer = CsvWriter::from_writer(stdout()); + let mut buff: Vec = Vec::new(); + let mut writer = CsvWriter::from_writer(&mut buff); + writer + .write_field("File Count") + .expect("Write CSV first row"); csv_output::write_csv_header_row(&mut writer); - csv_output::write_single_csv_value(root_dir, stats, &mut writer); + writer + .write_field(file_count.to_string()) + .expect("Write CSV row field"); + csv_output::write_single_csv_value(stats, &mut writer); + drop(writer); + println!( + "{}", + str::from_utf8(&buff).expect("CSV output should be UTF-8") + ) } } } /// Prints to stdout a tabular representation for a single [Warlocs] values. fn single_stat_tabular(stats: &Warlocs) { - if stats.file_count >= 1 { - println!("File count: {}", stats.file_count); - } println!( "{0: <12} | {1: <12} | {2: <12} | {3: <12} | {4: <12} | {5: <12}", "Type", "Code", "Blank", "Doc comments", "Comments", "Total", @@ -126,16 +137,25 @@ fn single_stat_tabular(stats: &Warlocs) { ); } +#[derive(Serialize)] +struct SerializableTotalStats<'a> { + file_count: u64, + #[serde(flatten)] + stats: &'a Warlocs, +} + /// Simple wrapper struct representing the structure of serialized multi-file output. #[derive(Serialize)] struct SerializableMultiFileStats { + file_count: u64, totals: Warlocs, - files: HashMap, + files: BTreeMap, } impl SerializableMultiFileStats { - fn from_file_stats(files: HashMap) -> Self { + fn from_file_stats(files: BTreeMap) -> Self { SerializableMultiFileStats { + file_count: files.len() as u64, totals: files.values().fold(Warlocs::default(), |acc, v| acc + *v), files, } @@ -144,33 +164,34 @@ impl SerializableMultiFileStats { /// Module defining CSV output routines. mod csv_output { - use std::{ - collections::HashMap, - io::Write, - path::{Path, PathBuf}, - }; + use std::{collections::BTreeMap, io::Write, path::PathBuf}; use csv::Writer as CsvWriter; use crate::warlocs::Warlocs; /// Performs CSV serialization and outputs to the provided [Write]. - pub fn output_csv(totals: &Warlocs, files: &HashMap, writer: impl Write) { + pub fn output_csv(totals: &Warlocs, files: &BTreeMap, writer: impl Write) { let mut csvw = csv::Writer::from_writer(writer); + csvw.write_field("File").expect("Write CSV columns row"); write_csv_header_row(&mut csvw); - write_single_csv_value(PathBuf::from("Totals"), totals, &mut csvw); - for (path, stats) in files.iter() { - write_single_csv_value(path, stats, &mut csvw); + csvw.write_field(path.to_str().expect("Filepaths should be UTF-8")) + .expect("Write CSV stats value"); + write_single_csv_value(stats, &mut csvw); } + + let file_count = files.len(); + csvw.write_field(format!("{file_count} files total")) + .expect("Write CSV totals row"); + write_single_csv_value(totals, &mut csvw); } - /// Writes out the olumn row for the CSV output of [Warlocs]. + /// Writes out the column titles row for the CSV output of [Warlocs]. pub fn write_csv_header_row(writer: &mut CsvWriter) { let columns = [ - "File Path", "Main Code", "Main Docs", "Main Comments", @@ -187,30 +208,26 @@ mod csv_output { writer.write_record(columns).expect("Write CSV columns"); } - pub fn write_single_csv_value( - path: impl AsRef, - stats: &Warlocs, - writer: &mut CsvWriter, - ) { + pub fn write_single_csv_value(stats: &Warlocs, writer: &mut CsvWriter) { let s = stats; - let mut row: Vec = vec![path.as_ref().to_str().unwrap().to_string()]; - row.extend( - [ - s.main.code, - s.main.docs, - s.main.comments, - s.main.whitespaces, - s.tests.code, - s.tests.docs, - s.tests.comments, - s.tests.whitespaces, - s.examples.code, - s.examples.docs, - s.examples.comments, - s.examples.whitespaces, - ] - .map(|v| v.to_string()), - ); + + let row: Vec = [ + s.main.code, + s.main.docs, + s.main.comments, + s.main.whitespaces, + s.tests.code, + s.tests.docs, + s.tests.comments, + s.tests.whitespaces, + s.examples.code, + s.examples.docs, + s.examples.comments, + s.examples.whitespaces, + ] + .iter() + .map(|v| v.to_string()) + .collect(); writer.write_record(row).expect("Write CSV row"); } @@ -242,7 +259,7 @@ mod tests { let val = Warlocs::default(); for output_format in all_output_formats!() { - output_total_stats(PathBuf::from("."), &val, &output_format); + output_total_stats(1, &val, &output_format); } } diff --git a/src/visitor.rs b/src/visitor.rs index 4cbac0b..0d675d9 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -109,8 +109,6 @@ impl Visitor { impl Visitor { pub fn visit_file(mut self) -> Warlocs { - self.stats.file_count += 1; - self.visit_code(self.context); self.stats @@ -479,7 +477,6 @@ mod tests { let file = "\n"; let stats = stats(file); - assert_eq!(stats.file_count, 1); assert_eq!(stats.main.whitespaces, 1); assert_eq!(stats.main.sum(), 1); } @@ -489,7 +486,6 @@ mod tests { let file = " \t\t \n"; let stats = stats(file); - assert_eq!(stats.file_count, 1); assert_eq!(stats.main.whitespaces, 1); assert_eq!(stats.main.sum(), 1); } diff --git a/src/warlocs.rs b/src/warlocs.rs index 92537a9..7761089 100644 --- a/src/warlocs.rs +++ b/src/warlocs.rs @@ -6,8 +6,6 @@ use std::{ #[derive(Debug, Default, Copy, Clone, Serialize)] pub struct Warlocs { - #[serde(skip_serializing_if = "is_single_file_stats")] - pub file_count: u64, pub main: Locs, pub tests: Locs, pub examples: Locs, @@ -60,7 +58,6 @@ impl Add for Warlocs { fn add(self, rhs: Warlocs) -> Self::Output { Self { - file_count: self.file_count + rhs.file_count, main: self.main + rhs.main, tests: self.tests + rhs.tests, examples: self.examples + rhs.examples, @@ -86,7 +83,3 @@ impl Add for Locs { } } } - -fn is_single_file_stats(file_count: &u64) -> bool { - *file_count <= 1 -} From 299f74f50c06c82f4b12a7162f6f1923c508cd52 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Sun, 19 Apr 2026 12:55:01 +0300 Subject: [PATCH 4/4] fix: remove extra newline from CSV output Signed-off-by: Nashwan Azhari --- src/output.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/output.rs b/src/output.rs index aeb7e2e..d37afad 100644 --- a/src/output.rs +++ b/src/output.rs @@ -35,7 +35,7 @@ pub fn output_multiple_file_stats(output_format: &OutputFormat, stats: BTreeMap< let multistats = SerializableMultiFileStats::from_file_stats(stats); let mut buff: Vec = Vec::new(); csv_output::output_csv(&multistats.totals, &multistats.files, &mut buff); - println!( + print!( "{}", str::from_utf8(&buff).expect("CSV output should be UTF-8") ) @@ -76,7 +76,7 @@ pub fn output_total_stats(file_count: u64, stats: &Warlocs, output_format: &Outp .expect("Write CSV row field"); csv_output::write_single_csv_value(stats, &mut writer); drop(writer); - println!( + print!( "{}", str::from_utf8(&buff).expect("CSV output should be UTF-8") )