cargo-coupling 0.3.2

A coupling analysis tool for Rust projects - measuring the 'right distance' in your code
Documentation
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
//! cargo-coupling CLI - Coupling Analysis Tool
//!
//! Analyzes Rust projects for coupling patterns and generates reports.
//! Features parallel processing for large codebases.
//!
//! Usage:
//!   cargo coupling [OPTIONS] [PATH]
//!   cargo-coupling [OPTIONS] [PATH]

use std::fs::File;
use std::io::{BufWriter, Write, stdout};
use std::path::PathBuf;
use std::process;
use std::time::Instant;

use clap::{Parser, Subcommand};

use cargo_coupling::{
    CompiledConfig, IssueThresholds, VolatilityAnalyzer, analyze_workspace_with_config,
    cli_output::{
        CheckConfig, generate_check_output, generate_hotspots_output, generate_impact_output,
        generate_json_output, parse_grade, parse_severity,
    },
    generate_ai_output_with_thresholds, generate_report_with_thresholds,
    generate_summary_with_thresholds, load_compiled_config,
    web::{ServerConfig, start_server},
};

/// cargo-coupling - Measure the "right distance" in your Rust code
#[derive(Parser, Debug)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Analyze coupling in a Rust project
    Coupling(Args),
}

#[derive(Parser, Debug)]
struct Args {
    /// Path to the project or directory to analyze
    #[arg(default_value = "./src")]
    path: PathBuf,

    /// Output file for the report (default: stdout)
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Show summary only (no detailed report)
    #[arg(short, long)]
    summary: bool,

    /// AI-friendly output format for use with coding agents (Claude, Copilot, etc.)
    #[arg(long)]
    ai: bool,

    /// Analyze git history for volatility (months to look back)
    #[arg(long, default_value = "6")]
    git_months: usize,

    /// Skip git history analysis
    #[arg(long)]
    no_git: bool,

    /// Exclude test code from analysis (#[test], #[cfg(test)], mod tests)
    #[arg(long)]
    exclude_tests: bool,

    /// Config file path (default: search for .coupling.toml)
    #[arg(short, long)]
    config: Option<PathBuf>,

    /// Verbose output
    #[arg(short, long)]
    verbose: bool,

    /// Show timing information
    #[arg(long)]
    timing: bool,

    /// Number of threads for parallel processing (default: all CPU cores)
    #[arg(long, short = 'j', value_name = "N")]
    jobs: Option<usize>,

    // === Threshold options ===
    /// Max outgoing dependencies before flagging as High Efferent Coupling
    #[arg(long)]
    max_deps: Option<usize>,

    /// Max incoming dependencies before flagging as High Afferent Coupling
    #[arg(long)]
    max_dependents: Option<usize>,

    // === Web visualization options ===
    /// Start web server for interactive visualization
    #[arg(long)]
    web: bool,

    /// Port for web server (default: 3000)
    #[arg(long, default_value = "3000")]
    port: u16,

    /// Don't open browser automatically when starting web server
    #[arg(long)]
    no_open: bool,

    /// API endpoint URL for frontend (useful for separate deployments)
    #[arg(long)]
    api_endpoint: Option<String>,

    // === Job-focused CLI options ===
    /// Show top N refactoring hotspots (default: 5). Use --hotspots or --hotspots=N
    #[arg(long, value_name = "N", num_args = 0..=1, require_equals = true, default_missing_value = "5")]
    hotspots: Option<usize>,

    /// Analyze change impact for a specific module
    #[arg(long, value_name = "MODULE")]
    impact: Option<String>,

    /// Trace dependencies for a specific function/type (e.g., "analyze_file" or "BalanceScore")
    #[arg(long, value_name = "ITEM")]
    trace: Option<String>,

    /// Run quality gate check (returns non-zero exit code on failure)
    #[arg(long)]
    check: bool,

    /// Minimum grade for --check (A, B, C, D, F). Default: C
    #[arg(long, value_name = "GRADE", requires = "check")]
    min_grade: Option<String>,

    /// Maximum critical issues for --check. Default: 0
    #[arg(long, value_name = "N", requires = "check")]
    max_critical: Option<usize>,

