code complexity & repetition analysis tool
at main 204 lines 5.9 kB view raw
1mod commands; 2mod highlight; 3 4use anyhow::Result; 5use clap::{Parser, Subcommand}; 6use std::path::PathBuf; 7 8#[derive(Parser)] 9#[command(name = "mccabre")] 10#[command(about = "Code complexity & clone detection tool", long_about = None)] 11#[command(version)] 12struct Cli { 13 #[command(subcommand)] 14 command: Commands, 15} 16 17#[derive(Subcommand)] 18enum Commands { 19 /// Run full analysis (complexity + clones + LOC) 20 Analyze { 21 /// Path to file or directory to analyze 22 #[arg(value_name = "PATH", default_value = ".")] 23 path: PathBuf, 24 25 /// Output in JSON format 26 #[arg(short, long)] 27 json: bool, 28 29 /// Complexity threshold for warnings 30 #[arg(long)] 31 threshold: Option<usize>, 32 33 /// Minimum tokens for clone detection 34 #[arg(long, default_value = "30")] 35 min_tokens: usize, 36 37 /// Path to config file 38 #[arg(short, long)] 39 config: Option<PathBuf>, 40 41 /// Disable gitignore awareness 42 #[arg(long)] 43 no_gitignore: bool, 44 45 /// Disable syntax highlighting for clone code blocks 46 #[arg(long)] 47 no_highlight: bool, 48 }, 49 50 /// Analyze cyclomatic complexity and LOC only 51 Complexity { 52 /// Path to file or directory to analyze 53 #[arg(value_name = "PATH", default_value = ".")] 54 path: PathBuf, 55 56 /// Output in JSON format 57 #[arg(short, long)] 58 json: bool, 59 60 /// Complexity threshold for warnings 61 #[arg(long)] 62 threshold: Option<usize>, 63 64 /// Path to config file 65 #[arg(short, long)] 66 config: Option<PathBuf>, 67 68 /// Disable gitignore awareness 69 #[arg(long)] 70 no_gitignore: bool, 71 }, 72 73 /// Detect code clones only 74 Clones { 75 /// Path to file or directory to analyze 76 #[arg(value_name = "PATH", default_value = ".")] 77 path: PathBuf, 78 79 /// Output in JSON format 80 #[arg(short, long)] 81 json: bool, 82 83 /// Minimum tokens for clone detection 84 #[arg(long, default_value = "30")] 85 min_tokens: usize, 86 87 /// Path to config file 88 #[arg(short, long)] 89 config: Option<PathBuf>, 90 91 /// Disable gitignore awareness 92 #[arg(long)] 93 no_gitignore: bool, 94 95 /// Disable syntax highlighting for clone code blocks 96 #[arg(long)] 97 no_highlight: bool, 98 }, 99 100 /// Display current configuration 101 DumpConfig { 102 /// Path to config file (if not specified, shows defaults) 103 #[arg(short, long)] 104 config: Option<PathBuf>, 105 106 /// Save configuration to file (file path or directory) 107 #[arg(short = 'o', long)] 108 output: Option<PathBuf>, 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 }, 137 138 /// Analyze code coverage from LCOV data 139 Coverage { 140 /// Path to LCOV file 141 #[arg(long, value_name = "PATH")] 142 from: PathBuf, 143 144 /// Output as JSONL to file 145 #[arg(long, value_name = "PATH")] 146 jsonl: Option<PathBuf>, 147 148 /// Repository root for path normalization 149 #[arg(long, value_name = "PATH")] 150 repo_root: Option<PathBuf>, 151 152 /// View detailed coverage for a specific file 153 #[arg(long, value_name = "PATH")] 154 file: Option<PathBuf>, 155 }, 156} 157 158fn main() -> Result<()> { 159 let cli = Cli::parse(); 160 161 match cli.command { 162 Commands::Analyze { path, json, threshold, min_tokens, config, no_gitignore, no_highlight } => { 163 commands::analyze::run( 164 path, 165 json, 166 threshold, 167 Some(min_tokens), 168 config, 169 !no_gitignore, 170 !no_highlight, 171 ) 172 } 173 Commands::Complexity { path, json, threshold, config, no_gitignore } => { 174 commands::complexity::run(path, json, threshold, config, !no_gitignore) 175 } 176 Commands::Clones { path, json, min_tokens, config, no_gitignore, no_highlight } => { 177 commands::clones::run(path, json, Some(min_tokens), config, !no_gitignore, !no_highlight) 178 } 179 Commands::DumpConfig { config, output } => commands::dump_config::run(config, output), 180 Commands::Loc { path, json, rank_by, rank_dirs, config, no_gitignore } => { 181 use mccabre_core::complexity::loc::RankBy; 182 183 let rank_by = match rank_by.to_lowercase().as_str() { 184 "logical" => RankBy::Logical, 185 "physical" => RankBy::Physical, 186 "comments" => RankBy::Comments, 187 "blank" => RankBy::Blank, 188 _ => { 189 eprintln!("Invalid rank_by value. Use: logical, physical, comments, or blank"); 190 std::process::exit(1); 191 } 192 }; 193 194 commands::loc::run(path, json, rank_by, rank_dirs, config, !no_gitignore) 195 } 196 Commands::Coverage { from, jsonl, repo_root, file } => { 197 if let Some(file) = file { 198 commands::coverage::run_file_view(file, from) 199 } else { 200 commands::coverage::run(from, jsonl, repo_root) 201 } 202 } 203 } 204}