1use petgraph::graphmap::DiGraphMap;
9use petgraph::visit::Dfs;
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone)]
15pub struct PrunedCombination {
16 pub features: Vec<String>,
18 pub equivalent_to: Vec<String>,
21}
22
23#[derive(Debug)]
25pub struct PruneResult<'a> {
26 pub keep: Vec<Vec<&'a String>>,
28 pub pruned: Vec<PrunedCombination>,
30}
31
32fn build_implication_graph(features: &BTreeMap<String, Vec<String>>) -> DiGraphMap<&str, ()> {
38 let mut graph = DiGraphMap::new();
39
40 for key in features.keys() {
43 graph.add_node(key.as_str());
44 }
45
46 for (feature_name, implied_values) in features {
47 for implied in implied_values {
48 if implied.contains('/') || implied.contains(':') {
55 continue;
56 }
57 if features.contains_key(implied) && implied != feature_name {
58 graph.add_edge(feature_name.as_str(), implied.as_str(), ());
59 }
60 }
61 }
62
63 graph
64}
65
66fn resolved_set<'a>(combo: &[&'a String], graph: &DiGraphMap<&'a str, ()>) -> BTreeSet<&'a str> {
72 let mut resolved = BTreeSet::new();
73 for feature in combo {
74 if resolved.insert(feature.as_str()) {
75 let mut dfs = Dfs::new(graph, feature.as_str());
76 while let Some(reachable) = dfs.next(graph) {
77 resolved.insert(reachable);
78 }
79 }
80 }
81 resolved
82}
83
84#[must_use]
90pub fn maybe_prune<'a>(
91 combos: Vec<Vec<&'a String>>,
92 features: &'a BTreeMap<String, Vec<String>>,
93 config: &crate::config::Config,
94 no_prune: bool,
95) -> PruneResult<'a> {
96 let active = config.prune_implied && !no_prune && config.allow_feature_sets.is_empty();
100 if active {
101 prune_implied_combinations(combos, features)
102 } else {
103 PruneResult {
104 keep: combos,
105 pruned: Vec::new(),
106 }
107 }
108}
109
110fn prune_implied_combinations<'a>(
116 combos: Vec<Vec<&'a String>>,
117 features: &'a BTreeMap<String, Vec<String>>,
118) -> PruneResult<'a> {
119 let graph = build_implication_graph(features);
120
121 let mut groups: BTreeMap<BTreeSet<&'a str>, Vec<Vec<&'a String>>> = BTreeMap::new();
125 for combo in combos {
126 let resolved = resolved_set(&combo, &graph);
127 groups.entry(resolved).or_default().push(combo);
128 }
129
130 let mut keep = Vec::new();
131 let mut pruned = Vec::new();
132
133 for (_resolved, mut group) in groups {
134 group.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
139
140 let representative = group.remove(0);
141 let representative_features: Vec<String> =
142 representative.iter().copied().cloned().collect();
143
144 for redundant in group {
145 pruned.push(PrunedCombination {
146 features: redundant.iter().copied().cloned().collect(),
147 equivalent_to: representative_features.clone(),
148 });
149 }
150
151 keep.push(representative);
152 }
153
154 keep.sort();
155 pruned.sort_by(|a, b| a.features.cmp(&b.features));
156
157 PruneResult { keep, pruned }
158}
159
160#[cfg(test)]
161mod test {
162 use super::*;
163 use color_eyre::eyre;
164 use similar_asserts::assert_eq as sim_assert_eq;
165
166 fn features(pairs: &[(&str, &[&str])]) -> BTreeMap<String, Vec<String>> {
167 pairs
168 .iter()
169 .map(|(k, v)| {
170 (
171 (*k).to_string(),
172 v.iter().copied().map(String::from).collect(),
173 )
174 })
175 .collect()
176 }
177
178 fn vs(strs: &[&str]) -> Vec<String> {
179 strs.iter().copied().map(String::from).collect()
180 }
181
182 type Kept = Vec<Vec<String>>;
183 type Pruned = Vec<(Vec<String>, Vec<String>)>;
184
185 fn run_prune(feat: &BTreeMap<String, Vec<String>>) -> eyre::Result<(Kept, Pruned)> {
186 use crate::config::Config;
187 use crate::package::Package;
188
189 let mut pkg = crate::package::test::package_with_features(&[])?;
190 pkg.features = feat.clone();
191
192 let config = Config {
193 prune_implied: true,
194 ..Config::default()
195 };
196 let combos = pkg.feature_combinations(&config)?;
197 let result = prune_implied_combinations(combos, &pkg.features);
198
199 let kept: Vec<Vec<String>> = result
200 .keep
201 .iter()
202 .map(|c| c.iter().map(|s| (*s).clone()).collect())
203 .collect();
204 let pruned: Vec<(Vec<String>, Vec<String>)> = result
205 .pruned
206 .iter()
207 .map(|p| (p.features.clone(), p.equivalent_to.clone()))
208 .collect();
209 Ok((kept, pruned))
210 }
211
212 #[test]
213 fn no_implications_no_pruning() -> eyre::Result<()> {
214 let feat = features(&[("A", &[]), ("B", &[]), ("C", &[])]);
215 let (kept, pruned) = run_prune(&feat)?;
216 sim_assert_eq!(pruned.len(), 0);
217 sim_assert_eq!(kept.len(), 8); Ok(())
219 }
220
221 #[test]
222 fn simple_implication_b_implies_a() -> eyre::Result<()> {
223 let feat = features(&[("A", &[]), ("B", &["A"])]);
224 let (kept, pruned) = run_prune(&feat)?;
225
226 sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
228 sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
229 Ok(())
230 }
231
232 #[test]
233 fn transitive_chain_c_b_a() -> eyre::Result<()> {
234 let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["B"])]);
235 let (kept, pruned) = run_prune(&feat)?;
236
237 sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"]), vs(&["C"])]);
238 sim_assert_eq!(pruned.len(), 4);
239 Ok(())
240 }
241
242 #[test]
243 fn dep_syntax_ignored() -> eyre::Result<()> {
244 let feat = features(&[("A", &[]), ("B", &["dep:some_dep", "A"])]);
245 let (kept, pruned) = run_prune(&feat)?;
246
247 sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
248 sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
249 Ok(())
250 }
251
252 #[test]
253 fn slash_syntax_ignored() -> eyre::Result<()> {
254 let feat = features(&[("A", &[]), ("B", &["some-crate/feature", "A"])]);
255 let (_kept, pruned) = run_prune(&feat)?;
256
257 sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
258 Ok(())
259 }
260
261 #[test]
262 fn weak_dep_syntax_ignored() -> eyre::Result<()> {
263 let feat = features(&[("A", &[]), ("B", &["?dep:x/y", "A"])]);
264 let (_kept, pruned) = run_prune(&feat)?;
265
266 sim_assert_eq!(pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
267 Ok(())
268 }
269
270 #[test]
271 fn nonexistent_implied_feature_ignored() -> eyre::Result<()> {
272 let feat = features(&[("A", &[]), ("B", &["NonExistent"])]);
273 let (kept, pruned) = run_prune(&feat)?;
274
275 sim_assert_eq!(pruned.len(), 0);
276 sim_assert_eq!(kept.len(), 4); Ok(())
278 }
279
280 #[test]
281 fn diamond_graph() -> eyre::Result<()> {
282 let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["A"]), ("D", &["B", "C"])]);
283 let (kept, pruned) = run_prune(&feat)?;
284
285 sim_assert_eq!(
286 kept,
287 vec![
288 vs(&[]),
289 vs(&["A"]),
290 vs(&["B"]),
291 vs(&["B", "C"]),
292 vs(&["C"]),
293 vs(&["D"]),
294 ]
295 );
296 sim_assert_eq!(pruned.len(), 10);
297 Ok(())
298 }
299
300 #[test]
301 fn mutual_implication_lexicographic_tiebreak() -> eyre::Result<()> {
302 let feat = features(&[("A", &["B"]), ("B", &["A"])]);
303 let (kept, pruned) = run_prune(&feat)?;
304
305 sim_assert_eq!(kept, vec![vs(&[]), vs(&["A"])]);
306 sim_assert_eq!(pruned.len(), 2);
307 Ok(())
308 }
309
310 #[test]
311 fn resolved_set_correctness() {
312 let feat = features(&[("A", &[]), ("B", &["A"]), ("C", &["B"])]);
313 let graph = build_implication_graph(&feat);
314
315 let a = "A".to_string();
316 let b = "B".to_string();
317 let c = "C".to_string();
318
319 sim_assert_eq!(resolved_set(&[], &graph), BTreeSet::<&str>::new());
320 sim_assert_eq!(resolved_set(&[&a], &graph), BTreeSet::from(["A"]));
321 sim_assert_eq!(resolved_set(&[&b], &graph), BTreeSet::from(["A", "B"]));
322 sim_assert_eq!(resolved_set(&[&c], &graph), BTreeSet::from(["A", "B", "C"]));
323 sim_assert_eq!(resolved_set(&[&a, &b], &graph), BTreeSet::from(["A", "B"]));
324 }
325
326 #[test]
327 fn self_referencing_feature_no_crash() -> eyre::Result<()> {
328 let feat = features(&[("A", &["A"]), ("B", &[])]);
329 let (kept, pruned) = run_prune(&feat)?;
330
331 sim_assert_eq!(pruned.len(), 0);
332 sim_assert_eq!(kept.len(), 4);
333 Ok(())
334 }
335
336 #[test]
337 fn empty_features_no_pruning() {
338 let feat: BTreeMap<String, Vec<String>> = BTreeMap::new();
339 let result = prune_implied_combinations(Vec::new(), &feat);
340 sim_assert_eq!(result.keep.len(), 0);
341 sim_assert_eq!(result.pruned.len(), 0);
342 }
343}