    /// Maximum circular dependencies for --check. Default: 0
    #[arg(long, value_name = "N", requires = "check")]
    max_circular: Option<usize>,

    /// Fail --check on any issue at this severity or higher (critical, high, medium, low)
    #[arg(long, value_name = "SEVERITY", requires = "check")]
    fail_on: Option<String>,

    /// Output in JSON format (machine-readable)
    #[arg(long)]
    json: bool,

    /// Show all issues including Low severity (default: only Medium/High/Critical)
    #[arg(long)]
    all: bool,

    /// Show explanations in Japanese (日本語で解説を表示)
    #[arg(long, visible_alias = "jp")]
    japanese: bool,
}

fn main() {
    if let Err(e) = run() {
        eprintln!("Error: {}", e);
        process::exit(1);
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    let Commands::Coupling(args) = cli.command;

    // Detect available CPU cores
    let available_cores = std::thread::available_parallelism()
        .map(|p| p.get())
        .unwrap_or(1);

    // Configure thread pool
    let num_threads = args.jobs.unwrap_or(available_cores);
    if num_threads != available_cores || args.jobs.is_some() {
        rayon::ThreadPoolBuilder::new()
            .num_threads(num_threads)
            .build_global()
            .unwrap_or_else(|e| eprintln!("Warning: Could not set thread count: {}", e));
    }

    if args.verbose || args.timing {
        eprintln!(
            "Using {} thread(s) for parallel processing ({} CPU cores available)",
            num_threads, available_cores
        );
    }

    let total_start = Instant::now();

    // Load configuration file
    let config_path = args.config.as_ref().unwrap_or(&args.path);
    let mut config = match load_compiled_config(config_path) {
        Ok(config) => {
            if args.verbose && config.has_volatility_overrides() {
                eprintln!("Loaded configuration from .coupling.toml");
            }
            config
        }
        Err(e) => {
            if args.verbose {
                eprintln!("Note: No config file loaded: {}", e);
            }
            CompiledConfig::empty()
        }
    };

    // Apply CLI flags to config (CLI takes precedence over config file)
    if args.exclude_tests {
        config.set_exclude_tests(true);
    }

    if args.verbose && config.exclude_tests {
        eprintln!("Test code will be excluded from analysis");
    }

    if args.verbose && config.prelude_module_count() > 0 {
        eprintln!(
            "Prelude modules configured: {} pattern(s)",
            config.prelude_module_count()
        );
    }

    // Print analysis header
    eprintln!("Analyzing project at '{}'...", args.path.display());

    // Analyze the project (uses cargo metadata for better accuracy)
    let analysis_start = Instant::now();
    let mut metrics = analyze_workspace_with_config(&args.path, &config)?;
    let analysis_time = analysis_start.elapsed();

    // Analyze git history for volatility (if not disabled)
    if !args.no_git {
        if args.verbose {
            eprintln!("Analyzing git history ({} months)...", args.git_months);
        }

        let mut volatility = VolatilityAnalyzer::new(args.git_months);
        match volatility.analyze(&args.path) {
            Ok(()) => {
                if args.verbose {
                    let stats = volatility.statistics();
                    eprintln!(
                        "Git analysis: {} files, {} total changes",
                        stats.total_files, stats.total_changes
                    );
                }

                // Copy file changes to project metrics (must be after statistics())
                metrics.file_changes = volatility.file_changes;

                // Update volatility for all couplings based on git history
                metrics.update_volatility_from_git();
            }
            Err(e) => {
                if args.verbose {
                    eprintln!("Warning: Git analysis failed: {}", e);
                }
            }
        }
    }

    // Apply volatility overrides from config
    if config.has_volatility_overrides() {
        let mut override_count = 0;
        for coupling in &mut metrics.couplings {
            // Use the target path for volatility lookup
            if let Some(override_vol) = config.get_volatility_override(&coupling.target) {
                coupling.volatility = override_vol;
                override_count += 1;
            }
        }
        if args.verbose && override_count > 0 {
            eprintln!(
                "Applied {} volatility overrides from config",
                override_count
            );
        }
    }

    if args.timing {
        eprintln!(
            "Analysis complete: {} files, {} modules (took {:.2?})\n",
            metrics.total_files,
            metrics.module_count(),
            analysis_time
        );
    } else {
        eprintln!(
            "Analysis complete: {} files, {} modules\n",
            metrics.total_files,
            metrics.module_count()
        );
    }

    // Create custom thresholds - CLI args override config, which overrides defaults
    let thresholds = IssueThresholds {
        max_dependencies: args.max_deps.unwrap_or(config.thresholds.max_dependencies),
        max_dependents: args
            .max_dependents
            .unwrap_or(config.thresholds.max_dependents),
        strict_mode: !args.all, // Default is strict (hide Low), --all shows everything
        japanese: args.japanese,
        exclude_tests: config.exclude_tests,
        prelude_module_count: config.prelude_module_count(),
        ..IssueThresholds::default()
    };

    if args.verbose {
        eprintln!(
            "Thresholds: max_deps={}, max_dependents={}",
            thresholds.max_dependencies, thresholds.max_dependents
        );
    }

    // Web visualization mode
    if args.web {
        let server_config = ServerConfig {
            port: args.port,
            open_browser: !args.no_open,
            api_endpoint: args.api_endpoint.clone(),
        };

        // Run the web server using tokio runtime
        let rt = tokio::runtime::Runtime::new()?;
        rt.block_on(start_server(metrics, thresholds, server_config))
            .map_err(|e| -> Box<dyn std::error::Error> { e })?;

        return Ok(());
    }

    // Generate output
    let output: Box<dyn Write> = match &args.output {
        Some(path) => {
            let file = File::create(path)?;
            Box::new(BufWriter::new(file))
        }
        None => Box::new(stdout()),
    };

    let mut writer = output;

    // Job-focused CLI modes (mutually exclusive with other modes)

    // --json: Machine-readable JSON output
    if args.json {
        generate_json_output(&metrics, &thresholds, &mut writer)?;
        return Ok(());
    }

    // --check: Quality gate check (returns exit code)
    if args.check {
        let check_config = CheckConfig {
            min_grade: args.min_grade.as_ref().and_then(|s| parse_grade(s)),
            max_critical: args.max_critical,
            max_circular: args.max_circular,
            fail_on: args.fail_on.as_ref().and_then(|s| parse_severity(s)),
        };
        let exit_code = generate_check_output(&metrics, &thresholds, &check_config, &mut writer)?;
        process::exit(exit_code);
    }

    // --hotspots: Show top refactoring targets
    if let Some(limit) = args.hotspots {
        generate_hotspots_output(&metrics, &thresholds, limit, args.verbose, &mut writer)?;
        return Ok(());
    }

    // --impact: Analyze impact of a specific module
    if let Some(module_name) = &args.impact {
        let found = generate_impact_output(&metrics, module_name, &mut writer)?;
        if !found {
            process::exit(1);
        }
        return Ok(());
    }

    // --trace: Trace dependencies for a specific function/type
    if let Some(item_name) = &args.trace {
        let found =
            cargo_coupling::cli_output::generate_trace_output(&metrics, item_name, &mut writer)?;
        if !found {
            process::exit(1);
        }
        return Ok(());
    }

    // Default modes
    if args.ai {
        generate_ai_output_with_thresholds(&metrics, &thresholds, &mut writer)?;
    } else if args.summary {
        generate_summary_with_thresholds(&metrics, &thresholds, &mut writer)?;
    } else {
        generate_report_with_thresholds(&metrics, &thresholds, &mut writer)?;
    }

    // Notify about output file
    if let Some(path) = &args.output {
        eprintln!("Report written to: {}", path.display());
    }

    // Show total timing
    if args.timing {
        let total_time = total_start.elapsed();
        let files_per_sec = metrics.total_files as f64 / total_time.as_secs_f64();
        eprintln!(
            "Total time: {:.2?} ({:.1} files/sec)",
            total_time, files_per_sec
        );
    }

    Ok(())
}