Skip to main content

features_cli/
dependency_resolver.rs

1//! Module for resolving dependencies between features
2//!
3//! This module takes import statements and determines which features they belong to,
4//! and what type of relationship exists between features (parent, child, sibling).
5
6use crate::import_detector::{ImportStatement, resolve_import_path};
7use crate::models::{Dependency, DependencyType};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11/// Represents a feature with its path for dependency resolution
12#[derive(Debug, Clone)]
13pub struct FeatureInfo {
14    pub name: String,
15    pub path: PathBuf,
16}
17
18/// Build a map of file paths to their containing features (using feature path as identifier)
19pub 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    // Sort features by path length (longest first) to ensure more specific features
26    // take precedence over parent features when mapping files
27    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        // Map all files within this feature directory using the feature's path as identifier
39        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
47/// Recursively map all files in a directory to a feature path
48/// Only maps files that haven't been mapped yet (most specific feature wins)
49fn 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                // Canonicalize the path to resolve .. and .
56                let canonical_path = if let Ok(canonical) = path.canonicalize() {
57                    canonical
58                } else {
59                    path.clone()
60                };
61
62                // Only insert if not already mapped (most specific feature takes precedence)
63                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
76/// Check if a directory is a nested feature directory
77/// A directory is considered a feature if:
78/// 1. It's a direct child of a "features" directory, OR
79/// 2. It has a features.toml file
80fn is_nested_feature_directory(dir: &Path) -> bool {
81    // Check for features.toml
82    if dir.join("features.toml").exists() {
83        return true;
84    }
85
86    // Check if it's a direct child of a "features" directory
87    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
97/// Check if a directory should be skipped
98fn should_skip_directory(dir_name: &str) -> bool {
99    matches!(
100        dir_name,
101        "node_modules" | "target" | "dist" | "build" | ".git" | "__pycache__" | "coverage"
102    )
103}
104
105/// Determine the relationship type between two features based on their paths
106pub fn determine_dependency_type(
107    source_feature_path: &Path,
108    target_feature_path: &Path,
109) -> DependencyType {
110    // Check if target is a child (descendant) of source
111    if target_feature_path.starts_with(source_feature_path) {
112        return DependencyType::Child;
113    }
114
115    // Check if source is a child (descendant) of target
116    if source_feature_path.starts_with(target_feature_path) {
117        return DependencyType::Parent;
118    }
119
120    // Otherwise, they're siblings
121    DependencyType::Sibling
122}
123
124/// Resolve imports to dependencies for a specific feature
125pub 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        // Resolve the import to an actual file path
141        if let Some(resolved_path) =
142            resolve_import_path(&import.imported_path, source_file, base_path, file_map)
143        {
144            // Find which feature this file belongs to (returns feature path)
145            if let Some(target_feature_path_str) = file_to_feature_map.get(&resolved_path) {
146                // Skip if it's the same feature
147                if target_feature_path_str == feature_path.to_string_lossy().as_ref() {
148                    continue;
149                }
150
151                // Create a unique key to avoid duplicates
152                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                // Get the target feature name from the path (for validation)
165                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                    // Determine the dependency type
174                    let dependency_type =
175                        determine_dependency_type(&full_source_path, &full_target_path);
176
177                    // Convert source file path to be relative to base_path
178                    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                    // Convert target file path to be relative to base_path
196                    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                    // Create dependency
208                    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
224/// Collect all feature information recursively
225pub 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        // Feature paths are already relative to base, not to parent
232        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        // Recursively collect nested features (don't pass parent_path since paths are base-relative)
240        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}