debtmap/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Copy, ValueEnum)]
5pub enum ThresholdPreset {
6    /// Strict thresholds for high code quality standards
7    Strict,
8    /// Balanced thresholds for typical projects (default)
9    Balanced,
10    /// Lenient thresholds for legacy or complex domains
11    Lenient,
12}
13
14#[derive(Parser, Debug)]
15#[command(name = "debtmap")]
16#[command(about = "Code complexity and technical debt analyzer", long_about = None)]
17#[command(version)]
18pub struct Cli {
19    #[command(subcommand)]
20    pub command: Commands,
21}
22
23#[derive(Subcommand, Debug)]
24pub enum Commands {
25    /// Analyze code for complexity and technical debt
26    Analyze {
27        /// Path to analyze
28        path: PathBuf,
29
30        /// Output format
31        #[arg(short, long, value_enum, default_value = "terminal")]
32        format: OutputFormat,
33
34        /// Output file (defaults to stdout)
35        #[arg(short, long)]
36        output: Option<PathBuf>,
37
38        /// Complexity threshold
39        #[arg(long, default_value = "10")]
40        threshold_complexity: u32,
41
42        /// Duplication threshold (lines)
43        #[arg(long, default_value = "50")]
44        threshold_duplication: usize,
45
46        /// Languages to analyze
47        #[arg(long, value_delimiter = ',')]
48        languages: Option<Vec<String>>,
49
50        /// Optional LCOV coverage file for risk analysis
51        #[arg(long = "coverage-file", visible_alias = "lcov")]
52        coverage_file: Option<PathBuf>,
53
54        /// Enable context-aware risk analysis
55        #[arg(long = "context", visible_alias = "enable-context")]
56        enable_context: bool,
57
58        /// Context providers to use (critical_path, dependency, git_history)
59        #[arg(long = "context-providers", value_delimiter = ',')]
60        context_providers: Option<Vec<String>>,
61
62        /// Disable specific context providers
63        #[arg(long = "disable-context", value_delimiter = ',')]
64        disable_context: Option<Vec<String>>,
65
66        /// Show only top N priority items
67        #[arg(long = "top", visible_alias = "head")]
68        top: Option<usize>,
69
70        /// Show only bottom N priority items (lowest priority)
71        #[arg(long = "tail")]
72        tail: Option<usize>,
73
74        /// Disable semantic analysis (fallback mode)
75        #[arg(long = "semantic-off")]
76        semantic_off: bool,
77
78        /// Show score breakdown for debugging (deprecated: use -v instead)
79        #[arg(long = "explain-score", hide = true)]
80        explain_score: bool,
81
82        /// Increase verbosity level (can be repeated: -v, -vv, -vvv)
83        /// -v: Show main score factors
84        /// -vv: Show detailed calculations
85        /// -vvv: Show all debug information
86        #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
87        verbosity: u8,
88
89        /// Show verbose macro parsing warnings
90        #[arg(long = "verbose-macro-warnings")]
91        verbose_macro_warnings: bool,
92
93        /// Show macro expansion statistics at the end of analysis
94        #[arg(long = "show-macro-stats")]
95        show_macro_stats: bool,
96
97        /// Group output by debt category
98        #[arg(long = "group-by-category")]
99        group_by_category: bool,
100
101        /// Minimum priority to display (low, medium, high, critical)
102        #[arg(long = "min-priority")]
103        min_priority: Option<String>,
104
105        /// Filter by debt categories (comma-separated)
106        #[arg(long = "filter", value_delimiter = ',')]
107        filter_categories: Option<Vec<String>>,
108
109        /// Disable context-aware false positive reduction (enabled by default)
110        #[arg(long = "no-context-aware")]
111        no_context_aware: bool,
112
113        /// Complexity threshold preset (strict, balanced, lenient)
114        #[arg(long = "threshold-preset", value_enum)]
115        threshold_preset: Option<ThresholdPreset>,
116    },
117
118    /// Initialize configuration file
119    Init {
120        /// Force overwrite existing config
121        #[arg(short, long)]
122        force: bool,
123    },
124
125    /// Validate code against thresholds
126    Validate {
127        /// Path to analyze
128        path: PathBuf,
129
130        /// Configuration file
131        #[arg(short, long)]
132        config: Option<PathBuf>,
133
134        /// Optional LCOV coverage file for risk analysis
135        #[arg(long = "coverage-file", visible_alias = "lcov")]
136        coverage_file: Option<PathBuf>,
137
138        /// Output format
139        #[arg(short, long, value_enum)]
140        format: Option<OutputFormat>,
141
142        /// Output file (defaults to stdout)
143        #[arg(short, long)]
144        output: Option<PathBuf>,
145
146        /// Enable context-aware risk analysis
147        #[arg(long = "context", visible_alias = "enable-context")]
148        enable_context: bool,
149
150        /// Context providers to use (critical_path, dependency, git_history)
151        #[arg(long = "context-providers", value_delimiter = ',')]
152        context_providers: Option<Vec<String>>,
153
154        /// Disable specific context providers
155        #[arg(long = "disable-context", value_delimiter = ',')]
156        disable_context: Option<Vec<String>>,
157
158        /// Show only top N priority items
159        #[arg(long = "top", visible_alias = "head")]
160        top: Option<usize>,
161
162        /// Show only bottom N priority items (lowest priority)
163        #[arg(long = "tail")]
164        tail: Option<usize>,
165
166        /// Disable semantic analysis (fallback mode)
167        #[arg(long = "semantic-off")]
168        semantic_off: bool,
169
170        /// Show score breakdown for debugging (deprecated: use -v instead)
171        #[arg(long = "explain-score", hide = true)]
172        explain_score: bool,
173
174        /// Increase verbosity level (can be repeated: -v, -vv, -vvv)
175        /// -v: Show main score factors
176        /// -vv: Show detailed calculations
177        /// -vvv: Show all debug information
178        #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
179        verbosity: u8,
180    },
181}
182
183#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
184pub enum OutputFormat {
185    Json,
186    Markdown,
187    Terminal,
188}
189
190#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
191pub enum Priority {
192    Low,
193    Medium,
194    High,
195    Critical,
196}
197
198impl From<Priority> for crate::core::Priority {
199    fn from(p: Priority) -> Self {
200        match p {
201            Priority::Low => crate::core::Priority::Low,
202            Priority::Medium => crate::core::Priority::Medium,
203            Priority::High => crate::core::Priority::High,
204            Priority::Critical => crate::core::Priority::Critical,
205        }
206    }
207}
208
209impl From<OutputFormat> for crate::io::output::OutputFormat {
210    fn from(f: OutputFormat) -> Self {
211        match f {
212            OutputFormat::Json => crate::io::output::OutputFormat::Json,
213            OutputFormat::Markdown => crate::io::output::OutputFormat::Markdown,
214            OutputFormat::Terminal => crate::io::output::OutputFormat::Terminal,
215        }
216    }
217}
218
219pub fn parse_args() -> Cli {
220    Cli::parse()
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_priority_conversion() {
229        // Test conversion from CLI Priority to core Priority
230        assert_eq!(
231            crate::core::Priority::from(Priority::Low),
232            crate::core::Priority::Low
233        );
234        assert_eq!(
235            crate::core::Priority::from(Priority::Medium),
236            crate::core::Priority::Medium
237        );
238        assert_eq!(
239            crate::core::Priority::from(Priority::High),
240            crate::core::Priority::High
241        );
242        assert_eq!(
243            crate::core::Priority::from(Priority::Critical),
244            crate::core::Priority::Critical
245        );
246    }
247
248    #[test]
249    fn test_output_format_conversion() {
250        // Test conversion from CLI OutputFormat to io::output::OutputFormat
251        assert_eq!(
252            crate::io::output::OutputFormat::from(OutputFormat::Json),
253            crate::io::output::OutputFormat::Json
254        );
255        assert_eq!(
256            crate::io::output::OutputFormat::from(OutputFormat::Markdown),
257            crate::io::output::OutputFormat::Markdown
258        );
259        assert_eq!(
260            crate::io::output::OutputFormat::from(OutputFormat::Terminal),
261            crate::io::output::OutputFormat::Terminal
262        );
263    }
264
265    #[test]
266    fn test_cli_parsing_analyze_command() {
267        use clap::Parser;
268
269        let args = vec![
270            "debtmap",
271            "analyze",
272            "/test/path",
273            "--format",
274            "json",
275            "--threshold-complexity",
276            "15",
277            "--threshold-duplication",
278            "100",
279        ];
280
281        let cli = Cli::parse_from(args);
282
283        match cli.command {
284            Commands::Analyze {
285                path,
286                format,
287                threshold_complexity,
288                threshold_duplication,
289                ..
290            } => {
291                assert_eq!(path, PathBuf::from("/test/path"));
292                assert_eq!(format, OutputFormat::Json);
293                assert_eq!(threshold_complexity, 15);
294                assert_eq!(threshold_duplication, 100);
295            }
296            _ => panic!("Expected Analyze command"),
297        }
298    }
299
300    #[test]
301    fn test_cli_parsing_init_command() {
302        use clap::Parser;
303
304        let args = vec!["debtmap", "init", "--force"];
305
306        let cli = Cli::parse_from(args);
307
308        match cli.command {
309            Commands::Init { force } => {
310                assert!(force);
311            }
312            _ => panic!("Expected Init command"),
313        }
314    }
315
316    #[test]
317    fn test_cli_parsing_validate_command() {
318        use clap::Parser;
319
320        let args = vec![
321            "debtmap",
322            "validate",
323            "/test/path",
324            "--config",
325            "/config/path",
326        ];
327
328        let cli = Cli::parse_from(args);
329
330        match cli.command {
331            Commands::Validate { path, config, .. } => {
332                assert_eq!(path, PathBuf::from("/test/path"));
333                assert_eq!(config, Some(PathBuf::from("/config/path")));
334            }
335            _ => panic!("Expected Validate command"),
336        }
337    }
338
339    #[test]
340    fn test_priority_ordering() {
341        // Test that Priority enum ordering is correct
342        assert!(Priority::Low < Priority::Medium);
343        assert!(Priority::Medium < Priority::High);
344        assert!(Priority::High < Priority::Critical);
345    }
346
347    #[test]
348    fn test_output_format_equality() {
349        // Test OutputFormat equality
350        assert_eq!(OutputFormat::Json, OutputFormat::Json);
351        assert_ne!(OutputFormat::Json, OutputFormat::Markdown);
352        assert_ne!(OutputFormat::Terminal, OutputFormat::Json);
353    }
354
355    #[test]
356    fn test_parse_args_wrapper() {
357        // Since parse_args() calls Cli::parse() which requires actual CLI args,
358        // we'll test it indirectly through the Cli structure
359        use clap::Parser;
360
361        // Verify that the parse_args function would work with valid arguments
362        let test_args = vec!["debtmap", "analyze", "."];
363        let cli = Cli::parse_from(test_args);
364
365        // Verify the CLI was parsed correctly
366        match cli.command {
367            Commands::Analyze { .. } => {
368                // Success - the structure was created properly
369            }
370            _ => panic!("Expected Analyze command from test args"),
371        }
372    }
373}