1use anyhow::{Context, Result};
26use serde::{Deserialize, Serialize};
27use std::collections::{HashMap, HashSet};
28use std::path::{Path, PathBuf};
29
30#[cfg(feature = "native")]
31use walkdir::WalkDir;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum SymbolKind {
36 Function,
37 Class,
38 Variable,
39 Constant,
40 Module,
41 Import,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SymbolReference {
47 pub symbol: String,
49 pub kind: SymbolKind,
51 pub file: PathBuf,
53 pub line: usize,
55 pub context: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub enum CodePattern {
62 DuplicateCode { pattern: String, occurrences: Vec<(PathBuf, usize)> },
64 TechDebt { message: String, file: PathBuf, line: usize },
66 DeprecatedApi { api: String, file: PathBuf, line: usize },
68 ErrorHandling { pattern: String, file: PathBuf, line: usize },
70 ResourceManagement { resource_type: String, file: PathBuf, line: usize },
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct FileDependency {
77 pub from: PathBuf,
79 pub to: PathBuf,
81 pub kind: DependencyKind,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub enum DependencyKind {
88 Import,
89 Include,
90 Require,
91 ModuleUse,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DeadCode {
97 pub symbol: String,
99 pub kind: SymbolKind,
101 pub file: PathBuf,
103 pub line: usize,
105 pub reason: String,
107}
108
109pub struct ParfAnalyzer {
111 file_cache: HashMap<PathBuf, Vec<String>>,
113 symbol_definitions: HashMap<String, Vec<SymbolReference>>,
115 symbol_references: HashMap<String, Vec<SymbolReference>>,
117}
118
119impl Default for ParfAnalyzer {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl ParfAnalyzer {
126 pub fn new() -> Self {
128 Self {
129 file_cache: HashMap::new(),
130 symbol_definitions: HashMap::new(),
131 symbol_references: HashMap::new(),
132 }
133 }
134
135 #[cfg(feature = "native")]
137 pub fn index_codebase(&mut self, path: &Path) -> Result<()> {
138 for entry in WalkDir::new(path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
139 if entry.file_type().is_file() {
140 if let Some(ext) = entry.path().extension() {
141 if ["rs", "py", "js", "ts", "c", "cpp", "h", "hpp"]
143 .contains(&ext.to_str().unwrap_or(""))
144 {
145 self.index_file(entry.path())?;
146 }
147 }
148 }
149 }
150 Ok(())
151 }
152
153 #[cfg(not(feature = "native"))]
155 pub fn index_codebase(&mut self, _path: &Path) -> Result<()> {
156 Ok(())
157 }
158
159 fn index_file(&mut self, path: &Path) -> Result<()> {
161 let content = std::fs::read_to_string(path)
162 .with_context(|| format!("Failed to read file: {}", path.display()))?;
163
164 let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
165 self.file_cache.insert(path.to_path_buf(), lines.clone());
166
167 for (line_num, line) in lines.iter().enumerate() {
169 if line.contains("fn ") && line.contains('(') {
171 if let Some(name) = Self::extract_function_name(line) {
172 self.add_definition(
173 name,
174 SymbolKind::Function,
175 path,
176 line_num + 1,
177 line.trim(),
178 );
179 }
180 }
181
182 if line.contains("struct ") || line.contains("enum ") {
184 if let Some(name) = Self::extract_type_name(line) {
185 self.add_definition(name, SymbolKind::Class, path, line_num + 1, line.trim());
186 }
187 }
188
189 if line.trim_start().starts_with("def ") {
191 if let Some(name) = Self::extract_python_function_name(line) {
192 self.add_definition(
193 name,
194 SymbolKind::Function,
195 path,
196 line_num + 1,
197 line.trim(),
198 );
199 }
200 }
201
202 if line.trim_start().starts_with("class ") {
204 if let Some(name) = Self::extract_python_class_name(line) {
205 self.add_definition(name, SymbolKind::Class, path, line_num + 1, line.trim());
206 }
207 }
208 }
209
210 Ok(())
211 }
212
213 fn add_definition(
215 &mut self,
216 symbol: String,
217 kind: SymbolKind,
218 file: &Path,
219 line: usize,
220 context: &str,
221 ) {
222 let reference = SymbolReference {
223 symbol: symbol.clone(),
224 kind,
225 file: file.to_path_buf(),
226 line,
227 context: context.to_string(),
228 };
229
230 self.symbol_definitions.entry(symbol).or_default().push(reference);
231 }
232
233 pub fn find_references(&self, symbol: &str, _kind: SymbolKind) -> Vec<SymbolReference> {
235 let mut references = Vec::new();
236
237 for (path, lines) in &self.file_cache {
239 for (line_num, line) in lines.iter().enumerate() {
240 if line.contains(symbol) {
241 references.push(SymbolReference {
242 symbol: symbol.to_string(),
243 kind: SymbolKind::Function, file: path.clone(),
245 line: line_num + 1,
246 context: line.trim().to_string(),
247 });
248 }
249 }
250 }
251
252 references
253 }
254
255 pub fn detect_patterns(&self) -> Vec<CodePattern> {
260 const PATTERN_MARKERS: &[(&str, &str)] = &[
268 ("TO\x44O", "tech_debt"),
269 ("FIX\x4dE", "tech_debt"),
270 ("HACK", "tech_debt"),
271 ("deprecated", "deprecated_api"),
272 ("@deprecated", "deprecated_api"),
273 ("unwrap()", "error_handling"),
274 ("expect(", "error_handling"),
275 ("File::open", "resource_management"),
276 ("fs::read", "resource_management"),
277 ];
278
279 let mut patterns = Vec::new();
280
281 for (path, lines) in &self.file_cache {
282 for (line_num, line) in lines.iter().enumerate() {
283 for &(marker, category) in PATTERN_MARKERS {
284 if !line.contains(marker) {
285 continue;
286 }
287 if marker == "unwrap()" && line.contains("//") {
289 continue;
290 }
291 let trimmed = line.trim().to_string();
292 let loc = (path.clone(), line_num + 1);
293 patterns.push(match category {
294 "tech_debt" => {
295 CodePattern::TechDebt { message: trimmed, file: loc.0, line: loc.1 }
296 }
297 "deprecated_api" => {
298 CodePattern::DeprecatedApi { api: trimmed, file: loc.0, line: loc.1 }
299 }
300 "error_handling" => CodePattern::ErrorHandling {
301 pattern: format!("{marker} without error handling"),
302 file: loc.0,
303 line: loc.1,
304 },
305 "resource_management" => CodePattern::ResourceManagement {
306 resource_type: "file".to_string(),
307 file: loc.0,
308 line: loc.1,
309 },
310 _ => unreachable!("unknown pattern category: {category}"),
311 });
312 }
313 }
314 }
315
316 patterns
317 }
318
319 pub fn analyze_dependencies(&self) -> Vec<FileDependency> {
321 contract_pre_analyze!(self);
322 let mut dependencies = Vec::new();
323
324 for (path, lines) in &self.file_cache {
325 for line in lines {
326 if line.trim_start().starts_with("use ") {
328 dependencies.push(FileDependency {
330 from: path.clone(),
331 to: PathBuf::from("module"), kind: DependencyKind::ModuleUse,
333 });
334 }
335
336 if line.trim_start().starts_with("import ")
338 || line.trim_start().starts_with("from ")
339 {
340 dependencies.push(FileDependency {
341 from: path.clone(),
342 to: PathBuf::from("module"), kind: DependencyKind::Import,
344 });
345 }
346 }
347 }
348
349 dependencies
350 }
351
352 pub fn find_dead_code(&self) -> Vec<DeadCode> {
354 let mut dead_code = Vec::new();
355 let mut referenced_symbols = HashSet::new();
356
357 for lines in self.file_cache.values() {
359 for line in lines {
360 for def in self.symbol_definitions.keys() {
362 if line.contains(def) {
363 referenced_symbols.insert(def.clone());
364 }
365 }
366 }
367 }
368
369 for (symbol, defs) in &self.symbol_definitions {
371 if !referenced_symbols.contains(symbol) {
372 for def in defs {
373 if def.context.contains("#[test]") || def.context.contains("test_") {
375 continue;
376 }
377
378 if symbol == "main" {
380 continue;
381 }
382
383 dead_code.push(DeadCode {
384 symbol: symbol.clone(),
385 kind: def.kind,
386 file: def.file.clone(),
387 line: def.line,
388 reason: "No references found".to_string(),
389 });
390 }
391 }
392 }
393
394 dead_code
395 }
396
397 pub fn generate_report(&self) -> String {
399 let mut report = String::from("PARF Analysis Report\n");
400 report.push_str("====================\n\n");
401
402 report.push_str(&format!("Files analyzed: {}\n", self.file_cache.len()));
403 report.push_str(&format!("Symbols defined: {}\n", self.symbol_definitions.len()));
404 report.push_str(&format!("Patterns detected: {}\n", self.detect_patterns().len()));
405 report.push_str(&format!("Dependencies: {}\n\n", self.analyze_dependencies().len()));
406
407 let dead_code = self.find_dead_code();
409 report.push_str(&format!("Potentially dead code: {}\n", dead_code.len()));
410
411 if !dead_code.is_empty() {
412 report.push_str("\nDead Code Candidates:\n");
413 report.push_str("---------------------\n");
414 for (i, dc) in dead_code.iter().take(10).enumerate() {
415 report.push_str(&format!(
416 "{}. {} ({:?}) in {}:{}\n",
417 i + 1,
418 dc.symbol,
419 dc.kind,
420 dc.file.display(),
421 dc.line
422 ));
423 }
424 if dead_code.len() > 10 {
425 report.push_str(&format!("... and {} more\n", dead_code.len() - 10));
426 }
427 }
428
429 report
430 }
431
432 fn extract_function_name(line: &str) -> Option<String> {
435 if let Some(fn_pos) = line.find("fn ") {
437 let after_fn = &line[fn_pos + 3..];
438 if let Some(paren_pos) = after_fn.find('(') {
439 return Some(after_fn[..paren_pos].trim().to_string());
440 }
441 }
442 None
443 }
444
445 fn extract_type_name(line: &str) -> Option<String> {
446 for keyword in &["struct ", "enum "] {
448 if let Some(pos) = line.find(keyword) {
449 let after_keyword = &line[pos + keyword.len()..];
450 if let Some(space_or_brace) =
451 after_keyword.find(|c: char| c.is_whitespace() || c == '{' || c == '<')
452 {
453 return Some(after_keyword[..space_or_brace].trim().to_string());
454 }
455 }
456 }
457 None
458 }
459
460 fn extract_python_function_name(line: &str) -> Option<String> {
461 if let Some(def_pos) = line.find("def ") {
463 let after_def = &line[def_pos + 4..];
464 if let Some(paren_pos) = after_def.find('(') {
465 return Some(after_def[..paren_pos].trim().to_string());
466 }
467 }
468 None
469 }
470
471 fn extract_python_class_name(line: &str) -> Option<String> {
472 if let Some(class_pos) = line.find("class ") {
474 let after_class = &line[class_pos + 6..];
475 if let Some(end_pos) = after_class.find([':', '(']) {
476 return Some(after_class[..end_pos].trim().to_string());
477 }
478 }
479 None
480 }
481}
482
483#[cfg(test)]
484#[path = "parf_tests.rs"]
485mod tests;