cargo_guppy/
core.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Implementations for options shared by commands.
5
6use clap::{ArgEnum, Parser};
7use color_eyre::eyre::{ensure, eyre, Result, WrapErr};
8use guppy::{
9    graph::{DependencyDirection, DependencyReq, PackageGraph, PackageLink, PackageQuery},
10    platform::EnabledTernary,
11    PackageId,
12};
13use guppy_cmdlib::string_to_platform_spec;
14use std::collections::HashSet;
15
16#[derive(ArgEnum, Copy, Clone, Debug)]
17pub enum Kind {
18    All,
19    Workspace,
20    DirectThirdParty,
21    ThirdParty,
22}
23
24impl Kind {
25    /// Returns true if this link should be traversed.
26    pub fn should_traverse(self, link: &PackageLink<'_>) -> bool {
27        // NOTE: We always retain all workspace deps in the graph, otherwise
28        // we'll get a disconnected graph.
29        match self {
30            Kind::All | Kind::ThirdParty => true,
31            Kind::DirectThirdParty => link.from().in_workspace(),
32            Kind::Workspace => link.from().in_workspace() && link.to().in_workspace(),
33        }
34    }
35}
36
37#[derive(Debug, Parser)]
38pub struct QueryOptions {
39    /// Query reverse transitive dependencies (default: forward)
40    #[clap(long = "query-reverse", parse(from_flag = parse_direction))]
41    direction: DependencyDirection,
42
43    #[clap(rename_all = "screaming_snake_case")]
44    /// The root packages to start the query from
45    roots: Vec<String>,
46}
47
48impl QueryOptions {
49    /// Constructs a `PackageQuery` based on these options.
50    pub fn apply<'g>(&self, pkg_graph: &'g PackageGraph) -> Result<PackageQuery<'g>> {
51        if !self.roots.is_empty() {
52            // NOTE: The root set packages are specified by name. The tool currently
53            // does not handle multiple version of the same package as the current use
54            // cases are passing workspace members as the root set, which won't be
55            // duplicated.
56            let root_set = self.roots.iter().map(|s| s.as_str()).collect();
57            Ok(pkg_graph.query_directed(names_to_ids(pkg_graph, root_set), self.direction)?)
58        } else {
59            ensure!(
60                self.direction == DependencyDirection::Forward,
61                eyre!("--query-reverse requires roots to be specified")
62            );
63            Ok(pkg_graph.query_workspace())
64        }
65    }
66}
67
68#[derive(Debug, Parser)]
69pub struct BaseFilterOptions {
70    #[clap(long, rename_all = "kebab-case", name = "package")]
71    /// Omit edges that point into a given package; useful for seeing how
72    /// removing a dependency affects the graph
73    pub omit_edges_into: Vec<String>,
74
75    #[clap(long, short, arg_enum, default_value = "all")]
76    /// Kind of crates to select
77    pub kind: Kind,
78}
79
80impl BaseFilterOptions {
81    /// Return the set of omitted package IDs.
82    pub fn omitted_package_ids<'g: 'a, 'a>(
83        &'a self,
84        pkg_graph: &'g PackageGraph,
85    ) -> impl Iterator<Item = &'g PackageId> + 'a {
86        let omitted_set: HashSet<&str> = self.omit_edges_into.iter().map(|s| s.as_str()).collect();
87        names_to_ids(pkg_graph, omitted_set)
88    }
89}
90
91#[derive(Debug, Parser)]
92pub struct FilterOptions {
93    #[clap(flatten)]
94    pub base_opts: BaseFilterOptions,
95
96    #[clap(long, rename_all = "kebab-case")]
97    /// Include dev dependencies
98    pub include_dev: bool,
99
100    #[clap(long, rename_all = "kebab-case")]
101    /// Include build dependencies
102    pub include_build: bool,
103
104    #[clap(long)]
105    /// Target to filter, "current", "any" or "always" [default: any]
106    pub target: Option<String>,
107}
108
109impl FilterOptions {
110    /// Construct a package resolver based on the filter options.
111    pub fn make_resolver<'g>(
112        &'g self,
113        pkg_graph: &'g PackageGraph,
114    ) -> Result<impl Fn(&PackageQuery<'g>, PackageLink<'g>) -> bool + 'g> {
115        let omitted_package_ids: HashSet<_> =
116            self.base_opts.omitted_package_ids(pkg_graph).collect();
117
118        let platform_spec = string_to_platform_spec(self.target.as_deref())
119            .wrap_err_with(|| "target platform isn't known")?;
120
121        let ret = move |_: &PackageQuery<'g>, link| {
122            // filter by the kind of dependency (--kind)
123            let include_kind = self.base_opts.kind.should_traverse(&link);
124
125            let include_type = self.eval(link, |req| {
126                req.status().enabled_on(&platform_spec.clone()) != EnabledTernary::Disabled
127            });
128
129            // filter out provided edge targets (--omit-edges-into)
130            let include_edge = !omitted_package_ids.contains(link.to().id());
131
132            include_kind && include_type && include_edge
133        };
134        Ok(ret)
135    }
136
137    /// Select normal, dev, or build dependencies as requested (--include-build, --include-dev), and
138    /// apply `pred_fn` to whatever's selected.
139    fn eval(
140        &self,
141        link: PackageLink<'_>,
142        mut pred_fn: impl FnMut(DependencyReq<'_>) -> bool,
143    ) -> bool {
144        pred_fn(link.normal())
145            || self.include_dev && pred_fn(link.dev())
146            || self.include_build && pred_fn(link.build())
147    }
148}
149
150pub(crate) fn parse_direction(reverse: bool) -> DependencyDirection {
151    if reverse {
152        DependencyDirection::Reverse
153    } else {
154        DependencyDirection::Forward
155    }
156}
157
158pub(crate) fn names_to_ids<'g: 'a, 'a>(
159    pkg_graph: &'g PackageGraph,
160    names: HashSet<&'a str>,
161) -> impl Iterator<Item = &'g PackageId> + 'a {
162    pkg_graph.packages().filter_map(move |metadata| {
163        if names.contains(metadata.name()) {
164            Some(metadata.id())
165        } else {
166            None
167        }
168    })
169}