code complexity & repetition analysis tool
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}