1use 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
9pub struct DependencyAnalyzer {
11 metadata: cargo_metadata::Metadata,
12 graph: DiGraph<String, ()>,
13 node_map: HashMap<String, NodeIndex>,
14}
15
16#[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#[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#[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 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 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 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 pub fn root_package(&self) -> Option<CrateInfo> {
104 self.metadata
105 .root_package()
106 .map(|pkg| self.package_to_info(pkg))
107 }
108
109 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 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 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 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 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 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 assert!(deps
247 .iter()
248 .any(|d| d.name == "ratatui" || d.name == "crossterm"));
249 }
250}