1use std::collections::{BTreeMap, BTreeSet};
21
22pub const TERMINAL: [&str; 4] = ["ratatui", "crossterm", "clap", "rustyline"];
24
25pub fn found(manifest: &str, lock: &str) -> Vec<String> {
29 let graph = packages(lock);
30 let mut seen = BTreeSet::new();
31 let mut next: Vec<String> = dependencies(manifest);
32 while let Some(name) = next.pop() {
33 if seen.insert(name.clone())
34 && let Some(more) = graph.get(&name)
35 {
36 next.extend(more.iter().cloned());
37 }
38 }
39 seen.into_iter()
40 .filter(|name| TERMINAL.contains(&name.as_str()))
41 .collect()
42}
43
44pub fn dependencies(manifest: &str) -> Vec<String> {
54 let mut names = Vec::new();
55 let mut in_table = false;
56 for line in manifest.lines().map(str::trim) {
57 if let Some(header) = line.strip_prefix('[') {
58 let header = header.trim_end_matches(']');
59 let (table, crate_name) = match header.rsplit_once("dependencies.") {
60 Some((table, name)) => (format!("{table}dependencies"), Some(name)),
61 None => (header.to_owned(), None),
62 };
63 let normal = (table == "dependencies" || table.ends_with(".dependencies"))
64 || table == "build-dependencies"
65 || table.ends_with(".build-dependencies");
66 match crate_name {
67 Some(name) if normal => {
68 names.push(name.trim_matches('"').to_owned());
69 in_table = false;
70 }
71 _ => in_table = normal,
72 }
73 continue;
74 }
75 if !in_table || line.is_empty() || line.starts_with('#') {
76 continue;
77 }
78 if let Some((key, _)) = line.split_once('=') {
79 let name = key.trim().split('.').next().unwrap_or_default();
80 names.push(name.trim_matches('"').to_owned());
81 }
82 }
83 names
84}
85
86fn packages(lock: &str) -> BTreeMap<String, BTreeSet<String>> {
88 let mut graph: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
89 let mut name = String::new();
90 let mut in_list = false;
91 for line in lock.lines().map(str::trim) {
92 if line == "[[package]]" {
93 name.clear();
94 in_list = false;
95 } else if let Some(value) = line.strip_prefix("name = ") {
96 name = value.trim_matches('"').to_owned();
97 graph.entry(name.clone()).or_default();
98 } else if line.starts_with("dependencies = [") {
99 in_list = true;
100 } else if in_list && line == "]" {
101 in_list = false;
102 } else if in_list {
103 let dependency = line.trim_matches(|c| c == '"' || c == ',');
104 let dependency = dependency.split(' ').next().unwrap_or_default();
105 graph
106 .entry(name.clone())
107 .or_default()
108 .insert(dependency.to_owned());
109 }
110 }
111 graph
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn a_crate_of_the_terminal_through_two_crates_counts() {
120 let manifest = "[dependencies]\na = \"1\"\n";
121 let lock = concat!(
122 "[[package]]\nname = \"a\"\nversion = \"1.0.0\"\ndependencies = [\n \"b\",\n]\n\n",
123 "[[package]]\nname = \"b\"\nversion = \"1.0.0\"\ndependencies = [\n",
124 " \"rustyline 18.0.1\",\n \"serde\",\n]\n\n",
125 "[[package]]\nname = \"rustyline\"\nversion = \"18.0.1\"\n",
126 );
127 assert_eq!(found(manifest, lock), ["rustyline"]);
128 }
129
130 #[test]
131 fn a_dev_dependency_does_not_count() {
132 let manifest = "[dependencies]\na = \"1\"\n\n[dev-dependencies]\nratatui = \"0.30\"\n";
133 assert!(found(manifest, "").is_empty());
134 }
135
136 #[test]
137 fn a_build_dependency_and_a_target_dependency_count() {
138 let manifest = "[build-dependencies]\nclap = \"4\"\n\n\
139 [target.'cfg(unix)'.dependencies]\ncrossterm = \"0.29\"\n";
140 assert_eq!(found(manifest, ""), ["clap", "crossterm"]);
141 }
142}