guppy/graph/
query.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    graph::{
6        feature::{FeatureFilter, FeatureQuery},
7        query_core::QueryParams,
8        DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageMetadata,
9        PackageResolver, PackageSet, ResolverFn,
10    },
11    sorted_set::SortedSet,
12    Error, PackageId,
13};
14use camino::Utf8Path;
15use petgraph::prelude::*;
16
17/// A query over a package graph.
18///
19/// This is the entry point for iterators over IDs and dependency links, and dot graph presentation.
20/// A `PackageQuery` is constructed through the `query_` methods on `PackageGraph`.
21#[derive(Clone, Debug)]
22pub struct PackageQuery<'g> {
23    // The fields are pub(super) for access within the graph module.
24    pub(super) graph: &'g PackageGraph,
25    pub(super) params: QueryParams<PackageGraph>,
26}
27
28assert_covariant!(PackageQuery);
29
30/// ## Queries
31///
32/// The methods in this section create *queries* over subsets of this package graph. Use the methods
33/// here to analyze transitive dependencies.
34impl PackageGraph {
35    /// Creates a new forward query over the entire workspace.
36    ///
37    /// `query_workspace` will select all workspace packages and their transitive dependencies. To
38    /// create a `PackageSet` with just workspace packages, use `resolve_workspace`.
39    pub fn query_workspace(&self) -> PackageQuery {
40        self.query_forward(self.workspace().member_ids())
41            .expect("workspace packages should all be known")
42    }
43
44    /// Creates a new forward query over the specified workspace packages by path.
45    ///
46    /// Returns an error if any workspace paths were unknown.
47    pub fn query_workspace_paths(
48        &self,
49        paths: impl IntoIterator<Item = impl AsRef<Utf8Path>>,
50    ) -> Result<PackageQuery, Error> {
51        let workspace = self.workspace();
52        let package_ixs = paths
53            .into_iter()
54            .map(|path| {
55                workspace
56                    .member_by_path(path.as_ref())
57                    .map(|package| package.package_ix())
58            })
59            .collect::<Result<SortedSet<_>, Error>>()?;
60
61        Ok(self.query_from_parts(package_ixs, DependencyDirection::Forward))
62    }
63
64    /// Creates a new forward query over the specified workspace packages by name.
65    ///
66    /// This is similar to `cargo`'s `--package` option.
67    ///
68    /// Returns an error if any package names were unknown.
69    pub fn query_workspace_names(
70        &self,
71        names: impl IntoIterator<Item = impl AsRef<str>>,
72    ) -> Result<PackageQuery, Error> {
73        let workspace = self.workspace();
74        let package_ixs = names
75            .into_iter()
76            .map(|name| {
77                workspace
78                    .member_by_name(name.as_ref())
79                    .map(|package| package.package_ix())
80            })
81            .collect::<Result<SortedSet<_>, Error>>()?;
82
83        Ok(self.query_from_parts(package_ixs, DependencyDirection::Forward))
84    }
85
86    /// Creates a new query that returns transitive dependencies of the given packages in the
87    /// specified direction.
88    ///
89    /// Returns an error if any package IDs are unknown.
90    pub fn query_directed<'g, 'a>(
91        &'g self,
92        package_ids: impl IntoIterator<Item = &'a PackageId>,
93        dep_direction: DependencyDirection,
94    ) -> Result<PackageQuery<'g>, Error> {
95        match dep_direction {
96            DependencyDirection::Forward => self.query_forward(package_ids),
97            DependencyDirection::Reverse => self.query_reverse(package_ids),
98        }
99    }
100
101    /// Creates a new query that returns transitive dependencies of the given packages.
102    ///
103    /// Returns an error if any package IDs are unknown.
104    pub fn query_forward<'g, 'a>(
105        &'g self,
106        package_ids: impl IntoIterator<Item = &'a PackageId>,
107    ) -> Result<PackageQuery<'g>, Error> {
108        Ok(PackageQuery {
109            graph: self,
110            params: QueryParams::Forward(self.package_ixs(package_ids)?),
111        })
112    }
113
114    /// Creates a new query that returns transitive reverse dependencies of the given packages.
115    ///
116    /// Returns an error if any package IDs are unknown.
117    pub fn query_reverse<'g, 'a>(
118        &'g self,
119        package_ids: impl IntoIterator<Item = &'a PackageId>,
120    ) -> Result<PackageQuery<'g>, Error> {
121        Ok(PackageQuery {
122            graph: self,
123            params: QueryParams::Reverse(self.package_ixs(package_ids)?),
124        })
125    }
126
127    pub(super) fn query_from_parts(
128        &self,
129        package_ixs: SortedSet<NodeIndex<PackageIx>>,
130        direction: DependencyDirection,
131    ) -> PackageQuery {
132        let params = match direction {
133            DependencyDirection::Forward => QueryParams::Forward(package_ixs),
134            DependencyDirection::Reverse => QueryParams::Reverse(package_ixs),
135        };
136        PackageQuery {
137            graph: self,
138            params,
139        }
140    }
141}
142
143impl<'g> PackageQuery<'g> {
144    /// Returns the package graph on which the query is going to be executed.
145    pub fn graph(&self) -> &'g PackageGraph {
146        self.graph
147    }
148
149    /// Returns the direction the query is happening in.
150    pub fn direction(&self) -> DependencyDirection {
151        self.params.direction()
152    }
153
154    /// Returns the list of initial packages specified in the query.
155    ///
156    /// The order of packages is unspecified.
157    pub fn initials<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageMetadata<'g>> + 'a {
158        let graph = self.graph;
159        self.params.initials().iter().map(move |package_ix| {
160            graph
161                .metadata(&graph.dep_graph[*package_ix])
162                .expect("valid ID")
163        })
164    }
165
166    /// Returns true if the query starts from the given package ID.
167    ///
168    /// Returns an error if this package ID is unknown.
169    pub fn starts_from(&self, package_id: &PackageId) -> Result<bool, Error> {
170        Ok(self.params.has_initial(self.graph.package_ix(package_id)?))
171    }
172
173    /// Converts this `PackageQuery` into a `FeatureQuery`, using the given feature filter.
174    ///
175    /// This will cause the feature graph to be constructed if it hasn't been done so already.
176    pub fn to_feature_query(&self, filter: impl FeatureFilter<'g>) -> FeatureQuery<'g> {
177        let package_ixs = self.params.initials();
178        let feature_graph = self.graph.feature_graph();
179        let feature_ixs =
180            feature_graph.feature_ixs_for_package_ixs_filtered(package_ixs.iter().copied(), filter);
181        feature_graph.query_from_parts(feature_ixs, self.direction())
182    }
183
184    /// Resolves this query into a set of known packages, following every link found along the
185    /// way.
186    ///
187    /// This is the entry point for iterators.
188    pub fn resolve(self) -> PackageSet<'g> {
189        PackageSet::new(self)
190    }
191
192    /// Resolves this query into a set of known packages, using the provided resolver to
193    /// determine which links are followed.
194    pub fn resolve_with(self, resolver: impl PackageResolver<'g>) -> PackageSet<'g> {
195        PackageSet::with_resolver(self, resolver)
196    }
197
198    /// Resolves this query into a set of known packages, using the provided resolver function
199    /// to determine which links are followed.
200    pub fn resolve_with_fn(
201        self,
202        resolver_fn: impl FnMut(&PackageQuery<'g>, PackageLink<'g>) -> bool,
203    ) -> PackageSet<'g> {
204        self.resolve_with(ResolverFn(resolver_fn))
205    }
206}