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}