1use crate::config::Config;
4use crate::print_warning;
5use crate::{DEFAULT_METADATA_KEY, find_metadata_value, pkg_metadata_section};
6use color_eyre::eyre;
7use itertools::Itertools;
8use std::collections::{BTreeMap, BTreeSet, HashSet};
9use std::fmt;
10
11const MAX_FEATURE_COMBINATIONS: u128 = 100_000;
12
13#[derive(Debug)]
15pub enum FeatureCombinationError {
16 TooManyConfigurations {
19 package: String,
21 num_features: usize,
23 num_configurations: Option<u128>,
25 limit: u128,
27 },
28}
29
30impl fmt::Display for FeatureCombinationError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::TooManyConfigurations {
34 package,
35 num_features,
36 num_configurations,
37 limit,
38 } => {
39 write!(
40 f,
41 "too many configurations for package `{package}`: {num_features} feature(s) would produce {} combinations (limit: {limit})",
42 num_configurations
43 .map(|v| v.to_string())
44 .unwrap_or_else(|| "an unbounded number of".to_string()),
45 )
46 }
47 }
48 }
49}
50
51impl std::error::Error for FeatureCombinationError {}
52
53pub trait Package {
55 fn config(&self) -> eyre::Result<Config>;
67 fn feature_combinations<'a>(&'a self, config: &'a Config)
75 -> eyre::Result<Vec<Vec<&'a String>>>;
76 fn feature_matrix(&self, config: &Config) -> eyre::Result<Vec<String>>;
83}
84
85impl Package for cargo_metadata::Package {
86 fn config(&self) -> eyre::Result<Config> {
87 let (mut config, key) = match find_metadata_value(&self.metadata) {
88 Some((value, key)) => (serde_json::from_value(value.clone())?, key),
89 None => (Config::default(), DEFAULT_METADATA_KEY),
90 };
91
92 let section = pkg_metadata_section(key);
93
94 if !config.deprecated.skip_feature_sets.is_empty() {
95 print_warning!(
96 "{section}.skip_feature_sets in package `{}` is deprecated; use exclude_feature_sets instead",
97 self.name,
98 );
99 }
100
101 if !config.deprecated.denylist.is_empty() {
102 print_warning!(
103 "{section}.denylist in package `{}` is deprecated; use exclude_features instead",
104 self.name,
105 );
106 }
107
108 if !config.deprecated.exact_combinations.is_empty() {
109 print_warning!(
110 "{section}.exact_combinations in package `{}` is deprecated; use include_feature_sets instead",
111 self.name,
112 );
113 }
114
115 config
117 .exclude_feature_sets
118 .append(&mut config.deprecated.skip_feature_sets);
119 config
120 .exclude_features
121 .extend(config.deprecated.denylist.drain());
122 config
123 .include_feature_sets
124 .append(&mut config.deprecated.exact_combinations);
125
126 Ok(config)
127 }
128
129 fn feature_combinations<'a>(
130 &'a self,
131 config: &'a Config,
132 ) -> eyre::Result<Vec<Vec<&'a String>>> {
133 if !config.allow_feature_sets.is_empty() {
141 let mut allowed = config
142 .allow_feature_sets
143 .iter()
144 .map(|proposed_allowed_set| {
145 proposed_allowed_set
148 .iter()
149 .filter_map(|maybe_feature| {
150 self.features.get_key_value(maybe_feature).map(|(k, _v)| k)
151 })
152 .collect::<BTreeSet<_>>()
153 })
154 .collect::<BTreeSet<_>>();
155
156 if config.no_empty_feature_set {
157 allowed.retain(|set| !set.is_empty());
160 }
161
162 return Ok(allowed
163 .into_iter()
164 .map(|set| set.into_iter().sorted().collect::<Vec<_>>())
165 .sorted()
166 .collect::<Vec<_>>());
167 }
168
169 let mut effective_exclude_features = config.exclude_features.clone();
183
184 if config.skip_optional_dependencies {
185 let mut implicit_features: HashSet<String> = HashSet::new();
186 let mut optional_dep_used_with_dep_syntax_outside: HashSet<String> = HashSet::new();
187
188 for (feature_name, implied) in &self.features {
193 for value in implied.iter().filter(|v| v.starts_with("dep:")) {
194 let dep_name = value.trim_start_matches("dep:");
195 if implied.len() == 1 && dep_name == feature_name {
196 implicit_features.insert(feature_name.clone());
198 } else {
199 optional_dep_used_with_dep_syntax_outside.insert(dep_name.to_string());
203 }
204 }
205 }
206
207 for dep_name in &optional_dep_used_with_dep_syntax_outside {
211 implicit_features.remove(dep_name);
212 }
213
214 effective_exclude_features.extend(implicit_features);
217 }
218
219 let base_powerset = if config.isolated_feature_sets.is_empty() {
223 generate_global_base_powerset(
224 &self.name,
225 &self.features,
226 &effective_exclude_features,
227 &config.include_features,
228 &config.only_features,
229 )?
230 } else {
231 generate_isolated_base_powerset(
232 &self.name,
233 &self.features,
234 &config.isolated_feature_sets,
235 &effective_exclude_features,
236 &config.include_features,
237 &config.only_features,
238 )?
239 };
240
241 let mut filtered_powerset = base_powerset
243 .into_iter()
244 .filter(|feature_set| {
245 !config.exclude_feature_sets.iter().any(|skip_set| {
246 if skip_set.is_empty() {
247 feature_set.is_empty()
254 } else {
255 skip_set
257 .iter()
258 .all(|skip_feature| feature_set.contains(skip_feature))
260 }
261 })
262 })
263 .collect::<BTreeSet<_>>();
264
265 for proposed_exact_combination in &config.include_feature_sets {
267 let exact_combination = proposed_exact_combination
269 .iter()
270 .filter_map(|maybe_feature| {
271 self.features.get_key_value(maybe_feature).map(|(k, _v)| k)
272 })
273 .collect::<BTreeSet<_>>();
274
275 filtered_powerset.insert(exact_combination);
277 }
278
279 if config.no_empty_feature_set {
280 filtered_powerset.retain(|set| !set.is_empty());
282 }
283
284 Ok(filtered_powerset
286 .into_iter()
287 .map(|set| set.into_iter().sorted().collect::<Vec<_>>())
288 .sorted()
289 .collect::<Vec<_>>())
290 }
291
292 fn feature_matrix(&self, config: &Config) -> eyre::Result<Vec<String>> {
293 Ok(self
294 .feature_combinations(config)?
295 .into_iter()
296 .map(|features| features.iter().join(","))
297 .collect())
298 }
299}
300
301fn checked_num_combinations(num_features: usize) -> Option<u128> {
302 if num_features >= u128::BITS as usize {
303 return None;
304 }
305 let shift: u32 = num_features.try_into().ok()?;
306 Some(1u128 << shift)
307}
308
309fn ensure_within_combination_limit(
310 package_name: &str,
311 num_features: usize,
312) -> Result<(), FeatureCombinationError> {
313 let num_configurations = checked_num_combinations(num_features);
314 let exceeds = match num_configurations {
315 Some(n) => n > MAX_FEATURE_COMBINATIONS,
316 None => true,
317 };
318
319 if exceeds {
320 return Err(FeatureCombinationError::TooManyConfigurations {
321 package: package_name.to_string(),
322 num_features,
323 num_configurations,
324 limit: MAX_FEATURE_COMBINATIONS,
325 });
326 }
327
328 Ok(())
329}
330
331fn generate_global_base_powerset<'a>(
338 package_name: &str,
339 package_features: &'a BTreeMap<String, Vec<String>>,
340 exclude_features: &HashSet<String>,
341 include_features: &'a HashSet<String>,
342 only_features: &HashSet<String>,
343) -> Result<BTreeSet<BTreeSet<&'a String>>, FeatureCombinationError> {
344 let features = package_features
345 .keys()
346 .collect::<BTreeSet<_>>()
347 .into_iter()
348 .filter(|ft| !exclude_features.contains(*ft))
349 .filter(|ft| only_features.is_empty() || only_features.contains(*ft))
350 .collect::<BTreeSet<_>>();
351
352 ensure_within_combination_limit(package_name, features.len())?;
353
354 Ok(features
355 .into_iter()
356 .powerset()
357 .map(|combination| {
358 combination
359 .into_iter()
360 .chain(include_features)
361 .collect::<BTreeSet<&'a String>>()
362 })
363 .collect())
364}
365
366fn generate_isolated_base_powerset<'a>(
374 package_name: &str,
375 package_features: &'a BTreeMap<String, Vec<String>>,
376 isolated_feature_sets: &[HashSet<String>],
377 exclude_features: &HashSet<String>,
378 include_features: &'a HashSet<String>,
379 only_features: &HashSet<String>,
380) -> Result<BTreeSet<BTreeSet<&'a String>>, FeatureCombinationError> {
381 let known_features = package_features.keys().collect::<HashSet<_>>();
383
384 let mut worst_case_total: u128 = 0;
385 for isolated_feature_set in isolated_feature_sets {
386 let num_features = isolated_feature_set
387 .iter()
388 .filter(|ft| known_features.contains(*ft))
389 .filter(|ft| !exclude_features.contains(*ft))
390 .filter(|ft| only_features.is_empty() || only_features.contains(*ft))
391 .count();
392
393 let Some(n) = checked_num_combinations(num_features) else {
394 return Err(FeatureCombinationError::TooManyConfigurations {
395 package: package_name.to_string(),
396 num_features,
397 num_configurations: None,
398 limit: MAX_FEATURE_COMBINATIONS,
399 });
400 };
401
402 worst_case_total = worst_case_total.saturating_add(n);
403 if worst_case_total > MAX_FEATURE_COMBINATIONS {
404 return Err(FeatureCombinationError::TooManyConfigurations {
405 package: package_name.to_string(),
406 num_features,
407 num_configurations: Some(worst_case_total),
408 limit: MAX_FEATURE_COMBINATIONS,
409 });
410 }
411 }
412
413 Ok(isolated_feature_sets
414 .iter()
415 .flat_map(|isolated_feature_set| {
416 isolated_feature_set
417 .iter()
418 .filter(|ft| known_features.contains(*ft)) .filter(|ft| !exclude_features.contains(*ft)) .filter(|ft| only_features.is_empty() || only_features.contains(*ft))
421 .powerset()
422 .map(|combination| {
423 combination
424 .into_iter()
425 .filter_map(|feature| known_features.get(feature).copied())
426 .chain(include_features)
427 .collect::<BTreeSet<_>>()
428 })
429 })
430 .collect())
431}
432
433#[cfg(test)]
434pub(crate) mod test {
435 use super::{FeatureCombinationError, Package};
436 use crate::config::Config;
437 use color_eyre::eyre;
438 use similar_asserts::assert_eq as sim_assert_eq;
439 use std::collections::HashSet;
440
441 static INIT: std::sync::Once = std::sync::Once::new();
442
443 fn init() {
444 INIT.call_once(|| {
445 color_eyre::install().ok();
446 });
447 }
448
449 pub(crate) fn package_with_features(
450 features: &[&str],
451 ) -> eyre::Result<cargo_metadata::Package> {
452 use cargo_metadata::{PackageBuilder, PackageId, PackageName};
453 use semver::Version;
454 use std::str::FromStr as _;
455
456 let mut package = PackageBuilder::new(
457 PackageName::from_str("test")?,
458 Version::parse("0.1.0")?,
459 PackageId {
460 repr: "test".to_string(),
461 },
462 "",
463 )
464 .build()?;
465 package.features = features
466 .iter()
467 .map(|feature| ((*feature).to_string(), vec![]))
468 .collect();
469 Ok(package)
470 }
471
472 #[test]
473 fn combinations() -> eyre::Result<()> {
474 init();
475 let package = package_with_features(&["foo-c", "foo-a", "foo-b"])?;
476 let config = Config::default();
477 let want = vec![
478 vec![],
479 vec!["foo-a"],
480 vec!["foo-a", "foo-b"],
481 vec!["foo-a", "foo-b", "foo-c"],
482 vec!["foo-a", "foo-c"],
483 vec!["foo-b"],
484 vec!["foo-b", "foo-c"],
485 vec!["foo-c"],
486 ];
487 let have = package.feature_combinations(&config)?;
488
489 sim_assert_eq!(have: have, want: want);
490 Ok(())
491 }
492
493 #[test]
494 fn combinations_only_features() -> eyre::Result<()> {
495 init();
496 let package = package_with_features(&["foo", "bar", "baz"])?;
497 let config = Config {
498 exclude_features: HashSet::from(["default".to_string()]),
499 only_features: HashSet::from(["foo".to_string(), "bar".to_string()]),
500 ..Default::default()
501 };
502
503 let want = vec![vec![], vec!["bar"], vec!["bar", "foo"], vec!["foo"]];
504 let have = package.feature_combinations(&config)?;
505
506 sim_assert_eq!(have: have, want: want);
507 Ok(())
508 }
509
510 #[test]
511 fn combinations_isolated() -> eyre::Result<()> {
512 init();
513 let package =
514 package_with_features(&["foo-a", "foo-b", "bar-b", "bar-a", "car-b", "car-a"])?;
515 let config = Config {
516 isolated_feature_sets: vec![
517 HashSet::from(["foo-a".to_string(), "foo-b".to_string()]),
518 HashSet::from(["bar-a".to_string(), "bar-b".to_string()]),
519 ],
520 ..Default::default()
521 };
522 let want = vec![
523 vec![],
524 vec!["bar-a"],
525 vec!["bar-a", "bar-b"],
526 vec!["bar-b"],
527 vec!["foo-a"],
528 vec!["foo-a", "foo-b"],
529 vec!["foo-b"],
530 ];
531 let have = package.feature_combinations(&config)?;
532
533 sim_assert_eq!(have: have, want: want);
534 Ok(())
535 }
536
537 #[test]
538 fn combinations_isolated_non_existent() -> eyre::Result<()> {
539 init();
540 let package =
541 package_with_features(&["foo-a", "foo-b", "bar-a", "bar-b", "car-a", "car-b"])?;
542 let config = Config {
543 isolated_feature_sets: vec![
544 HashSet::from(["foo-a".to_string(), "non-existent".to_string()]),
545 HashSet::from(["bar-a".to_string(), "bar-b".to_string()]),
546 ],
547 ..Default::default()
548 };
549 let want = vec![
550 vec![],
551 vec!["bar-a"],
552 vec!["bar-a", "bar-b"],
553 vec!["bar-b"],
554 vec!["foo-a"],
555 ];
556 let have = package.feature_combinations(&config)?;
557
558 sim_assert_eq!(have: have, want: want);
559 Ok(())
560 }
561
562 #[test]
563 fn combinations_isolated_denylist() -> eyre::Result<()> {
564 init();
565 let package =
566 package_with_features(&["foo-a", "foo-b", "bar-b", "bar-a", "car-a", "car-b"])?;
567 let config = Config {
568 isolated_feature_sets: vec![
569 HashSet::from(["foo-a".to_string(), "foo-b".to_string()]),
570 HashSet::from(["bar-a".to_string(), "bar-b".to_string()]),
571 ],
572 exclude_features: HashSet::from(["bar-a".to_string()]),
573 ..Default::default()
574 };
575 let want = vec![
576 vec![],
577 vec!["bar-b"],
578 vec!["foo-a"],
579 vec!["foo-a", "foo-b"],
580 vec!["foo-b"],
581 ];
582 let have = package.feature_combinations(&config)?;
583
584 sim_assert_eq!(have: have, want: want);
585 Ok(())
586 }
587
588 #[test]
589 fn combinations_isolated_non_existent_denylist() -> eyre::Result<()> {
590 init();
591 let package =
592 package_with_features(&["foo-b", "foo-a", "bar-a", "bar-b", "car-a", "car-b"])?;
593 let config = Config {
594 isolated_feature_sets: vec![
595 HashSet::from(["foo-a".to_string(), "non-existent".to_string()]),
596 HashSet::from(["bar-a".to_string(), "bar-b".to_string()]),
597 ],
598 exclude_features: HashSet::from(["bar-a".to_string()]),
599 ..Default::default()
600 };
601 let want = vec![vec![], vec!["bar-b"], vec!["foo-a"]];
602 let have = package.feature_combinations(&config)?;
603
604 sim_assert_eq!(have: have, want: want);
605 Ok(())
606 }
607
608 #[test]
609 fn combinations_isolated_non_existent_denylist_exact() -> eyre::Result<()> {
610 init();
611 let package =
612 package_with_features(&["foo-a", "foo-b", "bar-a", "bar-b", "car-a", "car-b"])?;
613 let config = Config {
614 isolated_feature_sets: vec![
615 HashSet::from(["foo-a".to_string(), "non-existent".to_string()]),
616 HashSet::from(["bar-a".to_string(), "bar-b".to_string()]),
617 ],
618 exclude_features: HashSet::from(["bar-a".to_string()]),
619 include_feature_sets: vec![HashSet::from([
620 "car-a".to_string(),
621 "bar-a".to_string(),
622 "non-existent".to_string(),
623 ])],
624 ..Default::default()
625 };
626 let want = vec![vec![], vec!["bar-a", "car-a"], vec!["bar-b"], vec!["foo-a"]];
627 let have = package.feature_combinations(&config)?;
628
629 sim_assert_eq!(have: have, want: want);
630 Ok(())
631 }
632
633 #[test]
634 fn combinations_allow_feature_sets_exact() -> eyre::Result<()> {
635 init();
636 let package = package_with_features(&["hydrate", "ssr", "other"])?;
637 let config = Config {
638 allow_feature_sets: vec![
639 HashSet::from(["ssr".to_string()]),
640 HashSet::from(["hydrate".to_string()]),
641 ],
642 ..Default::default()
643 };
644
645 let want = vec![vec!["hydrate"], vec!["ssr"]];
646 let have = package.feature_combinations(&config)?;
647
648 sim_assert_eq!(have: have, want: want);
649 Ok(())
650 }
651
652 #[test]
653 fn combinations_allow_feature_sets_ignores_other_options() -> eyre::Result<()> {
654 init();
655 let package = package_with_features(&["hydrate", "ssr"])?;
656 let config = Config {
657 allow_feature_sets: vec![HashSet::from(["hydrate".to_string()])],
658 exclude_features: HashSet::from(["hydrate".to_string()]),
659 exclude_feature_sets: vec![HashSet::from(["hydrate".to_string()])],
660 include_feature_sets: vec![HashSet::from(["ssr".to_string()])],
661 only_features: HashSet::from(["ssr".to_string()]),
662 ..Default::default()
663 };
664
665 let want = vec![vec!["hydrate"]];
666 let have = package.feature_combinations(&config)?;
667
668 sim_assert_eq!(have: have, want: want);
669 Ok(())
670 }
671
672 #[test]
673 fn combinations_no_empty_feature_set_filters_generated_empty() -> eyre::Result<()> {
674 init();
675 let package = package_with_features(&["foo", "bar"])?;
676 let config = Config {
677 no_empty_feature_set: true,
678 ..Default::default()
679 };
680
681 let want = vec![vec!["bar"], vec!["bar", "foo"], vec!["foo"]];
682 let have = package.feature_combinations(&config)?;
683
684 sim_assert_eq!(have: have, want: want);
685 Ok(())
686 }
687
688 #[test]
689 fn combinations_no_empty_feature_set_filters_included_empty() -> eyre::Result<()> {
690 init();
691 let package = package_with_features(&["foo"])?;
692 let config = Config {
693 include_feature_sets: vec![HashSet::new()],
694 no_empty_feature_set: true,
695 ..Default::default()
696 };
697
698 let want = vec![vec!["foo"]];
699 let have = package.feature_combinations(&config)?;
700
701 sim_assert_eq!(have: have, want: want);
702 Ok(())
703 }
704
705 #[test]
706 fn combinations_exclude_empty_feature_set_only() -> eyre::Result<()> {
707 init();
708 let package = package_with_features(&["foo", "bar"])?;
709 let config = Config {
710 exclude_feature_sets: vec![HashSet::new()],
711 ..Default::default()
712 };
713
714 let want = vec![vec!["bar"], vec!["bar", "foo"], vec!["foo"]];
715 let have = package.feature_combinations(&config)?;
716
717 sim_assert_eq!(have: have, want: want);
718 Ok(())
719 }
720
721 #[test]
722 fn too_many_feature_configurations() -> eyre::Result<()> {
723 init();
724 let features: Vec<String> = (0..25).map(|i| format!("f{i}")).collect();
725 let feature_refs: Vec<&str> = features.iter().map(String::as_str).collect();
726 let package = package_with_features(&feature_refs)?;
727
728 let config = Config::default();
729 let Err(err) = package.feature_combinations(&config) else {
730 eyre::bail!("expected too-many-configurations error");
731 };
732 let Some(err) = err.downcast_ref::<FeatureCombinationError>() else {
733 eyre::bail!("expected FeatureCombinationError");
734 };
735 assert!(
736 err.to_string().contains("too many configurations"),
737 "expected 'too many configurations' error, got: {err}"
738 );
739 Ok(())
740 }
741
742 pub(crate) fn package_with_metadata(
744 features: &[&str],
745 metadata_key: &str,
746 config: &serde_json::Value,
747 ) -> eyre::Result<cargo_metadata::Package> {
748 let mut package = package_with_features(features)?;
749 package.metadata = serde_json::json!({ metadata_key: config });
750 Ok(package)
751 }
752
753 #[test]
754 fn config_from_cargo_fc_alias() -> eyre::Result<()> {
755 init();
756 let package = package_with_metadata(
757 &["foo", "bar"],
758 "cargo-fc",
759 &serde_json::json!({ "exclude_features": ["foo"] }),
760 )?;
761 let config = package.config()?;
762 assert!(config.exclude_features.contains("foo"));
763 assert!(!config.exclude_features.contains("bar"));
764 Ok(())
765 }
766
767 #[test]
768 fn config_from_fc_alias() -> eyre::Result<()> {
769 init();
770 let package = package_with_metadata(
771 &["foo", "bar"],
772 "fc",
773 &serde_json::json!({ "exclude_features": ["bar"] }),
774 )?;
775 let config = package.config()?;
776 assert!(config.exclude_features.contains("bar"));
777 assert!(!config.exclude_features.contains("foo"));
778 Ok(())
779 }
780
781 #[test]
782 fn config_from_feature_combinations_alias() -> eyre::Result<()> {
783 init();
784 let package = package_with_metadata(
785 &["a", "b"],
786 "feature-combinations",
787 &serde_json::json!({ "no_empty_feature_set": true }),
788 )?;
789 let config = package.config()?;
790 assert!(config.no_empty_feature_set);
791 Ok(())
792 }
793
794 #[test]
795 fn config_from_cargo_feature_combinations_alias() -> eyre::Result<()> {
796 init();
797 let package = package_with_metadata(
798 &["a", "b"],
799 "cargo-feature-combinations",
800 &serde_json::json!({ "exclude_features": ["a"] }),
801 )?;
802 let config = package.config()?;
803 assert!(config.exclude_features.contains("a"));
804 Ok(())
805 }
806
807 #[test]
808 fn config_default_when_no_metadata() -> eyre::Result<()> {
809 init();
810 let package = package_with_features(&["foo"])?;
811 let config = package.config()?;
812 assert!(config.exclude_features.is_empty());
813 assert!(!config.no_empty_feature_set);
814 Ok(())
815 }
816
817 #[test]
818 fn config_alias_affects_feature_matrix() -> eyre::Result<()> {
819 init();
820 let package = package_with_metadata(
821 &["foo", "bar"],
822 "cargo-fc",
823 &serde_json::json!({ "exclude_features": ["foo"] }),
824 )?;
825 let config = package.config()?;
826 let matrix = package.feature_combinations(&config)?;
827
828 assert!(
830 !matrix.iter().any(|combo| combo.iter().any(|f| *f == "foo")),
831 "expected no combination to contain 'foo', got: {matrix:?}"
832 );
833 assert!(
835 matrix.iter().any(|combo| combo.iter().any(|f| *f == "bar")),
836 "expected 'bar' in at least one combination, got: {matrix:?}"
837 );
838 Ok(())
839 }
840}