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
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    for feature in features {
26        let feature_path = base_path.join(&feature.path);
27
28        // Map all files within this feature directory
29        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
37/// Recursively map all files in a directory to a feature name
38fn 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                // Canonicalize the path to resolve .. and .
45                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
60/// Check if a directory should be skipped
61fn should_skip_directory(dir_name: &str) -> bool {
62    matches!(
63        dir_name,
64        "node_modules" | "target" | "dist" | "build" | ".git" | "__pycache__" | "coverage"
65    )
66}
67
68/// Determine the relationship type between two features based on their paths
69pub fn determine_dependency_type(
70    source_feature_path: &Path,
71    target_feature_path: &Path,
72) -> DependencyType {
73    // Check if target is a child (descendant) of source
74    if target_feature_path.starts_with(source_feature_path) {
75        return DependencyType::Child;
76    }
77
78    // Check if source is a child (descendant) of target
79    if source_feature_path.starts_with(target_feature_path) {
80        return DependencyType::Parent;
81    }
82
83    // Otherwise, they're siblings
84    DependencyType::Sibling
85}
86
87/// Resolve imports to dependencies for a specific feature
88pub 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        // Resolve the import to an actual file path
104        if let Some(resolved_path) =
105            resolve_import_path(&import.imported_path, source_file, base_path, file_map)
106        {
107            // Find which feature this file belongs to
108            if let Some(target_feature_name) = file_to_feature_map.get(&resolved_path) {
109                // Skip if it's the same feature
110                if target_feature_name == feature_name {
111                    continue;
112                }
113
114                // Create a unique key to avoid duplicates
115                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                // Get the target feature path
128                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                    // Determine the dependency type
133                    let dependency_type =
134                        determine_dependency_type(&full_source_path, &full_target_path);
135
136                    // Convert resolved_path to be relative to base_path
137                    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                    // Create dependency
148                    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
163/// Collect all feature information recursively
164pub 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        // Feature paths are already relative to base, not to parent
171        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        // Recursively collect nested features (don't pass parent_path since paths are base-relative)
179        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}