code complexity & repetition analysis tool
1use crate::Result;
2use crate::tokenizer::{Language, TokenType, Tokenizer};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8enum LineKind {
9 Code,
10 Comment,
11 Blank,
12}
13
14/// Lines of Code metrics for a single file
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct LocMetrics {
17 /// Total number of lines in the file
18 pub physical: usize,
19 /// Number of non-blank, non-comment lines
20 pub logical: usize,
21 /// Number of comment lines
22 pub comments: usize,
23 /// Number of blank lines
24 pub blank: usize,
25}
26
27impl LocMetrics {
28 pub fn calculate(source: &str, language: Language) -> Result<Self> {
29 let tokens = Tokenizer::new(source, language).tokenize()?;
30 let physical = if source.is_empty() { 0 } else { source.split('\n').count() };
31 let mut line_types = vec![LineKind::Blank; physical];
32
33 for token in &tokens {
34 let line_idx = token.line.saturating_sub(1);
35 if line_idx >= line_types.len() {
36 continue;
37 }
38
39 match token.token_type {
40 _ if token.token_type.is_significant() => {
41 line_types[line_idx] = LineKind::Code;
42 }
43 TokenType::Comment => {
44 if line_types[line_idx] != LineKind::Code {
45 line_types[line_idx] = LineKind::Comment;
46 }
47 }
48 _ => {}
49 }
50 }
51
52 for (idx, line) in source.lines().enumerate() {
53 if line.trim().is_empty() && idx < line_types.len() {
54 line_types[idx] = LineKind::Blank;
55 }
56 }
57
58 let comments = line_types.iter().filter(|&&t| t == LineKind::Comment).count();
59 let blank = line_types.iter().filter(|&&t| t == LineKind::Blank).count();
60 let logical = physical - comments - blank;
61
62 Ok(LocMetrics { physical, logical, comments, blank })
63 }
64
65 /// Add two LocMetrics together
66 fn add(&self, other: &Self) -> Self {
67 Self {
68 physical: self.physical + other.physical,
69 logical: self.logical + other.logical,
70 comments: self.comments + other.comments,
71 blank: self.blank + other.blank,
72 }
73 }
74}
75
76/// Ranking criteria for LOC analysis
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum RankBy {
79 /// Rank by logical lines of code
80 Logical,
81 /// Rank by physical lines of code
82 Physical,
83 /// Rank by comment lines
84 Comments,
85 /// Rank by blank lines
86 Blank,
87}
88
89impl RankBy {
90 /// Get the value from LocMetrics based on ranking criteria
91 pub fn value_from(&self, metrics: &LocMetrics) -> usize {
92 match self {
93 Self::Logical => metrics.logical,
94 Self::Physical => metrics.physical,
95 Self::Comments => metrics.comments,
96 Self::Blank => metrics.blank,
97 }
98 }
99}
100
101/// LOC metrics for a single file with path information
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct FileLocReport {
104 /// File path
105 pub path: PathBuf,
106 /// LOC metrics
107 pub metrics: LocMetrics,
108}
109
110/// Aggregated LOC metrics for a directory
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct DirectoryLocMetrics {
113 /// Directory path
114 pub path: PathBuf,
115 /// Total LOC metrics for all files in this directory
116 pub total: LocMetrics,
117 /// Files in this directory
118 pub files: Vec<FileLocReport>,
119}
120
121/// Complete LOC analysis report with ranking capabilities
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct LocReport {
124 /// Per-file reports
125 pub files: Vec<FileLocReport>,
126 /// Per-directory aggregation (if enabled)
127 pub directories: Option<Vec<DirectoryLocMetrics>>,
128 /// Summary statistics
129 pub summary: LocSummary,
130}
131
132/// Summary statistics for LOC report
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct LocSummary {
135 /// Total number of files analyzed
136 pub total_files: usize,
137 /// Total physical lines of code
138 pub total_physical: usize,
139 /// Total logical lines of code
140 pub total_logical: usize,
141 /// Total comment lines
142 pub total_comments: usize,
143 /// Total blank lines
144 pub total_blank: usize,
145}
146
147impl LocReport {
148 /// Create a new LOC report from file reports
149 pub fn new(mut files: Vec<FileLocReport>, rank_by: RankBy, rank_dirs: bool) -> Self {
150 files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics)));
151
152 let directories = if rank_dirs { Some(Self::aggregate_by_directory(&files, rank_by)) } else { None };
153 let summary = LocSummary::from_files(&files);
154
155 Self { files, directories, summary }
156 }
157
158 /// Aggregate files by directory
159 fn aggregate_by_directory(files: &[FileLocReport], rank_by: RankBy) -> Vec<DirectoryLocMetrics> {
160 let mut dir_map: HashMap<PathBuf, Vec<FileLocReport>> = HashMap::new();
161
162 for file in files {
163 let dir = file.path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf();
164 dir_map.entry(dir).or_default().push(file.clone());
165 }
166
167 let mut directories: Vec<DirectoryLocMetrics> = dir_map
168 .into_iter()
169 .map(|(path, files)| {
170 let total = files.iter().fold(
171 LocMetrics { physical: 0, logical: 0, comments: 0, blank: 0 },
172 |acc, f| acc.add(&f.metrics),
173 );
174
175 let mut sorted_files = files;
176 sorted_files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics)));
177
178 DirectoryLocMetrics { path, total, files: sorted_files }
179 })
180 .collect();
181
182 directories.sort_by(|a, b| rank_by.value_from(&b.total).cmp(&rank_by.value_from(&a.total)));
183
184 directories
185 }
186
187 /// Serialize to JSON
188 pub fn to_json(&self) -> serde_json::Result<String> {
189 serde_json::to_string_pretty(self)
190 }
191}
192
193impl LocSummary {
194 fn from_files(files: &[FileLocReport]) -> Self {
195 let total_files = files.len();
196 let total_physical = files.iter().map(|f| f.metrics.physical).sum();
197 let total_logical = files.iter().map(|f| f.metrics.logical).sum();
198 let total_comments = files.iter().map(|f| f.metrics.comments).sum();
199 let total_blank = files.iter().map(|f| f.metrics.blank).sum();
200
201 Self { total_files, total_physical, total_logical, total_comments, total_blank }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_empty_file() {
211 let source = "";
212 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
213 assert_eq!(metrics.physical, 0);
214 assert_eq!(metrics.logical, 0);
215 }
216
217 #[test]
218 fn test_simple_code() {
219 let source = r#"
220fn main() {
221 println!("Hello");
222}
223"#;
224 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
225 assert_eq!(metrics.physical, 5);
226 assert!(metrics.logical >= 2);
227 assert!(metrics.blank >= 1);
228 }
229
230 #[test]
231 fn test_comments() {
232 let source = r#"
233// This is a comment
234/* Multi-line
235 comment */
236let x = 5; // inline comment
237"#;
238 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
239 assert!(metrics.comments >= 2);
240 assert!(metrics.logical >= 1);
241 }
242
243 #[test]
244 fn test_blank_lines() {
245 let source = r#"
246
247
248fn test() {}
249
250
251"#;
252 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
253 assert!(metrics.blank >= 4);
254 assert!(metrics.logical >= 1);
255 }
256
257 #[test]
258 fn test_javascript() {
259 let source = r#"
260function hello() {
261 console.log("Hello");
262}
263"#;
264 let metrics = LocMetrics::calculate(source, Language::JavaScript).unwrap();
265 assert_eq!(metrics.physical, 5);
266 assert!(metrics.logical >= 2);
267 }
268
269 #[test]
270 fn test_all_comments() {
271 let source = r#"
272// Comment 1
273// Comment 2
274/* Comment 3 */
275"#;
276 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap();
277 assert!(metrics.comments >= 3);
278 assert_eq!(metrics.logical, 0);
279 }
280
281 #[test]
282 fn test_loc_metrics_add() {
283 let m1 = LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 };
284 let m2 = LocMetrics { physical: 20, logical: 15, comments: 3, blank: 2 };
285 let result = m1.add(&m2);
286
287 assert_eq!(result.physical, 30);
288 assert_eq!(result.logical, 23);
289 assert_eq!(result.comments, 4);
290 assert_eq!(result.blank, 3);
291 }
292
293 #[test]
294 fn test_rank_by_value_from() {
295 let metrics = LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 };
296
297 assert_eq!(RankBy::Physical.value_from(&metrics), 100);
298 assert_eq!(RankBy::Logical.value_from(&metrics), 80);
299 assert_eq!(RankBy::Comments.value_from(&metrics), 10);
300 assert_eq!(RankBy::Blank.value_from(&metrics), 10);
301 }
302
303 #[test]
304 fn test_loc_report_new() {
305 let files = vec![
306 FileLocReport {
307 path: PathBuf::from("test1.rs"),
308 metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 },
309 },
310 FileLocReport {
311 path: PathBuf::from("test2.rs"),
312 metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 },
313 },
314 ];
315
316 let report = LocReport::new(files, RankBy::Logical, false);
317
318 assert_eq!(report.summary.total_files, 2);
319 assert_eq!(report.summary.total_physical, 150);
320 assert_eq!(report.summary.total_logical, 120);
321 assert_eq!(report.summary.total_comments, 15);
322 assert_eq!(report.summary.total_blank, 15);
323
324 assert_eq!(report.files[0].metrics.logical, 80);
325 assert_eq!(report.files[1].metrics.logical, 40);
326 }
327
328 #[test]
329 fn test_loc_report_with_directories() {
330 let files = vec![
331 FileLocReport {
332 path: PathBuf::from("src/main.rs"),
333 metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 },
334 },
335 FileLocReport {
336 path: PathBuf::from("src/lib.rs"),
337 metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 },
338 },
339 FileLocReport {
340 path: PathBuf::from("tests/test.rs"),
341 metrics: LocMetrics { physical: 30, logical: 25, comments: 3, blank: 2 },
342 },
343 ];
344
345 let report = LocReport::new(files, RankBy::Logical, true);
346
347 assert!(report.directories.is_some());
348 let dirs = report.directories.unwrap();
349 assert_eq!(dirs.len(), 2);
350
351 assert_eq!(dirs[0].path, PathBuf::from("src"));
352 assert_eq!(dirs[0].total.logical, 120);
353 assert_eq!(dirs[0].files.len(), 2);
354
355 assert_eq!(dirs[1].path, PathBuf::from("tests"));
356 assert_eq!(dirs[1].total.logical, 25);
357 assert_eq!(dirs[1].files.len(), 1);
358 }
359
360 #[test]
361 fn test_loc_report_to_json() {
362 let files = vec![FileLocReport {
363 path: PathBuf::from("test.rs"),
364 metrics: LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 },
365 }];
366
367 let report = LocReport::new(files, RankBy::Logical, false);
368 let json = report.to_json().unwrap();
369
370 assert!(json.contains("files"));
371 assert!(json.contains("summary"));
372 assert!(json.contains("test.rs"));
373 }
374}