1use super::{GodObjectAnalysis, GodObjectConfidence};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct GodObjectMetrics {
10 pub snapshots: Vec<GodObjectSnapshot>,
12 pub summary: MetricsSummary,
14}
15
16impl Default for GodObjectMetrics {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl GodObjectMetrics {
23 pub fn new() -> Self {
24 Self {
25 snapshots: Vec::new(),
26 summary: MetricsSummary::default(),
27 }
28 }
29
30 pub fn record_snapshot(&mut self, file_path: PathBuf, analysis: GodObjectAnalysis) {
32 let snapshot = GodObjectSnapshot {
33 timestamp: Utc::now(),
34 file_path,
35 is_god_object: analysis.is_god_object,
36 method_count: analysis.method_count,
37 field_count: analysis.field_count,
38 responsibility_count: analysis.responsibility_count,
39 lines_of_code: analysis.lines_of_code,
40 god_object_score: analysis.god_object_score.value(),
41 confidence: analysis.confidence,
42 };
43
44 self.snapshots.push(snapshot);
45 self.update_summary();
46 }
47
48 fn update_summary(&mut self) {
50 if self.snapshots.is_empty() {
51 self.summary = MetricsSummary::default();
52 return;
53 }
54
55 let mut total_god_objects = 0;
56 let mut total_methods = 0;
57 let mut total_score = 0.0;
58 let mut file_metrics: HashMap<PathBuf, FileMetricHistory> = HashMap::new();
59
60 for snapshot in &self.snapshots {
61 if snapshot.is_god_object {
62 total_god_objects += 1;
63 }
64 total_methods += snapshot.method_count;
65 total_score += snapshot.god_object_score;
66
67 let file_entry = file_metrics
69 .entry(snapshot.file_path.clone())
70 .or_insert_with(|| FileMetricHistory {
71 file_path: snapshot.file_path.clone(),
72 first_seen: snapshot.timestamp,
73 last_seen: snapshot.timestamp,
74 max_methods: 0,
75 max_score: 0.0,
76 current_is_god_object: false,
77 });
78
79 file_entry.last_seen = snapshot.timestamp;
80 file_entry.max_methods = file_entry.max_methods.max(snapshot.method_count);
81 file_entry.max_score = file_entry.max_score.max(snapshot.god_object_score);
82 file_entry.current_is_god_object = snapshot.is_god_object;
83 }
84
85 let avg_methods = total_methods as f64 / self.snapshots.len() as f64;
86 let avg_score = total_score / self.snapshots.len() as f64;
87
88 self.summary = MetricsSummary {
89 total_snapshots: self.snapshots.len(),
90 total_god_objects_detected: total_god_objects,
91 average_method_count: avg_methods,
92 average_god_object_score: avg_score,
93 files_tracked: file_metrics.len(),
94 file_histories: file_metrics.into_values().collect(),
95 };
96 }
97
98 pub fn get_file_trend(&self, file_path: &PathBuf) -> Option<FileTrend> {
100 let file_snapshots: Vec<&GodObjectSnapshot> = self
101 .snapshots
102 .iter()
103 .filter(|s| &s.file_path == file_path)
104 .collect();
105
106 if file_snapshots.len() < 2 {
107 return None;
108 }
109
110 let first = file_snapshots.first()?;
111 let last = file_snapshots.last()?;
112
113 let method_change = last.method_count as i32 - first.method_count as i32;
114 let score_change = last.god_object_score - first.god_object_score;
115
116 Some(FileTrend {
117 file_path: file_path.clone(),
118 method_count_change: method_change,
119 score_change,
120 trend_direction: determine_trend(score_change),
121 improved: score_change < 0.0,
122 })
123 }
124
125 pub fn get_new_god_objects(&self) -> Vec<PathBuf> {
127 let mut new_god_objects = Vec::new();
128 let mut file_status: HashMap<PathBuf, bool> = HashMap::new();
129
130 for snapshot in &self.snapshots {
132 let was_god_object = file_status.get(&snapshot.file_path).copied();
133 let is_god_object = snapshot.is_god_object;
134
135 if !was_god_object.unwrap_or(false) && is_god_object {
136 new_god_objects.push(snapshot.file_path.clone());
137 }
138
139 file_status.insert(snapshot.file_path.clone(), is_god_object);
140 }
141
142 new_god_objects
143 }
144
145 pub fn get_resolved_god_objects(&self) -> Vec<PathBuf> {
147 let mut resolved = Vec::new();
148 let mut file_status: HashMap<PathBuf, bool> = HashMap::new();
149
150 for snapshot in &self.snapshots {
151 let was_god_object = file_status.get(&snapshot.file_path).copied();
152 let is_god_object = snapshot.is_god_object;
153
154 if was_god_object.unwrap_or(false) && !is_god_object {
155 resolved.push(snapshot.file_path.clone());
156 }
157
158 file_status.insert(snapshot.file_path.clone(), is_god_object);
159 }
160
161 resolved
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct GodObjectSnapshot {
168 pub timestamp: DateTime<Utc>,
169 pub file_path: PathBuf,
170 pub is_god_object: bool,
171 pub method_count: usize,
172 pub field_count: usize,
173 pub responsibility_count: usize,
174 pub lines_of_code: usize,
175 pub god_object_score: f64,
176 pub confidence: GodObjectConfidence,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
181pub struct MetricsSummary {
182 pub total_snapshots: usize,
183 pub total_god_objects_detected: usize,
184 pub average_method_count: f64,
185 pub average_god_object_score: f64,
186 pub files_tracked: usize,
187 pub file_histories: Vec<FileMetricHistory>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct FileMetricHistory {
193 pub file_path: PathBuf,
194 pub first_seen: DateTime<Utc>,
195 pub last_seen: DateTime<Utc>,
196 pub max_methods: usize,
197 pub max_score: f64,
198 pub current_is_god_object: bool,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct FileTrend {
204 pub file_path: PathBuf,
205 pub method_count_change: i32,
206 pub score_change: f64,
207 pub trend_direction: TrendDirection,
208 pub improved: bool,
209}
210
211#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
213pub enum TrendDirection {
214 Improving,
215 Stable,
216 Worsening,
217}
218
219fn determine_trend(score_change: f64) -> TrendDirection {
220 if score_change < -10.0 {
221 TrendDirection::Improving
222 } else if score_change > 10.0 {
223 TrendDirection::Worsening
224 } else {
225 TrendDirection::Stable
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::priority::score_types::Score0To100;
233
234 fn create_test_analysis(
235 is_god_object: bool,
236 method_count: usize,
237 score: f64,
238 ) -> GodObjectAnalysis {
239 GodObjectAnalysis {
240 is_god_object,
241 method_count,
242 field_count: 5,
243 responsibility_count: 3,
244 lines_of_code: 500,
245 complexity_sum: 100,
246 god_object_score: Score0To100::new(score),
247 recommended_splits: Vec::new(),
248 confidence: if is_god_object {
249 GodObjectConfidence::Probable
250 } else {
251 GodObjectConfidence::NotGodObject
252 },
253 responsibilities: Vec::new(),
254 responsibility_method_counts: Default::default(),
255 purity_distribution: None,
256 module_structure: None,
257 detection_type: crate::organization::DetectionType::GodClass,
258 struct_name: None,
259 struct_line: None,
260 struct_location: None, visibility_breakdown: None, domain_count: 0,
263 domain_diversity: 0.0,
264 struct_ratio: 0.0,
265 analysis_method: crate::organization::SplitAnalysisMethod::None,
266 cross_domain_severity: None,
267 domain_diversity_metrics: None, aggregated_entropy: None,
269 aggregated_error_swallowing_count: None,
270 aggregated_error_swallowing_patterns: None,
271 }
272 }
273
274 #[test]
275 fn test_record_snapshot() {
276 let mut metrics = GodObjectMetrics::new();
277 let analysis = create_test_analysis(true, 30, 150.0);
278
279 metrics.record_snapshot(PathBuf::from("test.rs"), analysis);
280
281 assert_eq!(metrics.snapshots.len(), 1);
282 assert_eq!(metrics.summary.total_snapshots, 1);
283 assert_eq!(metrics.summary.total_god_objects_detected, 1);
284 }
285
286 #[test]
287 fn test_multiple_snapshots() {
288 let mut metrics = GodObjectMetrics::new();
289
290 metrics.record_snapshot(
291 PathBuf::from("file1.rs"),
292 create_test_analysis(true, 30, 150.0),
293 );
294 metrics.record_snapshot(
295 PathBuf::from("file2.rs"),
296 create_test_analysis(false, 10, 50.0),
297 );
298 metrics.record_snapshot(
299 PathBuf::from("file3.rs"),
300 create_test_analysis(true, 50, 250.0),
301 );
302
303 assert_eq!(metrics.snapshots.len(), 3);
304 assert_eq!(metrics.summary.total_god_objects_detected, 2);
305 assert_eq!(metrics.summary.files_tracked, 3);
306 assert_eq!(metrics.summary.average_method_count, 30.0);
307 }
308
309 #[test]
310 fn test_file_trend() {
311 let mut metrics = GodObjectMetrics::new();
312 let file_path = PathBuf::from("evolving.rs");
313
314 metrics.record_snapshot(file_path.clone(), create_test_analysis(false, 15, 75.0));
316
317 metrics.record_snapshot(file_path.clone(), create_test_analysis(true, 35, 175.0));
319
320 let trend = metrics.get_file_trend(&file_path).unwrap();
321 assert_eq!(trend.method_count_change, 20);
322 assert_eq!(trend.score_change, 25.0);
324 assert_eq!(trend.trend_direction, TrendDirection::Worsening);
325 assert!(!trend.improved);
326 }
327
328 #[test]
329 fn test_new_god_objects() {
330 let mut metrics = GodObjectMetrics::new();
331
332 metrics.record_snapshot(
333 PathBuf::from("file1.rs"),
334 create_test_analysis(false, 10, 50.0),
335 );
336 metrics.record_snapshot(
337 PathBuf::from("file1.rs"),
338 create_test_analysis(true, 30, 150.0),
339 );
340 metrics.record_snapshot(
341 PathBuf::from("file2.rs"),
342 create_test_analysis(true, 25, 125.0),
343 );
344
345 let new_god_objects = metrics.get_new_god_objects();
346 assert_eq!(new_god_objects.len(), 2);
347 assert!(new_god_objects.contains(&PathBuf::from("file1.rs")));
348 assert!(new_god_objects.contains(&PathBuf::from("file2.rs")));
349 }
350
351 #[test]
352 fn test_resolved_god_objects() {
353 let mut metrics = GodObjectMetrics::new();
354
355 metrics.record_snapshot(
356 PathBuf::from("file1.rs"),
357 create_test_analysis(true, 30, 150.0),
358 );
359 metrics.record_snapshot(
360 PathBuf::from("file1.rs"),
361 create_test_analysis(false, 15, 75.0),
362 );
363
364 let resolved = metrics.get_resolved_god_objects();
365 assert_eq!(resolved.len(), 1);
366 assert!(resolved.contains(&PathBuf::from("file1.rs")));
367 }
368
369 #[test]
370 fn test_trend_direction() {
371 assert_eq!(determine_trend(-20.0), TrendDirection::Improving);
372 assert_eq!(determine_trend(0.0), TrendDirection::Stable);
373 assert_eq!(determine_trend(5.0), TrendDirection::Stable);
374 assert_eq!(determine_trend(20.0), TrendDirection::Worsening);
375 }
376}