Skip to main content

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    Error, PackageId,
6    graph::{
7        DependencyDirection, FeatureIx, PackageMetadata,
8        feature::{ConditionalLink, FeatureGraph, FeatureId, FeatureLabel, FeatureSet},
9    },
10};
11use itertools::Itertools;
12use petgraph::graph::NodeIndex;
13use std::collections::HashSet;
14
15/// Trait representing whether a feature within a package should be selected.
16///
17/// This is conceptually similar to passing `--features` or other similar command-line options to
18/// Cargo.
19///
20/// Most uses will involve using one of the predefined filters: `all_filter`, `default_filter`, or
21/// `none_filter`. A customized filter can be provided either through `filter_fn` or by implementing
22/// this trait.
23pub trait FeatureFilter<'g> {
24    /// Returns true if this feature ID should be selected in the graph.
25    ///
26    /// Returning false does not prevent this feature ID from being included if it's reachable
27    /// through other means.
28    ///
29    /// In general, `accept` should return true if `feature_id.is_base()` is true.
30    ///
31    /// The feature ID is guaranteed to be in this graph, so it is OK to panic if it isn't found.
32    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool;
33}
34
35impl<'g, T> FeatureFilter<'g> for &mut T
36where
37    T: FeatureFilter<'g>,
38{
39    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
40        (**self).accept(graph, feature_id)
41    }
42}
43
44impl<'g> FeatureFilter<'g> for Box<dyn FeatureFilter<'g> + '_> {
45    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
46        (**self).accept(graph, feature_id)
47    }
48}
49
50impl<'g> FeatureFilter<'g> for &mut dyn FeatureFilter<'g> {
51    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
52        (**self).accept(graph, feature_id)
53    }
54}
55
56/// A `FeatureFilter` which calls the function that's passed in.
57#[derive(Clone, Debug)]
58pub struct FeatureFilterFn<F>(F);
59
60impl<'g, F> FeatureFilterFn<F>
61where
62    F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool,
63{
64    /// Returns a new instance of this wrapper.
65    pub fn new(f: F) -> Self {
66        FeatureFilterFn(f)
67    }
68}
69
70impl<'g, F> FeatureFilter<'g> for FeatureFilterFn<F>
71where
72    F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool,
73{
74    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
75        (self.0)(graph, feature_id)
76    }
77}
78
79/// Describes one of the standard sets of features recognized by Cargo: none, all or default.
80///
81/// `StandardFeatures` implements `FeatureFilter<'g>`, so it can be passed in as a feature filter
82/// wherever necessary.
83#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
84pub enum StandardFeatures {
85    /// No features. Equivalent to a build with `--no-default-features`.
86    None,
87
88    /// Default features. Equivalent to a standard `cargo build`.
89    Default,
90
91    /// All features. Equivalent to `cargo build --all-features`.
92    All,
93}
94
95impl StandardFeatures {
96    /// A list of all the possible values of `StandardFeatures`.
97    pub const VALUES: &'static [Self; 3] = &[
98        StandardFeatures::None,
99        StandardFeatures::Default,
100        StandardFeatures::All,
101    ];
102}
103
104impl<'g> FeatureFilter<'g> for StandardFeatures {
105    fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool {
106        match self {
107            StandardFeatures::None => {
108                // The only feature ID that should be accepted is the base one.
109                feature_id.is_base()
110            }
111            StandardFeatures::Default => {
112                // XXX it kinda sucks that we already know about the exact feature ixs but need to go
113                // through the feature ID over here. Might be worth reorganizing the code to not do that.
114                graph
115                    .is_default_feature(feature_id)
116                    .expect("feature IDs should be valid")
117            }
118            StandardFeatures::All => true,
119        }
120    }
121}
122
123/// Returns a `FeatureFilter` that selects everything from the base filter, plus these additional
124/// feature names -- regardless of what package they are in.
125///
126/// This is equivalent to a build with `--features`, and is typically meant to be used with one
127/// package.
128///
129/// For filtering by feature IDs, use `feature_id_filter`.
130pub fn named_feature_filter<'g: 'a, 'a>(
131    base: impl FeatureFilter<'g> + 'a,
132    features: impl IntoIterator<Item = &'a str>,
133) -> impl FeatureFilter<'g> + 'a {
134    let mut base = base;
135    let features: HashSet<_> = features.into_iter().collect();
136    FeatureFilterFn::new(move |feature_graph, feature_id| {
137        if base.accept(feature_graph, feature_id) {
138            return true;
139        }
140        match feature_id.label() {
141            FeatureLabel::Named(feature) => features.contains(feature),
142            _ => {
143                // This is the base feature. Assume that it has already been selected by the base
144                // filter.
145                false
146            }
147        }
148    })
149}
150
151/// Returns a `FeatureFilter` that selects everything from the base filter, plus some additional
152/// feature IDs.
153///
154/// This is a more advanced version of `feature_filter`.
155pub fn feature_id_filter<'g: 'a, 'a>(
156    base: impl FeatureFilter<'g> + 'a,
157    feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
158) -> impl FeatureFilter<'g> + 'a {
159    let mut base = base;
160    let feature_ids: HashSet<_> = feature_ids
161        .into_iter()
162        .map(|feature_id| feature_id.into())
163        .collect();
164    FeatureFilterFn::new(move |feature_graph, feature_id| {
165        base.accept(feature_graph, feature_id) || feature_ids.contains(&feature_id)
166    })
167}
168
169/// A query over a feature graph.
170///
171/// A `FeatureQuery` is the entry point for Cargo resolution, and also provides iterators over
172/// feature IDs and links. This struct is constructed through the `query_` methods on
173/// `FeatureGraph`, or through `PackageQuery::to_feature_query`.
174#[derive(Clone, Debug)]
175pub struct FeatureQuery<'g> {
176    pub(super) initials: FeatureSet<'g>,
177    pub(super) direction: DependencyDirection,
178}
179
180assert_covariant!(FeatureQuery);
181
182/// ## Queries
183///
184/// The methods in this section create queries over subsets of this feature graph. Use the methods
185/// here to analyze transitive dependencies.
186impl<'g> FeatureGraph<'g> {
187    /// Creates a new query over the entire workspace.
188    ///
189    /// `query_workspace` will select all workspace packages (subject to the provided filter) and
190    /// their transitive dependencies.
191    pub fn query_workspace(&self, filter: impl FeatureFilter<'g>) -> FeatureQuery<'g> {
192        self.package_graph
193            .query_workspace()
194            .to_feature_query(filter)
195    }
196
197    /// Creates a new query that returns transitive dependencies of the given feature IDs in the
198    /// specified direction.
199    ///
200    /// Returns an error if any feature IDs are unknown.
201    pub fn query_directed<'a>(
202        &self,
203        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
204        dep_direction: DependencyDirection,
205    ) -> Result<FeatureQuery<'g>, Error> {
206        match dep_direction {
207            DependencyDirection::Forward => self.query_forward(feature_ids),
208            DependencyDirection::Reverse => self.query_reverse(feature_ids),
209        }
210    }
211
212    /// Creates a new query that returns transitive dependencies of the given feature IDs.
213    ///
214    /// Returns an error if any feature IDs are unknown.
215    pub fn query_forward<'a>(
216        &self,
217        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
218    ) -> Result<FeatureQuery<'g>, Error> {
219        let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into());
220        let feature_ixs: Vec<_> = self.feature_ixs(feature_ids)?;
221        Ok(self.query_from_parts(feature_ixs, DependencyDirection::Forward))
222    }
223
224    /// Creates a new query that returns transitive reverse dependencies of the given feature IDs.
225    ///
226    /// Returns an error if any feature IDs are unknown.
227    pub fn query_reverse<'a>(
228        &self,
229        feature_ids: impl IntoIterator<Item = impl Into<FeatureId<'a>>>,
230    ) -> Result<FeatureQuery<'g>, Error> {
231        let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into());
232        let feature_ixs: Vec<_> = self.feature_ixs(feature_ids)?;
233        Ok(self.query_from_parts(feature_ixs, DependencyDirection::Reverse))
234    }
235
236    pub(in crate::graph) fn query_from_parts(
237        &self,
238        feature_ixs: impl IntoIterator<Item = NodeIndex<FeatureIx>>,
239        direction: DependencyDirection,
240    ) -> FeatureQuery<'g> {
241        FeatureQuery {
242            initials: FeatureSet::from_ixs(*self, feature_ixs),
243            direction,
244        }
245    }
246}
247
248impl<'g> FeatureQuery<'g> {
249    /// Returns the feature graph the query is going to be executed on.
250    pub fn graph(&self) -> &FeatureGraph<'g> {
251        self.initials.graph()
252    }
253
254    /// Returns the direction the query is happening in.
255    pub fn direction(&self) -> DependencyDirection {
256        self.direction
257    }
258
259    /// Returns the set of initial features specified in the query.
260    pub fn initials(&self) -> &FeatureSet<'g> {
261        &self.initials
262    }
263
264    /// Returns the list of initial packages specified in the query.
265    ///
266    /// The order of packages is unspecified.
267    pub fn initial_packages<'a>(&'a self) -> impl Iterator<Item = PackageMetadata<'g>> + 'a {
268        let graph = *self.graph();
269        // sorted_ixs() iterates in ascending ix order and each package's
270        // feature ixs are contiguous, so dedup() is fine.
271        self.initials
272            .sorted_ixs()
273            .map(move |feature_ix| graph.metadata_for_ix(feature_ix).package())
274            .dedup()
275    }
276
277    /// Returns true if the query starts from the given package.
278    ///
279    /// Returns an error if the package ID is unknown.
280    pub fn starts_from_package(&self, package_id: &PackageId) -> Result<bool, Error> {
281        self.initials.contains_package(package_id)
282    }
283
284    /// Resolves this query into a set of known feature IDs.
285    ///
286    /// This is the entry point for iterators.
287    ///
288    /// The result is every feature reachable from the initials, ignoring any
289    /// conditions attached to it (such as whether it is a dev-dependency or a
290    /// weak dependency). This does not determine which features Cargo would
291    /// enable in a particular build.
292    ///
293    /// To simulate a Cargo build, pass a set of workspace features to
294    /// [`FeatureSet::into_cargo_set`], then inspect
295    /// [`CargoSet::target_features`] and [`CargoSet::host_features`].
296    ///
297    /// [`CargoSet::target_features`]: crate::graph::cargo::CargoSet::target_features
298    /// [`CargoSet::host_features`]: crate::graph::cargo::CargoSet::host_features
299    pub fn resolve(self) -> FeatureSet<'g> {
300        FeatureSet::new(self)
301    }
302
303    /// Resolves this query into a set of known feature IDs, using the provided visitor to
304    /// determine which links are followed.
305    ///
306    /// The visitor can be called twice for a weak dependency feature
307    /// (`dep?/feature`). See [`FeatureLinkVisitor::visit_link`] for details.
308    ///
309    /// With a visitor that accepts every link, a reverse query returns the same
310    /// set as [`resolve`](Self::resolve). Note that (unlike with `resolve`), a
311    /// forward query can return a smaller set of features because of weak links
312    /// not being activated.
313    pub fn resolve_with(self, visitor: impl FeatureLinkVisitor<'g>) -> FeatureSet<'g> {
314        FeatureSet::with_link_visitor(self, visitor)
315    }
316
317    /// Resolves this query into a set of known feature IDs, using the provided visitor function to
318    /// determine which links are followed.
319    ///
320    /// The visitor function can be called twice for a weak dependency feature
321    /// (`dep?/feature`). See [`FeatureLinkVisitor::visit_link`] for details.
322    pub fn resolve_with_fn(
323        self,
324        visitor_fn: impl FnMut(&FeatureLinkContext<'g>, ConditionalLink<'g>) -> bool,
325    ) -> FeatureSet<'g> {
326        self.resolve_with(LinkVisitorFn(visitor_fn))
327    }
328}
329
330/// Context passed to a [`FeatureLinkVisitor`] for each link visited during a
331/// resolve operation.
332#[derive(Clone, Debug)]
333pub struct FeatureLinkContext<'g> {
334    query: FeatureQuery<'g>,
335}
336
337impl<'g> FeatureLinkContext<'g> {
338    pub(super) fn new(query: FeatureQuery<'g>) -> Self {
339        Self { query }
340    }
341
342    /// Returns the query this resolve operation was started from.
343    pub fn query(&self) -> &FeatureQuery<'g> {
344        &self.query
345    }
346
347    /// Returns the direction of the traversal.
348    pub fn direction(&self) -> DependencyDirection {
349        self.query.direction()
350    }
351
352    /// Returns true if the link's starting endpoint (`from` for forward
353    /// queries, `to` for reverse queries) is one of the query's initials.
354    pub fn starts_from_initial(&self, link: &ConditionalLink<'g>) -> bool {
355        let (start, _) = link.endpoints_in(self.direction());
356        self.query.initials.contains_ix(start.feature_ix())
357    }
358}
359
360/// Represents whether a particular link within a feature graph should be followed during a
361/// resolve operation.
362pub trait FeatureLinkVisitor<'g> {
363    /// Returns true if this conditional link should be followed during a resolve operation.
364    ///
365    /// # Weak dependency features
366    ///
367    /// Most links are visited at most once per resolve. The exception is a weak
368    /// dependency feature (`dep?/feature`) on a dependency with both required
369    /// and optional declarations. For example:
370    ///
371    /// ```toml
372    /// [dependencies]
373    /// foo = { version = "1" }
374    ///
375    /// [build-dependencies]
376    /// foo = { version = "1", optional = true }
377    ///
378    /// [features]
379    /// weak = ["foo?/std"]
380    /// ```
381    ///
382    /// Cargo applies `foo?/std` to each declaration of `foo` separately, so
383    /// this method can be called twice for the link from `weak` to `foo/std`:
384    ///
385    /// * First with a link covering the required declarations of `foo`: here,
386    ///   `[dependencies]`.
387    /// * Then with a link covering the optional ones: here,
388    ///   `[build-dependencies]`.
389    ///
390    /// Both links have the same endpoints. Use
391    /// [`ConditionalLink::declarations`] to tell them apart. The feature graph
392    /// has one edge for the two links, and that edge is followed if this method
393    /// returns true for either of them.
394    ///
395    /// A visitor that keeps state for each link, such as a count of visits,
396    /// should take this into account.
397    ///
398    /// When the two links are offered depends on the query's direction:
399    ///
400    /// * A forward query offers the `Required` link as soon as `weak` is
401    ///   reached. It offers the `Optional` link only once `dep:foo` is
402    ///   activated, which may be at an unrelated point later in the traversal.
403    /// * A reverse query offers both links (`Required` first) as soon as
404    ///   `foo/std` is reached. Reverse queries don't try to model when `foo` is
405    ///   activated.
406    fn visit_link(&mut self, cx: &FeatureLinkContext<'g>, link: ConditionalLink<'g>) -> bool;
407}
408
409impl<'g, T> FeatureLinkVisitor<'g> for &mut T
410where
411    T: FeatureLinkVisitor<'g>,
412{
413    fn visit_link(&mut self, cx: &FeatureLinkContext<'g>, link: ConditionalLink<'g>) -> bool {
414        (**self).visit_link(cx, link)
415    }
416}
417
418impl<'g> FeatureLinkVisitor<'g> for Box<dyn FeatureLinkVisitor<'g> + '_> {
419    fn visit_link(&mut self, cx: &FeatureLinkContext<'g>, link: ConditionalLink<'g>) -> bool {
420        (**self).visit_link(cx, link)
421    }
422}
423
424impl<'g> FeatureLinkVisitor<'g> for &mut dyn FeatureLinkVisitor<'g> {
425    fn visit_link(&mut self, cx: &FeatureLinkContext<'g>, link: ConditionalLink<'g>) -> bool {
426        (**self).visit_link(cx, link)
427    }
428}
429
430#[derive(Clone, Debug)]
431struct LinkVisitorFn<F>(pub F);
432
433impl<'g, F> FeatureLinkVisitor<'g> for LinkVisitorFn<F>
434where
435    F: FnMut(&FeatureLinkContext<'g>, ConditionalLink<'g>) -> bool,
436{
437    fn visit_link(&mut self, cx: &FeatureLinkContext<'g>, link: ConditionalLink<'g>) -> bool {
438        (self.0)(cx, link)
439    }
440}