use crate::cargo::manifest_analyzer::DepKind;
use crate::cargo::multi_target_metadata::ComputedMsrv;
use rustc_hash::FxHashMap;
use semver::VersionReq;
use std::collections::{BTreeMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct UnifiedDep {
pub name: Arc<str>,
pub version_req: VersionReq,
pub features: Vec<Arc<str>>,
pub default_features: bool,
pub used_by: Vec<Arc<str>>,
pub target: Option<Arc<str>>,
pub path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub enum MemberEdit {
UseWorkspace {
dep_name: Arc<str>,
dep_kind: DepKind,
target: Option<Arc<str>>,
local_features: Vec<Arc<str>>,
is_optional: bool,
},
RemoveDep {
dep_name: Arc<str>,
dep_kind: DepKind,
target: Option<Arc<str>>,
},
RemoveFeature {
feature_name: Arc<str>,
},
AddFeatures {
dep_name: Arc<str>,
dep_kind: DepKind,
target: Option<Arc<str>>,
features_to_add: Vec<Arc<str>>,
},
EnforceMsrvInheritance,
}
#[derive(Debug, Clone)]
pub struct UnifyIssue {
pub kind: UnifyIssueKind,
pub dep_name: Arc<str>,
pub severity: IssueSeverity,
pub message: Arc<str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnifyIssueKind {
General,
WorkspaceMemberCohortSplitRisk,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueSeverity {
Error,
Warning,
}
#[derive(Debug)]
pub struct ValidationResult {
pub target: Arc<str>,
pub success: bool,
pub error: Option<Arc<str>>,
}
#[derive(Debug, Clone)]
pub struct DuplicateCleanup {
pub dep_name: Arc<str>,
pub versions_found: Vec<Arc<str>>,
pub selected_version: Arc<str>,
}
#[derive(Debug, Clone)]
pub struct PrunedFeature {
pub crate_name: Arc<str>,
pub feature_name: Arc<str>,
}
#[derive(Debug, Clone)]
pub struct OptionalFeature {
pub crate_name: Arc<str>,
pub feature_name: Arc<str>,
pub enables: Vec<Arc<str>>,
}
#[derive(Debug, Clone)]
pub struct VersionMismatch {
pub member: Arc<str>,
pub dep_name: Arc<str>,
pub member_version: Arc<str>,
pub workspace_version: Arc<str>,
}
#[derive(Debug, Clone)]
pub struct UnusedDep {
pub member: Arc<str>,
pub dep_name: Arc<str>,
pub kind: DepKind,
pub reason: UnusedReason,
}
#[derive(Debug, Clone)]
pub enum UnusedReason {
NotUsedInSource,
NotInResolvedGraph,
TargetConfiguredButNotResolved {
target_cfg: Arc<str>,
},
}
#[derive(Debug, Clone)]
pub struct TransitivePin {
pub name: Arc<str>,
pub version: semver::Version,
pub features: Vec<Arc<str>>,
}
#[derive(Debug, Clone)]
pub struct UndeclaredFeature {
pub member: Arc<str>,
pub dep_name: Arc<str>,
pub undeclared_features: Vec<Arc<str>>,
pub declared_features: Vec<Arc<str>>,
pub resolved_features: Vec<Arc<str>>,
pub dep_kind: DepKind,
pub target: Option<Arc<str>>,
pub borrowed_from: Vec<Arc<str>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnifyDecisionSubject {
WorkspaceDependency,
SkippedDependency,
TransitivePin,
UndeclaredFeatureFix,
}
impl UnifyDecisionSubject {
pub fn as_str(self) -> &'static str {
match self {
Self::WorkspaceDependency => "workspace_dependency",
Self::SkippedDependency => "skipped_dependency",
Self::TransitivePin => "transitive_pin",
Self::UndeclaredFeatureFix => "undeclared_feature_fix",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnifyDecisionCode {
FeatureIntersection,
FeatureUnion,
ExactPinPreserved,
ExactPinWarnCaret,
ExactPinSkipped,
CohortEnforced,
TransitivePin,
UndeclaredFeatureFix,
}
impl UnifyDecisionCode {
pub fn as_str(self) -> &'static str {
match self {
Self::FeatureIntersection => "intersection",
Self::FeatureUnion => "union",
Self::ExactPinPreserved => "exact_pin_preserved",
Self::ExactPinWarnCaret => "exact_pin_warn_caret",
Self::ExactPinSkipped => "exact_pin_skipped",
Self::CohortEnforced => "cohort_enforced",
Self::TransitivePin => "transitive_pin",
Self::UndeclaredFeatureFix => "undeclared_feature_fix",
}
}
}
#[derive(Debug, Clone)]
pub struct UnifyDecisionReason {
pub code: UnifyDecisionCode,
pub summary: Arc<str>,
pub features: Vec<Arc<str>>,
pub members: Vec<Arc<str>>,
pub borrowed_from: Vec<Arc<str>>,
}
#[derive(Debug, Clone)]
pub struct UnifyDecision {
pub dep_name: Arc<str>,
pub subject: UnifyDecisionSubject,
pub member: Option<Arc<str>>,
pub target: Option<Arc<str>>,
pub reasons: Vec<UnifyDecisionReason>,
}
#[derive(Debug)]
pub struct UnificationPlan {
pub workspace_deps: Vec<UnifiedDep>,
pub member_edits: FxHashMap<Arc<str>, Vec<MemberEdit>>,
pub member_paths: FxHashMap<Arc<str>, PathBuf>,
pub transitive_pins: Vec<TransitivePin>,
pub validation_results: Vec<ValidationResult>,
pub issues: Vec<UnifyIssue>,
pub computed_msrv: Option<ComputedMsrv>,
pub duplicates_cleaned: Vec<DuplicateCleanup>,
pub pruned_features: Vec<PrunedFeature>,
pub optional_features: Vec<OptionalFeature>,
pub version_mismatches: Vec<VersionMismatch>,
pub unused_deps: Vec<UnusedDep>,
pub undeclared_features: Vec<UndeclaredFeature>,
pub dependency_decisions: Vec<UnifyDecision>,
}
impl UnificationPlan {
pub fn has_blocking_issues(&self) -> bool {
self.issues.iter().any(|i| i.severity == IssueSeverity::Error)
}
pub fn member_edit_count(&self) -> usize {
self.member_edits.values().map(|v| v.len()).sum()
}
pub fn has_planned_changes(&self, msrv_write_needed: bool) -> bool {
!self.workspace_deps.is_empty()
|| self.member_edit_count() > 0
|| !self.transitive_pins.is_empty()
|| msrv_write_needed
}
pub fn summary(&self) -> String {
let mut s = String::new();
s.push_str("=== Unification Plan ===\n\n");
if !self.workspace_deps.is_empty() {
s.push_str(&format!("Dependencies to unify: {}\n", self.workspace_deps.len()));
for dep in &self.workspace_deps {
s.push_str(&format!(" - {} = \"{}\"", dep.name, dep.version_req));
if !dep.features.is_empty() {
let feats: Vec<&str> = dep.features.iter().map(|f| &**f).collect();
s.push_str(&format!(", features = [{}]", feats.join(", ")));
}
if !dep.default_features {
s.push_str(", default-features = false");
}
s.push_str(&format!(" (used by {} crates)\n", dep.used_by.len()));
}
s.push('\n');
}
if !self.member_edits.is_empty() {
let mut converted_deps: HashSet<Arc<str>> = HashSet::new();
let mut removed_deps: HashSet<Arc<str>> = HashSet::new();
let mut removed_features: HashSet<Arc<str>> = HashSet::new();
let mut features_added_count = 0usize;
let mut features_added_crates: HashSet<Arc<str>> = HashSet::new();
let mut msrv_inheritance_crates: HashSet<Arc<str>> = HashSet::new();
for (member_name, edits) in &self.member_edits {
for edit in edits {
match edit {
MemberEdit::UseWorkspace { dep_name, .. } => {
converted_deps.insert(Arc::clone(dep_name));
}
MemberEdit::RemoveDep { dep_name, .. } => {
removed_deps.insert(Arc::clone(dep_name));
}
MemberEdit::RemoveFeature { feature_name } => {
removed_features.insert(Arc::clone(feature_name));
}
MemberEdit::AddFeatures { features_to_add, .. } => {
features_added_count += features_to_add.len();
features_added_crates.insert(Arc::clone(member_name));
}
MemberEdit::EnforceMsrvInheritance => {
msrv_inheritance_crates.insert(Arc::clone(member_name));
}
}
}
}
let workspace_dep_names: HashSet<Arc<str>> = self.workspace_deps.iter().map(|d| Arc::clone(&d.name)).collect();
let conversion_only: Vec<_> = converted_deps.difference(&workspace_dep_names).collect();
if !conversion_only.is_empty() {
s.push_str(&format!(
"Dependencies to convert to workspace inheritance: {}\n",
conversion_only.len()
));
for dep_name in conversion_only {
s.push_str(&format!(" - {} (already in workspace.dependencies)\n", dep_name));
}
s.push('\n');
}
if !removed_deps.is_empty() {
s.push_str(&format!("Dependencies to remove (unused): {}\n", removed_deps.len()));
for dep_name in &removed_deps {
s.push_str(&format!(" - {}\n", dep_name));
}
s.push('\n');
}
if features_added_count > 0 {
s.push_str(&format!(
"Undeclared features to fix: {} features across {} crates\n",
features_added_count,
features_added_crates.len()
));
s.push('\n');
}
if !msrv_inheritance_crates.is_empty() {
s.push_str(&format!(
"MSRV inheritance to enforce: {} crates\n",
msrv_inheritance_crates.len()
));
s.push('\n');
}
}
s.push_str(&format!("Member edits: {}\n", self.member_edits.len()));
s.push_str(&format!("Transitive pins: {}\n", self.transitive_pins.len()));
if !self.issues.is_empty() {
s.push_str(&format!("\nIssues requiring attention: {}\n", self.issues.len()));
for issue in &self.issues {
let kind_suffix = if issue.kind == UnifyIssueKind::General {
""
} else {
" [WorkspaceMemberCohortSplitRisk]"
};
s.push_str(&format!(
" - [{}]{} {}: {}\n",
if issue.severity == IssueSeverity::Error {
"ERROR"
} else {
"WARN"
},
kind_suffix,
issue.dep_name,
issue.message
));
}
}
if !self.validation_results.is_empty() {
let failed = self.validation_results.iter().filter(|v| !v.success).count();
if failed > 0 {
s.push_str(&format!("\n {} target validations failed\n", failed));
}
}
if let Some(ref msrv) = self.computed_msrv {
s.push_str(&format!(
"\nComputed MSRV: {} (from {} deps with rust-version)\n",
msrv.version, msrv.deps_with_msrv
));
if let Some(ref warning) = msrv.warning {
s.push_str(&format!(" Warning: {}\n", warning));
}
if !msrv.contributors.is_empty() {
let contributors_str = if msrv.contributors.len() > 3 {
format!(
"{}, ... ({} total)",
msrv.contributors[..3].join(", "),
msrv.contributors.len()
)
} else {
msrv.contributors.join(", ")
};
s.push_str(&format!(" Contributors: {}\n", contributors_str));
}
}
if !self.duplicates_cleaned.is_empty() {
s.push_str(&format!(
"\nDuplicate versions unified: {}\n",
self.duplicates_cleaned.len()
));
for dup in &self.duplicates_cleaned {
s.push_str(&format!(
" - {} -> {} (was: {})\n",
dup.dep_name,
dup.selected_version,
dup.versions_found.join(", ")
));
}
}
if !self.pruned_features.is_empty() {
s.push_str(&format!(
"\nDead features (empty no-ops, safe to prune): {}\n",
self.pruned_features.len()
));
let mut by_crate: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for pf in &self.pruned_features {
by_crate.entry(&pf.crate_name).or_default().push(&pf.feature_name);
}
for (crate_name, mut features) in by_crate {
features.sort_unstable();
s.push_str(&format!(" - {}: {}\n", crate_name, features.join(", ")));
}
}
if !self.optional_features.is_empty() {
s.push_str(&format!(
"\nOptional features (user-facing API, NOT removed): {}\n",
self.optional_features.len()
));
let mut by_crate: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for of in &self.optional_features {
by_crate.entry(&of.crate_name).or_default().push(&of.feature_name);
}
for (crate_name, mut features) in by_crate {
features.sort_unstable();
s.push_str(&format!(" - {}: {}\n", crate_name, features.join(", ")));
}
}
if !self.version_mismatches.is_empty() {
s.push_str(&format!(
"\n Version mismatches with workspace.dependencies: {}\n",
self.version_mismatches.len()
));
for mismatch in &self.version_mismatches {
s.push_str(&format!(
" - {} in {}: declares \"{}\" but workspace has \"{}\"\n",
mismatch.dep_name, mismatch.member, mismatch.member_version, mismatch.workspace_version
));
}
}
if !self.unused_deps.is_empty() {
s.push_str(&format!(
"\n Unused dependencies detected: {}\n",
self.unused_deps.len()
));
let mut by_member: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for ud in &self.unused_deps {
by_member.entry(&ud.member).or_default().push(&ud.dep_name);
}
for (member, mut deps) in by_member {
deps.sort_unstable();
s.push_str(&format!(" - {}: {}\n", member, deps.join(", ")));
}
}
let has_feature_fixes = self
.member_edits
.values()
.any(|edits| edits.iter().any(|e| matches!(e, MemberEdit::AddFeatures { .. })));
if !self.undeclared_features.is_empty() && !has_feature_fixes {
s.push_str(&format!(
"\n⚠️ Undeclared features detected (will break standalone builds): {}\n",
self.undeclared_features.len()
));
for uf in &self.undeclared_features {
let borrowed_from_str = if uf.borrowed_from.is_empty() {
String::new()
} else {
format!(" (borrowed from {})", uf.borrowed_from.join(", "))
};
s.push_str(&format!(
" - {}/{}: [{}]{}\n",
uf.member,
uf.dep_name,
uf.undeclared_features.join(", "),
borrowed_from_str
));
}
s.push_str(" These crates rely on feature unification from other workspace members.\n");
s.push_str(" After unification, standalone builds (cargo test -p <crate>) will fail.\n");
s.push_str(" Fix: Set fix_undeclared_features = true in rail.toml to auto-fix.\n");
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
fn arc(s: &str) -> Arc<str> {
Arc::from(s)
}
#[test]
fn test_member_edit_add_features_struct() {
let edit = MemberEdit::AddFeatures {
dep_name: arc("serde"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("derive"), arc("std")],
};
match edit {
MemberEdit::AddFeatures {
dep_name,
dep_kind,
target,
features_to_add,
} => {
assert_eq!(&*dep_name, "serde");
assert_eq!(dep_kind, DepKind::Normal);
assert!(target.is_none());
assert_eq!(features_to_add.len(), 2);
assert!(features_to_add.iter().any(|f| &**f == "derive"));
}
_ => panic!("Expected AddFeatures variant"),
}
}
#[test]
fn test_member_edit_add_features_with_target() {
let edit = MemberEdit::AddFeatures {
dep_name: arc("libc"),
dep_kind: DepKind::Normal,
target: Some(arc("cfg(unix)")),
features_to_add: vec![arc("extra_traits")],
};
match edit {
MemberEdit::AddFeatures { target, .. } => {
assert_eq!(target.as_deref(), Some("cfg(unix)"));
}
_ => panic!("Expected AddFeatures variant"),
}
}
#[test]
fn test_member_edit_add_features_dev_dependency() {
let edit = MemberEdit::AddFeatures {
dep_name: arc("tokio"),
dep_kind: DepKind::Dev,
target: None,
features_to_add: vec![arc("rt-multi-thread"), arc("macros")],
};
match edit {
MemberEdit::AddFeatures { dep_kind, .. } => {
assert_eq!(dep_kind, DepKind::Dev);
}
_ => panic!("Expected AddFeatures variant"),
}
}
#[test]
fn test_member_edit_add_features_build_dependency() {
let edit = MemberEdit::AddFeatures {
dep_name: arc("cc"),
dep_kind: DepKind::Build,
target: None,
features_to_add: vec![arc("parallel")],
};
match edit {
MemberEdit::AddFeatures { dep_kind, .. } => {
assert_eq!(dep_kind, DepKind::Build);
}
_ => panic!("Expected AddFeatures variant"),
}
}
#[test]
fn test_undeclared_feature_struct() {
let uf = UndeclaredFeature {
member: arc("my-crate"),
dep_name: arc("backoff"),
undeclared_features: vec![arc("futures"), arc("tokio")],
declared_features: vec![arc("default")],
resolved_features: vec![arc("default"), arc("futures"), arc("tokio")],
dep_kind: DepKind::Normal,
target: None,
borrowed_from: vec![arc("other-crate")],
};
assert_eq!(&*uf.member, "my-crate");
assert_eq!(&*uf.dep_name, "backoff");
assert_eq!(uf.undeclared_features.len(), 2);
assert_eq!(uf.declared_features.len(), 1);
assert_eq!(uf.resolved_features.len(), 3);
assert_eq!(uf.dep_kind, DepKind::Normal);
assert!(uf.target.is_none());
assert_eq!(uf.borrowed_from.len(), 1);
}
#[test]
fn test_undeclared_feature_with_target() {
let uf = UndeclaredFeature {
member: arc("platform-crate"),
dep_name: arc("libc"),
undeclared_features: vec![arc("extra_traits")],
declared_features: vec![],
resolved_features: vec![arc("extra_traits")],
dep_kind: DepKind::Normal,
target: Some(arc("cfg(unix)")),
borrowed_from: vec![arc("unix-crate")],
};
assert_eq!(uf.target.as_deref(), Some("cfg(unix)"));
}
#[test]
fn test_undeclared_feature_dev_dependency() {
let uf = UndeclaredFeature {
member: arc("test-crate"),
dep_name: arc("tokio"),
undeclared_features: vec![arc("macros")],
declared_features: vec![arc("rt")],
resolved_features: vec![arc("rt"), arc("macros")],
dep_kind: DepKind::Dev,
target: None,
borrowed_from: vec![arc("main-crate")],
};
assert_eq!(uf.dep_kind, DepKind::Dev);
}
#[test]
fn test_summary_with_add_features_edit() {
let mut plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![],
dependency_decisions: vec![],
};
plan.member_edits.insert(
arc("test-crate"),
vec![MemberEdit::AddFeatures {
dep_name: arc("serde"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("derive")],
}],
);
let summary = plan.summary();
assert!(summary.contains("Undeclared features to fix: 1 features across 1 crates"));
}
#[test]
fn test_summary_with_multiple_add_features_edits() {
let mut plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![],
dependency_decisions: vec![],
};
plan.member_edits.insert(
arc("crate-a"),
vec![
MemberEdit::AddFeatures {
dep_name: arc("serde"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("derive"), arc("std")],
},
MemberEdit::AddFeatures {
dep_name: arc("tokio"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("rt")],
},
],
);
plan.member_edits.insert(
arc("crate-b"),
vec![MemberEdit::AddFeatures {
dep_name: arc("backoff"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("futures")],
}],
);
let summary = plan.summary();
assert!(summary.contains("Undeclared features to fix: 4 features across 2 crates"));
}
#[test]
fn test_summary_undeclared_warning_when_no_fixes() {
let plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![UndeclaredFeature {
member: arc("my-crate"),
dep_name: arc("backoff"),
undeclared_features: vec![arc("futures")],
declared_features: vec![],
resolved_features: vec![arc("futures")],
dep_kind: DepKind::Normal,
target: None,
borrowed_from: vec![arc("other-crate")],
}],
dependency_decisions: vec![],
};
let summary = plan.summary();
assert!(summary.contains("⚠️ Undeclared features detected"));
assert!(summary.contains("fix_undeclared_features = true"));
}
#[test]
fn test_summary_no_undeclared_warning_when_fixes_present() {
let mut plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![UndeclaredFeature {
member: arc("my-crate"),
dep_name: arc("backoff"),
undeclared_features: vec![arc("futures")],
declared_features: vec![],
resolved_features: vec![arc("futures")],
dep_kind: DepKind::Normal,
target: None,
borrowed_from: vec![arc("other-crate")],
}],
dependency_decisions: vec![],
};
plan.member_edits.insert(
arc("my-crate"),
vec![MemberEdit::AddFeatures {
dep_name: arc("backoff"),
dep_kind: DepKind::Normal,
target: None,
features_to_add: vec![arc("futures")],
}],
);
let summary = plan.summary();
assert!(!summary.contains("⚠️ Undeclared features detected"));
assert!(summary.contains("Undeclared features to fix: 1 features across 1 crates"));
}
#[test]
fn test_member_edit_clone() {
let edit = MemberEdit::AddFeatures {
dep_name: arc("test"),
dep_kind: DepKind::Normal,
target: Some(arc("cfg(test)")),
features_to_add: vec![arc("a"), arc("b")],
};
let cloned = edit.clone();
match (edit, cloned) {
(
MemberEdit::AddFeatures {
dep_name: a,
features_to_add: fa,
..
},
MemberEdit::AddFeatures {
dep_name: b,
features_to_add: fb,
..
},
) => {
assert_eq!(a, b);
assert_eq!(fa, fb);
}
_ => panic!("Expected AddFeatures"),
}
}
#[test]
fn test_undeclared_feature_clone() {
let uf = UndeclaredFeature {
member: arc("test"),
dep_name: arc("dep"),
undeclared_features: vec![arc("f1")],
declared_features: vec![arc("f2")],
resolved_features: vec![arc("f1"), arc("f2")],
dep_kind: DepKind::Dev,
target: Some(arc("cfg(windows)")),
borrowed_from: vec![arc("source-crate")],
};
let cloned = uf.clone();
assert_eq!(uf.member, cloned.member);
assert_eq!(uf.dep_name, cloned.dep_name);
assert_eq!(uf.undeclared_features, cloned.undeclared_features);
assert_eq!(uf.target, cloned.target);
assert_eq!(uf.borrowed_from, cloned.borrowed_from);
}
#[test]
fn test_summary_shows_borrowed_from_in_undeclared_warning() {
let plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![UndeclaredFeature {
member: arc("my-crate"),
dep_name: arc("tokio"),
undeclared_features: vec![arc("macros"), arc("rt")],
declared_features: vec![],
resolved_features: vec![arc("macros"), arc("rt")],
dep_kind: DepKind::Normal,
target: None,
borrowed_from: vec![arc("other-crate"), arc("third-crate")],
}],
dependency_decisions: vec![],
};
let summary = plan.summary();
assert!(summary.contains("⚠️ Undeclared features detected"));
assert!(summary.contains("my-crate/tokio"));
assert!(summary.contains("macros"));
assert!(summary.contains("rt"));
assert!(summary.contains("(borrowed from other-crate, third-crate)"));
}
#[test]
fn test_summary_borrowed_from_empty_not_shown() {
let plan = UnificationPlan {
workspace_deps: vec![],
member_edits: FxHashMap::default(),
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![UndeclaredFeature {
member: arc("my-crate"),
dep_name: arc("backoff"),
undeclared_features: vec![arc("futures")],
declared_features: vec![],
resolved_features: vec![arc("futures")],
dep_kind: DepKind::Normal,
target: None,
borrowed_from: vec![], }],
dependency_decisions: vec![],
};
let summary = plan.summary();
assert!(summary.contains("my-crate/backoff"));
assert!(summary.contains("futures"));
assert!(!summary.contains("borrowed from"));
}
#[test]
fn test_member_edit_count_ignores_empty_entries() {
let mut member_edits: FxHashMap<Arc<str>, Vec<MemberEdit>> = FxHashMap::default();
member_edits.insert(arc("crate-a"), vec![]);
let plan = UnificationPlan {
workspace_deps: vec![],
member_edits,
member_paths: FxHashMap::default(),
transitive_pins: vec![],
validation_results: vec![],
issues: vec![],
computed_msrv: None,
duplicates_cleaned: vec![],
pruned_features: vec![],
optional_features: vec![OptionalFeature {
crate_name: arc("crate-a"),
feature_name: arc("serde"),
enables: vec![arc("serde/derive")],
}],
version_mismatches: vec![],
unused_deps: vec![],
undeclared_features: vec![],
dependency_decisions: vec![],
};
assert_eq!(plan.member_edit_count(), 0);
assert!(!plan.has_planned_changes(false));
}
}