context_creator/core/
project_analyzer.rs1use 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
15pub struct ProjectAnalysis {
17 pub all_files: Vec<FileInfo>,
19 pub file_map: HashMap<PathBuf, FileInfo>,
21 pub project_root: PathBuf,
23}
24
25impl ProjectAnalysis {
26 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 let project_root = if start_path.is_file() {
35 super::file_expander::detect_project_root(start_path)
36 } else {
37 super::file_expander::detect_project_root(start_path)
39 };
40
41 let mut project_walk_options = base_walk_options.clone();
43 project_walk_options.include_patterns.clear();
44
45 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 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 let mut file_map = HashMap::with_capacity(all_files.len());
66 for file in &all_files {
67 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 pub fn get_file(&self, path: &Path) -> Option<&FileInfo> {
83 if let Some(file) = self.file_map.get(path) {
85 return Some(file);
86 }
87
88 if let Ok(canonical) = path.canonicalize() {
90 self.file_map.get(&canonical)
91 } else {
92 None
93 }
94 }
95
96 pub fn filter_files(&self, walk_options: &WalkOptions) -> Vec<FileInfo> {
98 self.all_files
99 .iter()
100 .filter(|file| {
101 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 true
116 })
117 .cloned()
118 .collect()
119 }
120}