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>) {
49 if let Ok(entries) = std::fs::read_dir(dir) {
50 for entry in entries.flatten() {
51 let path = entry.path();
52
53 if path.is_file() {
54 if let Ok(canonical_path) = path.canonicalize() {
56 map.insert(canonical_path, feature_path.to_string());
57 } else {
58 map.insert(path.clone(), feature_path.to_string());
59 }
60 } else if path.is_dir()
61 && let Some(dir_name) = path.file_name().and_then(|n| n.to_str())
62 && !should_skip_directory(dir_name)
63 && !is_nested_feature_directory(&path)
64 {
65 map_directory_files(&path, feature_path, map);
66 }
67 }
68 }
69}
70
71fn is_nested_feature_directory(dir: &Path) -> bool {
76 if dir.join("features.toml").exists() {
78 return true;
79 }
80
81 if let Some(parent) = dir.parent()
83 && let Some(parent_name) = parent.file_name()
84 && parent_name == "features"
85 {
86 return true;
87 }
88
89 false
90}
91
92fn should_skip_directory(dir_name: &str) -> bool {
94 matches!(
95 dir_name,
96 "node_modules" | "target" | "dist" | "build" | ".git" | "__pycache__" | "coverage"
97 )
98}
99
100pub fn determine_dependency_type(
102 source_feature_path: &Path,
103 target_feature_path: &Path,
104) -> DependencyType {
105 if target_feature_path.starts_with(source_feature_path) {
107 return DependencyType::Child;
108 }
109
110 if source_feature_path.starts_with(target_feature_path) {
112 return DependencyType::Parent;
113 }
114
115 DependencyType::Sibling
117}
118
119pub fn resolve_feature_dependencies(
121 _feature_name: &str,
122 feature_path: &Path,
123 base_path: &Path,
124 imports: &[ImportStatement],
125 file_to_feature_map: &HashMap<PathBuf, String>,
126 feature_path_to_name_map: &HashMap<String, String>,
127 file_map: &HashMap<String, PathBuf>,
128) -> Vec<Dependency> {
129 let mut dependencies = Vec::new();
130 let mut seen = std::collections::HashSet::new();
131
132 for import in imports {
133 let source_file = Path::new(&import.file_path);
134
135 if let Some(resolved_path) =
137 resolve_import_path(&import.imported_path, source_file, base_path, file_map)
138 {
139 if let Some(target_feature_path_str) = file_to_feature_map.get(&resolved_path) {
141 if target_feature_path_str == feature_path.to_string_lossy().as_ref() {
143 continue;
144 }
145
146 let dep_key = format!(
148 "{}:{}:{}",
149 resolved_path.display(),
150 import.line_number,
151 target_feature_path_str
152 );
153
154 if seen.contains(&dep_key) {
155 continue;
156 }
157 seen.insert(dep_key);
158
159 if feature_path_to_name_map
161 .get(target_feature_path_str)
162 .is_some()
163 {
164 let target_path = PathBuf::from(target_feature_path_str);
165 let full_target_path = base_path.join(&target_path);
166 let full_source_path = base_path.join(feature_path);
167
168 let dependency_type =
170 determine_dependency_type(&full_source_path, &full_target_path);
171
172 let relative_source_filename = if let Ok(canonical_base) =
174 base_path.canonicalize()
175 {
176 let source_path = Path::new(&import.file_path);
177 if let Ok(canonical_source) = source_path.canonicalize() {
178 if let Ok(rel_path) = canonical_source.strip_prefix(&canonical_base) {
179 rel_path.to_string_lossy().to_string()
180 } else {
181 import.file_path.clone()
182 }
183 } else {
184 import.file_path.clone()
185 }
186 } else {
187 import.file_path.clone()
188 };
189
190 let relative_target_filename =
192 if let Ok(canonical_base) = base_path.canonicalize() {
193 if let Ok(rel_path) = resolved_path.strip_prefix(&canonical_base) {
194 rel_path.to_string_lossy().to_string()
195 } else {
196 resolved_path.to_string_lossy().to_string()
197 }
198 } else {
199 resolved_path.to_string_lossy().to_string()
200 };
201
202 dependencies.push(Dependency {
204 source_filename: relative_source_filename,
205 target_filename: relative_target_filename,
206 line: import.line_number,
207 content: import.line_content.clone(),
208 feature_path: target_feature_path_str.to_string(),
209 dependency_type,
210 });
211 }
212 }
213 }
214 }
215
216 dependencies
217}
218
219pub fn collect_feature_info(
221 features: &[crate::models::Feature],
222 _parent_path: Option<&Path>,
223 result: &mut Vec<FeatureInfo>,
224) {
225 for feature in features {
226 let feature_path = PathBuf::from(&feature.path);
228
229 result.push(FeatureInfo {
230 name: feature.name.clone(),
231 path: feature_path.clone(),
232 });
233
234 if !feature.features.is_empty() {
236 collect_feature_info(&feature.features, None, result);
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_determine_dependency_type_child() {
247 let source = Path::new("/project/features/parent");
248 let target = Path::new("/project/features/parent/child");
249
250 assert!(matches!(
251 determine_dependency_type(source, target),
252 DependencyType::Child
253 ));
254 }
255
256 #[test]
257 fn test_determine_dependency_type_parent() {
258 let source = Path::new("/project/features/parent/child");
259 let target = Path::new("/project/features/parent");
260
261 assert!(matches!(
262 determine_dependency_type(source, target),
263 DependencyType::Parent
264 ));
265 }
266
267 #[test]
268 fn test_determine_dependency_type_sibling() {
269 let source = Path::new("/project/features/feature-a");
270 let target = Path::new("/project/features/feature-b");
271
272 assert!(matches!(
273 determine_dependency_type(source, target),
274 DependencyType::Sibling
275 ));
276 }
277}