Skip to main content

oracle_lib/analyzer/
dependency.rs

1//! Dependency analysis using cargo_metadata
2
3use crate::error::Result;
4use cargo_metadata::{DependencyKind as CargoDependencyKind, MetadataCommand, Package};
5use petgraph::graph::{DiGraph, NodeIndex};
6use std::collections::HashMap;
7use std::path::Path;
8
9/// Analyzer for crate dependencies using cargo_metadata
10pub struct DependencyAnalyzer {
11    metadata: cargo_metadata::Metadata,
12    graph: DiGraph<String, ()>,
13    node_map: HashMap<String, NodeIndex>,
14}
15
16/// Information about a crate
17#[derive(Debug, Clone)]
18pub struct CrateInfo {
19    pub name: String,
20    pub version: String,
21    pub authors: Vec<String>,
22    pub license: Option<String>,
23    pub description: Option<String>,
24    pub homepage: Option<String>,
25    pub repository: Option<String>,
26    pub documentation: Option<String>,
27    pub dependencies: Vec<DependencyInfo>,
28    pub features: Vec<String>,
29    pub default_features: Vec<String>,
30    pub edition: String,
31    pub rust_version: Option<String>,
32}
33
34/// Information about a dependency
35#[derive(Debug, Clone)]
36pub struct DependencyInfo {
37    pub name: String,
38    pub version: String,
39    pub optional: bool,
40    pub features: Vec<String>,
41    pub kind: DependencyKind,
42}
43
44/// Kind of dependency
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum DependencyKind {
47    Normal,
48    Dev,
49    Build,
50}
51
52impl std::fmt::Display for DependencyKind {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            DependencyKind::Normal => write!(f, "normal"),
56            DependencyKind::Dev => write!(f, "dev"),
57            DependencyKind::Build => write!(f, "build"),
58        }
59    }
60}
61
62impl DependencyAnalyzer {
63    /// Create a new dependency analyzer from a Cargo.toml path
64    pub fn from_manifest(manifest_path: &Path) -> Result<Self> {
65        let metadata = MetadataCommand::new().manifest_path(manifest_path).exec()?;
66        Ok(Self::from_metadata(metadata))
67    }
68
69    /// Create a new dependency analyzer from the current directory
70    pub fn from_current_dir() -> Result<Self> {
71        let metadata = MetadataCommand::new().exec()?;
72        Ok(Self::from_metadata(metadata))
73    }
74
75    fn from_metadata(metadata: cargo_metadata::Metadata) -> Self {
76        let mut graph = DiGraph::new();
77        let mut node_map = HashMap::new();
78
79        // Build dependency graph
80        for package in &metadata.packages {
81            let node = graph.add_node(package.name.clone());
82            node_map.insert(package.name.clone(), node);
83        }
84
85        for package in &metadata.packages {
86            if let Some(&from_node) = node_map.get(&package.name) {
87                for dep in &package.dependencies {
88                    if let Some(&to_node) = node_map.get(&dep.name) {
89                        graph.add_edge(from_node, to_node, ());
90                    }
91                }
92            }
93        }
94
95        Self {
96            metadata,
97            graph,
98            node_map,
99        }
100    }
101
102    /// Get the root package (if this is a single-crate project)
103    pub fn root_package(&self) -> Option<CrateInfo> {
104        self.metadata
105            .root_package()
106            .map(|pkg| self.package_to_info(pkg))
107    }
108
109    /// Get information about a specific crate
110    pub fn get_crate_info(&self, name: &str) -> Option<CrateInfo> {
111        self.metadata
112            .packages
113            .iter()
114            .find(|p| p.name == name)
115            .map(|pkg| self.package_to_info(pkg))
116    }
117
118    /// Get all packages in the workspace
119    pub fn all_packages(&self) -> Vec<CrateInfo> {
120        self.metadata
121            .packages
122            .iter()
123            .map(|pkg| self.package_to_info(pkg))
124            .collect()
125    }
126
127    /// Get direct dependencies of a crate
128    pub fn direct_dependencies(&self, name: &str) -> Vec<DependencyInfo> {
129        self.metadata
130            .packages
131            .iter()
132            .find(|p| p.name == name)
133            .map(|pkg| self.extract_dependencies(pkg))
134            .unwrap_or_default()
135    }
136
137    /// Get the dependency tree as a flat list with depth indicators
138    pub fn dependency_tree(&self, root: &str) -> Vec<(String, usize)> {
139        if let Some(&root_node) = self.node_map.get(root) {
140            let mut result = Vec::new();
141            let mut visited = HashMap::new();
142            self.traverse_deps(root_node, 0, &mut result, &mut visited);
143            result
144        } else {
145            Vec::new()
146        }
147    }
148
149    /// Get total number of dependencies (transitive)
150    pub fn total_dependency_count(&self, name: &str) -> usize {
151        self.dependency_tree(name).len().saturating_sub(1)
152    }
153
154    fn traverse_deps(
155        &self,
156        node: NodeIndex,
157        depth: usize,
158        result: &mut Vec<(String, usize)>,
159        visited: &mut HashMap<NodeIndex, bool>,
160    ) {
161        if visited.contains_key(&node) {
162            return;
163        }
164
165        visited.insert(node, true);
166        let name = self.graph[node].clone();
167        result.push((name, depth));
168
169        for neighbor in self.graph.neighbors(node) {
170            self.traverse_deps(neighbor, depth + 1, result, visited);
171        }
172    }
173
174    fn package_to_info(&self, pkg: &Package) -> CrateInfo {
175        let dependencies = self.extract_dependencies(pkg);
176        let features: Vec<String> = pkg.features.keys().cloned().collect();
177        let default_features = pkg.features.get("default").cloned().unwrap_or_default();
178
179        CrateInfo {
180            name: pkg.name.clone(),
181            version: pkg.version.to_string(),
182            authors: pkg.authors.clone(),
183            license: pkg.license.clone(),
184            description: pkg.description.clone(),
185            homepage: pkg.homepage.clone(),
186            repository: pkg.repository.clone(),
187            documentation: pkg.documentation.clone(),
188            dependencies,
189            features,
190            default_features,
191            edition: pkg.edition.to_string(),
192            rust_version: pkg.rust_version.as_ref().map(|v| v.to_string()),
193        }
194    }
195
196    fn extract_dependencies(&self, pkg: &Package) -> Vec<DependencyInfo> {
197        pkg.dependencies
198            .iter()
199            .map(|dep| DependencyInfo {
200                name: dep.name.clone(),
201                version: dep.req.to_string(),
202                optional: dep.optional,
203                features: dep.features.clone(),
204                kind: match dep.kind {
205                    CargoDependencyKind::Normal => DependencyKind::Normal,
206                    CargoDependencyKind::Development => DependencyKind::Dev,
207                    CargoDependencyKind::Build => DependencyKind::Build,
208                    _ => DependencyKind::Normal,
209                },
210            })
211            .collect()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::path::Path;
219
220    #[test]
221    fn test_dependency_tree_from_manifest() {
222        // Run from crate root: Cargo.toml exists
223        let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
224        if !manifest.exists() {
225            return;
226        }
227        let analyzer = DependencyAnalyzer::from_manifest(&manifest).unwrap();
228        let root = analyzer.root_package().expect("root package");
229        assert_eq!(root.name, "oracle-tui");
230        let tree = analyzer.dependency_tree(&root.name);
231        assert!(!tree.is_empty());
232        assert_eq!(tree[0].0, root.name);
233        assert_eq!(tree[0].1, 0);
234    }
235
236    #[test]
237    fn test_direct_dependencies() {
238        let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
239        if !manifest.exists() {
240            return;
241        }
242        let analyzer = DependencyAnalyzer::from_manifest(&manifest).unwrap();
243        let root = analyzer.root_package().unwrap();
244        let deps = analyzer.direct_dependencies(&root.name);
245        // Oracle has at least ratatui, crossterm, etc.
246        assert!(deps
247            .iter()
248            .any(|d| d.name == "ratatui" || d.name == "crossterm"));
249    }
250}