features_cli/
dependency_resolver.rs1use crate::import_detector::{ImportStatement, resolve_import_path};
7use crate::models::{Dependency, DependencyType};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
13pub struct FeatureInfo {
14 pub name: String,
15 pub path: PathBuf,
16}
17
18pub fn build_file_to_feature_map(
20 features: &[FeatureInfo],
21 base_path: &Path,
22) -> HashMap<PathBuf, String> {
23 let mut map = HashMap::new();
24
25 let mut sorted_features = features.to_vec();
28 sorted_features.sort_by(|a, b| {
29 b.path
30 .to_string_lossy()
31 .len()
32 .cmp(&a.path.to_string_lossy().len())
33 });
34
35 for feature in sorted_features {
36 let feature_path = base_path.join(&feature.path);
37
38 if std::fs::read_dir(&feature_path).is_ok() {
40 map_directory_files(&feature_path, &feature.path.to_string_lossy(), &mut map);
41 }
42 }
43
44 map
45}
46
47fn map_directory_files(dir: &Path, feature_path: &str, map: &mut HashMap<PathBuf, String>) {
50 if let Ok(entries) = std::fs::read_dir(dir) {
51 for entry in entries.flatten() {
52 let path = entry.path();
53
54 if path.is_file() {
55 let canonical_path = if let Ok(canonical) = path.canonicalize() {
57 canonical
58 } else {
59 path.clone()
60 };
61
62 map.entry(canonical_path)
64 .or_insert_with(|| feature_path.to_string());
65 } else if path.is_dir()
66 && let Some(dir_name) = path.file_name().and_then(|n| n.to_str())
67 && !should_skip_directory(dir_name)
68 && !is_nested_feature_directory(&path)
69 {
70 map_directory_files(&path, feature_path, map);
71 }
72 }
73 }
74}
75
76fn is_nested_feature_directory(dir: &Path) -> bool {
81 if dir.join("features.toml").exists() {
83 return true;
84 }
85
86 if let Some(parent) = dir.parent()
88 && let Some(parent_name) = parent.file_name()
89 && parent_name == "features"
90 {
91 return true;
92 }
93
94 false
95}
96
97fn should_skip_directory(dir_name: &str) -> bool {
99 matches!(
100 dir_name,
101 "node_modules" | "target" | "dist" | "build" | ".git" | "__pycache__" | "coverage"
102 )
103}
104
105pub fn determine_dependency_type(
107 source_feature_path: &Path,
108 target_feature_path: &Path,
109) -> DependencyType {
110 if target_feature_path.starts_with(source_feature_path) {
112 return DependencyType::Child;
113 }
114
115 if source_feature_path.starts_with(target_feature_path) {
117 return DependencyType::Parent;
118 }
119
120 DependencyType::Sibling
122}
123
124pub fn resolve_feature_dependencies(
126 _feature_name: &str,
127 feature_path: &Path,
128 base_path: &Path,
129 imports: &[ImportStatement],
130 file_to_feature_map: &HashMap<PathBuf, String>,
131 feature_path_to_name_map: &HashMap<String, String>,
132 file_map: &HashMap<String, PathBuf>,
133) -> Vec<Dependency> {
134 let mut dependencies = Vec::new();
135 let mut seen = std::collections::HashSet::new();
136
137 for import in imports {
138 let source_file = Path::new(&import.file_path);
139
140 if let Some(resolved_path) =
142 resolve_import_path(&import.imported_path, source_file, base_path, file_map)
143 {
144 if let Some(target_feature_path_str) = file_to_feature_map.get(&resolved_path) {
146 if target_feature_path_str == feature_path.to_string_lossy().as_ref() {
148 continue;
149 }
150
151 let dep_key = format!(
153 "{}:{}:{}",
154 resolved_path.display(),
155 import.line_number,
156 target_feature_path_str
157 );
158
159 if seen.contains(&dep_key) {
160 continue;
161 }
162 seen.insert(dep_key);
163
164 if feature_path_to_name_map
166 .get(target_feature_path_str)
167 .is_some()
168 {
169 let target_path = PathBuf::from(target_feature_path_str);
170 let full_target_path = base_path.join(&target_path);
171 let full_source_path = base_path.join(feature_path);
172
173 let dependency_type =
175 determine_dependency_type(&full_source_path, &full_target_path);
176
177 let relative_source_filename = if let Ok(canonical_base) =
179 base_path.canonicalize()
180 {
181 let source_path = Path::new(&import.file_path);
182 if let Ok(canonical_source) = source_path.canonicalize() {
183 if let Ok(rel_path) = canonical_source.strip_prefix(&canonical_base) {
184 rel_path.to_string_lossy().to_string()
185 } else {
186 import.file_path.clone()
187 }
188 } else {
189 import.file_path.clone()
190 }
191 } else {
192 import.file_path.clone()
193 };
194
195 let relative_target_filename =
197 if let Ok(canonical_base) = base_path.canonicalize() {
198 if let Ok(rel_path) = resolved_path.strip_prefix(&canonical_base) {
199 rel_path.to_string_lossy().to_string()
200 } else {
201 resolved_path.to_string_lossy().to_string()
202 }
203 } else {
204 resolved_path.to_string_lossy().to_string()
205 };
206
207 dependencies.push(Dependency {
209 source_filename: relative_source_filename,
210 target_filename: relative_target_filename,
211 line: import.line_number,
212 content: import.line_content.clone(),
213 feature_path: target_feature_path_str.to_string(),
214 dependency_type,
215 });
216 }
217 }
218 }
219 }
220
221 dependencies
222}
223
224pub fn collect_feature_info(
226 features: &[crate::models::Feature],
227 _parent_path: Option<&Path>,
228 result: &mut Vec<FeatureInfo>,
229) {
230 for feature in features {
231 let feature_path = PathBuf::from(&feature.path);
233
234 result.push(FeatureInfo {
235 name: feature.name.clone(),
236 path: feature_path.clone(),
237 });
238
239 if !feature.features.is_empty() {
241 collect_feature_info(&feature.features, None, result);
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_determine_dependency_type_child() {
252 let source = Path::new("/project/features/parent");
253 let target = Path::new("/project/features/parent/child");
254
255 assert!(matches!(
256 determine_dependency_type(source, target),
257 DependencyType::Child
258 ));
259 }
260
261 #[test]
262 fn test_determine_dependency_type_parent() {
263 let source = Path::new("/project/features/parent/child");
264 let target = Path::new("/project/features/parent");
265
266 assert!(matches!(
267 determine_dependency_type(source, target),
268 DependencyType::Parent
269 ));
270 }
271
272 #[test]
273 fn test_determine_dependency_type_sibling() {
274 let source = Path::new("/project/features/feature-a");
275 let target = Path::new("/project/features/feature-b");
276
277 assert!(matches!(
278 determine_dependency_type(source, target),
279 DependencyType::Sibling
280 ));
281 }
282}