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}