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.name, &mut map);
31 }
32 }
33
34 map
35}
36
37fn map_directory_files(dir: &Path, feature_name: &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_name.to_string());
47 } else {
48 map.insert(path.clone(), feature_name.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_name, 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_info_map: &HashMap<String, PathBuf>,
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_name) = file_to_feature_map.get(&resolved_path) {
109 if target_feature_name == feature_name {
111 continue;
112 }
113
114 let dep_key = format!(
116 "{}:{}:{}",
117 resolved_path.display(),
118 import.line_number,
119 target_feature_name
120 );
121
122 if seen.contains(&dep_key) {
123 continue;
124 }
125 seen.insert(dep_key);
126
127 if let Some(target_feature_path) = feature_info_map.get(target_feature_name) {
129 let full_target_path = base_path.join(target_feature_path);
130 let full_source_path = base_path.join(feature_path);
131
132 let dependency_type =
134 determine_dependency_type(&full_source_path, &full_target_path);
135
136 let relative_filename = if let Ok(canonical_base) = base_path.canonicalize() {
138 if let Ok(rel_path) = resolved_path.strip_prefix(&canonical_base) {
139 rel_path.to_string_lossy().to_string()
140 } else {
141 resolved_path.to_string_lossy().to_string()
142 }
143 } else {
144 resolved_path.to_string_lossy().to_string()
145 };
146
147 dependencies.push(Dependency {
149 filename: relative_filename,
150 line: import.line_number,
151 content: import.line_content.clone(),
152 feature: target_feature_name.clone(),
153 dependency_type,
154 });
155 }
156 }
157 }
158 }
159
160 dependencies
161}
162
163pub fn collect_feature_info(
165 features: &[crate::models::Feature],
166 _parent_path: Option<&Path>,
167 result: &mut Vec<FeatureInfo>,
168) {
169 for feature in features {
170 let feature_path = PathBuf::from(&feature.path);
172
173 result.push(FeatureInfo {
174 name: feature.name.clone(),
175 path: feature_path.clone(),
176 });
177
178 if !feature.features.is_empty() {
180 collect_feature_info(&feature.features, None, result);
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_determine_dependency_type_child() {
191 let source = Path::new("/project/features/parent");
192 let target = Path::new("/project/features/parent/child");
193
194 assert!(matches!(
195 determine_dependency_type(source, target),
196 DependencyType::Child
197 ));
198 }
199
200 #[test]
201 fn test_determine_dependency_type_parent() {
202 let source = Path::new("/project/features/parent/child");
203 let target = Path::new("/project/features/parent");
204
205 assert!(matches!(
206 determine_dependency_type(source, target),
207 DependencyType::Parent
208 ));
209 }
210
211 #[test]
212 fn test_determine_dependency_type_sibling() {
213 let source = Path::new("/project/features/feature-a");
214 let target = Path::new("/project/features/feature-b");
215
216 assert!(matches!(
217 determine_dependency_type(source, target),
218 DependencyType::Sibling
219 ));
220 }
221}