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 for feature in features {
26 let feature_path = base_path.join(&feature.path);
27
28 if std::fs::read_dir(&feature_path).is_ok() {
30 map_directory_files(&feature_path, &feature.path.to_string_lossy(), &mut map);
31 }
32 }
33
34 map
35}
36
37fn map_directory_files(dir: &Path, feature_path: &str, map: &mut HashMap<PathBuf, String>) {
39 if let Ok(entries) = std::fs::read_dir(dir) {
40 for entry in entries.flatten() {
41 let path = entry.path();
42
43 if path.is_file() {
44 if let Ok(canonical_path) = path.canonicalize() {
46 map.insert(canonical_path, feature_path.to_string());
47 } else {
48 map.insert(path.clone(), feature_path.to_string());
49 }
50 } else if path.is_dir()
51 && let Some(dir_name) = path.file_name().and_then(|n| n.to_str())
52 && !should_skip_directory(dir_name)
53 {
54 map_directory_files(&path, feature_path, map);
55 }
56 }
57 }
58}
59
60fn should_skip_directory(dir_name: &str) -> bool {
62 matches!(
63 dir_name,
64 "node_modules" | "target" | "dist" | "build" | ".git" | "__pycache__" | "coverage"
65 )
66}
67
68pub fn determine_dependency_type(
70 source_feature_path: &Path,
71 target_feature_path: &Path,
72) -> DependencyType {
73 if target_feature_path.starts_with(source_feature_path) {
75 return DependencyType::Child;
76 }
77
78 if source_feature_path.starts_with(target_feature_path) {
80 return DependencyType::Parent;
81 }
82
83 DependencyType::Sibling
85}
86
87pub fn resolve_feature_dependencies(
89 _feature_name: &str,
90 feature_path: &Path,
91 base_path: &Path,
92 imports: &[ImportStatement],
93 file_to_feature_map: &HashMap<PathBuf, String>,
94 feature_path_to_name_map: &HashMap<String, String>,
95 file_map: &HashMap<String, PathBuf>,
96) -> Vec<Dependency> {
97 let mut dependencies = Vec::new();
98 let mut seen = std::collections::HashSet::new();
99
100 for import in imports {
101 let source_file = Path::new(&import.file_path);
102
103 if let Some(resolved_path) =
105 resolve_import_path(&import.imported_path, source_file, base_path, file_map)
106 {
107 if let Some(target_feature_path_str) = file_to_feature_map.get(&resolved_path) {
109 if target_feature_path_str == feature_path.to_string_lossy().as_ref() {
111 continue;
112 }
113
114 let dep_key = format!(
116 "{}:{}:{}",
117 resolved_path.display(),
118 import.line_number,
119 target_feature_path_str
120 );
121
122 if seen.contains(&dep_key) {
123 continue;
124 }
125 seen.insert(dep_key);
126
127 if let Some(target_feature_name) =
129 feature_path_to_name_map.get(target_feature_path_str)
130 {
131 let target_path = PathBuf::from(target_feature_path_str);
132 let full_target_path = base_path.join(&target_path);
133 let full_source_path = base_path.join(feature_path);
134
135 let dependency_type =
137 determine_dependency_type(&full_source_path, &full_target_path);
138
139 let relative_source_filename = if let Ok(canonical_base) =
141 base_path.canonicalize()
142 {
143 let source_path = Path::new(&import.file_path);
144 if let Ok(canonical_source) = source_path.canonicalize() {
145 if let Ok(rel_path) = canonical_source.strip_prefix(&canonical_base) {
146 rel_path.to_string_lossy().to_string()
147 } else {
148 import.file_path.clone()
149 }
150 } else {
151 import.file_path.clone()
152 }
153 } else {
154 import.file_path.clone()
155 };
156
157 let relative_target_filename =
159 if let Ok(canonical_base) = base_path.canonicalize() {
160 if let Ok(rel_path) = resolved_path.strip_prefix(&canonical_base) {
161 rel_path.to_string_lossy().to_string()
162 } else {
163 resolved_path.to_string_lossy().to_string()
164 }
165 } else {
166 resolved_path.to_string_lossy().to_string()
167 };
168
169 dependencies.push(Dependency {
171 source_filename: relative_source_filename,
172 target_filename: relative_target_filename,
173 line: import.line_number,
174 content: import.line_content.clone(),
175 feature: target_feature_name.clone(),
176 dependency_type,
177 });
178 }
179 }
180 }
181 }
182
183 dependencies
184}
185
186pub fn collect_feature_info(
188 features: &[crate::models::Feature],
189 _parent_path: Option<&Path>,
190 result: &mut Vec<FeatureInfo>,
191) {
192 for feature in features {
193 let feature_path = PathBuf::from(&feature.path);
195
196 result.push(FeatureInfo {
197 name: feature.name.clone(),
198 path: feature_path.clone(),
199 });
200
201 if !feature.features.is_empty() {
203 collect_feature_info(&feature.features, None, result);
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_determine_dependency_type_child() {
214 let source = Path::new("/project/features/parent");
215 let target = Path::new("/project/features/parent/child");
216
217 assert!(matches!(
218 determine_dependency_type(source, target),
219 DependencyType::Child
220 ));
221 }
222
223 #[test]
224 fn test_determine_dependency_type_parent() {
225 let source = Path::new("/project/features/parent/child");
226 let target = Path::new("/project/features/parent");
227
228 assert!(matches!(
229 determine_dependency_type(source, target),
230 DependencyType::Parent
231 ));
232 }
233
234 #[test]
235 fn test_determine_dependency_type_sibling() {
236 let source = Path::new("/project/features/feature-a");
237 let target = Path::new("/project/features/feature-b");
238
239 assert!(matches!(
240 determine_dependency_type(source, target),
241 DependencyType::Sibling
242 ));
243 }
244}