cargo_perf/engine/
mod.rs

1//! Analysis engine - coordinates file discovery and rule execution.
2
3mod 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/// Progress information for streaming analysis.
24#[derive(Debug, Clone)]
25pub struct AnalysisProgress {
26    /// Number of files analyzed so far
27    pub files_analyzed: usize,
28    /// Total number of files to analyze
29    pub total_files: usize,
30    /// Number of diagnostics found so far
31    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    /// Analyze with progress callback for streaming feedback.
44    ///
45    /// The callback is invoked after each file is analyzed, providing
46    /// progress information. This enables UI feedback during long analyses
47    /// without requiring full architectural changes.
48    ///
49    /// # Arguments
50    ///
51    /// * `path` - Path to analyze (file or directory)
52    /// * `progress_callback` - Called after each file with current progress
53    ///
54    /// # Example
55    ///
56    /// ```ignore
57    /// engine.analyze_with_progress(path, |progress| {
58    ///     eprintln!("Progress: {}/{} files, {} issues",
59    ///         progress.files_analyzed,
60    ///         progress.total_files,
61    ///         progress.diagnostics_found);
62    /// })?;
63    /// ```
64    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        // First, collect all valid file paths (sequential - fast)
73        let files = self.collect_files(path);
74        let total_files = files.len();
75
76        // Shared counters for progress tracking
77        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        // Analyze files in parallel
82        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                        // Collect errors but continue analyzing other files
89                        if let Ok(mut errs) = errors.lock() {
90                            errs.push((file_path.clone(), e));
91                        }
92                        Vec::new()
93                    }
94                };
95
96                // Update progress
97                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        // Report errors at the end
112        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    /// Collect all Rust files to analyze (sequential, fast).
122    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        // Use shared file analysis logic with static registry rules
128        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        // Also create a src file that should be analyzed
146        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        // Should have analyzed src/lib.rs but not target/test.rs
155        // (no diagnostics expected for clean code, but no errors either)
156        assert!(diagnostics.is_empty());
157    }
158
159    #[test]
160    fn test_excludes_hidden_directories() {
161        let temp_dir = TempDir::new().unwrap();
162
163        // Hidden directory
164        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        // Visible file
169        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        // Create a symlink to /etc/passwd (or any system file)
186        let symlink_path = temp_dir.path().join("evil.rs");
187        let _ = symlink("/etc/passwd", &symlink_path);
188
189        // Create a real file
190        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        // Should succeed without trying to parse /etc/passwd
197        assert!(result.is_ok());
198    }
199}