Skip to main content

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