use petgraph::graphmap::DiGraphMap;
use petgraph::visit::Dfs;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)]
pub struct PrunedCombination {
pub features: Vec<String>,
pub equivalent_to: Vec<String>,
}
#[derive(Debug)]
pub struct PruneResult<'a> {
pub keep: Vec<Vec<&'a String>>,
pub pruned: Vec<PrunedCombination>,
}
fn build_implication_graph(features: &BTreeMap<String, Vec<String>>) -> DiGraphMap<&str, ()> {
let mut graph = DiGraphMap::new();
for key in features.keys() {
graph.add_node(key.as_str());
}
for (feature_name, implied_values) in features {
for implied in implied_values {
if implied.contains('/') || implied.contains(':') {
continue;
}
if features.contains_key(implied) && implied != feature_name {
graph.add_edge(feature_name.as_str(), implied.as_str(), ());
}
}
}
graph
}
fn resolved_set<'a>(combo: &[&'a String], graph: &DiGraphMap<&'a str, ()>) -> BTreeSet<&'a str> {
let mut resolved = BTreeSet::new();
for feature in combo {
if resolved.insert(feature.as_str()) {
let mut dfs = Dfs::new(graph, feature.as_str());
while let Some(reachable) = dfs.next(graph) {
resolved.insert(reachable);
}
}
}
resolved
}
#[must_use]
pub fn maybe_prune<'a>(
combos: Vec<Vec<&'a String>>,
features: &'a BTreeMap<String, Vec<String>>,
config: &crate::config::Config,
no_prune: bool,
) -> PruneResult<'a> {
let active = config.prune_implied && !no_prune && config.allow_feature_sets.is_empty();
if active {
prune_implied_combinations(combos, features)
} else {
PruneResult {
keep: combos,
pruned: Vec::new(),
}
}
}
fn prune_implied_combinations<'a>(
combos: Vec<Vec<&'a String>>,
features: &'a BTreeMap<String, Vec<String>>,
) -> PruneResult<'a> {
let graph = build_implication_graph(features);
let mut groups: BTreeMap<BTreeSet<&'a str>, Vec<Vec<&'a String>>> = BTreeMap::new();
for combo in combos {
let resolved = resolved_set(&combo, &graph);
groups.entry(resolved).or_default().push(combo);
}
let mut keep = Vec::new();
let mut pruned = Vec::new();
for (_resolved, mut group) in groups {
group.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
let representative = group.remove(0);
let representative_features: Vec<String> =
representative.iter().copied().cloned().collect();
for redundant in group {
pruned.push(PrunedCombination {
features: redundant.iter().copied().cloned().collect(),
equivalent_to: representative_features.clone(),
});
}
keep.push(representative);
}
keep.sort();
pruned.sort_by(|a, b| a.features.cmp(&b.features));
PruneResult { keep, pruned }
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::eyre;
use similar_asserts::assert_eq as sim_assert_eq;
fn features(pairs: &[(&str, &[&str])]) -> BTreeMap<String, Vec<String>> {
pairs
.iter()
.map(|(k, v)| {
(
(*k).to_string(),
v.iter().copied().map(String::from).collect(),
)
})
.collect()
}
fn vs(strs: &[&str]) -> Vec<String> {
strs.iter().copied().map(String::from).collect()
}
type Kept = Vec<Vec<String>>;
type Pruned = Vec<(Vec<String>, Vec<String>)>;
fn run_prune(feat: &BTreeMap<String, Vec<String>>) -> eyre::Result<(Kept, Pruned)> {
use crate::config::Config;
use crate::package::Package;
let mut pkg = crate::package::test::package_with_features(&[])?;
pkg.features = feat.clone();
let config = Config {
prune_implied: true,
..Config::default()
};
let combos = pkg.feature_combinations(&config)?;
let result = prune_implied_combinations(combos, &pkg.features);
let kept: Vec<Vec<String>> = result
.keep
.iter()
.map(|c| c.iter().map(|s| (*s).clone()).collect())
.collect();
let pruned: Vec<(Vec<String>, Vec<String>)> = result
.pruned
.iter()
.map(|p| (p.features.clone(), p.equivalent_to.clone()))
.collect();
Ok((kept, pruned))
}
#[test]
fn no_implications_no_pruning() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &[]), ("C", &[])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(pruned.len(), 0);
sim_assert_eq!(kept.len(), 8); Ok(())
}
#[test]
fn simple_implication_b_implies_a() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["A"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
Ok(())
}
#[test]
fn transitive_chain_c_b_a() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["B"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"]), vs(&["C"])]);
sim_assert_eq!(pruned.len(), 4);
Ok(())
}
#[test]
fn dep_syntax_ignored() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["dep:some_dep", "A"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
Ok(())
}
#[test]
fn slash_syntax_ignored() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["some-crate/feature", "A"])]);
let (_kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
Ok(())
}
#[test]
fn weak_dep_syntax_ignored() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["?dep:x/y", "A"])]);
let (_kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
Ok(())
}
#[test]
fn nonexistent_implied_feature_ignored() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["NonExistent"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(pruned.len(), 0);
sim_assert_eq!(kept.len(), 4); Ok(())
}
#[test]
fn diamond_graph() -> eyre::Result<()> {
let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["A"]), ("D", &["B", "C"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(
kept,
vec![
vs(&[]),
vs(&["A"]),
vs(&["B"]),
vs(&["B", "C"]),
vs(&["C"]),
vs(&["D"]),
]
);
sim_assert_eq!(pruned.len(), 10);
Ok(())
}
#[test]
fn mutual_implication_lexicographic_tiebreak() -> eyre::Result<()> {
let feat = features(&[("A", &["B"]), ("B", &["A"])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"])]);
sim_assert_eq!(pruned.len(), 2);
Ok(())
}
#[test]
fn resolved_set_correctness() {
let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["B"])]);
let graph = build_implication_graph(&feat);
let a = "A".to_string();
let b = "B".to_string();
let c = "C".to_string();
sim_assert_eq!(resolved_set(&[], &graph), BTreeSet::<&str>::new());
sim_assert_eq!(resolved_set(&[&a], &graph), BTreeSet::from(["A"]));
sim_assert_eq!(resolved_set(&[&b], &graph), BTreeSet::from(["A", "B"]));
sim_assert_eq!(resolved_set(&[&c], &graph), BTreeSet::from(["A", "B", "C"]));
sim_assert_eq!(resolved_set(&[&a, &b], &graph), BTreeSet::from(["A", "B"]));
}
#[test]
fn self_referencing_feature_no_crash() -> eyre::Result<()> {
let feat = features(&[("A", &["A"]), ("B", &[])]);
let (kept, pruned) = run_prune(&feat)?;
sim_assert_eq!(pruned.len(), 0);
sim_assert_eq!(kept.len(), 4);
Ok(())
}
#[test]
fn empty_features_no_pruning() {
let feat: BTreeMap<String, Vec<String>> = BTreeMap::new();
let result = prune_implied_combinations(Vec::new(), &feat);
sim_assert_eq!(result.keep.len(), 0);
sim_assert_eq!(result.pruned.len(), 0);
}
}