Skip to main content

code_analyze/analyze/
mod.rs

1// Copyright 2024 Block, Inc. (original code from https://github.com/block/goose)
2// Copyright 2025 utapyngo (modifications)
3// SPDX-License-Identifier: Apache-2.0
4
5pub 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
24/// Helper to safely lock a mutex with poison recovery
25pub(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/// Code analyzer with caching and tree-sitter parsing
40#[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
215/// Simplified public API for the analyze tool
216use 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 focus is specified with non-focused mode, filter results
303    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}