Skip to main content

garbage_code_hunter/debt_invoice/
cost_model.rs

1//! Cost model for estimating technical debt in time and money.
2
3use crate::analyzer::CodeIssue;
4use std::collections::HashMap;
5
6/// A single line item on the debt invoice.
7#[derive(Debug, Clone)]
8pub struct InvoiceItem {
9    pub category: String,
10    pub count: usize,
11    pub estimated_hours: f64,
12    pub estimated_cost: f64,
13    pub pain_description: String,
14}
15
16/// The full debt invoice.
17#[derive(Debug, Clone)]
18pub struct Invoice {
19    pub items: Vec<InvoiceItem>,
20    pub total_hours: f64,
21    pub total_cost: f64,
22    pub project_lifespan_months: u32,
23    pub weekly_interest_rate: f64,
24}
25
26/// Hourly rate for cost estimation (USD).
27const HOURLY_RATE: f64 = 75.0;
28
29/// Generate an invoice from code issues.
30pub fn generate_invoice(issues: &[CodeIssue], total_lines: usize) -> Invoice {
31    let mut categories: HashMap<String, Vec<&CodeIssue>> = HashMap::new();
32
33    for issue in issues {
34        let cat = categorize(&issue.rule_name);
35        categories.entry(cat.to_string()).or_default().push(issue);
36    }
37
38    let mut items = Vec::new();
39
40    for (category, cat_issues) in &categories {
41        let count = cat_issues.len();
42        let (hours_per, pain) = category_cost(category);
43        let estimated_hours = count as f64 * hours_per;
44        let estimated_cost = estimated_hours * HOURLY_RATE;
45
46        items.push(InvoiceItem {
47            category: category.clone(),
48            count,
49            estimated_hours,
50            estimated_cost,
51            pain_description: pain.to_string(),
52        });
53    }
54
55    // Sort by cost descending
56    items.sort_by(|a, b| b.estimated_cost.partial_cmp(&a.estimated_cost).unwrap());
57
58    let total_hours: f64 = items.iter().map(|i| i.estimated_hours).sum();
59    let total_cost = total_hours * HOURLY_RATE;
60
61    // Estimate project lifespan based on debt density
62    let debt_density = if total_lines > 0 {
63        issues.len() as f64 / total_lines as f64 * 1000.0
64    } else {
65        0.0
66    };
67    let project_lifespan = estimate_lifespan(debt_density);
68
69    Invoice {
70        items,
71        total_hours,
72        total_cost,
73        project_lifespan_months: project_lifespan,
74        weekly_interest_rate: 3.0,
75    }
76}
77
78/// Map a rule name to a cost category.
79fn categorize(rule_name: &str) -> &'static str {
80    let lower = rule_name.to_lowercase();
81    if lower.contains("unwrap") {
82        "unwrap() abuse"
83    } else if lower.contains("nest") || lower.contains("complex") {
84        "Deep nesting / complexity"
85    } else if lower.contains("long") || lower.contains("function_length") {
86        "Long functions"
87    } else if lower.contains("name")
88        || lower.contains("single_letter")
89        || lower.contains("meaningless")
90    {
91        "Poor naming"
92    } else if lower.contains("magic") {
93        "Magic numbers"
94    } else if lower.contains("duplicat") || lower.contains("copy") {
95        "Code duplication"
96    } else if lower.contains("todo") || lower.contains("fixme") || lower.contains("hack") {
97        "Legacy comments"
98    } else {
99        "Other issues"
100    }
101}
102
103/// Returns (hours_per_issue, pain_description) for a category.
104fn category_cost(category: &str) -> (f64, &'static str) {
105    match category {
106        "unwrap() abuse" => (2.0, "Every unwrap() is a potential panic in production"),
107        "Deep nesting / complexity" => (3.0, "Cyclomatic complexity makes code unmaintainable"),
108        "Long functions" => (1.5, "Long functions are hard to test and understand"),
109        "Poor naming" => (0.5, "Meaningless names slow down every future reader"),
110        "Magic numbers" => (0.5, "Magic numbers hide intent and cause bugs"),
111        "Code duplication" => (2.0, "Duplicated code multiplies bug-fix cost"),
112        "Legacy comments" => (0.3, "TODOs that never get done are lies in the code"),
113        _ => (0.5, "General code quality issue"),
114    }
115}
116
117/// Estimate project lifespan in months based on debt density.
118fn estimate_lifespan(density: f64) -> u32 {
119    match density as u32 {
120        0..=5 => 24,
121        6..=15 => 18,
122        16..=30 => 12,
123        31..=50 => 6,
124        51..=80 => 3,
125        _ => 1,
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_categorize() {
135        assert_eq!(categorize("unwrap_abuse"), "unwrap() abuse");
136        assert_eq!(categorize("deep_nesting"), "Deep nesting / complexity");
137        assert_eq!(categorize("magic_number"), "Magic numbers");
138    }
139
140    #[test]
141    fn test_category_cost() {
142        let (hours, _) = category_cost("unwrap() abuse");
143        assert_eq!(hours, 2.0);
144    }
145
146    #[test]
147    fn test_estimate_lifespan() {
148        assert_eq!(estimate_lifespan(0.0), 24);
149        assert_eq!(estimate_lifespan(100.0), 1);
150    }
151}