guppy/graph/feature/
query.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    debug_ignore::DebugIgnore,
6    graph::{
7        feature::{
8            ConditionalLink, FeatureGraph, FeatureId, FeatureLabel, FeatureMetadata, FeatureSet,
9        },
10        query_core::QueryParams,
11        DependencyDirection, FeatureGraphSpec, FeatureIx, PackageIx, PackageMetadata,
12    },
13    sorted_set::SortedSet,
14    Error, PackageId,
15};
16use itertools::Itertools;
17use petgraph::graph::NodeIndex;
18use std::collections::HashSet;
19
20/// Trait representing whether a feature within a package should be selected.
21///
22/// This is conceptually similar to passing `--features` or other similar command-line options to
23/// Cargo.
24///
25/// Most uses will involve using one of the predefined filters: `all_filter`, `default_filter`, or
26/// `none_filter`. A customized filter can be provided either through `filter_fn` or by implementing
27/// this trait.
28pub trait FeatureFilter<'g> {
29    /// Returns true if this feature ID should be selected in the graph.
30    ///
31    /// Returning false does not prevent this feature ID from being included if it's reachable
32    /// through other means.
33    ///
34    /// In general, `accept` should return true if `feature_id.is_base()` is true.
35    ///
36    /// The feature ID is guaranteed to be in this graph, so it is OK to panic if it isn't found.
37    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool;
38}
39
40impl<'g, T> FeatureFilter<'g> for &mut T
41where
42    T: FeatureFilter<'g>,
43{
44    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
45        (**self).accept(graph, feature_id)
46    }
47}
48
49impl<'g> FeatureFilter<'g> for Box<dyn FeatureFilter<'g> + '_> {
50    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
51        (**self).accept(graph, feature_id)
52    }
53}
54
55impl<'g> FeatureFilter<'g> for &mut dyn FeatureFilter<'g> {
56    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
57        (**self).accept(graph, feature_id)
58    }
59}
60
61/// A `FeatureFilter` which calls the function that's passed in.
62#[derive(Clone, Debug)]
63pub struct FeatureFilterFn<F>(F);
64
65impl<'g, F> FeatureFilterFn<F>
66where
67    F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool,
68{
69    /// Returns a new instance of this wrapper.
70    pub fn new(f: F) -> Self {
71        FeatureFilterFn(f)
72    }
73}
74
75impl<'g, F> FeatureFilter<'g> for FeatureFilterFn<F>
76where
77    F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool,
78{
79    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
80        (self.0)(graph, feature_id)
81    }
82}
83
84/// Describes one of the standard sets of features recognized by Cargo: none, all or default.
85///
86/// `StandardFeatures` implements `FeatureFilter<'g>`, so it can be passed in as a feature filter
87/// wherever necessary.
88#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
89pub enum StandardFeatures {
90    /// No features. Equivalent to a build with `--no-default-features`.
91    None,
92
93    /// Default features. Equivalent to a standard `cargo build`.
94    Default,
95
96    /// All features. Equivalent to `cargo build --all-features`.
97    All,
98}
99
100impl StandardFeatures {
101    /// A list of all the possible values of `StandardFeatures`.
102    pub const VALUES: &'static [Self; 3] = &[
103        StandardFeatures::None,
104        StandardFeatures::Default,
105        StandardFeatures::All,
106    ];
107}
108
109impl<'g> FeatureFilter<'g> for StandardFeatures {
110    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
111        match self {
112            StandardFeatures::None => {
113                // The only feature ID that should be accepted is the base one.
114                feature_id.is_base()
115            }
116            StandardFeatures::Default => {
117                // XXX it kinda sucks that we already know about the exact feature ixs but need to go
118                // through the feature ID over here. Might be worth reorganizing the code to not do that.
119                graph
120                    .is_default_feature(feature_id)
121                    .expect("feature IDs should be valid")
122            }
123            StandardFeatures::All => true,
124        }
125    }
126}
127
128/// Returns a `FeatureFilter` that selects everything from the base filter, plus these additional
129/// feature names -- regardless of what package they are in.
130///
131/// This is equivalent to a build with `--features`, and is typically meant to be used with one
132/// package.
133///
134/// For filtering by feature IDs, use `feature_id_filter`.
135pub fn named_feature_filter<'g: 'a, 'a>(
136    base: impl FeatureFilter<'g> + 'a,
137    features: impl IntoIterator<Item = &'a str>,
138) -> impl FeatureFilter<'g> + 'a {
139    let mut base = base;
140    let features: HashSet<_> = features.into_iter().collect();
141    FeatureFilterFn::new(move |feature_graph, feature_id| {
142        if base.accept(feature_graph, feature_id) {
143            return true;
144        }
145        match feature_id.label() {
146            FeatureLabel::Named(feature) => features.contains(feature),
147            _ => {
148                // This is the base feature. Assume that it has already been selected by the base
149                // filter.
150                false
151            }
152        }
153    })
154}
155
156/// Returns a `FeatureFilter` that selects everything from the base filter, plus some additional
157/// feature IDs.
158///
159/// This is a more advanced version of `feature_filter`.
160pub fn feature_id_filter<'g: 'a, 'a>(
161    base: impl FeatureFilter<'g> + 'a,
162    feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
163) -> impl FeatureFilter<'g> + 'a {
164    let mut base = base;
165    let feature_ids: HashSet<_> = feature_ids
166        .into_iter()
167        .map(|feature_id| feature_id.into())
168        .collect();
169    FeatureFilterFn::new(move |feature_graph, feature_id| {
170        base.accept(feature_graph, feature_id) || feature_ids.contains(&feature_id)
171    })
172}
173
174/// A query over a feature graph.
175///
176/// A `FeatureQuery` is the entry point for Cargo resolution, and also provides iterators over
177/// feature IDs and links. This struct is constructed through the `query_` methods on
178/// `FeatureGraph`, or through `PackageQuery::to_feature_query`.
179#[derive(Clone, Debug)]
180pub struct FeatureQuery<'g> {
181    pub(super) graph: DebugIgnore<FeatureGraph<'g>>,
182    pub(in crate::graph) params: QueryParams<FeatureGraphSpec>,
183}
184
185assert_covariant!(FeatureQuery);
186
187/// ## Queries
188///
189/// The methods in this section create queries over subsets of this feature graph. Use the methods
190/// here to analyze transitive dependencies.
191impl<'g> FeatureGraph<'g> {
192    /// Creates a new query over the entire workspace.
193    ///
194    /// `query_workspace` will select all workspace packages (subject to the provided filter) and
195    /// their transitive dependencies.
196    pub fn query_workspace(&self, filter: impl FeatureFilter<'g>) -> FeatureQuery<'g> {
197        self.package_graph
198            .query_workspace()
199            .to_feature_query(filter)
200    }
201
202    /// Creates a new query that returns transitive dependencies of the given feature IDs in the
203    /// specified direction.
204    ///
205    /// Returns an error if any feature IDs are unknown.
206    pub fn query_directed<'a>(
207        &self,
208        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
209        dep_direction: DependencyDirection,
210    ) -> Result<FeatureQuery<'g>, Error> {
211        match dep_direction {
212            DependencyDirection::Forward => self.query_forward(feature_ids),
213            DependencyDirection::Reverse => self.query_reverse(feature_ids),
214        }
215    }
216
217    /// Creates a new query that returns transitive dependencies of the given feature IDs.
218    ///
219    /// Returns an error if any feature IDs are unknown.
220    pub fn query_forward<'a>(
221        &self,
222        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
223    ) -> Result<FeatureQuery<'g>, Error> {
224        let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into());
225        Ok(FeatureQuery {
226            graph: DebugIgnore(*self),
227            params: QueryParams::Forward(self.feature_ixs(feature_ids)?),
228        })
229    }
230
231    /// Creates a new query that returns transitive reverse dependencies of the given feature IDs.
232    ///
233    /// Returns an error if any feature IDs are unknown.
234    pub fn query_reverse<'a>(
235        &self,
236        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
237    ) -> Result<FeatureQuery<'g>, Error> {
238        let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into());
239        Ok(FeatureQuery {
240            graph: DebugIgnore(*self),
241            params: QueryParams::Reverse(self.feature_ixs(feature_ids)?),
242        })
243    }
244
245    pub(in crate::graph) fn query_from_parts(
246        &self,
247        feature_ixs: SortedSet<NodeIndex<FeatureIx>>,
248        direction: DependencyDirection,
249    ) -> FeatureQuery<'g> {
250        let params = match direction {
251            DependencyDirection::Forward => QueryParams::Forward(feature_ixs),
252            DependencyDirection::Reverse => QueryParams::Reverse(feature_ixs),
253        };
254        FeatureQuery {
255            graph: DebugIgnore(*self),
256            params,
257        }
258    }
259}
260
261impl<'g> FeatureQuery<'g> {
262    /// Returns the feature graph the query is going to be executed on.
263    pub fn graph(&self) -> &FeatureGraph<'g> {
264        &self.graph
265    }
266
267    /// Returns the direction the query is happening in.
268    pub fn direction(&self) -> DependencyDirection {
269        self.params.direction()
270    }
271
272    /// Returns the list of initial features specified in the query.
273    ///
274    /// The order of features is unspecified.
275    pub fn initials<'a>(&'a self) -> impl ExactSizeIterator<Item = FeatureMetadata<'g>> + 'a {
276        let graph = self.graph;
277        self.params
278            .initials()
279            .iter()
280            .map(move |feature_ix| graph.metadata_for_ix(*feature_ix))
281    }
282
283    /// Returns the list of initial packages specified in the query.
284    ///
285    /// The order of packages is unspecified.
286    pub fn initial_packages<'a>(&'a self) -> impl Iterator<Item = PackageMetadata<'g>> + 'a {
287        // feature ixs are stored in sorted order by package ix, so dedup() is fine.
288        self.initials().map(|feature| feature.package()).dedup()
289    }
290
291    /// Returns true if the query starts from the given package.
292    ///
293    /// Returns an error if the package ID is unknown.
294    pub fn starts_from_package(&self, package_id: &PackageId) -> Result<bool, Error> {
295        let package_ix = self.graph.package_graph.package_ix(package_id)?;
296        Ok(self.starts_from_package_ix(package_ix))
297    }
298
299    /// Returns true if the query starts from the given feature ID.
300    ///
301    /// Returns an error if this feature ID is unknown.
302    pub fn starts_from<'a>(&self, feature_id: impl Into<FeatureId<'a>>) -> Result<bool, Error> {
303        Ok(self
304            .params
305            .has_initial(self.graph.feature_ix(feature_id.into())?))
306    }
307
308    /// Resolves this query into a set of known feature IDs.
309    ///
310    /// This is the entry point for iterators.
311    pub fn resolve(self) -> FeatureSet<'g> {
312        FeatureSet::new(self)
313    }
314
315    /// Resolves this query into a set of known feature IDs, using the provided resolver to
316    /// determine which links are followed.
317    pub fn resolve_with(self, resolver: impl FeatureResolver<'g>) -> FeatureSet<'g> {
318        FeatureSet::with_resolver(self, resolver)
319    }
320
321    /// Resolves this query into a set of known feature IDs, using the provided resolver function to
322    /// determine which links are followed.
323    pub fn resolve_with_fn(
324        self,
325        resolver_fn: impl FnMut(&FeatureQuery<'g>, ConditionalLink<'g>) -> bool,
326    ) -> FeatureSet<'g> {
327        self.resolve_with(ResolverFn(resolver_fn))
328    }
329
330    // ---
331    // Helper methods
332    // ---
333
334    pub(in crate::graph) fn starts_from_package_ix(
335        &self,
336        package_ix: NodeIndex<PackageIx>,
337    ) -> bool {
338        self.graph
339            .feature_ixs_for_package_ix(package_ix)
340            .any(|feature_ix| self.params.has_initial(feature_ix))
341    }
342}
343
344/// Represents whether a particular link within a feature graph should be followed during a
345/// resolve operation.
346pub trait FeatureResolver<'g> {
347    /// Returns true if this conditional link should be followed during a resolve operation.
348    fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool;
349}
350
351impl<'g, T> FeatureResolver<'g> for &mut T
352where
353    T: FeatureResolver<'g>,
354{
355    fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool {
356        (**self).accept(query, link)
357    }
358}
359
360impl<'g> FeatureResolver<'g> for Box<dyn FeatureResolver<'g> + '_> {
361    fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool {
362        (**self).accept(query, link)
363    }
364}
365
366impl<'g> FeatureResolver<'g> for &mut dyn FeatureResolver<'g> {
367    fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool {
368        (**self).accept(query, link)
369    }
370}
371
372#[derive(Clone, Debug)]
373struct ResolverFn<F>(pub F);
374
375impl<'g, F> FeatureResolver<'g> for ResolverFn<F>
376where
377    F: FnMut(&FeatureQuery<'g>, ConditionalLink<'g>) -> bool,
378{
379    fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool {
380        (self.0)(query, link)
381    }
382}