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::{Parser, ValueEnum};
7use color_eyre::eyre::{Result, WrapErr, ensure, eyre};
8use guppy::{
9    PackageId,
10    graph::{DependencyDirection, DependencyReq, PackageGraph, PackageLink, PackageQuery},
11    platform::EnabledTernary,
12};
13use guppy_cmdlib::string_to_platform_spec;
14use std::collections::HashSet;
15
16#[derive(ValueEnum, 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", action = clap::ArgAction::SetTrue)]
41    reverse: bool,
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    fn direction(&self) -> DependencyDirection {
50        if self.reverse {
51            DependencyDirection::Reverse
52        } else {
53            DependencyDirection::Forward
54        }
55    }
56
57    /// Constructs a `PackageQuery` based on these options.
58    pub fn apply<'g>(&self, pkg_graph: &'g PackageGraph) -> Result<PackageQuery<'g>> {
59        if !self.roots.is_empty() {
60            // NOTE: The root set packages are specified by name. The tool currently
61            // does not handle multiple version of the same package as the current use
62            // cases are passing workspace members as the root set, which won't be
63            // duplicated.
64            let root_set = self.roots.iter().map(|s| s.as_str()).collect();
65            Ok(pkg_graph.query_directed(names_to_ids(pkg_graph, root_set), self.direction())?)
66        } else {
67            ensure!(
68                self.direction() == DependencyDirection::Forward,
69                eyre!("--query-reverse requires roots to be specified")
70            );
71            Ok(pkg_graph.query_workspace())
72        }
73    }
74}
75
76#[derive(Debug, Parser)]
77pub struct BaseFilterOptions {
78    #[clap(long, rename_all = "kebab-case", name = "package")]
79    /// Omit edges that point into a given package; useful for seeing how
80    /// removing a dependency affects the graph
81    pub omit_edges_into: Vec<String>,
82
83    #[clap(long, short, value_enum, default_value = "all")]
84    /// Kind of crates to select
85    pub kind: Kind,
86}
87
88impl BaseFilterOptions {
89    /// Return the set of omitted package IDs.
90    pub fn omitted_package_ids<'g: 'a, 'a>(
91        &'a self,
92        pkg_graph: &'g PackageGraph,
93    ) -> impl Iterator<Item = &'g PackageId> + 'a {
94        let omitted_set: HashSet<&str> = self.omit_edges_into.iter().map(|s| s.as_str()).collect();
95        names_to_ids(pkg_graph, omitted_set)
96    }
97}
98
99#[derive(Debug, Parser)]
100pub struct FilterOptions {
101    #[clap(flatten)]
102    pub base_opts: BaseFilterOptions,
103
104    #[clap(long, rename_all = "kebab-case")]
105    /// Include dev dependencies
106    pub include_dev: bool,
107
108    #[clap(long, rename_all = "kebab-case")]
109    /// Include build dependencies
110    pub include_build: bool,
111
112    #[clap(long)]
113    /// Target to filter, "current", "any" or "always" [default: any]
114    pub target: Option<String>,
115}
116
117impl FilterOptions {
118    /// Construct a package resolver based on the filter options.
119    pub fn make_resolver<'g>(
120        &'g self,
121        pkg_graph: &'g PackageGraph,
122    ) -> Result<impl Fn(&PackageQuery<'g>, PackageLink<'g>) -> bool + 'g> {
123        let omitted_package_ids: HashSet<_> =
124            self.base_opts.omitted_package_ids(pkg_graph).collect();
125
126        let platform_spec = string_to_platform_spec(self.target.as_deref())
127            .wrap_err_with(|| "target platform isn't known")?;
128
129        let ret = move |_: &PackageQuery<'g>, link| {
130            // filter by the kind of dependency (--kind)
131            let include_kind = self.base_opts.kind.should_traverse(&link);
132
133            let include_type = self.eval(link, |req| {
134                req.status().enabled_on(&platform_spec.clone()) != EnabledTernary::Disabled
135            });
136
137            // filter out provided edge targets (--omit-edges-into)
138            let include_edge = !omitted_package_ids.contains(link.to().id());
139
140            include_kind && include_type && include_edge
141        };
142        Ok(ret)
143    }
144
145    /// Select normal, dev, or build dependencies as requested (--include-build, --include-dev), and
146    /// apply `pred_fn` to whatever's selected.
147    fn eval(
148        &self,
149        link: PackageLink<'_>,
150        mut pred_fn: impl FnMut(DependencyReq<'_>) -> bool,
151    ) -> bool {
152        pred_fn(link.normal())
153            || self.include_dev && pred_fn(link.dev())
154            || self.include_build && pred_fn(link.build())
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}