context_creator/core/
project_analyzer.rs

1//! Project-wide analysis cache for semantic features
2//!
3//! This module provides a single-pass project analysis that can be reused
4//! across different semantic features to avoid redundant directory walks.
5
6use crate::cli::Config;
7use crate::core::cache::FileCache;
8use crate::core::walker::{walk_directory, FileInfo, WalkOptions};
9use crate::utils::error::ContextCreatorError;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use tracing::info;
14
15/// Cached project analysis results
16pub struct ProjectAnalysis {
17    /// All files in the project with semantic analysis
18    pub all_files: Vec<FileInfo>,
19    /// Map from canonical paths to file info for fast lookups
20    pub file_map: HashMap<PathBuf, FileInfo>,
21    /// Project root directory
22    pub project_root: PathBuf,
23}
24
25impl ProjectAnalysis {
26    /// Perform a single comprehensive analysis of the entire project
27    pub fn analyze_project(
28        start_path: &Path,
29        base_walk_options: &WalkOptions,
30        config: &Config,
31        cache: &Arc<FileCache>,
32    ) -> Result<Self, ContextCreatorError> {
33        // Detect project root
34        let project_root = if start_path.is_file() {
35            super::file_expander::detect_project_root(start_path)
36        } else {
37            // For directories, detect project root directly
38            super::file_expander::detect_project_root(start_path)
39        };
40
41        // Create walk options for full project scan (no include patterns)
42        let mut project_walk_options = base_walk_options.clone();
43        project_walk_options.include_patterns.clear();
44
45        // Single walk of the entire project
46        if config.progress && !config.quiet {
47            info!("Analyzing project from: {}", project_root.display());
48        }
49
50        let mut all_files = walk_directory(&project_root, project_walk_options)
51            .map_err(|e| ContextCreatorError::ContextGenerationError(e.to_string()))?;
52
53        // Perform semantic analysis once
54        if config.trace_imports || config.include_callers || config.include_types {
55            super::walker::perform_semantic_analysis(&mut all_files, config, cache)
56                .map_err(|e| ContextCreatorError::ContextGenerationError(e.to_string()))?;
57
58            if config.progress && !config.quiet {
59                let import_count: usize = all_files.iter().map(|f| f.imports.len()).sum();
60                info!("Found {} import relationships in project", import_count);
61            }
62        }
63
64        // Build file map for fast lookups
65        let mut file_map = HashMap::with_capacity(all_files.len());
66        for file in &all_files {
67            // Use both original and canonical paths as keys
68            file_map.insert(file.path.clone(), file.clone());
69            if let Ok(canonical) = file.path.canonicalize() {
70                file_map.insert(canonical, file.clone());
71            }
72        }
73
74        Ok(ProjectAnalysis {
75            all_files,
76            file_map,
77            project_root,
78        })
79    }
80
81    /// Get a file by path (handles both canonical and non-canonical paths)
82    pub fn get_file(&self, path: &Path) -> Option<&FileInfo> {
83        // Try direct lookup first
84        if let Some(file) = self.file_map.get(path) {
85            return Some(file);
86        }
87
88        // Try canonical path
89        if let Ok(canonical) = path.canonicalize() {
90            self.file_map.get(&canonical)
91        } else {
92            None
93        }
94    }
95
96    /// Filter files by the original walk options
97    pub fn filter_files(&self, walk_options: &WalkOptions) -> Vec<FileInfo> {
98        self.all_files
99            .iter()
100            .filter(|file| {
101                // Apply include patterns if any
102                if !walk_options.include_patterns.is_empty() {
103                    let matches_include = walk_options.include_patterns.iter().any(|pattern| {
104                        glob::Pattern::new(pattern)
105                            .ok()
106                            .map(|p| p.matches_path(&file.relative_path))
107                            .unwrap_or(false)
108                    });
109                    if !matches_include {
110                        return false;
111                    }
112                }
113
114                // File passed all filters
115                true
116            })
117            .cloned()
118            .collect()
119    }
120}