tokmd_analysis_maintainability/
lib.rs1use tokmd_analysis_types::{ComplexityReport, HalsteadMetrics, MaintainabilityIndex};
4
5pub fn compute_maintainability_index(
13 avg_cyclomatic: f64,
14 avg_loc: f64,
15 halstead_volume: Option<f64>,
16) -> Option<MaintainabilityIndex> {
17 if avg_loc <= 0.0 {
18 return None;
19 }
20
21 let avg_loc = round_f64(avg_loc, 2);
22 let (raw_score, avg_halstead_volume) = match halstead_volume {
23 Some(volume) if volume > 0.0 => (
24 171.0 - 5.2 * volume.ln() - 0.23 * avg_cyclomatic - 16.2 * avg_loc.ln(),
25 Some(volume),
26 ),
27 _ => (171.0 - 0.23 * avg_cyclomatic - 16.2 * avg_loc.ln(), None),
28 };
29
30 let score = round_f64(raw_score.max(0.0), 2);
31 Some(MaintainabilityIndex {
32 score,
33 avg_cyclomatic,
34 avg_loc,
35 avg_halstead_volume,
36 grade: grade_for_score(score).to_string(),
37 })
38}
39
40pub fn attach_halstead_metrics(complexity: &mut ComplexityReport, halstead: HalsteadMetrics) {
46 if let Some(ref mut mi) = complexity.maintainability_index
47 && halstead.volume > 0.0
48 && let Some(updated) =
49 compute_maintainability_index(mi.avg_cyclomatic, mi.avg_loc, Some(halstead.volume))
50 {
51 *mi = updated;
52 }
53
54 complexity.halstead = Some(halstead);
55}
56
57fn grade_for_score(score: f64) -> &'static str {
58 if score >= 85.0 {
59 "A"
60 } else if score >= 65.0 {
61 "B"
62 } else {
63 "C"
64 }
65}
66
67fn round_f64(val: f64, decimals: u32) -> f64 {
68 let factor = 10f64.powi(decimals as i32);
69 (val * factor).round() / factor
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use tokmd_analysis_types::{ComplexityRisk, FileComplexity, TechnicalDebtRatio};
76
77 #[test]
78 fn compute_simplified_index() {
79 let mi = compute_maintainability_index(10.0, 100.0, None).expect("mi");
80 assert!((mi.score - 94.1).abs() < f64::EPSILON);
81 assert_eq!(mi.grade, "A");
82 assert_eq!(mi.avg_halstead_volume, None);
83 }
84
85 #[test]
86 fn compute_full_index_with_halstead() {
87 let mi = compute_maintainability_index(10.0, 100.0, Some(200.0)).expect("mi");
88 assert!((mi.score - 66.54).abs() < f64::EPSILON);
89 assert_eq!(mi.grade, "B");
90 assert_eq!(mi.avg_halstead_volume, Some(200.0));
91 }
92
93 #[test]
94 fn attach_halstead_recomputes_maintainability() {
95 let mut complexity = sample_complexity();
96 let before = complexity
97 .maintainability_index
98 .as_ref()
99 .map(|mi| mi.score)
100 .expect("maintainability");
101
102 attach_halstead_metrics(
103 &mut complexity,
104 HalsteadMetrics {
105 distinct_operators: 20,
106 distinct_operands: 30,
107 total_operators: 120,
108 total_operands: 240,
109 vocabulary: 50,
110 length: 360,
111 volume: 200.0,
112 difficulty: 8.0,
113 effort: 1600.0,
114 time_seconds: 88.89,
115 estimated_bugs: 0.0667,
116 },
117 );
118
119 let mi = complexity
120 .maintainability_index
121 .as_ref()
122 .expect("maintainability");
123 assert!(mi.score < before);
124 assert_eq!(mi.avg_halstead_volume, Some(200.0));
125 assert_eq!(mi.grade, "B");
126 assert_eq!(complexity.halstead.as_ref().map(|h| h.volume), Some(200.0));
127 }
128
129 #[test]
130 fn attach_halstead_keeps_existing_index_when_volume_is_zero() {
131 let mut complexity = sample_complexity();
132 let before = complexity
133 .maintainability_index
134 .as_ref()
135 .map(|mi| (mi.score, mi.avg_halstead_volume))
136 .expect("maintainability");
137
138 attach_halstead_metrics(
139 &mut complexity,
140 HalsteadMetrics {
141 distinct_operators: 0,
142 distinct_operands: 0,
143 total_operators: 0,
144 total_operands: 0,
145 vocabulary: 0,
146 length: 0,
147 volume: 0.0,
148 difficulty: 0.0,
149 effort: 0.0,
150 time_seconds: 0.0,
151 estimated_bugs: 0.0,
152 },
153 );
154
155 let after = complexity
156 .maintainability_index
157 .as_ref()
158 .map(|mi| (mi.score, mi.avg_halstead_volume))
159 .expect("maintainability");
160 assert_eq!(before, after);
161 assert_eq!(complexity.halstead.as_ref().map(|h| h.volume), Some(0.0));
162 }
163
164 fn sample_complexity() -> ComplexityReport {
165 ComplexityReport {
166 total_functions: 3,
167 avg_function_length: 10.0,
168 max_function_length: 20,
169 avg_cyclomatic: 10.0,
170 max_cyclomatic: 18,
171 avg_cognitive: Some(6.5),
172 max_cognitive: Some(10),
173 avg_nesting_depth: Some(2.0),
174 max_nesting_depth: Some(4),
175 high_risk_files: 1,
176 histogram: None,
177 halstead: None,
178 maintainability_index: compute_maintainability_index(10.0, 100.0, None),
179 technical_debt: Some(TechnicalDebtRatio {
180 ratio: 20.0,
181 complexity_points: 20,
182 code_kloc: 1.0,
183 level: tokmd_analysis_types::TechnicalDebtLevel::Low,
184 }),
185 files: vec![FileComplexity {
186 path: "src/lib.rs".to_string(),
187 module: "src".to_string(),
188 function_count: 3,
189 max_function_length: 20,
190 cyclomatic_complexity: 18,
191 cognitive_complexity: Some(10),
192 max_nesting: Some(4),
193 risk_level: ComplexityRisk::Moderate,
194 functions: None,
195 }],
196 }
197 }
198}