Skip to main content

guppy/graph/feature/
cycles.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Code for handling cycles in feature graphs.
5
6use crate::{
7    Error,
8    graph::{
9        FeatureIx,
10        feature::{FeatureGraph, FeatureId},
11    },
12    petgraph_support::scc::Sccs,
13};
14
15/// Contains information about dependency cycles in feature graphs.
16///
17/// Cargo permits cycles if at least one of the links is dev-only. `Cycles` exposes information
18/// about such dependencies.
19///
20/// Constructed through `PackageGraph::cycles`.
21pub struct Cycles<'g> {
22    feature_graph: FeatureGraph<'g>,
23    sccs: &'g Sccs<FeatureIx>,
24}
25
26impl<'g> Cycles<'g> {
27    pub(super) fn new(feature_graph: FeatureGraph<'g>) -> Self {
28        Self {
29            feature_graph,
30            sccs: feature_graph.sccs(),
31        }
32    }
33
34    /// Returns true if `a` and `b` lie on a common directed cycle in the
35    /// feature graph.
36    ///
37    /// "Lie on a common cycle" means: in the same Strongly Connected
38    /// Component *and* that SCC is non-trivial.
39    ///
40    /// * For distinct feature IDs, that's the same as being in a multi-node SCC.
41    /// * For the same feature ID, the node must either be in a multi-node SCC,
42    ///   or have a self-loop edge in the dependency graph (e.g., from a `path`
43    ///   dev-dependency on the package's own crate).
44    ///
45    /// In particular, `is_cyclic(a, a)` is *not* reflexively true: it
46    /// returns `false` for features that aren't on any cycle.
47    pub fn is_cyclic<'a>(
48        &self,
49        a: impl Into<FeatureId<'a>>,
50        b: impl Into<FeatureId<'a>>,
51    ) -> Result<bool, Error> {
52        let a = a.into();
53        let b = b.into();
54        let a_ix = self.feature_graph.feature_ix(a)?;
55        let b_ix = self.feature_graph.feature_ix(b)?;
56
57        if a_ix != b_ix {
58            // Different features lie on a common cycle iff they're in the
59            // same SCC -- which, for distinct features, can only be a
60            // multi-node SCC.
61            return Ok(self.sccs.is_same_scc(a_ix, b_ix));
62        }
63
64        // Same feature: on a cycle iff its SCC is non-trivial.
65        Ok(
66            self.sccs.in_multi_scc(a_ix)
67                || self.feature_graph.dep_graph().contains_edge(a_ix, a_ix),
68        )
69    }
70
71    /// Returns all the cyclic Strongly Connected Components of this graph:
72    /// every multi-node SCC, plus every single-node SCC whose feature has
73    /// a self-loop edge.
74    ///
75    /// Cycles are returned in topological order: if features in cycle B
76    /// depend on features in cycle A, A is returned before B.
77    ///
78    /// Within a cycle, nodes are returned in non-dev order: if feature Foo
79    /// has a dependency on Bar, and Bar has a dev-dependency on Foo, then
80    /// Foo is returned before Bar.
81    pub fn all_cycles(&self) -> impl Iterator<Item = Vec<FeatureId<'g>>> + 'g + use<'g> {
82        let dep_graph = self.feature_graph.dep_graph();
83        let package_graph = self.feature_graph.package_graph;
84        self.sccs.all_sccs().filter_map(move |class| {
85            let is_cyclic = match class {
86                [_, _, ..] => true,
87                &[ix] => dep_graph.contains_edge(ix, ix),
88                [] => false,
89            };
90            is_cyclic.then(|| {
91                class
92                    .iter()
93                    .map(move |feature_ix| {
94                        FeatureId::from_node(package_graph, &dep_graph[*feature_ix])
95                    })
96                    .collect()
97            })
98        })
99    }
100}