Skip to main content

codelens_core/
lib.rs

1//! # codelens-core
2//!
3//! Core library for codelens - a high performance code statistics tool.
4//!
5//! ## Features
6//!
7//! - Fast parallel file traversal using `ignore` crate
8//! - Support for 70+ programming languages
9//! - Accurate line counting (code, comments, blanks)
10//! - Cyclomatic complexity analysis
11//! - Multiple output formats (Console, JSON, CSV, Markdown, HTML)
12//! - Respects `.gitignore` rules
13//! - Smart directory exclusion based on project type
14//!
15//! ## Example
16//!
17//! ```rust,no_run
18//! use codelens_core::{analyze, Config};
19//! use std::path::PathBuf;
20//!
21//! let config = Config::default();
22//! let paths = vec![PathBuf::from(".")];
23//! let result = analyze(&paths, &config).unwrap();
24//!
25//! println!("Total files: {}", result.summary.total_files);
26//! println!("Total lines: {}", result.summary.lines.total);
27//! ```
28
29pub mod analyzer;
30pub mod config;
31pub mod error;
32pub mod filter;
33pub mod language;
34pub mod output;
35pub mod walker;
36
37pub use analyzer::stats::{
38    AnalysisResult, Complexity, FileStats, LanguageSummary, LineStats, RepoStats, RepoSummary,
39    SizeDistribution, Summary,
40};
41pub use config::Config;
42pub use error::{Error, Result};
43pub use language::{Language, LanguageRegistry};
44pub use output::{OutputFormat, OutputOptions};
45
46use std::path::Path;
47use std::sync::Arc;
48use std::time::Instant;
49
50use analyzer::FileAnalyzer;
51use filter::FilterChain;
52use walker::ParallelWalker;
53
54/// Analyze code statistics for the given paths.
55///
56/// This is the main entry point for the library.
57pub fn analyze<P: AsRef<Path>>(paths: &[P], config: &Config) -> Result<AnalysisResult> {
58    let start = Instant::now();
59
60    // Initialize components
61    let registry = Arc::new(LanguageRegistry::with_builtin()?);
62    let analyzer = Arc::new(FileAnalyzer::new(Arc::clone(&registry), config));
63    let filter: Arc<dyn filter::Filter> = Arc::new(FilterChain::new(config)?);
64    let walker = ParallelWalker::new(config.walker.clone());
65
66    // Collect all file stats
67    let mut all_stats = Vec::new();
68    let mut scanned_files = 0;
69    let mut skipped_files = 0;
70
71    for path in paths {
72        let path = path.as_ref();
73        if !path.exists() {
74            return Err(Error::DirectoryNotFound {
75                path: path.to_path_buf(),
76            });
77        }
78
79        walker.walk_and_analyze(
80            path,
81            Arc::clone(&analyzer),
82            Arc::clone(&filter),
83            |stats| {
84                all_stats.push(stats);
85                scanned_files += 1;
86            },
87            |_| {
88                skipped_files += 1;
89            },
90        )?;
91    }
92
93    // Build summary
94    let summary = Summary::from_file_stats(&all_stats);
95    let elapsed = start.elapsed();
96
97    Ok(AnalysisResult {
98        files: all_stats,
99        summary,
100        elapsed,
101        scanned_files,
102        skipped_files,
103    })
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::fs;
110    use std::path::PathBuf;
111    use tempfile::TempDir;
112
113    fn create_test_file(dir: &Path, name: &str, content: &str) {
114        let path = dir.join(name);
115        if let Some(parent) = path.parent() {
116            fs::create_dir_all(parent).unwrap();
117        }
118        fs::write(path, content).unwrap();
119    }
120
121    #[test]
122    fn test_analyze_empty_directory() {
123        let dir = TempDir::new().unwrap();
124        let config = Config::default();
125
126        let result = analyze(&[dir.path()], &config).unwrap();
127
128        assert_eq!(result.summary.total_files, 0);
129        assert_eq!(result.summary.lines.total, 0);
130    }
131
132    #[test]
133    fn test_analyze_single_rust_file() {
134        let dir = TempDir::new().unwrap();
135        let rust_code = "fn main() {\n    println!(\"Hello, world!\");\n}\n";
136        create_test_file(dir.path(), "main.rs", rust_code);
137
138        let config = Config::default();
139        let result = analyze(&[dir.path()], &config).unwrap();
140
141        assert_eq!(result.summary.total_files, 1);
142        assert!(result.summary.by_language.contains_key("Rust"));
143        assert_eq!(result.summary.lines.total, 3);
144        assert_eq!(result.summary.lines.code, 3);
145    }
146
147    #[test]
148    fn test_analyze_multiple_languages() {
149        let dir = TempDir::new().unwrap();
150
151        let rust_code = "fn main() {}\n";
152        let python_code = "def main():\n    pass\n";
153        let js_code = "function main() {}\n";
154
155        create_test_file(dir.path(), "main.rs", rust_code);
156        create_test_file(dir.path(), "main.py", python_code);
157        create_test_file(dir.path(), "main.js", js_code);
158
159        let config = Config::default();
160        let result = analyze(&[dir.path()], &config).unwrap();
161
162        assert_eq!(result.summary.total_files, 3);
163        assert!(result.summary.by_language.contains_key("Rust"));
164        assert!(result.summary.by_language.contains_key("Python"));
165        assert!(result.summary.by_language.contains_key("JavaScript"));
166    }
167
168    #[test]
169    fn test_analyze_with_comments() {
170        let dir = TempDir::new().unwrap();
171        let rust_code = r#"// This is a comment
172fn main() {
173    /* block comment */
174    println!("Hello");
175}
176"#;
177        create_test_file(dir.path(), "main.rs", rust_code);
178
179        let config = Config::default();
180        let result = analyze(&[dir.path()], &config).unwrap();
181
182        assert_eq!(result.summary.total_files, 1);
183        assert!(result.summary.lines.comment > 0);
184        assert!(result.summary.lines.code > 0);
185    }
186
187    #[test]
188    fn test_analyze_nested_directories() {
189        let dir = TempDir::new().unwrap();
190
191        create_test_file(dir.path(), "src/main.rs", "fn main() {}\n");
192        create_test_file(dir.path(), "src/lib.rs", "pub fn lib() {}\n");
193        create_test_file(dir.path(), "tests/test.rs", "#[test]\nfn test() {}\n");
194
195        let config = Config::default();
196        let result = analyze(&[dir.path()], &config).unwrap();
197
198        assert_eq!(result.summary.total_files, 3);
199    }
200
201    #[test]
202    fn test_analyze_nonexistent_path() {
203        let config = Config::default();
204        let result = analyze(&[PathBuf::from("/nonexistent/path")], &config);
205
206        assert!(result.is_err());
207        match result.unwrap_err() {
208            Error::DirectoryNotFound { path } => {
209                assert_eq!(path, PathBuf::from("/nonexistent/path"));
210            }
211            _ => panic!("Expected DirectoryNotFound error"),
212        }
213    }
214
215    #[test]
216    fn test_analyze_multiple_paths() {
217        let dir1 = TempDir::new().unwrap();
218        let dir2 = TempDir::new().unwrap();
219
220        create_test_file(dir1.path(), "a.rs", "fn a() {}\n");
221        create_test_file(dir2.path(), "b.rs", "fn b() {}\n");
222
223        let config = Config::default();
224        let result = analyze(&[dir1.path(), dir2.path()], &config).unwrap();
225
226        assert_eq!(result.summary.total_files, 2);
227    }
228
229    #[test]
230    fn test_analyze_respects_gitignore() {
231        let dir = TempDir::new().unwrap();
232
233        // Initialize as git repo (required for .gitignore to work)
234        std::process::Command::new("git")
235            .args(["init"])
236            .current_dir(dir.path())
237            .output()
238            .ok();
239
240        // Create .gitignore
241        create_test_file(dir.path(), ".gitignore", "ignored/\n");
242
243        // Create files
244        create_test_file(dir.path(), "main.rs", "fn main() {}\n");
245        create_test_file(dir.path(), "ignored/skip.rs", "fn skip() {}\n");
246
247        let config = Config::default();
248        let result = analyze(&[dir.path()], &config).unwrap();
249
250        // Should only count main.rs, not ignored/skip.rs
251        assert_eq!(result.summary.total_files, 1);
252    }
253
254    #[test]
255    fn test_analyze_result_contains_elapsed_time() {
256        let dir = TempDir::new().unwrap();
257        create_test_file(dir.path(), "main.rs", "fn main() {}\n");
258
259        let config = Config::default();
260        let result = analyze(&[dir.path()], &config).unwrap();
261
262        // Elapsed time should be > 0
263        assert!(result.elapsed.as_nanos() > 0);
264    }
265
266    #[test]
267    fn test_analyze_scanned_files_count() {
268        let dir = TempDir::new().unwrap();
269
270        create_test_file(dir.path(), "a.rs", "fn a() {}\n");
271        create_test_file(dir.path(), "b.rs", "fn b() {}\n");
272        create_test_file(dir.path(), "c.txt", "not code\n"); // will be skipped
273
274        let config = Config::default();
275        let result = analyze(&[dir.path()], &config).unwrap();
276
277        assert_eq!(result.scanned_files, 2);
278        assert!(result.skipped_files >= 1); // at least c.txt
279    }
280}