1pub mod context;
2pub mod dependencies;
3pub mod extractors;
4pub mod flow;
5pub mod parser;
6
7use context::{AnalysisContext, ParsedFile};
8use pecto_core::model::ProjectSpec;
9use rayon::prelude::*;
10use std::path::Path;
11
12pub fn analyze_project(path: &Path) -> Result<ProjectSpec, PythonAnalysisError> {
14 let project_name = path
15 .file_name()
16 .map(|n| n.to_string_lossy().to_string())
17 .unwrap_or_else(|| "unknown".to_string());
18
19 let py_files: Vec<walkdir::DirEntry> = walkdir::WalkDir::new(path)
20 .into_iter()
21 .filter_map(|e| e.ok())
22 .filter(|e| {
23 let p = e.path().to_string_lossy();
24 e.path().extension().is_some_and(|ext| ext == "py")
25 && !p.contains("__pycache__")
26 && !p.contains("/venv/")
27 && !p.contains("/.venv/")
28 && !p.contains("/env/")
29 && !p.contains("/node_modules/")
30 && !p.contains("/migrations/")
31 && !p.contains("/test/")
32 && !p.contains("/tests/")
33 })
34 .collect();
35
36 let parsed_results: Vec<Result<ParsedFile, PythonAnalysisError>> = py_files
37 .par_iter()
38 .map(|entry| {
39 let source = std::fs::read_to_string(entry.path()).map_err(|e| {
40 PythonAnalysisError::IoError(entry.path().to_string_lossy().to_string(), e)
41 })?;
42
43 let relative_path = entry
44 .path()
45 .strip_prefix(path)
46 .unwrap_or(entry.path())
47 .to_string_lossy()
48 .to_string();
49
50 ParsedFile::parse(source, relative_path)
51 })
52 .collect();
53
54 let mut parsed_files = Vec::with_capacity(parsed_results.len());
55 for result in parsed_results {
56 parsed_files.push(result?);
57 }
58
59 let files_analyzed = parsed_files.len();
60 let ctx = AnalysisContext::new(parsed_files);
61
62 let mut spec = ProjectSpec::new(project_name);
63 spec.files_analyzed = files_analyzed;
64
65 for file in &ctx.files {
66 if let Some(capability) = extractors::controller::extract(file)
67 && !capability.is_empty()
68 {
69 spec.capabilities.push(capability);
70 }
71
72 if let Some(capability) = extractors::entity::extract(file)
73 && !capability.is_empty()
74 {
75 spec.capabilities.push(capability);
76 }
77
78 if let Some(capability) = extractors::service::extract(file)
79 && !capability.is_empty()
80 {
81 spec.capabilities.push(capability);
82 }
83
84 if let Some(capability) = extractors::scheduled::extract(file)
85 && !capability.is_empty()
86 {
87 spec.capabilities.push(capability);
88 }
89 }
90
91 dependencies::resolve_dependencies(&mut spec, &ctx);
92 flow::extract_flows(&mut spec, &ctx);
93
94 Ok(spec)
95}
96
97#[derive(Debug, thiserror::Error)]
98pub enum PythonAnalysisError {
99 #[error("Failed to read {0}: {1}")]
100 IoError(String, std::io::Error),
101 #[error("Parse error in {0}: {1}")]
102 ParseError(String, String),
103}