1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "howmany")]
#[command(about = "Count files and lines of code in your projects")]
// Taken from the manifest so `--version` cannot disagree with the crate that
// was actually built, or with the version stamped into every report.
#[command(version = env!("CARGO_PKG_VERSION"))]
pub struct Config {
/// Directory to analyze (defaults to current directory)
#[arg(value_name = "PATH")]
pub path: Option<PathBuf>,
/// Output format: text, json, csv, html, or sarif
#[arg(short = 'o', long = "output", default_value = "text")]
pub format: OutputFormat,
/// Show individual file statistics
#[arg(short = 'f', long = "files")]
pub show_files: bool,
/// Simple CLI mode - show only basic file and line counts
#[arg(long = "cli")]
pub cli_mode: bool,
/// Disable interactive mode (interactive mode is enabled by default)
#[arg(long = "no-interactive")]
pub no_interactive: bool,
/// Show detailed breakdown by file extension
#[arg(short = 'v', long = "verbose")]
pub verbose: bool,
/// Maximum directory depth to traverse
#[arg(short = 'd', long = "depth")]
pub max_depth: Option<usize>,
/// Only count specific file extensions (comma-separated: rs,py,js)
#[arg(short = 'e', long = "ext")]
pub extensions: Option<String>,
/// Include hidden files and directories
#[arg(long = "hidden")]
pub include_hidden: bool,
/// Sort results by: files, lines, code, comments, size, complexity, quality, functions
#[arg(short = 's', long = "sort", default_value = "files")]
pub sort_by: SortBy,
/// Sort in descending order
#[arg(long = "desc")]
pub descending: bool,
/// Additional patterns to ignore (comma-separated: node_modules,target,dist)
#[arg(long = "ignore")]
pub ignore_patterns: Option<String>,
/// List files that would be counted (useful for debugging)
#[arg(short = 'l', long = "list")]
pub list_files: bool,
// Filter options
/// Minimum lines per file to include
#[arg(long = "min-lines")]
pub min_lines: Option<usize>,
/// Maximum lines per file to include
#[arg(long = "max-lines")]
pub max_lines: Option<usize>,
/// Minimum file size to include (e.g., 1KB, 500MB)
#[arg(long = "min-size")]
pub min_size: Option<String>,
/// Maximum file size to include (e.g., 1KB, 500MB)
#[arg(long = "max-size")]
pub max_size: Option<String>,
/// Include only these languages (comma-separated: rs,py,js)
#[arg(long = "only")]
pub only_languages: Option<String>,
/// Exclude these languages (comma-separated: rs,py,js)
#[arg(long = "exclude")]
pub exclude_languages: Option<String>,
// Enhanced CLI output options
/// Show complexity information in CLI mode
#[arg(long = "show-complexity")]
pub show_complexity: bool,
/// Show quality scores in CLI mode
#[arg(long = "show-quality")]
pub show_quality: bool,
/// Show code ratios in CLI mode
#[arg(long = "show-ratios")]
pub show_ratios: bool,
/// Show size information in CLI mode
#[arg(long = "show-size")]
pub show_size: bool,
// Advanced filtering options
/// Minimum complexity score to include (0.0-100.0)
#[arg(long = "min-complexity")]
pub min_complexity: Option<f64>,
/// Maximum complexity score to include (0.0-100.0)
#[arg(long = "max-complexity")]
pub max_complexity: Option<f64>,
/// Minimum functions per file to include
#[arg(long = "min-functions")]
pub min_functions: Option<usize>,
/// Maximum functions per file to include
#[arg(long = "max-functions")]
pub max_functions: Option<usize>,
/// Minimum quality score to include (0-100)
#[arg(long = "min-quality")]
pub min_quality_score: Option<f64>,
/// Maximum quality score to include (0-100)
#[arg(long = "max-quality")]
pub max_quality_score: Option<f64>,
/// Minimum documentation ratio to include (0.0-1.0)
#[arg(long = "min-doc-ratio")]
pub min_doc_ratio: Option<f64>,
/// Maximum documentation ratio to include (0.0-1.0)
#[arg(long = "max-doc-ratio")]
pub max_doc_ratio: Option<f64>,
// Advanced filter shortcuts
/// Only show files with high complexity (complexity > 10)
#[arg(long = "high-complexity")]
pub high_complexity_only: bool,
/// Only show files with low quality scores (quality < 60)
#[arg(long = "low-quality")]
pub low_quality_only: bool,
/// Only show files with poor documentation (doc ratio < 0.1)
#[arg(long = "undocumented")]
pub undocumented_only: bool,
// Output enhancement options
/// Show time estimates in CLI mode
#[arg(long = "show-time")]
pub show_time_estimates: bool,
/// Compact output mode
#[arg(long = "compact")]
pub compact_output: bool,
/// Show only summary (no per-extension breakdown)
#[arg(long = "summary-only")]
pub summary_only: bool,
/// Show top N results only
#[arg(long = "top")]
pub top_n: Option<usize>,
/// Show file-level complexity details
#[arg(long = "show-functions")]
pub show_function_details: bool,
// Format options
/// Disable colors in output
#[arg(long = "no-color")]
pub no_color: bool,
/// Output preset (compact, detailed, minimal)
#[arg(long = "preset")]
pub output_preset: Option<String>,
// Developer experience
/// Quiet mode - minimal output
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
/// Explain why files were included/excluded
#[arg(long = "explain")]
pub explain_filtering: bool,
// Performance and reproducibility
/// Worker threads to use (0 = one per available core)
#[arg(short = 'j', long = "threads", default_value_t = 0)]
pub threads: usize,
/// Skip external language detection (faster, and identical on every machine)
#[arg(long = "no-detect")]
pub no_detect: bool,
/// Do not read or write the on-disk results cache
#[arg(long = "no-cache")]
pub no_cache: bool,
/// Write html/sarif reports here instead of the default name in the
/// current directory
#[arg(long = "output-file")]
pub output_file: Option<PathBuf>,
/// Reproducible mode: no language detection, no cache, single threaded
#[arg(long = "reproducible")]
pub reproducible: bool,
}
#[derive(Clone)]
pub enum OutputFormat {
Text,
Json,
Csv,
Html,
Sarif,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" | "txt" => Ok(OutputFormat::Text),
"json" => Ok(OutputFormat::Json),
"csv" => Ok(OutputFormat::Csv),
"html" => Ok(OutputFormat::Html),
"sarif" => Ok(OutputFormat::Sarif),
_ => Err(format!("Invalid output format: {}", s)),
}
}
}
#[derive(Clone, Copy)]
pub enum SortBy {
Files,
Lines,
Code,
Comments,
Size,
Complexity,
Quality,
Functions,
DocRatio,
}
impl std::str::FromStr for SortBy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"files" | "file" => Ok(SortBy::Files),
"lines" | "line" => Ok(SortBy::Lines),
"code" => Ok(SortBy::Code),
"comments" | "comment" => Ok(SortBy::Comments),
"size" => Ok(SortBy::Size),
"complexity" | "complex" => Ok(SortBy::Complexity),
"quality" => Ok(SortBy::Quality),
"functions" | "function" | "func" => Ok(SortBy::Functions),
"doc-ratio" | "docs" | "documentation" => Ok(SortBy::DocRatio),
_ => Err(format!("Invalid sort option: {}", s)),
}
}
}
impl Config {
pub fn parse_args() -> Self {
Self::parse()
}
/// Check if interactive mode should be enabled (default true, unless --no-interactive is passed)
pub fn interactive(&self) -> bool {
!self.no_interactive
}
/// Convert comma-separated extensions string to Vec
pub fn get_extensions(&self) -> Vec<String> {
self.extensions
.as_ref()
.map(|s| s.split(',').map(|ext| ext.trim().to_string()).collect())
.unwrap_or_default()
}
/// Convert comma-separated ignore patterns string to Vec
pub fn get_ignore_patterns(&self) -> Vec<String> {
self.ignore_patterns
.as_ref()
.map(|s| {
s.split(',')
.map(|pattern| pattern.trim().to_string())
.collect()
})
.unwrap_or_default()
}
/// Apply advanced filter shortcuts to set specific filter values
pub fn apply_advanced_filter_shortcuts(&mut self) {
if self.high_complexity_only {
self.min_complexity = Some(10.0);
}
if self.low_quality_only {
self.max_quality_score = Some(60.0);
}
if self.undocumented_only {
self.max_doc_ratio = Some(0.1);
}
}
/// Get output preset configuration
pub fn apply_output_preset(&mut self) {
if let Some(preset) = &self.output_preset {
match preset.to_lowercase().as_str() {
"compact" => {
self.compact_output = true;
self.no_color = true;
self.top_n = Some(10);
}
"detailed" => {
self.verbose = true;
self.show_complexity = true;
self.show_quality = true;
self.show_ratios = true;
self.show_size = true;
self.show_time_estimates = true;
self.show_function_details = true;
}
"minimal" => {
self.quiet = true;
self.summary_only = true;
self.no_color = true;
self.compact_output = true;
}
_ => {} // Unknown preset, ignore
}
}
}
/// Translate the CLI flags into engine options.
pub fn analysis_options(
&self,
collect_individual_files: bool,
) -> crate::core::engine::AnalysisOptions {
use crate::core::engine::{AnalysisOptions, DetectionMode, Parallelism};
AnalysisOptions {
max_depth: self.max_depth,
include_hidden: self.include_hidden,
ignore_patterns: self.get_ignore_patterns(),
extensions: self.get_extensions(),
collect_individual_files,
detection: if self.no_detect || self.reproducible {
DetectionMode::Disabled
} else {
DetectionMode::Auto
},
parallelism: if self.reproducible {
Parallelism::Fixed(1)
} else if self.threads == 0 {
Parallelism::Auto
} else {
Parallelism::Fixed(self.threads)
},
use_cache: !self.no_cache && !self.reproducible,
compute_complexity: self.reports_complexity(),
}
}
/// Whether the selected output actually shows complexity and quality.
///
/// Computing it is the expensive half of a run, so it is skipped for the
/// formats that only print totals. It must not be skipped for the formats
/// that print the numbers: those used to emit a section of zeros with a
/// maintainability index of 100, which reads as a perfect score.
pub fn reports_complexity(&self) -> bool {
match self.format {
OutputFormat::Json | OutputFormat::Html | OutputFormat::Sarif => true,
// Plain text is either the interactive dashboard or the summary
// tables, both of which show quality; `--cli` and `--quiet` are the
// totals-only paths and take their options from elsewhere.
OutputFormat::Text => !self.cli_mode && !self.quiet,
OutputFormat::Csv => false,
}
}
/// Destination for html/sarif reports.
pub fn report_path(&self, default_name: &str) -> PathBuf {
self.output_file
.clone()
.unwrap_or_else(|| PathBuf::from(default_name))
}
/// Convert CLI options to FilterOptions
pub fn get_filter_options(&self) -> crate::ui::filters::FilterOptions {
use crate::ui::filters::{FilterOptions, FilterParser};
FilterOptions {
min_lines: self.min_lines,
max_lines: self.max_lines,
min_size_bytes: self
.min_size
.as_ref()
.and_then(|s| FilterParser::parse_size(s)),
max_size_bytes: self
.max_size
.as_ref()
.and_then(|s| FilterParser::parse_size(s)),
min_complexity: self.min_complexity,
max_complexity: self.max_complexity,
min_functions: self.min_functions,
max_functions: self.max_functions,
min_quality_score: self.min_quality_score,
max_quality_score: self.max_quality_score,
min_doc_ratio: self.min_doc_ratio,
max_doc_ratio: self.max_doc_ratio,
include_languages: self
.only_languages
.as_ref()
.map(|s| FilterParser::parse_languages(s))
.unwrap_or_default(),
exclude_languages: self
.exclude_languages
.as_ref()
.map(|s| FilterParser::parse_languages(s))
.unwrap_or_default(),
show_complexity: self.show_complexity,
show_quality: self.show_quality,
show_ratios: self.show_ratios,
show_size_info: self.show_size,
compact_output: self.compact_output,
}
}
}