pecto_python/
dependencies.rs1use crate::context::AnalysisContext;
2use crate::extractors::common::*;
3use pecto_core::model::*;
4
5pub fn resolve_dependencies(spec: &mut ProjectSpec, ctx: &AnalysisContext) {
7 for file in &ctx.files {
8 let root = file.tree.root_node();
9 let source = file.source.as_bytes();
10
11 let Some(from_cap) = spec
13 .capabilities
14 .iter()
15 .find(|c| c.source == file.path)
16 .map(|c| c.name.clone())
17 else {
18 continue;
19 };
20
21 for i in 0..root.named_child_count() {
23 let node = root.named_child(i).unwrap();
24
25 match node.kind() {
26 "import_from_statement" => {
28 let module = node
29 .child_by_field_name("module_name")
30 .map(|m| node_text(&m, source))
31 .unwrap_or_default();
32
33 if module.is_empty() {
34 continue;
35 }
36
37 if let Some(to_cap) =
39 resolve_module_to_capability(&module, &from_cap, &spec.capabilities)
40 {
41 let mut imported_names = Vec::new();
43 for j in 0..node.named_child_count() {
44 let child = node.named_child(j).unwrap();
45 if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
46 imported_names.push(node_text(&child, source));
47 }
48 }
49
50 let reference = format!(
51 "from {} import {}",
52 module,
53 if imported_names.is_empty() {
54 "*".to_string()
55 } else {
56 imported_names.join(", ")
57 }
58 );
59
60 add_dependency_edge(
61 &mut spec.dependencies,
62 &from_cap,
63 &to_cap,
64 infer_dependency_kind(&to_cap, &module),
65 reference,
66 );
67 }
68 }
69 "import_statement" => {
71 let text = node_text(&node, source);
72 let module = text.trim_start_matches("import ").trim();
73
74 if let Some(to_cap) =
75 resolve_module_to_capability(module, &from_cap, &spec.capabilities)
76 {
77 add_dependency_edge(
78 &mut spec.dependencies,
79 &from_cap,
80 &to_cap,
81 infer_dependency_kind(&to_cap, module),
82 text,
83 );
84 }
85 }
86 _ => {}
87 }
88 }
89 }
90}
91
92fn resolve_module_to_capability(
95 module: &str,
96 from_cap: &str,
97 capabilities: &[Capability],
98) -> Option<String> {
99 let module_as_path = module.replace('.', "/");
102
103 for cap in capabilities {
104 if cap.name == from_cap {
105 continue; }
107
108 let source_no_ext = cap.source.trim_end_matches(".py");
111
112 if source_no_ext.ends_with(&module_as_path)
113 || source_no_ext.contains(&module_as_path)
114 || module_path_matches_source(module, &cap.source)
115 {
116 return Some(cap.name.clone());
117 }
118 }
119
120 let module_segments: Vec<&str> = module.split('.').collect();
123 let last_segment = module_segments.last().unwrap_or(&"");
124
125 for cap in capabilities {
126 if cap.name == from_cap {
127 continue;
128 }
129
130 let cap_source_segments: Vec<&str> = cap.source.split('/').collect();
132 let cap_file = cap_source_segments
133 .last()
134 .unwrap_or(&"")
135 .trim_end_matches(".py");
136
137 if cap_file == *last_segment || cap.name == *last_segment || cap.name.contains(last_segment)
138 {
139 return Some(cap.name.clone());
140 }
141 }
142
143 None
144}
145
146fn module_path_matches_source(module: &str, source: &str) -> bool {
148 let source_parts: Vec<&str> = source.trim_end_matches(".py").split('/').collect();
149 let module_parts: Vec<&str> = module.split('.').collect();
150
151 if module_parts.len() > source_parts.len() {
152 return false;
153 }
154
155 let offset = source_parts.len() - module_parts.len();
157 module_parts
158 .iter()
159 .zip(source_parts[offset..].iter())
160 .all(|(m, s)| m == s)
161}
162
163fn infer_dependency_kind(target: &str, module: &str) -> DependencyKind {
164 if target.contains("model") || module.contains("model") {
165 DependencyKind::Queries
166 } else if target.contains("event") || module.contains("event") || module.contains("task") {
167 DependencyKind::Listens
168 } else {
169 DependencyKind::Calls
170 }
171}
172
173fn add_dependency_edge(
174 deps: &mut Vec<DependencyEdge>,
175 from: &str,
176 to: &str,
177 kind: DependencyKind,
178 reference: String,
179) {
180 if let Some(existing) = deps.iter_mut().find(|d| d.from == from && d.to == to) {
181 if !existing.references.contains(&reference) {
182 existing.references.push(reference);
183 }
184 } else {
185 deps.push(DependencyEdge {
186 from: from.to_string(),
187 to: to.to_string(),
188 kind,
189 references: vec![reference],
190 });
191 }
192}