Skip to main content

bytes_radar/core/
analysis.rs

1use crate::error::{AnalysisError, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum FileCategory {
9    Source,
10    Documentation,
11    Configuration,
12    Data,
13    Binary,
14    Test,
15    Build,
16}
17
18impl Default for FileCategory {
19    fn default() -> Self {
20        Self::Source
21    }
22}
23
24impl Display for FileCategory {
25    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26        let category = match self {
27            Self::Source => "Source",
28            Self::Documentation => "Documentation",
29            Self::Configuration => "Configuration",
30            Self::Data => "Data",
31            Self::Binary => "Binary",
32            Self::Test => "Test",
33            Self::Build => "Build",
34        };
35        write!(f, "{}", category)
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct FileMetrics {
41    pub file_path: String,
42    pub total_lines: usize,
43    pub code_lines: usize,
44    pub comment_lines: usize,
45    pub blank_lines: usize,
46    pub category: FileCategory,
47    pub language: String,
48    pub size_bytes: u64,
49}
50
51impl FileMetrics {
52    pub fn new<P: AsRef<Path>>(
53        file_path: P,
54        language: String,
55        total_lines: usize,
56        code_lines: usize,
57        comment_lines: usize,
58        blank_lines: usize,
59    ) -> Result<Self> {
60        let path_str = file_path.as_ref().to_string_lossy().to_string();
61
62        if total_lines != code_lines + comment_lines + blank_lines {
63            return Err(AnalysisError::invalid_statistics(format!(
64                "Line count mismatch: total={}, sum of parts={}",
65                total_lines,
66                code_lines + comment_lines + blank_lines
67            )));
68        }
69
70        Ok(Self {
71            file_path: path_str,
72            total_lines,
73            code_lines,
74            comment_lines,
75            blank_lines,
76            category: FileCategory::default(),
77            language,
78            size_bytes: 0,
79        })
80    }
81
82    pub fn with_category(mut self, category: FileCategory) -> Self {
83        self.category = category;
84        self
85    }
86
87    pub fn with_size_bytes(mut self, size_bytes: u64) -> Self {
88        self.size_bytes = size_bytes;
89        self
90    }
91
92    pub fn complexity_ratio(&self) -> f64 {
93        if self.total_lines == 0 {
94            0.0
95        } else {
96            self.code_lines as f64 / self.total_lines as f64
97        }
98    }
99
100    pub fn documentation_ratio(&self) -> f64 {
101        if self.code_lines == 0 {
102            0.0
103        } else {
104            self.comment_lines as f64 / self.code_lines as f64
105        }
106    }
107
108    pub fn validate(&self) -> Result<()> {
109        if self.file_path.is_empty() {
110            return Err(AnalysisError::invalid_statistics(
111                "File path cannot be empty",
112            ));
113        }
114
115        if self.language.is_empty() {
116            return Err(AnalysisError::invalid_statistics(
117                "Language cannot be empty",
118            ));
119        }
120
121        if self.total_lines != self.code_lines + self.comment_lines + self.blank_lines {
122            return Err(AnalysisError::invalid_statistics(
123                "Line counts don't add up",
124            ));
125        }
126
127        Ok(())
128    }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct LanguageAnalysis {
133    pub language_name: String,
134    pub file_metrics: Vec<FileMetrics>,
135    pub aggregate_metrics: AggregateMetrics,
136}
137
138impl LanguageAnalysis {
139    pub fn new(language_name: String) -> Self {
140        Self {
141            language_name,
142            file_metrics: Vec::new(),
143            aggregate_metrics: AggregateMetrics::default(),
144        }
145    }
146
147    pub fn add_file_metrics(&mut self, metrics: FileMetrics) -> Result<()> {
148        metrics.validate()?;
149
150        if metrics.language != self.language_name {
151            return Err(AnalysisError::invalid_statistics(format!(
152                "Language mismatch: expected '{}', got '{}'",
153                self.language_name, metrics.language
154            )));
155        }
156
157        self.aggregate_metrics.incorporate(&metrics);
158        self.file_metrics.push(metrics);
159        Ok(())
160    }
161
162    pub fn merge(&mut self, other: LanguageAnalysis) -> Result<()> {
163        if self.language_name != other.language_name {
164            return Err(AnalysisError::aggregation(format!(
165                "Cannot merge different languages: '{}' and '{}'",
166                self.language_name, other.language_name
167            )));
168        }
169
170        for metrics in other.file_metrics {
171            self.add_file_metrics(metrics)?;
172        }
173
174        Ok(())
175    }
176
177    pub fn calculate_statistics(&self) -> LanguageStatistics {
178        LanguageStatistics {
179            language_name: self.language_name.clone(),
180            file_count: self.file_metrics.len(),
181            total_lines: self.aggregate_metrics.total_lines,
182            code_lines: self.aggregate_metrics.code_lines,
183            comment_lines: self.aggregate_metrics.comment_lines,
184            blank_lines: self.aggregate_metrics.blank_lines,
185            total_size_bytes: self.aggregate_metrics.total_size_bytes,
186            average_file_size: if self.file_metrics.is_empty() {
187                0.0
188            } else {
189                self.aggregate_metrics.total_lines as f64 / self.file_metrics.len() as f64
190            },
191            complexity_ratio: self.aggregate_metrics.complexity_ratio(),
192            documentation_ratio: self.aggregate_metrics.documentation_ratio(),
193        }
194    }
195}
196
197#[derive(Debug, Clone, Default, Serialize, Deserialize)]
198pub struct AggregateMetrics {
199    pub total_lines: usize,
200    pub code_lines: usize,
201    pub comment_lines: usize,
202    pub blank_lines: usize,
203    pub total_size_bytes: u64,
204    pub file_count: usize,
205}
206
207impl AggregateMetrics {
208    pub fn incorporate(&mut self, metrics: &FileMetrics) {
209        self.total_lines += metrics.total_lines;
210        self.code_lines += metrics.code_lines;
211        self.comment_lines += metrics.comment_lines;
212        self.blank_lines += metrics.blank_lines;
213        self.total_size_bytes += metrics.size_bytes;
214        self.file_count += 1;
215    }
216
217    pub fn complexity_ratio(&self) -> f64 {
218        if self.total_lines == 0 {
219            0.0
220        } else {
221            self.code_lines as f64 / self.total_lines as f64
222        }
223    }
224
225    pub fn documentation_ratio(&self) -> f64 {
226        if self.code_lines == 0 {
227            0.0
228        } else {
229            self.comment_lines as f64 / self.code_lines as f64
230        }
231    }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct LanguageStatistics {
236    pub language_name: String,
237    pub file_count: usize,
238    pub total_lines: usize,
239    pub code_lines: usize,
240    pub comment_lines: usize,
241    pub blank_lines: usize,
242    pub total_size_bytes: u64,
243    pub average_file_size: f64,
244    pub complexity_ratio: f64,
245    pub documentation_ratio: f64,
246}
247
248impl Display for LanguageStatistics {
249    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250        write!(
251            f,
252            "{}: {} files, {} lines ({} code, {} comments, {} blank) - {:.1}% complexity, {:.1}% documented",
253            self.language_name,
254            self.file_count,
255            self.total_lines,
256            self.code_lines,
257            self.comment_lines,
258            self.blank_lines,
259            self.complexity_ratio * 100.0,
260            self.documentation_ratio * 100.0
261        )
262    }
263}
264
265#[derive(Debug, Default, Serialize, Deserialize)]
266pub struct ProjectAnalysis {
267    pub project_name: String,
268    pub language_analyses: HashMap<String, LanguageAnalysis>,
269    pub global_metrics: AggregateMetrics,
270}
271
272impl ProjectAnalysis {
273    pub fn new<N: Into<String>>(project_name: N) -> Self {
274        Self {
275            project_name: project_name.into(),
276            language_analyses: HashMap::new(),
277            global_metrics: AggregateMetrics::default(),
278        }
279    }
280
281    pub fn add_file_metrics(&mut self, metrics: FileMetrics) -> Result<()> {
282        metrics.validate()?;
283
284        let language_analysis = self
285            .language_analyses
286            .entry(metrics.language.clone())
287            .or_insert_with(|| LanguageAnalysis::new(metrics.language.clone()));
288
289        language_analysis.add_file_metrics(metrics.clone())?;
290        self.global_metrics.incorporate(&metrics);
291
292        Ok(())
293    }
294
295    pub fn get_language_statistics(&self) -> Vec<LanguageStatistics> {
296        let mut stats: Vec<_> = self
297            .language_analyses
298            .values()
299            .map(|analysis| analysis.calculate_statistics())
300            .collect();
301
302        stats.sort_by(|a, b| b.total_lines.cmp(&a.total_lines));
303        stats
304    }
305
306    pub fn get_summary(&self) -> ProjectSummary {
307        let language_stats = self.get_language_statistics();
308
309        ProjectSummary {
310            project_name: self.project_name.clone(),
311            total_files: self.global_metrics.file_count,
312            total_lines: self.global_metrics.total_lines,
313            total_code_lines: self.global_metrics.code_lines,
314            total_comment_lines: self.global_metrics.comment_lines,
315            total_blank_lines: self.global_metrics.blank_lines,
316            total_size_bytes: self.global_metrics.total_size_bytes,
317            language_count: self.language_analyses.len(),
318            primary_language: language_stats.first().map(|s| s.language_name.clone()),
319            overall_complexity_ratio: self.global_metrics.complexity_ratio(),
320            overall_documentation_ratio: self.global_metrics.documentation_ratio(),
321        }
322    }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ProjectSummary {
327    pub project_name: String,
328    pub total_files: usize,
329    pub total_lines: usize,
330    pub total_code_lines: usize,
331    pub total_comment_lines: usize,
332    pub total_blank_lines: usize,
333    pub total_size_bytes: u64,
334    pub language_count: usize,
335    pub primary_language: Option<String>,
336    pub overall_complexity_ratio: f64,
337    pub overall_documentation_ratio: f64,
338}