1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! Parallel file analysis module for semantic analysis
//!
//! This module is responsible for managing parallel processing of file analysis.
//! It follows the Single Responsibility Principle by focusing solely on parallelization.
use crate::core::cache::FileCache;
use crate::core::semantic::analyzer::SemanticContext;
use crate::core::semantic::dependency_types::{DependencyEdgeType, FileAnalysisResult};
use crate::core::semantic::{get_analyzer_for_file, get_resolver_for_file};
use crate::core::semantic_cache::SemanticCache;
use anyhow::Result;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tracing::warn;
/// Options for file analysis
#[derive(Debug, Clone)]
pub struct AnalysisOptions {
/// Maximum depth for semantic analysis
pub semantic_depth: usize,
/// Whether to trace imports
pub trace_imports: bool,
/// Whether to include type references
pub include_types: bool,
/// Whether to include function calls
pub include_functions: bool,
}
impl Default for AnalysisOptions {
fn default() -> Self {
Self {
semantic_depth: 3,
trace_imports: true,
include_types: true,
include_functions: true,
}
}
}
/// Parallel analyzer for file processing
pub struct ParallelAnalyzer<'a> {
cache: &'a FileCache,
semantic_cache: Arc<SemanticCache>,
thread_count: Option<usize>,
options: AnalysisOptions,
}
impl<'a> ParallelAnalyzer<'a> {
/// Create a new ParallelAnalyzer
pub fn new(cache: &'a FileCache) -> Self {
Self {
cache,
semantic_cache: Arc::new(SemanticCache::new()),
thread_count: None,
options: AnalysisOptions::default(),
}
}
/// Create a new ParallelAnalyzer with a specific thread count
pub fn with_thread_count(cache: &'a FileCache, thread_count: usize) -> Self {
Self {
cache,
semantic_cache: Arc::new(SemanticCache::new()),
thread_count: Some(thread_count),
options: AnalysisOptions::default(),
}
}
/// Create a new ParallelAnalyzer with specific options
pub fn with_options(cache: &'a FileCache, options: AnalysisOptions) -> Self {
Self {
cache,
semantic_cache: Arc::new(SemanticCache::new()),
thread_count: None,
options,
}
}
/// Analyze multiple files in parallel
pub fn analyze_files(
&self,
files: &[PathBuf],
project_root: &Path,
options: &AnalysisOptions,
valid_files: &std::collections::HashSet<PathBuf>,
) -> Result<Vec<FileAnalysisResult>> {
// Configure thread pool if specified
if let Some(count) = self.thread_count {
rayon::ThreadPoolBuilder::new()
.num_threads(count)
.build_global()
.ok(); // Ignore error if already initialized
}
// Create error collector
let errors = Arc::new(Mutex::new(Vec::new()));
let errors_ref = &errors;
// Analyze files in parallel
let results: Vec<FileAnalysisResult> = files
.par_iter()
.enumerate()
.map(|(index, file_path)| {
match self.analyze_single_file(index, file_path, project_root, options, valid_files)
{
Ok(result) => result,
Err(e) => {
let error_msg = format!("Failed to analyze {}: {}", file_path.display(), e);
errors_ref.lock().unwrap().push(error_msg.clone());
// Return a minimal result with error
FileAnalysisResult {
file_index: index,
imports: Vec::new(),
function_calls: Vec::new(),
type_references: Vec::new(),
exported_functions: Vec::new(),
content_hash: None,
error: Some(error_msg),
}
}
}
})
.collect();
// Print collected errors
let error_list = errors.lock().unwrap();
for error in error_list.iter() {
warn!("{}", error);
}
Ok(results)
}
/// Analyze a single file
#[allow(clippy::too_many_arguments)]
fn analyze_single_file(
&self,
file_index: usize,
file_path: &Path,
project_root: &Path,
options: &AnalysisOptions,
valid_files: &std::collections::HashSet<PathBuf>,
) -> Result<FileAnalysisResult> {
// Get analyzer for the file type
let analyzer = match get_analyzer_for_file(file_path)? {
Some(analyzer) => analyzer,
None => {
// No analyzer for this file type - return empty result
return Ok(FileAnalysisResult {
file_index,
imports: Vec::new(),
function_calls: Vec::new(),
type_references: Vec::new(),
exported_functions: Vec::new(),
content_hash: Some(self.compute_content_hash(file_path)?),
error: None,
});
}
};
// Read file content
let content = self.cache.get_or_load(file_path)?;
// Compute content hash
let content_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
};
// Check semantic cache first
let analysis_result =
if let Some(cached_result) = self.semantic_cache.get(file_path, content_hash) {
// Cache hit - use cached result
(*cached_result).clone()
} else {
// Cache miss - perform analysis
// Create semantic context
let context = SemanticContext::new(
file_path.to_path_buf(),
project_root.to_path_buf(),
options.semantic_depth,
);
// Perform analysis
let result = analyzer.analyze_file(file_path, &content, &context)?;
// Store in cache
self.semantic_cache
.insert(file_path, content_hash, result.clone());
result
};
// Process imports if enabled
let imports = if options.trace_imports {
self.process_imports(
file_path,
project_root,
&analysis_result.imports,
valid_files,
)?
} else {
Vec::new()
};
// Filter results based on options
let function_calls = if options.include_functions {
analysis_result.function_calls
} else {
Vec::new()
};
let type_references = if options.include_types {
analysis_result.type_references
} else {
Vec::new()
};
let exported_functions = if self.options.include_functions {
analysis_result.exported_functions
} else {
Vec::new()
};
Ok(FileAnalysisResult {
file_index,
imports,
function_calls,
type_references,
exported_functions,
content_hash: Some(content_hash),
error: None,
})
}
/// Process imports to create typed edges
fn process_imports(
&self,
file_path: &Path,
project_root: &Path,
imports: &[crate::core::semantic::analyzer::Import],
_valid_files: &std::collections::HashSet<PathBuf>,
) -> Result<Vec<(PathBuf, DependencyEdgeType)>> {
let mut typed_imports = Vec::new();
// Get resolver for the file type
if let Some(resolver) = get_resolver_for_file(file_path)? {
for import in imports {
// Debug logging
tracing::debug!(
"Resolving import '{}' with items {:?} from file {}",
import.module,
import.items,
file_path.display()
);
// Try to resolve the import
match resolver.resolve_import(&import.module, file_path, project_root) {
Ok(resolved) => {
tracing::debug!(
" Resolved to: {} (external: {})",
resolved.path.display(),
resolved.is_external
);
if !resolved.is_external {
// For trace_imports, we want to track ALL imports,
// not just those in valid_files, to support file expansion
let edge_type = DependencyEdgeType::Import {
symbols: import.items.clone(),
};
typed_imports.push((resolved.path, edge_type));
}
}
Err(e) => {
tracing::debug!(" Failed to resolve: {}", e);
// For relative imports, try to resolve manually
if import.module.starts_with(".") {
if let Some(parent) = file_path.parent() {
let module_base = import.module.trim_start_matches("./");
// Try common extensions
for ext in &["js", "jsx", "ts", "tsx"] {
let potential_path =
parent.join(format!("{module_base}.{ext}"));
if potential_path.exists() {
let edge_type = DependencyEdgeType::Import {
symbols: import.items.clone(),
};
typed_imports.push((potential_path, edge_type));
break;
}
}
}
} else {
// Fallback: For trace_imports, track the import even if unresolved
// This allows the file expander to attempt resolution later
let fallback_path = PathBuf::from(&import.module);
if fallback_path.is_absolute() && fallback_path.exists() {
let edge_type = DependencyEdgeType::Import {
symbols: import.items.clone(),
};
typed_imports.push((fallback_path, edge_type));
}
}
}
}
}
} else {
// No resolver available - for trace_imports, track absolute paths that exist
for import in imports {
let import_path = PathBuf::from(&import.module);
if import_path.is_absolute() && import_path.exists() {
let edge_type = DependencyEdgeType::Import {
symbols: import.items.clone(),
};
typed_imports.push((import_path, edge_type));
}
}
}
Ok(typed_imports)
}
/// Compute content hash for a file
fn compute_content_hash(&self, file_path: &Path) -> Result<u64> {
let content = self.cache.get_or_load(file_path)?;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
Ok(hasher.finish())
}
}
#[cfg(test)]
#[path = "parallel_analyzer_tests.rs"]
mod tests;