check_deprule/dependency_graph/
tree.rs

1use 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// TODO: dead code回避を精査すること
18
19#[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// TODO: lint回避の精査
170#[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            // println!("\npackage: {}\nid: {}", rule.package, package.id);
211            // println!("rule.package: {}", rule.package);
212            // println!("parent_package: {:?}", parent_package);
213            // println!("forbidden_dependencies: {:?}", rule.forbidden_dependencies);
214            if let Some(parent_package) = parent_package {
215                // println!("parent match: {}", rule.package == PackageId { repr: parent_package.name.clone() });
216                // println!("forbidden_dependencies match: {}", rule.forbidden_dependencies.contains(&PackageId { repr: package.name.clone() }));
217                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// TODO: lint回避の精査
284#[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    // ensure a consistent output ordering
318    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}