code complexity & repetition analysis tool
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(loc): extend LOC cmd with file ranking and directory aggregation

+553 -61
+13 -3
README
··· 55 55 - Quickly identifies copy/paste blocks and boilerplate. 56 56 ------------------------------------------------------------------------------- 57 57 3. Lines of Code (LOC) 58 - - Counts executable or logical lines. 58 + - Counts physical, logical, comment, and blank lines. 59 + - Token-based classification for accuracy. 60 + - Supports ranking by any metric (logical, physical, comments, blank). 61 + - Can aggregate and rank by directory. 59 62 - Required for future Maintainability Index calculations. 60 63 61 64 Purpose: 62 65 - Baseline size metric. 66 + - Identify largest files and directories. 67 + - Track codebase growth and comment density. 63 68 ------------------------------------------------------------------------------- 64 69 4. Halstead Complexity Metrics 65 70 - Based on counts of operators and operands. ··· 137 142 138 143 -------------------------------------------------------------------------------- 139 144 cli 140 - - commands: analyze, clones, complexity, dump-config 141 - - flags: --json, --threshold, --min-tokens, --sort, --path 145 + - commands: analyze, clones, complexity, loc, dump-config 146 + - analyze: full analysis (complexity + clones + LOC) 147 + - clones: detect code clones only 148 + - complexity: analyze cyclomatic complexity and LOC 149 + - loc: focused LOC analysis with ranking (by file or directory) 150 + - dump-config: display or save current configuration 151 + - flags: --json, --threshold, --min-tokens, --rank-by, --rank-dirs, --path 142 152 143 153 =============================================================================== 144 154 OUTPUT FORMATS
+129
crates/cli/src/commands/loc.rs
··· 1 + use anyhow::Result; 2 + use mccabre_core::{ 3 + complexity::loc::{FileLocReport, LocMetrics, LocReport, RankBy}, 4 + config::Config, 5 + loader::FileLoader, 6 + }; 7 + use owo_colors::OwoColorize; 8 + use std::path::PathBuf; 9 + 10 + pub fn run( 11 + path: PathBuf, json: bool, rank_by: RankBy, rank_dirs: bool, config_path: Option<PathBuf>, respect_gitignore: bool, 12 + ) -> Result<()> { 13 + let config = if let Some(config_path) = config_path { 14 + Config::from_file(config_path)? 15 + } else { 16 + Config::load_default()? 17 + }; 18 + 19 + let config = config.merge_with_cli(None, None, Some(respect_gitignore)); 20 + let loader = FileLoader::new().with_gitignore(config.files.respect_gitignore); 21 + let files = loader.load(&path)?; 22 + 23 + if files.is_empty() { 24 + eprintln!("{}", "No supported files found".yellow()); 25 + return Ok(()); 26 + } 27 + 28 + let mut file_reports = Vec::new(); 29 + 30 + for file in &files { 31 + let metrics = LocMetrics::calculate(&file.content, file.language)?; 32 + file_reports.push(FileLocReport { path: file.path.clone(), metrics }); 33 + } 34 + 35 + let report = LocReport::new(file_reports, rank_by, rank_dirs); 36 + 37 + if json { 38 + println!("{}", report.to_json()?); 39 + } else { 40 + print_loc_report(&report, rank_by, rank_dirs); 41 + } 42 + 43 + Ok(()) 44 + } 45 + 46 + fn print_loc_report(report: &LocReport, rank_by: RankBy, rank_dirs: bool) { 47 + println!("{}", "=".repeat(80).cyan()); 48 + println!("{}", "LINES OF CODE ANALYSIS".cyan().bold()); 49 + println!("{}\n", "=".repeat(80).cyan()); 50 + 51 + println!("{}", "SUMMARY".green().bold()); 52 + println!("{}", "-".repeat(80).cyan()); 53 + println!("Total files analyzed: {}", report.summary.total_files.bold()); 54 + println!("Total physical LOC: {}", report.summary.total_physical.bold()); 55 + println!("Total logical LOC: {}", report.summary.total_logical.bold()); 56 + println!("Total comment lines: {}", report.summary.total_comments.bold()); 57 + println!("Total blank lines: {}\n", report.summary.total_blank.bold()); 58 + 59 + let rank_label = match rank_by { 60 + RankBy::Logical => "Logical LOC", 61 + RankBy::Physical => "Physical LOC", 62 + RankBy::Comments => "Comment Lines", 63 + RankBy::Blank => "Blank Lines", 64 + }; 65 + 66 + if rank_dirs { 67 + if let Some(directories) = &report.directories { 68 + println!( 69 + "{} {}", 70 + "DIRECTORIES RANKED BY".green().bold(), 71 + rank_label.green().bold() 72 + ); 73 + println!("{}\n", "-".repeat(80).cyan()); 74 + 75 + for dir in directories { 76 + println!("{} {}", "DIRECTORY:".blue().bold(), dir.path.display().bold()); 77 + println!( 78 + " Total Physical: {} | Logical: {} | Comments: {} | Blank: {}", 79 + dir.total.physical.bold(), 80 + dir.total.logical.bold(), 81 + dir.total.comments.bold(), 82 + dir.total.blank.bold() 83 + ); 84 + println!(); 85 + 86 + if !dir.files.is_empty() { 87 + println!(" {}:", "Files".magenta()); 88 + for file in &dir.files { 89 + let filename = file.path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"); 90 + 91 + let rank_value = rank_by.value_from(&file.metrics); 92 + println!( 93 + " {} ({}: {}) - P: {} | L: {} | C: {} | B: {}", 94 + filename, 95 + rank_label.dimmed(), 96 + rank_value.to_string().yellow(), 97 + file.metrics.physical, 98 + file.metrics.logical, 99 + file.metrics.comments, 100 + file.metrics.blank 101 + ); 102 + } 103 + println!(); 104 + } 105 + } 106 + } 107 + } else { 108 + println!("{} {}", "FILES RANKED BY".green().bold(), rank_label.green().bold()); 109 + println!("{}\n", "-".repeat(80).cyan()); 110 + 111 + for (idx, file) in report.files.iter().enumerate() { 112 + let rank_value = rank_by.value_from(&file.metrics); 113 + println!( 114 + "{}. {} ({}: {})", 115 + (idx + 1).to_string().dimmed(), 116 + file.path.display().bold(), 117 + rank_label.dimmed(), 118 + rank_value.to_string().yellow() 119 + ); 120 + println!( 121 + " Physical: {} | Logical: {} | Comments: {} | Blank: {}", 122 + file.metrics.physical, file.metrics.logical, file.metrics.comments, file.metrics.blank 123 + ); 124 + println!(); 125 + } 126 + } 127 + 128 + println!("{}", "=".repeat(80).cyan()); 129 + }
+1
crates/cli/src/commands/mod.rs
··· 2 2 pub mod clones; 3 3 pub mod complexity; 4 4 pub mod dump_config; 5 + pub mod loc;
+59 -34
crates/cli/src/main.rs
··· 107 107 #[arg(short = 'o', long)] 108 108 output: Option<PathBuf>, 109 109 }, 110 + 111 + /// Analyze lines of code with ranking 112 + Loc { 113 + /// Path to file or directory to analyze 114 + #[arg(value_name = "PATH", default_value = ".")] 115 + path: PathBuf, 116 + 117 + /// Output in JSON format 118 + #[arg(short, long)] 119 + json: bool, 120 + 121 + /// Rank by criteria: logical, physical, comments, blank 122 + #[arg(long, default_value = "logical")] 123 + rank_by: String, 124 + 125 + /// Rank directories (with files ranked within each) 126 + #[arg(long)] 127 + rank_dirs: bool, 128 + 129 + /// Path to config file 130 + #[arg(short, long)] 131 + config: Option<PathBuf>, 132 + 133 + /// Disable gitignore awareness 134 + #[arg(long)] 135 + no_gitignore: bool, 136 + }, 110 137 } 111 138 112 139 fn main() -> Result<()> { 113 140 let cli = Cli::parse(); 114 141 115 142 match cli.command { 116 - Commands::Analyze { 117 - path, 118 - json, 119 - threshold, 120 - min_tokens, 121 - config, 122 - no_gitignore, 123 - no_highlight, 124 - } => commands::analyze::run( 125 - path, 126 - json, 127 - threshold, 128 - Some(min_tokens), 129 - config, 130 - !no_gitignore, 131 - !no_highlight, 132 - ), 143 + Commands::Analyze { path, json, threshold, min_tokens, config, no_gitignore, no_highlight } => { 144 + commands::analyze::run( 145 + path, 146 + json, 147 + threshold, 148 + Some(min_tokens), 149 + config, 150 + !no_gitignore, 151 + !no_highlight, 152 + ) 153 + } 154 + Commands::Complexity { path, json, threshold, config, no_gitignore } => { 155 + commands::complexity::run(path, json, threshold, config, !no_gitignore) 156 + } 157 + Commands::Clones { path, json, min_tokens, config, no_gitignore, no_highlight } => { 158 + commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore, !no_highlight) 159 + } 160 + Commands::DumpConfig { config, output } => commands::dump_config::run(config, output), 161 + Commands::Loc { path, json, rank_by, rank_dirs, config, no_gitignore } => { 162 + use mccabre_core::complexity::loc::RankBy; 133 163 134 - Commands::Complexity { 135 - path, 136 - json, 137 - threshold, 138 - config, 139 - no_gitignore, 140 - } => commands::complexity::run(path, json, threshold, config, !no_gitignore), 164 + let rank_by = match rank_by.to_lowercase().as_str() { 165 + "logical" => RankBy::Logical, 166 + "physical" => RankBy::Physical, 167 + "comments" => RankBy::Comments, 168 + "blank" => RankBy::Blank, 169 + _ => { 170 + eprintln!("Invalid rank_by value. Use: logical, physical, comments, or blank"); 171 + std::process::exit(1); 172 + } 173 + }; 141 174 142 - Commands::Clones { 143 - path, 144 - json, 145 - min_tokens, 146 - config, 147 - no_gitignore, 148 - no_highlight, 149 - } => commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore, !no_highlight), 150 - 151 - Commands::DumpConfig { config, output } => commands::dump_config::run(config, output), 175 + commands::loc::run(path, json, rank_by, rank_dirs, config, !no_gitignore) 176 + } 152 177 } 153 178 }
+1 -1
crates/core/src/cloner/rolling_hash.rs
··· 140 140 #[test] 141 141 fn test_rolling_preserves_pattern() { 142 142 let mut rh = RollingHash::new(3); 143 - let values = vec![1, 2, 3, 4, 5, 6, 1, 2, 3]; 143 + let values = [1, 2, 3, 4, 5, 6, 1, 2, 3]; 144 144 145 145 rh.init(&values[0..3]); 146 146 let first_hash = rh.get();
+235
crates/core/src/complexity/loc.rs
··· 1 1 use crate::Result; 2 2 use crate::tokenizer::{Language, TokenType, Tokenizer}; 3 3 use serde::{Deserialize, Serialize}; 4 + use std::collections::HashMap; 5 + use std::path::{Path, PathBuf}; 4 6 5 7 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 8 enum LineKind { ··· 59 61 60 62 Ok(LocMetrics { physical, logical, comments, blank }) 61 63 } 64 + 65 + /// Add two LocMetrics together 66 + fn add(&self, other: &Self) -> Self { 67 + Self { 68 + physical: self.physical + other.physical, 69 + logical: self.logical + other.logical, 70 + comments: self.comments + other.comments, 71 + blank: self.blank + other.blank, 72 + } 73 + } 74 + } 75 + 76 + /// Ranking criteria for LOC analysis 77 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 78 + pub enum RankBy { 79 + /// Rank by logical lines of code 80 + Logical, 81 + /// Rank by physical lines of code 82 + Physical, 83 + /// Rank by comment lines 84 + Comments, 85 + /// Rank by blank lines 86 + Blank, 87 + } 88 + 89 + impl RankBy { 90 + /// Get the value from LocMetrics based on ranking criteria 91 + pub fn value_from(&self, metrics: &LocMetrics) -> usize { 92 + match self { 93 + Self::Logical => metrics.logical, 94 + Self::Physical => metrics.physical, 95 + Self::Comments => metrics.comments, 96 + Self::Blank => metrics.blank, 97 + } 98 + } 99 + } 100 + 101 + /// LOC metrics for a single file with path information 102 + #[derive(Debug, Clone, Serialize, Deserialize)] 103 + pub struct FileLocReport { 104 + /// File path 105 + pub path: PathBuf, 106 + /// LOC metrics 107 + pub metrics: LocMetrics, 108 + } 109 + 110 + /// Aggregated LOC metrics for a directory 111 + #[derive(Debug, Clone, Serialize, Deserialize)] 112 + pub struct DirectoryLocMetrics { 113 + /// Directory path 114 + pub path: PathBuf, 115 + /// Total LOC metrics for all files in this directory 116 + pub total: LocMetrics, 117 + /// Files in this directory 118 + pub files: Vec<FileLocReport>, 119 + } 120 + 121 + /// Complete LOC analysis report with ranking capabilities 122 + #[derive(Debug, Clone, Serialize, Deserialize)] 123 + pub struct LocReport { 124 + /// Per-file reports 125 + pub files: Vec<FileLocReport>, 126 + /// Per-directory aggregation (if enabled) 127 + pub directories: Option<Vec<DirectoryLocMetrics>>, 128 + /// Summary statistics 129 + pub summary: LocSummary, 130 + } 131 + 132 + /// Summary statistics for LOC report 133 + #[derive(Debug, Clone, Serialize, Deserialize)] 134 + pub struct LocSummary { 135 + /// Total number of files analyzed 136 + pub total_files: usize, 137 + /// Total physical lines of code 138 + pub total_physical: usize, 139 + /// Total logical lines of code 140 + pub total_logical: usize, 141 + /// Total comment lines 142 + pub total_comments: usize, 143 + /// Total blank lines 144 + pub total_blank: usize, 145 + } 146 + 147 + impl LocReport { 148 + /// Create a new LOC report from file reports 149 + pub fn new(mut files: Vec<FileLocReport>, rank_by: RankBy, rank_dirs: bool) -> Self { 150 + files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics))); 151 + 152 + let directories = if rank_dirs { Some(Self::aggregate_by_directory(&files, rank_by)) } else { None }; 153 + let summary = LocSummary::from_files(&files); 154 + 155 + Self { files, directories, summary } 156 + } 157 + 158 + /// Aggregate files by directory 159 + fn aggregate_by_directory(files: &[FileLocReport], rank_by: RankBy) -> Vec<DirectoryLocMetrics> { 160 + let mut dir_map: HashMap<PathBuf, Vec<FileLocReport>> = HashMap::new(); 161 + 162 + for file in files { 163 + let dir = file.path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf(); 164 + dir_map.entry(dir).or_default().push(file.clone()); 165 + } 166 + 167 + let mut directories: Vec<DirectoryLocMetrics> = dir_map 168 + .into_iter() 169 + .map(|(path, files)| { 170 + let total = files.iter().fold( 171 + LocMetrics { physical: 0, logical: 0, comments: 0, blank: 0 }, 172 + |acc, f| acc.add(&f.metrics), 173 + ); 174 + 175 + let mut sorted_files = files; 176 + sorted_files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics))); 177 + 178 + DirectoryLocMetrics { path, total, files: sorted_files } 179 + }) 180 + .collect(); 181 + 182 + directories.sort_by(|a, b| rank_by.value_from(&b.total).cmp(&rank_by.value_from(&a.total))); 183 + 184 + directories 185 + } 186 + 187 + /// Serialize to JSON 188 + pub fn to_json(&self) -> serde_json::Result<String> { 189 + serde_json::to_string_pretty(self) 190 + } 191 + } 192 + 193 + impl LocSummary { 194 + fn from_files(files: &[FileLocReport]) -> Self { 195 + let total_files = files.len(); 196 + let total_physical = files.iter().map(|f| f.metrics.physical).sum(); 197 + let total_logical = files.iter().map(|f| f.metrics.logical).sum(); 198 + let total_comments = files.iter().map(|f| f.metrics.comments).sum(); 199 + let total_blank = files.iter().map(|f| f.metrics.blank).sum(); 200 + 201 + Self { total_files, total_physical, total_logical, total_comments, total_blank } 202 + } 62 203 } 63 204 64 205 #[cfg(test)] ··· 135 276 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 136 277 assert!(metrics.comments >= 3); 137 278 assert_eq!(metrics.logical, 0); 279 + } 280 + 281 + #[test] 282 + fn test_loc_metrics_add() { 283 + let m1 = LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 }; 284 + let m2 = LocMetrics { physical: 20, logical: 15, comments: 3, blank: 2 }; 285 + let result = m1.add(&m2); 286 + 287 + assert_eq!(result.physical, 30); 288 + assert_eq!(result.logical, 23); 289 + assert_eq!(result.comments, 4); 290 + assert_eq!(result.blank, 3); 291 + } 292 + 293 + #[test] 294 + fn test_rank_by_value_from() { 295 + let metrics = LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }; 296 + 297 + assert_eq!(RankBy::Physical.value_from(&metrics), 100); 298 + assert_eq!(RankBy::Logical.value_from(&metrics), 80); 299 + assert_eq!(RankBy::Comments.value_from(&metrics), 10); 300 + assert_eq!(RankBy::Blank.value_from(&metrics), 10); 301 + } 302 + 303 + #[test] 304 + fn test_loc_report_new() { 305 + let files = vec![ 306 + FileLocReport { 307 + path: PathBuf::from("test1.rs"), 308 + metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }, 309 + }, 310 + FileLocReport { 311 + path: PathBuf::from("test2.rs"), 312 + metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 }, 313 + }, 314 + ]; 315 + 316 + let report = LocReport::new(files, RankBy::Logical, false); 317 + 318 + assert_eq!(report.summary.total_files, 2); 319 + assert_eq!(report.summary.total_physical, 150); 320 + assert_eq!(report.summary.total_logical, 120); 321 + assert_eq!(report.summary.total_comments, 15); 322 + assert_eq!(report.summary.total_blank, 15); 323 + 324 + assert_eq!(report.files[0].metrics.logical, 80); 325 + assert_eq!(report.files[1].metrics.logical, 40); 326 + } 327 + 328 + #[test] 329 + fn test_loc_report_with_directories() { 330 + let files = vec![ 331 + FileLocReport { 332 + path: PathBuf::from("src/main.rs"), 333 + metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }, 334 + }, 335 + FileLocReport { 336 + path: PathBuf::from("src/lib.rs"), 337 + metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 }, 338 + }, 339 + FileLocReport { 340 + path: PathBuf::from("tests/test.rs"), 341 + metrics: LocMetrics { physical: 30, logical: 25, comments: 3, blank: 2 }, 342 + }, 343 + ]; 344 + 345 + let report = LocReport::new(files, RankBy::Logical, true); 346 + 347 + assert!(report.directories.is_some()); 348 + let dirs = report.directories.unwrap(); 349 + assert_eq!(dirs.len(), 2); 350 + 351 + assert_eq!(dirs[0].path, PathBuf::from("src")); 352 + assert_eq!(dirs[0].total.logical, 120); 353 + assert_eq!(dirs[0].files.len(), 2); 354 + 355 + assert_eq!(dirs[1].path, PathBuf::from("tests")); 356 + assert_eq!(dirs[1].total.logical, 25); 357 + assert_eq!(dirs[1].files.len(), 1); 358 + } 359 + 360 + #[test] 361 + fn test_loc_report_to_json() { 362 + let files = vec![FileLocReport { 363 + path: PathBuf::from("test.rs"), 364 + metrics: LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 }, 365 + }]; 366 + 367 + let report = LocReport::new(files, RankBy::Logical, false); 368 + let json = report.to_json().unwrap(); 369 + 370 + assert!(json.contains("files")); 371 + assert!(json.contains("summary")); 372 + assert!(json.contains("test.rs")); 138 373 } 139 374 }
+115 -23
docs/src/lines-of-code.md
··· 71 71 72 72 ## Using LOC with Mccabre 73 73 74 - ### Basic Usage 74 + ### Commands 75 + 76 + Mccabre provides multiple ways to analyze LOC: 75 77 76 78 ```bash 77 - # Analyze LOC for a directory 79 + # Dedicated LOC analysis with ranking 80 + mccabre loc src/ 81 + 82 + # Full analysis including complexity and clones 78 83 mccabre analyze src/ 79 84 80 - # Complexity command also includes LOC 85 + # Complexity analysis includes LOC 81 86 mccabre complexity src/ 82 87 ``` 83 88 89 + ### The `loc` Command 90 + 91 + The `loc` command provides focused LOC analysis with powerful ranking capabilities. 92 + 93 + #### Basic Usage 94 + 95 + ```bash 96 + # Rank files by logical LOC (default) 97 + mccabre loc src/ 98 + 99 + # Rank files by physical LOC 100 + mccabre loc src/ --rank-by physical 101 + 102 + # Rank files by comments 103 + mccabre loc src/ --rank-by comments 104 + 105 + # Rank files by blank lines 106 + mccabre loc src/ --rank-by blank 107 + ``` 108 + 109 + #### Directory Ranking 110 + 111 + Group and rank files by directory: 112 + 113 + ```bash 114 + # Rank directories by total LOC, files ranked within each 115 + mccabre loc src/ --rank-dirs 116 + 117 + # Rank directories by comments 118 + mccabre loc src/ --rank-dirs --rank-by comments 119 + ``` 120 + 84 121 ### Sample Output 85 122 123 + #### File Ranking 124 + 86 125 ```text 87 - FILE: src/main.rs 88 - Cyclomatic Complexity: 5 89 - Physical LOC: 120 90 - Logical LOC: 85 91 - Comment lines: 25 92 - Blank lines: 10 126 + LINES OF CODE ANALYSIS 127 + 128 + SUMMARY 129 + Total files analyzed: 12 130 + Total physical LOC: 2246 131 + Total logical LOC: 1360 132 + Total comment lines: 135 133 + Total blank lines: 751 134 + 135 + FILES RANKED BY Logical LOC 136 + 137 + 1. crates/core/src/complexity/loc.rs (Logical LOC: 297) 138 + Physical: 403 | Logical: 297 | Comments: 37 | Blank: 69 139 + 140 + 2. crates/core/src/reporter.rs (Logical LOC: 212) 141 + Physical: 260 | Logical: 212 | Comments: 16 | Blank: 32 93 142 ``` 94 143 95 - ### JSON 144 + #### Directory Ranking 145 + 146 + ```text 147 + DIRECTORIES RANKED BY Logical LOC 148 + 149 + DIRECTORY: crates/core/src 150 + Total Physical: 1126 | Logical: 583 | Comments: 42 | Blank: 501 151 + 152 + Files: 153 + reporter.rs (Logical LOC: 212) - P: 260 | L: 212 | C: 16 | B: 32 154 + loader.rs (Logical LOC: 150) - P: 204 | L: 150 | C: 8 | B: 46 155 + ``` 156 + 157 + ### JSON Output 96 158 97 159 ```bash 98 - mccabre analyze src/ --json 160 + # File ranking as JSON 161 + mccabre loc src/ --json 162 + 163 + # Directory ranking as JSON 164 + mccabre loc src/ --rank-dirs --json 99 165 ``` 100 166 101 167 ```json ··· 103 169 "files": [ 104 170 { 105 171 "path": "src/main.rs", 106 - "loc": { 172 + "metrics": { 107 173 "physical": 120, 108 174 "logical": 85, 109 175 "comments": 25, 110 176 "blank": 10 111 177 } 112 178 } 113 - ] 179 + ], 180 + "directories": null, 181 + "summary": { 182 + "total_files": 1, 183 + "total_physical": 120, 184 + "total_logical": 85, 185 + "total_comments": 25, 186 + "total_blank": 10 187 + } 114 188 } 115 189 ``` 116 190 ··· 158 232 159 233 Use `.gitignore` to exclude these (Mccabre respects gitignore). 160 234 161 - ## Tracking LOC Over Time 235 + ## Advanced Use Cases 236 + 237 + ### Finding the Largest Files 238 + 239 + ```bash 240 + # Top 10 largest files by logical LOC 241 + mccabre loc src/ --rank-by logical | head -30 242 + ``` 162 243 163 - ### Baseline 244 + ### Identifying Under-Commented Code 164 245 165 246 ```bash 166 - # Create baseline 167 - mccabre analyze src/ --json > baseline.json 247 + # Files ranked by comment count (ascending) 248 + mccabre loc src/ --rank-by comments 168 249 ``` 169 250 170 - ### Compare 251 + ### Directory Hotspots 171 252 172 253 ```bash 254 + # Find directories with the most code 255 + mccabre loc . --rank-dirs --rank-by logical 256 + ``` 257 + 258 + ### Tracking LOC Over Time 259 + 260 + ```bash 261 + # Create baseline 262 + mccabre loc src/ --json > baseline.json 263 + 173 264 # Later... 174 - mccabre analyze src/ --json > current.json 265 + mccabre loc src/ --json > current.json 175 266 176 - # Compare (using jq) 177 - jq '.summary.total_logical_loc' baseline.json 178 - jq '.summary.total_logical_loc' current.json 267 + # Compare using jq 268 + jq '.summary.total_logical' baseline.json 269 + jq '.summary.total_logical' current.json 179 270 ``` 180 271 181 - ### Visualize Growth 272 + ### CI Integration 182 273 183 274 Integrate with your CI to track: 184 275 185 276 - LOC growth per sprint 186 277 - LOC per feature 187 278 - Comment ratio trends 279 + - Detect large file additions 188 280 189 281 ## See Also 190 282