Skip to main content

codelens_core/
lib.rs

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