1mod context;
4pub mod file_analyzer;
5mod parser;
6
7pub use context::{AnalysisContext, LineIndex};
8pub use file_analyzer::{analyze_file_with_rules, read_file_secure};
9
10use crate::discovery::{discover_rust_files, DiscoveryOptions};
11use crate::error::{Error, Result};
12use crate::rules::{registry, Diagnostic};
13use crate::Config;
14use rayon::prelude::*;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::Mutex;
18
19pub struct Engine<'a> {
20 config: &'a Config,
21}
22
23#[derive(Debug, Clone)]
25pub struct AnalysisProgress {
26 pub files_analyzed: usize,
28 pub total_files: usize,
30 pub diagnostics_found: usize,
32}
33
34impl<'a> Engine<'a> {
35 pub fn new(config: &'a Config) -> Self {
36 Self { config }
37 }
38
39 pub fn analyze(&self, path: &Path) -> Result<Vec<Diagnostic>> {
40 self.analyze_with_progress(path, |_| {})
41 }
42
43 pub fn analyze_with_progress<F>(
65 &self,
66 path: &Path,
67 progress_callback: F,
68 ) -> Result<Vec<Diagnostic>>
69 where
70 F: Fn(AnalysisProgress) + Send + Sync,
71 {
72 let files = self.collect_files(path);
74 let total_files = files.len();
75
76 let files_analyzed = AtomicUsize::new(0);
78 let diagnostics_found = AtomicUsize::new(0);
79 let errors: Mutex<Vec<(PathBuf, Error)>> = Mutex::new(Vec::new());
80
81 let all_diagnostics: Vec<Diagnostic> = files
83 .par_iter()
84 .flat_map(|file_path| {
85 let result = match self.analyze_file(file_path) {
86 Ok(diagnostics) => diagnostics,
87 Err(e) => {
88 if let Ok(mut errs) = errors.lock() {
90 errs.push((file_path.clone(), e));
91 }
92 Vec::new()
93 }
94 };
95
96 let analyzed = files_analyzed.fetch_add(1, Ordering::Relaxed) + 1;
98 let found =
99 diagnostics_found.fetch_add(result.len(), Ordering::Relaxed) + result.len();
100
101 progress_callback(AnalysisProgress {
102 files_analyzed: analyzed,
103 total_files,
104 diagnostics_found: found,
105 });
106
107 result
108 })
109 .collect();
110
111 if let Ok(errs) = errors.lock() {
113 for (path, error) in errs.iter() {
114 eprintln!("Warning: Failed to analyze {}: {}", path.display(), error);
115 }
116 }
117
118 Ok(all_diagnostics)
119 }
120
121 fn collect_files(&self, path: &Path) -> Vec<PathBuf> {
123 discover_rust_files(path, &DiscoveryOptions::secure())
124 }
125
126 fn analyze_file(&self, file_path: &Path) -> Result<Vec<Diagnostic>> {
127 let rules = registry::all_rules().iter().map(|r| r.as_ref());
129 analyze_file_with_rules(file_path, self.config, rules)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use tempfile::TempDir;
137
138 #[test]
139 fn test_excludes_target_directory() {
140 let temp_dir = TempDir::new().unwrap();
141 let target_dir = temp_dir.path().join("target");
142 std::fs::create_dir(&target_dir).unwrap();
143 std::fs::write(target_dir.join("test.rs"), "fn main() {}").unwrap();
144
145 let src_dir = temp_dir.path().join("src");
147 std::fs::create_dir(&src_dir).unwrap();
148 std::fs::write(src_dir.join("lib.rs"), "fn main() {}").unwrap();
149
150 let config = Config::default();
151 let engine = Engine::new(&config);
152 let diagnostics = engine.analyze(temp_dir.path()).unwrap();
153
154 assert!(diagnostics.is_empty());
157 }
158
159 #[test]
160 fn test_excludes_hidden_directories() {
161 let temp_dir = TempDir::new().unwrap();
162
163 let hidden_dir = temp_dir.path().join(".hidden");
165 std::fs::create_dir(&hidden_dir).unwrap();
166 std::fs::write(hidden_dir.join("secret.rs"), "fn bad() {}").unwrap();
167
168 std::fs::write(temp_dir.path().join("visible.rs"), "fn good() {}").unwrap();
170
171 let config = Config::default();
172 let engine = Engine::new(&config);
173 let result = engine.analyze(temp_dir.path());
174
175 assert!(result.is_ok());
176 }
177
178 #[cfg(unix)]
179 #[test]
180 fn test_does_not_follow_symlinks() {
181 use std::os::unix::fs::symlink;
182
183 let temp_dir = TempDir::new().unwrap();
184
185 let symlink_path = temp_dir.path().join("evil.rs");
187 let _ = symlink("/etc/passwd", &symlink_path);
188
189 std::fs::write(temp_dir.path().join("real.rs"), "fn main() {}").unwrap();
191
192 let config = Config::default();
193 let engine = Engine::new(&config);
194 let result = engine.analyze(temp_dir.path());
195
196 assert!(result.is_ok());
198 }
199}