check_deprule/dependency_graph/
tree.rs1use crate::ReturnStatus;
2use crate::dependency_graph::formatter::Chunk;
3use crate::dependency_rule::DependencyRules;
4
5use super::Graph;
6use super::formatter::Pattern;
7use anyhow::{Context, Error, anyhow};
8use cargo::core::Workspace;
9use cargo::util::context::GlobalContext;
10use cargo_metadata::{DependencyKind, Package, PackageId};
11use petgraph::EdgeDirection;
12use petgraph::visit::EdgeRef;
13use semver::Version;
14use std::collections::HashSet;
15use std::path::Path;
16
17#[derive(Clone, Copy)]
20#[allow(dead_code)]
21enum Prefix {
22 None,
23 Indent,
24 Depth,
25}
26
27struct Symbols {
28 down: &'static str,
29 tee: &'static str,
30 ell: &'static str,
31 right: &'static str,
32}
33
34static UTF8_SYMBOLS: Symbols = Symbols {
35 down: "│",
36 tee: "├",
37 ell: "└",
38 right: "─",
39};
40
41#[allow(dead_code)]
42static ASCII_SYMBOLS: Symbols = Symbols {
43 down: "|",
44 tee: "|",
45 ell: "`",
46 right: "-",
47};
48
49pub enum Charset {
50 Utf8,
51 Ascii,
52}
53
54impl std::str::FromStr for Charset {
55 type Err = &'static str;
56
57 fn from_str(s: &str) -> Result<Charset, &'static str> {
58 match s {
59 "utf8" => Ok(Charset::Utf8),
60 "ascii" => Ok(Charset::Ascii),
61 _ => Err("invalid charset"),
62 }
63 }
64}
65
66pub fn print<P: AsRef<Path>>(
67 graph: &Graph,
68 manifest_path: P,
69 rules: DependencyRules,
70) -> Result<ReturnStatus, Error> {
71 let glcx = GlobalContext::default()?;
72 let ws = Workspace::new(std::path::absolute(manifest_path)?.as_path(), &glcx)?;
73
74 let format = Pattern::new("{p}")?;
75 let direction = EdgeDirection::Outgoing;
76 let symbols = &UTF8_SYMBOLS;
77 let prefix = Prefix::Indent;
78 let mut return_status = ReturnStatus::NoViolation;
79
80 for package in ws.members() {
81 let root = find_package(package.name().as_str(), graph)?;
82 let root = &graph.graph[graph.nodes[root]];
83
84 let result = print_tree(
85 graph, root, &format, direction, symbols, prefix, true, &rules,
86 );
87
88 if let ReturnStatus::Violation = result {
89 return_status = ReturnStatus::Violation
90 }
91 }
92
93 Ok(return_status)
94}
95
96fn find_package<'a>(package: &str, graph: &'a Graph) -> Result<&'a PackageId, Error> {
97 let mut it = package.split(':');
98 let name = it.next().unwrap();
99 let version = it
100 .next()
101 .map(Version::parse)
102 .transpose()
103 .context("error parsing package version")?;
104
105 let mut candidates = vec![];
106 for idx in graph.graph.node_indices() {
107 let package = &graph.graph[idx];
108 if package.name != name {
109 continue;
110 }
111
112 if let Some(version) = &version {
113 if package.version != *version {
114 continue;
115 }
116 }
117
118 candidates.push(package);
119 }
120
121 if candidates.is_empty() {
122 Err(anyhow!("no crates found for package `{}`", package))
123 } else if candidates.len() > 1 {
124 let specs = candidates
125 .iter()
126 .map(|p| format!("{}:{}", p.name, p.version))
127 .collect::<Vec<_>>()
128 .join(", ");
129 Err(anyhow!(
130 "multiple crates found for package `{}`: {}",
131 package,
132 specs,
133 ))
134 } else {
135 Ok(&candidates[0].id)
136 }
137}
138
139#[allow(clippy::too_many_arguments)]
140fn print_tree<'a>(
141 graph: &'a Graph,
142 root: &'a Package,
143 format: &Pattern,
144 direction: EdgeDirection,
145 symbols: &Symbols,
146 prefix: Prefix,
147 all: bool,
148 rules: &DependencyRules,
149) -> ReturnStatus {
150 let mut visited_deps = HashSet::new();
151 let mut levels_continue = vec![];
152
153 print_package(
154 graph,
155 None,
156 root,
157 format,
158 direction,
159 symbols,
160 prefix,
161 all,
162 &mut visited_deps,
163 &mut levels_continue,
164 rules,
165 ReturnStatus::NoViolation,
166 )
167}
168
169#[allow(clippy::too_many_arguments)]
171fn print_package<'a>(
172 graph: &'a Graph,
173 parent_package: Option<&'a Package>,
174 package: &'a Package,
175 format: &Pattern,
176 direction: EdgeDirection,
177 symbols: &Symbols,
178 prefix: Prefix,
179 all: bool,
180 visited_deps: &mut HashSet<&'a PackageId>,
181 levels_continue: &mut Vec<bool>,
182 rules: &DependencyRules,
183 parent_return_status: ReturnStatus,
184) -> ReturnStatus {
185 let new = all || visited_deps.insert(&package.id);
186
187 match prefix {
188 Prefix::Depth => print!("{}", levels_continue.len()),
189 Prefix::Indent => {
190 if let Some((last_continues, rest)) = levels_continue.split_last() {
191 for continues in rest {
192 let c = if *continues { symbols.down } else { " " };
193 print!("{} ", c);
194 }
195
196 let c = if *last_continues {
197 symbols.tee
198 } else {
199 symbols.ell
200 };
201 print!("{0}{1}{1} ", c, symbols.right);
202 }
203 }
204 Prefix::None => {}
205 }
206
207 let star = if new { "" } else { " (*)" };
208 let mut is_violation = {
209 rules.rules.iter().any(|rule| {
210 if let Some(parent_package) = parent_package {
215 rule.package
218 == PackageId {
219 repr: parent_package.name.clone(),
220 }
221 && rule.forbidden_dependencies.contains(&PackageId {
222 repr: package.name.clone(),
223 })
224 } else {
225 false
226 }
227 })
228 };
229 match is_violation {
230 true => {
231 let f = Pattern(vec![Chunk::ViolationPackage]);
232 println!("{}{}", f.display(package), star);
233 }
234 false => println!("{}{}", format.display(package), star),
235 };
236
237 if !new {
238 return match (is_violation, parent_return_status) {
239 (true, _) => ReturnStatus::Violation,
240 (false, ReturnStatus::Violation) => ReturnStatus::Violation,
241 _ => ReturnStatus::NoViolation,
242 };
243 }
244
245 for kind in &[
246 DependencyKind::Normal,
247 DependencyKind::Build,
248 DependencyKind::Development,
249 ] {
250 let current_return_status = match (is_violation, parent_return_status.clone()) {
251 (true, _) => ReturnStatus::Violation,
252 (false, ReturnStatus::Violation) => ReturnStatus::Violation,
253 _ => ReturnStatus::NoViolation,
254 };
255
256 let result = print_dependencies(
257 graph,
258 package,
259 format,
260 direction,
261 symbols,
262 prefix,
263 all,
264 visited_deps,
265 levels_continue,
266 *kind,
267 rules,
268 current_return_status,
269 );
270
271 if let ReturnStatus::Violation = result {
272 is_violation = true;
273 }
274 }
275
276 match (is_violation, parent_return_status) {
277 (true, _) => ReturnStatus::Violation,
278 (false, ReturnStatus::Violation) => ReturnStatus::Violation,
279 _ => ReturnStatus::NoViolation,
280 }
281}
282
283#[allow(clippy::too_many_arguments)]
285fn print_dependencies<'a>(
286 graph: &'a Graph,
287 package: &'a Package,
288 format: &Pattern,
289 direction: EdgeDirection,
290 symbols: &Symbols,
291 prefix: Prefix,
292 all: bool,
293 visited_deps: &mut HashSet<&'a PackageId>,
294 levels_continue: &mut Vec<bool>,
295 kind: DependencyKind,
296 rules: &DependencyRules,
297 parent_return_status: ReturnStatus,
298) -> ReturnStatus {
299 let idx = graph.nodes[&package.id];
300 let mut deps = vec![];
301 for edge in graph.graph.edges_directed(idx, direction) {
302 if *edge.weight() != kind {
303 continue;
304 }
305
306 let dep = match direction {
307 EdgeDirection::Incoming => &graph.graph[edge.source()],
308 EdgeDirection::Outgoing => &graph.graph[edge.target()],
309 };
310 deps.push(dep);
311 }
312
313 if deps.is_empty() {
314 return parent_return_status;
315 }
316
317 deps.sort_by_key(|p| &p.id);
319
320 let name = match kind {
321 DependencyKind::Normal => None,
322 DependencyKind::Build => Some("[build-dependencies]"),
323 DependencyKind::Development => Some("[dev-dependencies]"),
324 _ => unreachable!(),
325 };
326
327 if let Prefix::Indent = prefix {
328 if let Some(name) = name {
329 for continues in &**levels_continue {
330 let c = if *continues { symbols.down } else { " " };
331 print!("{} ", c);
332 }
333
334 println!("{}", name);
335 }
336 }
337
338 let mut is_violation = false;
339 let mut it = deps.iter().peekable();
340 while let Some(dependency) = it.next() {
341 levels_continue.push(it.peek().is_some());
342 let current_return_status = if is_violation {
343 ReturnStatus::Violation
344 } else {
345 parent_return_status.clone()
346 };
347
348 let result = print_package(
349 graph,
350 Some(package),
351 dependency,
352 format,
353 direction,
354 symbols,
355 prefix,
356 all,
357 visited_deps,
358 levels_continue,
359 rules,
360 current_return_status,
361 );
362
363 levels_continue.pop();
364 if let ReturnStatus::Violation = result {
365 is_violation = true;
366 }
367 }
368
369 match (is_violation, parent_return_status) {
370 (true, _) => ReturnStatus::Violation,
371 (false, ReturnStatus::Violation) => ReturnStatus::Violation,
372 _ => ReturnStatus::NoViolation,
373 }
374}