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