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 feature_path_to_name_map
129 .get(target_feature_path_str)
130 .is_some()
131 {
132 let target_path = PathBuf::from(target_feature_path_str);
133 let full_target_path = base_path.join(&target_path);
134 let full_source_path = base_path.join(feature_path);
135
136 let dependency_type =
138 determine_dependency_type(&full_source_path, &full_target_path);
139
140 let relative_source_filename = if let Ok(canonical_base) =
142 base_path.canonicalize()
143 {
144 let source_path = Path::new(&import.file_path);
145 if let Ok(canonical_source) = source_path.canonicalize() {
146 if let Ok(rel_path) = canonical_source.strip_prefix(&canonical_base) {
147 rel_path.to_string_lossy().to_string()
148 } else {
149 import.file_path.clone()
150 }
151 } else {
152 import.file_path.clone()
153 }
154 } else {
155 import.file_path.clone()
156 };
157
158 let relative_target_filename =
160 if let Ok(canonical_base) = base_path.canonicalize() {
161 if let Ok(rel_path) = resolved_path.strip_prefix(&canonical_base) {
162 rel_path.to_string_lossy().to_string()
163 } else {
164 resolved_path.to_string_lossy().to_string()
165 }
166 } else {
167 resolved_path.to_string_lossy().to_string()
168 };
169
170 dependencies.push(Dependency {
172 source_filename: relative_source_filename,
173 target_filename: relative_target_filename,
174 line: import.line_number,
175 content: import.line_content.clone(),
176 feature_path: target_feature_path_str.to_string(),
177 dependency_type,
178 });
179 }
180 }
181 }
182 }
183
184 dependencies
185}
186
187pub fn collect_feature_info(
189 features: &[crate::models::Feature],
190 _parent_path: Option<&Path>,
191 result: &mut Vec<FeatureInfo>,
192) {
193 for feature in features {
194 let feature_path = PathBuf::from(&feature.path);
196
197 result.push(FeatureInfo {
198 name: feature.name.clone(),
199 path: feature_path.clone(),
200 });
201
202 if !feature.features.is_empty() {
204 collect_feature_info(&feature.features, None, result);
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_determine_dependency_type_child() {
215 let source = Path::new("/project/features/parent");
216 let target = Path::new("/project/features/parent/child");
217
218 assert!(matches!(
219 determine_dependency_type(source, target),
220 DependencyType::Child
221 ));
222 }
223
224 #[test]
225 fn test_determine_dependency_type_parent() {
226 let source = Path::new("/project/features/parent/child");
227 let target = Path::new("/project/features/parent");
228
229 assert!(matches!(
230 determine_dependency_type(source, target),
231 DependencyType::Parent
232 ));
233 }
234
235 #[test]
236 fn test_determine_dependency_type_sibling() {
237 let source = Path::new("/project/features/feature-a");
238 let target = Path::new("/project/features/feature-b");
239
240 assert!(matches!(
241 determine_dependency_type(source, target),
242 DependencyType::Sibling
243 ));
244 }
245}