Skip to main content

tldr_cli/commands/
debt.rs

1//! Debt command - Technical debt analysis using SQALE method
2//!
3//! Analyzes source code to estimate technical debt using the SQALE
4//! (Software Quality Assessment based on Lifecycle Expectations) method.
5//! Each issue is assigned a remediation time in minutes, aggregated into
6//! summary statistics including debt ratio and density.
7
8use std::path::PathBuf;
9
10use anyhow::Result;
11use clap::Args;
12
13use tldr_core::quality::debt::{analyze_debt, DebtOptions, DebtReport};
14use tldr_core::Language;
15
16use crate::output::{OutputFormat, OutputWriter};
17
18/// Valid SQALE categories for filtering
19const VALID_CATEGORIES: [&str; 6] = [
20    "reliability",
21    "security",
22    "maintainability",
23    "efficiency",
24    "changeability",
25    "testability",
26];
27
28/// Analyze technical debt using SQALE method
29#[derive(Debug, Args)]
30pub struct DebtArgs {
31    /// Path to analyze (file or directory)
32    #[arg(default_value = ".")]
33    pub path: PathBuf,
34
35    /// Filter by SQALE category
36    #[arg(short = 'c', long, value_parser = ["reliability", "security", "maintainability", "efficiency", "changeability", "testability"])]
37    pub category: Option<String>,
38
39    /// Number of top files to show
40    #[arg(short = 'k', long, default_value = "20")]
41    pub top: usize,
42
43    /// Minimum debt minutes to include file
44    #[arg(long)]
45    pub min_debt: Option<u32>,
46
47    /// Hourly rate for cost estimation ($/hour)
48    #[arg(long)]
49    pub hourly_rate: Option<f64>,
50}
51
52impl DebtArgs {
53    /// Run the debt command
54    ///
55    /// `lang` is passed from the global CLI `--lang` / `-l` flag (already parsed as `Language` enum).
56    pub fn run(&self, format: OutputFormat, quiet: bool, lang: Option<Language>) -> Result<()> {
57        let writer = OutputWriter::new(format, quiet);
58
59        // Validate path exists (PM-5: exit code 1 for user errors)
60        if !self.path.exists() {
61            anyhow::bail!("Path not found: {}", self.path.display());
62        }
63
64        // Validate category if provided (PM-4: validate before analysis)
65        if let Some(ref cat) = self.category {
66            if !VALID_CATEGORIES.contains(&cat.as_str()) {
67                anyhow::bail!(
68                    "Invalid category '{}'. Valid categories: {}",
69                    cat,
70                    VALID_CATEGORIES.join(", ")
71                );
72            }
73        }
74
75        writer.progress(&format!(
76            "Analyzing technical debt in {}...",
77            self.path.display()
78        ));
79
80        // Language comes from global CLI flag (already parsed)
81        let language = lang;
82
83        let options = DebtOptions {
84            path: self.path.clone(),
85            category_filter: self.category.clone(),
86            language,
87            top_k: self.top,
88            min_debt: self.min_debt.unwrap_or(0),
89            hourly_rate: self.hourly_rate,
90        };
91
92        let report = analyze_debt(options)?;
93
94        // Output based on format
95        if writer.is_text() {
96            let text = report.to_text();
97            writer.write_text(&text)?;
98        } else {
99            writer.write(&report)?;
100        }
101
102        Ok(())
103    }
104}
105
106/// Parse language string to Language enum
107#[allow(dead_code)]
108fn parse_language(lang: &str) -> Option<Language> {
109    match lang.to_lowercase().as_str() {
110        "python" | "py" => Some(Language::Python),
111        "typescript" | "ts" => Some(Language::TypeScript),
112        "javascript" | "js" => Some(Language::JavaScript),
113        "rust" | "rs" => Some(Language::Rust),
114        "go" => Some(Language::Go),
115        "java" => Some(Language::Java),
116        "c" => Some(Language::C),
117        "cpp" | "c++" => Some(Language::Cpp),
118        "ruby" | "rb" => Some(Language::Ruby),
119        "php" => Some(Language::Php),
120        "swift" => Some(Language::Swift),
121        "kotlin" | "kt" => Some(Language::Kotlin),
122        "scala" => Some(Language::Scala),
123        "csharp" | "cs" | "c#" => Some(Language::CSharp),
124        "lua" => Some(Language::Lua),
125        "luau" => Some(Language::Luau),
126        "elixir" | "ex" => Some(Language::Elixir),
127        "ocaml" | "ml" => Some(Language::Ocaml),
128        _ => None,
129    }
130}
131
132/// Format debt report for text output (delegated to DebtReport::to_text())
133#[allow(dead_code)]
134fn format_debt_text(report: &DebtReport) -> String {
135    report.to_text()
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_parse_language_python() {
144        assert_eq!(parse_language("python"), Some(Language::Python));
145        assert_eq!(parse_language("py"), Some(Language::Python));
146        assert_eq!(parse_language("Python"), Some(Language::Python));
147    }
148
149    #[test]
150    fn test_parse_language_typescript() {
151        assert_eq!(parse_language("typescript"), Some(Language::TypeScript));
152        assert_eq!(parse_language("ts"), Some(Language::TypeScript));
153    }
154
155    #[test]
156    fn test_parse_language_unknown() {
157        assert_eq!(parse_language("unknown"), None);
158        assert_eq!(parse_language(""), None);
159    }
160
161    #[test]
162    fn test_valid_categories() {
163        assert!(VALID_CATEGORIES.contains(&"reliability"));
164        assert!(VALID_CATEGORIES.contains(&"maintainability"));
165        assert!(!VALID_CATEGORIES.contains(&"invalid"));
166    }
167}