use crate::cargo::{
manifest_analyzer::{ExistingWorkspaceDep, ManifestAnalyzer, parse_existing_workspace_deps},
multi_target_metadata::MultiTargetMetadata,
unify::version_utils::{find_major_version_conflicts, is_exact_pin, versions_compatible},
unify::{CandidateIterator, FeaturePruner, TransitivePlanner, UnusedDepFinder},
unify_types::{
DuplicateCleanup, IssueSeverity, MemberEdit, UndeclaredFeature, UnificationPlan, UnifiedDep, UnifyIssue,
UnifyIssueKind, ValidationResult, VersionMismatch,
},
};
use crate::config::{ExactPinHandling, MajorVersionConflict, UnifyConfig};
use crate::error::{RailResult, ResultExt};
use crate::progress;
use crate::workspace::WorkspaceContext;
use rustc_hash::{FxHashMap, FxHashSet};
use semver::Version;
use semver::VersionReq;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct UnifyAnalyzer {
metadata: Arc<MultiTargetMetadata>,
manifests: ManifestAnalyzer,
pub config: UnifyConfig,
existing_workspace_deps: FxHashMap<String, ExistingWorkspaceDep>,
cohort_issues: Vec<UnifyIssue>,
workspace_root: PathBuf,
canonical_workspace_root: PathBuf,
}
type FeatureSources = FxHashMap<String, FxHashMap<String, FxHashSet<String>>>;
type FeatureTargetSpecific = FxHashMap<String, FxHashMap<String, bool>>;
impl UnifyAnalyzer {
pub fn new(ctx: &WorkspaceContext) -> RailResult<Self> {
let metadata = ctx.multi_target_metadata()?;
let workspace_packages = metadata.workspace_packages();
let manifests = ManifestAnalyzer::parse_workspace(ctx.workspace_root(), &workspace_packages)?;
let existing_workspace_deps = parse_existing_workspace_deps(ctx.workspace_root())?;
let base_config = ctx.config.as_ref().map(|c| c.unify.clone()).unwrap_or_default();
let workspace_member_names: FxHashSet<Arc<str>> = workspace_packages
.iter()
.map(|pkg| Arc::from(pkg.name.as_str()))
.collect();
let (config, cohort_issues) =
Self::apply_workspace_member_cohort_policy(base_config, &manifests, &workspace_member_names);
let workspace_root = ctx.workspace_root().to_path_buf();
let canonical_workspace_root = workspace_root.canonicalize().unwrap_or_else(|_| workspace_root.clone());
Ok(Self {
metadata,
manifests,
config,
existing_workspace_deps,
cohort_issues,
workspace_root,
canonical_workspace_root,
})
}
fn build_workspace_member_cohorts(
manifests: &ManifestAnalyzer,
workspace_member_names: &FxHashSet<Arc<str>>,
) -> Vec<Vec<Arc<str>>> {
let mut adjacency: FxHashMap<Arc<str>, FxHashSet<Arc<str>>> = FxHashMap::default();
for member in workspace_member_names {
adjacency.entry(Arc::clone(member)).or_default();
}
for member in &manifests.members {
let member_name: Arc<str> = Arc::from(member.package_name.as_str());
if !workspace_member_names.contains(&member_name) {
continue;
}
for dep_key in member.dependencies.keys() {
if !workspace_member_names.contains(&dep_key.name) {
continue;
}
adjacency
.entry(Arc::clone(&member_name))
.or_default()
.insert(Arc::clone(&dep_key.name));
adjacency
.entry(Arc::clone(&dep_key.name))
.or_default()
.insert(Arc::clone(&member_name));
}
}
let mut visited: FxHashSet<Arc<str>> = FxHashSet::default();
let mut cohorts = Vec::new();
let mut nodes: Vec<_> = adjacency.keys().cloned().collect();
nodes.sort();
for node in nodes {
if visited.contains(&node) {
continue;
}
let mut stack = vec![Arc::clone(&node)];
let mut component = Vec::new();
while let Some(current) = stack.pop() {
if !visited.insert(Arc::clone(¤t)) {
continue;
}
component.push(Arc::clone(¤t));
if let Some(neighbors) = adjacency.get(¤t) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
stack.push(Arc::clone(neighbor));
}
}
}
}
component.sort();
cohorts.push(component);
}
cohorts
}
fn apply_workspace_member_cohort_policy(
mut config: UnifyConfig,
manifests: &ManifestAnalyzer,
workspace_member_names: &FxHashSet<Arc<str>>,
) -> (UnifyConfig, Vec<UnifyIssue>) {
let mut issues = Vec::new();
let cohorts = Self::build_workspace_member_cohorts(manifests, workspace_member_names);
for cohort in cohorts.into_iter().filter(|c| c.len() > 1) {
let excluded_members: Vec<&Arc<str>> = cohort.iter().filter(|name| config.should_exclude(name)).collect();
if !excluded_members.is_empty() {
if excluded_members.len() != cohort.len() {
let mut cohort_names: Vec<&str> = cohort.iter().map(|s| &**s).collect();
let mut excluded_names: Vec<&str> = excluded_members.iter().map(|s| &***s).collect();
cohort_names.sort_unstable();
excluded_names.sort_unstable();
issues.push(UnifyIssue {
kind: UnifyIssueKind::WorkspaceMemberCohortSplitRisk,
dep_name: Arc::clone(excluded_members[0]),
severity: IssueSeverity::Warning,
message: Arc::from(format!(
"Workspace-member cohort [{}] was partially excluded [{}]. \
Applying exclude atomically to the full cohort to prevent local-vs-registry source splits.",
cohort_names.join(", "),
excluded_names.join(", ")
)),
});
}
for member in &cohort {
if !config.should_exclude(member) {
config.exclude.push(member.to_string());
}
}
continue;
}
let mut low_usage_members = Vec::new();
let mut regular_members = Vec::new();
for member in &cohort {
if config.should_include(member) {
regular_members.push(member.to_string());
continue;
}
if manifests.package_usage_count(member) < 2 {
low_usage_members.push(member.to_string());
} else {
regular_members.push(member.to_string());
}
}
if !low_usage_members.is_empty() && !regular_members.is_empty() {
low_usage_members.sort();
regular_members.sort();
let mut cohort_names: Vec<&str> = cohort.iter().map(|s| &**s).collect();
cohort_names.sort_unstable();
issues.push(UnifyIssue {
kind: UnifyIssueKind::WorkspaceMemberCohortSplitRisk,
dep_name: Arc::from(regular_members[0].as_str()),
severity: IssueSeverity::Warning,
message: Arc::from(format!(
"Workspace-member cohort [{}] would split under single-user filtering \
(single-user members: [{}]). cargo-rail will unify the entire cohort atomically.",
cohort_names.join(", "),
low_usage_members.join(", ")
)),
});
}
for member in &cohort {
if !config.should_include(member) {
config.include.push(member.to_string());
}
}
}
(config, issues)
}
fn normalize_dep_path(&self, member_manifest_path: &Path, dep_path: &str) -> PathBuf {
let member_dir = member_manifest_path.parent().unwrap_or(member_manifest_path);
let absolute_dep_path = member_dir.join(dep_path);
let canonical_dep = absolute_dep_path.canonicalize().unwrap_or(absolute_dep_path.clone());
if let Ok(relative) = canonical_dep.strip_prefix(&self.canonical_workspace_root) {
return relative.to_path_buf();
}
if let Ok(relative) = absolute_dep_path.strip_prefix(&self.workspace_root) {
return relative.to_path_buf();
}
PathBuf::from(dep_path)
}
fn normalize_workspace_member_path(&self, member_manifest_path: &Path) -> PathBuf {
let member_dir = member_manifest_path.parent().unwrap_or(member_manifest_path);
let canonical_member = member_dir.canonicalize().unwrap_or_else(|_| member_dir.to_path_buf());
if let Ok(relative) = canonical_member.strip_prefix(&self.canonical_workspace_root) {
return relative.to_path_buf();
}
if let Ok(relative) = member_dir.strip_prefix(&self.workspace_root) {
return relative.to_path_buf();
}
member_dir.to_path_buf()
}
pub fn analyze(&self) -> RailResult<UnificationPlan> {
let dep_count = self.manifests.all_dependencies().len();
let mut workspace_deps = Vec::with_capacity(dep_count);
let mut member_edits: FxHashMap<Arc<str>, Vec<MemberEdit>> = FxHashMap::default();
let mut member_paths: FxHashMap<Arc<str>, PathBuf> = FxHashMap::default();
let mut issues = self.cohort_issues.clone();
issues.reserve(16); let mut duplicates_cleaned = Vec::with_capacity(8);
let mut version_mismatches = Vec::with_capacity(8);
let mut workspace_member_names: FxHashSet<Arc<str>> = FxHashSet::default();
let mut workspace_member_paths: FxHashMap<Arc<str>, PathBuf> = FxHashMap::default();
let mut workspace_member_versions: FxHashMap<Arc<str>, Version> = FxHashMap::default();
for pkg in self.metadata.workspace_packages() {
let member_name = Arc::from(pkg.name.as_str());
let manifest_path = pkg.manifest_path.clone().into_std_path_buf();
workspace_member_names.insert(Arc::clone(&member_name));
workspace_member_paths.insert(
Arc::clone(&member_name),
self.normalize_workspace_member_path(&manifest_path),
);
workspace_member_versions.insert(Arc::clone(&member_name), pkg.version.clone());
member_paths.insert(member_name, manifest_path);
}
progress!("Analyzing {} dependencies...", self.manifests.all_dependencies().len());
for candidate in CandidateIterator::new(&self.manifests, &self.config) {
let dep_key = candidate.dep_key;
let usage_sites = candidate.usages;
let major_versions = find_major_version_conflicts(&usage_sites);
if major_versions.len() > 1 {
let versions_str: Vec<_> = major_versions.iter().map(|v| v.to_string()).collect();
match self.config.major_version_conflict {
MajorVersionConflict::Warn => {
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::clone(&dep_key.name),
severity: IssueSeverity::Warning,
message: Arc::from(format!(
"Multiple major versions detected (majors: {}) - skipping unification. \
Consider consolidating to a single major version to reduce duplicate compilation, \
or set major_version_conflict = \"bump\" to force unification to highest version.",
versions_str.join(", ")
)),
});
continue; }
MajorVersionConflict::Bump => {
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::clone(&dep_key.name),
severity: IssueSeverity::Warning,
message: Arc::from(format!(
"Multiple major versions detected (majors: {}) - bumping to highest resolved version. \
This may break crates depending on older major versions.",
versions_str.join(", ")
)),
});
}
}
}
let has_exact_pin = usage_sites
.iter()
.any(|u| u.declared_version.as_ref().map(|v| is_exact_pin(v)).unwrap_or(false));
if has_exact_pin {
match self.config.exact_pin_handling {
ExactPinHandling::Skip => {
continue;
}
ExactPinHandling::Warn => {
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::clone(&dep_key.name),
severity: IssueSeverity::Warning,
message: Arc::from(
"Has exact version pin (=x.y.z) - converting to caret (^). \
Consider adding to unify.exclude or setting exact_pin_handling = \"skip\"",
),
});
}
ExactPinHandling::Preserve => {
}
}
}
let versions = self.metadata.direct_dep_versions(&dep_key.name);
let version = if versions.is_empty() {
if workspace_member_names.contains(&dep_key.name) {
match workspace_member_versions.get(&dep_key.name) {
Some(v) => v,
None => continue,
}
} else {
continue; }
} else {
let unique_versions: FxHashSet<_> = versions.values().collect();
match unique_versions.iter().max() {
Some(max) if unique_versions.len() > 1 => {
let mut versions_found: Vec<Arc<str>> = unique_versions.iter().map(|v| Arc::from(v.to_string())).collect();
versions_found.sort();
duplicates_cleaned.push(DuplicateCleanup {
dep_name: Arc::clone(&dep_key.name),
versions_found,
selected_version: Arc::from(max.to_string()),
});
*max
}
Some(version) => *version,
None => continue, }
};
let version_req = if has_exact_pin && self.config.exact_pin_handling == ExactPinHandling::Preserve {
let exact_version = usage_sites
.iter()
.find_map(|u| u.declared_version.as_ref().filter(|v| is_exact_pin(v)))
.cloned()
.unwrap_or_else(|| format!("={}", version));
VersionReq::parse(&exact_version)
.or_else(|_| VersionReq::parse(&format!("^{}", version)))
.unwrap_or_else(|_| VersionReq::default())
} else {
VersionReq::parse(&format!("^{}", version))
.or_else(|_| VersionReq::parse(&version.to_string()))
.unwrap_or_else(|_| VersionReq::default())
};
if let Some(existing_ws_dep) = self.existing_workspace_deps.get(&*dep_key.name)
&& let Some(ref ws_version) = existing_ws_dep.version
{
for usage in &usage_sites {
if let Some(ref declared) = usage.declared_version
&& !versions_compatible(declared, ws_version)
{
version_mismatches.push(VersionMismatch {
member: Arc::clone(&usage.used_by),
dep_name: Arc::clone(&dep_key.name),
member_version: Arc::from(declared.as_str()),
workspace_version: Arc::from(ws_version.as_str()),
});
let severity = if self.config.strict_version_compat {
IssueSeverity::Error
} else {
IssueSeverity::Warning
};
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::clone(&dep_key.name),
severity,
message: Arc::from(format!(
"{} declares \"{}\" but workspace.dependencies has \"{}\". \
Converting to workspace = true may break this crate.",
usage.used_by, declared, ws_version
)),
});
}
}
}
let is_workspace_member_dep = workspace_member_names.contains(&dep_key.name);
let (features, default_features) = if is_workspace_member_dep {
(std::collections::BTreeSet::new(), true)
} else {
let has_mixed_defaults = if self.config.include_renamed {
self.manifests.package_has_mixed_defaults(&dep_key.name)
} else {
self.manifests.has_mixed_defaults(dep_key)
};
if self.config.include_renamed {
let features = self.manifests.compute_package_union(&dep_key.name);
let df = if has_mixed_defaults {
true
} else {
self
.manifests
.package_default_features_policy(&dep_key.name)
.unwrap_or(true)
};
(features, df)
} else {
let intersection = self.manifests.compute_intersection(dep_key);
if has_mixed_defaults || intersection.is_empty() {
(self.manifests.compute_union(dep_key), true)
} else {
let df = self.manifests.default_features_policy(dep_key).unwrap_or(true);
(intersection, df)
}
}
};
let target = None;
let users: FxHashSet<Arc<str>> = usage_sites.iter().map(|u| Arc::clone(&u.used_by)).collect();
let dep_path: Option<PathBuf> = if is_workspace_member_dep {
workspace_member_paths.get(&dep_key.name).cloned()
} else if self.config.include_paths {
usage_sites.iter().find_map(|u| {
u.path.as_ref().and_then(|p| {
u.manifest_path
.as_ref()
.map(|manifest_path| self.normalize_dep_path(manifest_path, p))
})
})
} else {
None };
let computed_features: Vec<Arc<str>> = features.into_iter().map(Arc::from).collect();
let existing_dep = self.existing_workspace_deps.get(&*dep_key.name);
let needs_workspace_update = match existing_dep {
None => true, Some(existing) => {
let mut existing_features: Vec<&str> = existing.features.iter().map(String::as_str).collect();
let mut computed: Vec<&str> = computed_features.iter().map(|s| &**s).collect();
existing_features.sort();
computed.sort();
let path_differs = match (&dep_path, &existing.path) {
(Some(new_path), Some(existing_path)) => Path::new(existing_path) != new_path,
(Some(_), None) => true,
_ => false,
};
existing_features != computed || existing.default_features != default_features || path_differs
}
};
let workspace_features = computed_features.clone();
if needs_workspace_update {
let unified = UnifiedDep {
name: Arc::clone(&dep_key.name),
version_req,
features: workspace_features.clone(),
default_features,
used_by: users.into_iter().collect(),
target,
path: dep_path, };
workspace_deps.push(unified);
}
for usage in usage_sites {
let local_features: Vec<Arc<str>> = usage
.unconditional_features
.iter()
.filter(|f| !workspace_features.iter().any(|wf| &**wf == *f))
.map(|f| Arc::from(f.as_str()))
.collect();
let edit = MemberEdit::UseWorkspace {
dep_name: Arc::clone(&usage.cargo_toml_key),
dep_kind: usage.kind,
target: usage.target.as_ref().map(|t| Arc::from(t.as_str())), local_features,
is_optional: usage.optional,
};
member_edits.entry(Arc::clone(&usage.used_by)).or_default().push(edit);
}
}
let transitive_pins = if self.config.pin_transitives {
TransitivePlanner::new(&self.metadata).find_pins()
} else {
Vec::new()
};
let computed_msrv = if self.config.msrv {
progress!("Computing MSRV from dependency graph...");
self
.metadata
.compute_msrv_with_config(&self.workspace_root, self.config.msrv_source)
} else {
None
};
if self.config.enforce_msrv_inheritance {
if !self.config.msrv {
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::from("rust-version"),
severity: IssueSeverity::Warning,
message: Arc::from(
"enforce_msrv_inheritance is enabled but unify.msrv is false; skipping MSRV inheritance enforcement",
),
});
} else if computed_msrv.is_none() {
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::from("rust-version"),
severity: IssueSeverity::Warning,
message: Arc::from(
"enforce_msrv_inheritance is enabled but no workspace MSRV could be determined; skipping MSRV inheritance enforcement",
),
});
} else {
progress!("Enforcing MSRV inheritance on workspace members...");
for (member_name, manifest_path) in &member_paths {
if member_manifest_inherits_msrv(manifest_path)? {
continue;
}
member_edits
.entry(Arc::clone(member_name))
.or_default()
.push(MemberEdit::EnforceMsrvInheritance);
}
}
}
let feature_pruner = FeaturePruner::new(&self.metadata, &self.config);
let (pruned_features, optional_features) = if self.config.prune_dead_features {
progress!("Scanning for dead features in resolved graph...");
feature_pruner.scan()
} else {
(Vec::new(), Vec::new())
};
let unused_finder = UnusedDepFinder::new(
&self.workspace_root,
&self.metadata,
&self.manifests,
self.config.compiler_diag_cache,
);
let unused_deps = if self.config.detect_unused {
progress!("Detecting unused dependencies...");
unused_finder.find()
} else {
Vec::new()
};
if self.config.remove_unused && !unused_deps.is_empty() {
progress!(
"Generating removal edits for {} unused dependencies...",
unused_deps.len()
);
for (member, edits) in unused_finder.generate_removal_edits(&unused_deps) {
member_edits.entry(Arc::from(member)).or_default().extend(edits);
}
}
for (crate_name, edits) in feature_pruner.generate_prune_edits(&pruned_features) {
member_edits.entry(crate_name).or_default().extend(edits);
}
let undeclared_features = if self.config.detect_undeclared_features {
progress!("Checking for undeclared feature dependencies...");
self.detect_undeclared_features()
} else {
Vec::new()
};
if self.config.fix_undeclared_features && !undeclared_features.is_empty() {
progress!(
"Generating fixes for {} undeclared feature issues...",
undeclared_features.len()
);
for uf in &undeclared_features {
let edit = MemberEdit::AddFeatures {
dep_name: Arc::clone(&uf.dep_name),
dep_kind: uf.dep_kind,
target: uf.target.clone(),
features_to_add: uf.undeclared_features.clone(),
};
member_edits.entry(Arc::clone(&uf.member)).or_default().push(edit);
}
} else {
for uf in &undeclared_features {
let undeclared_str: Vec<&str> = uf.undeclared_features.iter().map(|s| &**s).collect();
let declared_str = if uf.declared_features.is_empty() {
"none".to_string()
} else {
let strs: Vec<&str> = uf.declared_features.iter().map(|s| &**s).collect();
strs.join(", ")
};
issues.push(UnifyIssue {
kind: UnifyIssueKind::General,
dep_name: Arc::clone(&uf.dep_name),
severity: IssueSeverity::Warning,
message: Arc::from(format!(
"{} uses {} features [{}] but only declares [{}]. \
This crate relies on feature unification from other workspace members. \
After unification, standalone builds will fail. \
Fix: Add the missing features to {}'s Cargo.toml.",
uf.member,
uf.dep_name,
undeclared_str.join(", "),
declared_str,
uf.member,
)),
});
}
}
let validation_results = self.validate_targets()?;
member_edits.retain(|_, edits| !edits.is_empty());
Ok(UnificationPlan {
workspace_deps,
member_edits,
member_paths,
transitive_pins,
validation_results,
issues,
computed_msrv,
duplicates_cleaned,
pruned_features,
optional_features,
version_mismatches,
unused_deps,
undeclared_features,
})
}
fn validate_targets(&self) -> RailResult<Vec<ValidationResult>> {
let targets = self.metadata.targets();
let mut results = Vec::with_capacity(targets.len());
for target in targets {
let success = self.metadata.get(target).is_some();
results.push(ValidationResult {
target: Arc::from(target),
success,
error: if !success {
Some(Arc::from("Failed to load metadata"))
} else {
None
},
});
}
Ok(results)
}
fn workspace_member_names(&self) -> FxHashSet<String> {
self
.metadata
.workspace_packages()
.iter()
.map(|p| p.name.to_string())
.collect()
}
fn workspace_baseline_features(&self) -> FxHashMap<String, FxHashSet<String>> {
self
.existing_workspace_deps
.iter()
.map(|(name, dep)| {
let mut feats: FxHashSet<String> = dep.features.iter().cloned().collect();
if dep.default_features {
feats.insert("default".to_string());
}
(name.clone(), feats)
})
.collect()
}
fn track_feature_sources(
&self,
workspace_baseline: &FxHashMap<String, FxHashSet<String>>,
) -> (FeatureSources, FeatureTargetSpecific) {
let mut feature_sources: FeatureSources = FxHashMap::default();
let mut feature_is_target_specific: FeatureTargetSpecific = FxHashMap::default();
for member in &self.manifests.members {
for (dep_key, usages) in &member.dependencies {
let baseline = workspace_baseline.get(&*dep_key.name);
let dep_name_str = dep_key.name.to_string();
for usage in usages {
let mut feats: FxHashSet<&String> = usage.unconditional_features.iter().collect();
feats.extend(usage.conditional_features.iter());
let is_target_specific = usage.target.is_some();
for feat in feats {
if baseline.is_some_and(|b| b.contains(feat)) {
continue;
}
feature_sources
.entry(dep_name_str.clone())
.or_default()
.entry(feat.clone())
.or_default()
.insert(member.package_name.clone());
let entry = feature_is_target_specific
.entry(dep_name_str.clone())
.or_default()
.entry(feat.clone())
.or_insert(true);
if !is_target_specific {
*entry = false;
}
}
}
}
}
(feature_sources, feature_is_target_specific)
}
fn detect_undeclared_features(&self) -> Vec<UndeclaredFeature> {
let workspace_member_names = self.workspace_member_names();
let workspace_baseline = self.workspace_baseline_features();
let (feature_sources, feature_is_target_specific) = self.track_feature_sources(&workspace_baseline);
let mut undeclared = Vec::with_capacity(16);
let mut skipped_platform_features: Vec<(String, String, String)> = Vec::with_capacity(8);
let mut skipped_workspace_member_deps: FxHashSet<String> = FxHashSet::default();
for member in &self.manifests.members {
for (dep_key, usages) in &member.dependencies {
if workspace_member_names.contains(&*dep_key.name) {
skipped_workspace_member_deps.insert(dep_key.name.to_string());
continue;
}
let Some(feat_sources) = feature_sources.get(&*dep_key.name) else {
continue;
};
let target_specific_map = feature_is_target_specific.get(&*dep_key.name);
for usage in usages {
if let Some(borrowed) = self.find_borrowed_features_for_usage(
member,
dep_key,
usage,
feat_sources,
target_specific_map,
&mut skipped_platform_features,
) {
undeclared.push(borrowed);
}
}
}
}
if !skipped_workspace_member_deps.is_empty() {
progress!(
" Skipped undeclared feature detection for {} workspace member deps (features are opt-in)",
skipped_workspace_member_deps.len()
);
}
if !skipped_platform_features.is_empty() {
progress!(
" Skipped {} platform-specific undeclared features (target-constrained declarations)",
skipped_platform_features.len()
);
}
undeclared
}
#[allow(clippy::too_many_arguments)]
fn find_borrowed_features_for_usage(
&self,
member: &crate::cargo::manifest_analyzer::ParsedManifest,
dep_key: &crate::cargo::manifest_analyzer::DepKey,
usage: &crate::cargo::manifest_analyzer::DepUsage,
feat_sources: &FxHashMap<String, FxHashSet<String>>,
target_specific_map: Option<&FxHashMap<String, bool>>,
skipped_platform_features: &mut Vec<(String, String, String)>,
) -> Option<UndeclaredFeature> {
let mut declared: FxHashSet<String> = usage.unconditional_features.iter().cloned().collect();
declared.extend(usage.conditional_features.iter().cloned());
if usage.default_features {
declared.insert("default".to_string());
}
let mut borrowed_features: Vec<Arc<str>> = Vec::with_capacity(feat_sources.len());
let mut borrowed_from_members: FxHashSet<Arc<str>> = FxHashSet::default();
for (feat, sources) in feat_sources {
if declared.contains(feat) {
continue;
}
if self.config.should_skip_undeclared_feature(feat) {
continue;
}
if let Some(ts_map) = target_specific_map
&& ts_map.get(feat).copied().unwrap_or(false)
{
skipped_platform_features.push((member.package_name.clone(), dep_key.name.to_string(), feat.clone()));
continue;
}
borrowed_features.push(Arc::from(feat.as_str()));
for source in sources {
if source != &member.package_name {
borrowed_from_members.insert(Arc::from(source.as_str()));
}
}
}
if borrowed_features.is_empty() {
return None;
}
borrowed_features.sort();
let mut borrowed_from: Vec<Arc<str>> = borrowed_from_members.into_iter().collect();
borrowed_from.sort();
Some(UndeclaredFeature {
member: Arc::from(member.package_name.as_str()),
dep_name: Arc::clone(&dep_key.name),
undeclared_features: borrowed_features,
declared_features: declared.into_iter().map(|s| Arc::from(s.as_str())).collect(),
resolved_features: feat_sources.keys().map(|s| Arc::from(s.as_str())).collect(),
dep_kind: usage.kind,
target: usage.target.as_ref().map(|t| Arc::from(t.as_str())),
borrowed_from,
})
}
}
fn member_manifest_inherits_msrv(manifest_path: &Path) -> RailResult<bool> {
let content =
std::fs::read_to_string(manifest_path).context(format!("Failed to read {}", manifest_path.display()))?;
let doc: toml_edit::DocumentMut = content
.parse()
.context(format!("Failed to parse {}", manifest_path.display()))?;
let Some(pkg) = doc.get("package").and_then(|p| p.as_table()) else {
return Ok(true);
};
let Some(rv) = pkg.get("rust-version") else {
return Ok(false);
};
let Some(rv_tbl) = rv.as_table_like() else {
return Ok(false);
};
Ok(rv_tbl.get("workspace").and_then(|v| v.as_bool()) == Some(true))
}