garbage_code_hunter/debt_invoice/
cost_model.rs1use crate::analyzer::CodeIssue;
4use std::collections::HashMap;
5
6#[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#[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
26const HOURLY_RATE: f64 = 75.0;
28
29pub 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 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 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
78fn 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
103fn 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
117fn 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}