1pub mod cache;
6pub mod formatter;
7pub mod graph;
8pub mod languages;
9pub mod parser;
10pub mod traversal;
11pub mod types;
12
13use std::path::{Path, PathBuf};
14
15use self::cache::AnalysisCache;
16use self::formatter::Formatter;
17use self::graph::CallGraph;
18use self::parser::{ElementExtractor, ParserManager};
19use self::traversal::FileTraverser;
20use self::types::{AnalysisMode, AnalysisResult, FocusedAnalysisData};
21
22use crate::lang;
23
24pub(crate) fn lock_or_recover<T, F>(
26 mutex: &std::sync::Mutex<T>,
27 recovery: F,
28) -> std::sync::MutexGuard<'_, T>
29where
30 F: FnOnce(&mut T),
31{
32 mutex.lock().unwrap_or_else(|poisoned| {
33 let mut guard = poisoned.into_inner();
34 recovery(&mut guard);
35 guard
36 })
37}
38
39#[derive(Clone)]
41pub struct CodeAnalyzer {
42 parser_manager: ParserManager,
43 cache: AnalysisCache,
44}
45
46impl Default for CodeAnalyzer {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl CodeAnalyzer {
53 pub fn new() -> Self {
54 Self {
55 parser_manager: ParserManager::new(),
56 cache: AnalysisCache::new(100),
57 }
58 }
59
60 fn determine_mode(&self, focus: &Option<String>, path: &Path) -> AnalysisMode {
61 if focus.is_some() {
62 return AnalysisMode::Focused;
63 }
64
65 if path.is_file() {
66 AnalysisMode::Semantic
67 } else {
68 AnalysisMode::Structure
69 }
70 }
71
72 fn analyze_file(
73 &self,
74 path: &Path,
75 mode: &AnalysisMode,
76 ast_recursion_limit: Option<usize>,
77 ) -> Result<AnalysisResult, String> {
78 let metadata = std::fs::metadata(path)
79 .map_err(|e| format!("Failed to get metadata for '{}': {}", path.display(), e))?;
80
81 let modified = metadata.modified().map_err(|e| {
82 format!(
83 "Failed to get modification time for '{}': {}",
84 path.display(),
85 e
86 )
87 })?;
88
89 if let Some(cached) = self.cache.get(path, modified, mode) {
90 return Ok(cached);
91 }
92
93 let content = match std::fs::read_to_string(path) {
94 Ok(content) => content,
95 Err(_) => {
96 return Ok(AnalysisResult::empty(0));
97 }
98 };
99
100 let line_count = content.lines().count();
101
102 let language = lang::get_language_identifier(path);
103 if language.is_empty() {
104 return Ok(AnalysisResult::empty(line_count));
105 }
106
107 let language_supported = languages::get_language_info(language)
108 .map(|info| !info.element_query.is_empty())
109 .unwrap_or(false);
110
111 if !language_supported {
112 return Ok(AnalysisResult::empty(line_count));
113 }
114
115 let tree = self.parser_manager.parse(&content, language)?;
116
117 let depth = mode.as_str();
118 let mut result = ElementExtractor::extract_with_depth(
119 &tree,
120 &content,
121 language,
122 depth,
123 ast_recursion_limit,
124 )?;
125
126 result.line_count = line_count;
127
128 self.cache
129 .put(path.to_path_buf(), modified, mode, result.clone());
130
131 Ok(result)
132 }
133
134 fn analyze_directory(
135 &self,
136 path: &Path,
137 max_depth: u32,
138 ast_recursion_limit: Option<usize>,
139 traverser: &FileTraverser,
140 mode: &AnalysisMode,
141 ) -> Result<String, String> {
142 let mode = *mode;
143
144 let results = traverser.collect_directory_results(path, max_depth, |file_path| {
145 self.analyze_file(file_path, &mode, ast_recursion_limit)
146 })?;
147
148 Ok(Formatter::format_directory_structure(
149 path, &results, max_depth,
150 ))
151 }
152
153 fn analyze_focused(
154 &self,
155 path: &Path,
156 focus: &str,
157 follow_depth: u32,
158 max_depth: u32,
159 ast_recursion_limit: Option<usize>,
160 traverser: &FileTraverser,
161 ) -> Result<String, String> {
162 let files_to_analyze = if path.is_file() {
163 vec![path.to_path_buf()]
164 } else {
165 traverser.collect_files_for_focused(path, max_depth)?
166 };
167
168 use rayon::prelude::*;
169 let all_results: Result<Vec<_>, _> = files_to_analyze
170 .par_iter()
171 .map(|file_path| {
172 self.analyze_file(file_path, &AnalysisMode::Semantic, ast_recursion_limit)
173 .map(|result| (file_path.clone(), result))
174 })
175 .collect();
176 let all_results = all_results?;
177
178 let graph = CallGraph::build_from_results(&all_results);
179
180 let incoming_chains = if follow_depth > 0 {
181 graph.find_incoming_chains(focus, follow_depth)
182 } else {
183 vec![]
184 };
185
186 let outgoing_chains = if follow_depth > 0 {
187 graph.find_outgoing_chains(focus, follow_depth)
188 } else {
189 vec![]
190 };
191
192 let definitions = graph.definitions.get(focus).cloned().unwrap_or_default();
193
194 let focus_data = FocusedAnalysisData {
195 focus_symbol: focus,
196 follow_depth,
197 files_analyzed: &files_to_analyze,
198 definitions: &definitions,
199 incoming_chains: &incoming_chains,
200 outgoing_chains: &outgoing_chains,
201 };
202
203 let mut output = Formatter::format_focused_output(&focus_data);
204
205 if path.is_file() {
206 let hint = "NOTE: Focus mode works best with directory paths. \
207 Use a parent directory in the path for cross-file analysis.\n\n";
208 output = format!("{}{}", hint, output);
209 }
210
211 Ok(output)
212 }
213}
214
215use std::sync::OnceLock;
217
218static ANALYZER: OnceLock<CodeAnalyzer> = OnceLock::new();
219
220fn get_analyzer() -> &'static CodeAnalyzer {
221 ANALYZER.get_or_init(CodeAnalyzer::new)
222}
223
224pub fn analyze(
225 path: &str,
226 focus: Option<&str>,
227 follow_depth: u32,
228 max_depth: u32,
229 ast_recursion_limit: Option<usize>,
230 cwd: &str,
231) -> String {
232 let abs_path = if Path::new(path).is_absolute() {
233 PathBuf::from(path)
234 } else {
235 PathBuf::from(cwd).join(path)
236 };
237
238 let analyzer = get_analyzer();
239 let traverser = FileTraverser::new();
240
241 if let Err(e) = traverser.validate_path(&abs_path) {
242 return e;
243 }
244
245 let focus_owned = focus.map(|s| s.to_string());
246 let mode = analyzer.determine_mode(&focus_owned, &abs_path);
247
248 let mut output = match mode {
249 AnalysisMode::Focused => {
250 match analyzer.analyze_focused(
251 &abs_path,
252 focus.unwrap_or(""),
253 follow_depth,
254 max_depth,
255 ast_recursion_limit,
256 &traverser,
257 ) {
258 Ok(output) => output,
259 Err(e) => return format!("Analysis error: {}", e),
260 }
261 }
262 AnalysisMode::Semantic => {
263 if abs_path.is_file() {
264 match analyzer.analyze_file(&abs_path, &mode, ast_recursion_limit) {
265 Ok(result) => Formatter::format_analysis_result(&abs_path, &result, &mode),
266 Err(e) => return format!("Analysis error: {}", e),
267 }
268 } else {
269 match analyzer.analyze_directory(
270 &abs_path,
271 max_depth,
272 ast_recursion_limit,
273 &traverser,
274 &mode,
275 ) {
276 Ok(output) => output,
277 Err(e) => return format!("Analysis error: {}", e),
278 }
279 }
280 }
281 AnalysisMode::Structure => {
282 if abs_path.is_file() {
283 match analyzer.analyze_file(&abs_path, &mode, ast_recursion_limit) {
284 Ok(result) => Formatter::format_analysis_result(&abs_path, &result, &mode),
285 Err(e) => return format!("Analysis error: {}", e),
286 }
287 } else {
288 match analyzer.analyze_directory(
289 &abs_path,
290 max_depth,
291 ast_recursion_limit,
292 &traverser,
293 &mode,
294 ) {
295 Ok(output) => output,
296 Err(e) => return format!("Analysis error: {}", e),
297 }
298 }
299 }
300 };
301
302 if let Some(focus_str) = focus
304 && mode != AnalysisMode::Focused
305 {
306 output = Formatter::filter_by_focus(&output, focus_str);
307 }
308
309 output
310}