use crate::cargo::manifest_analyzer::{DepKind, DepUsage, ManifestAnalyzer, ParsedManifest};
use crate::cargo::multi_target_metadata::{MultiTargetMetadata, ResolvedDependency};
use crate::cargo::unify_types::{
DependencyProof, IssueSeverity, MemberEdit, UnifyIssue, UnifyIssueKind, UnusedDep, UnusedReason,
};
use crate::compiler::cfg_eval::{TargetCfgSet, target_constraint_matches_target};
use crate::compiler::scheduler::planned_feature_selections;
use crate::compiler::{
CompilerArtifactBudget, CompilerCacheIdentity, CompilerCandidate, CompilerDiagnosticsCollector,
DependencyEvidenceState, DependencyIdentity, FeatureSelection, MemberEvidence,
};
use crate::config::{ConsumerScope, UnifyConfig};
use crate::error::RailResult;
use crate::progress;
use cargo_metadata::PackageId;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;
pub struct UnusedDepFinder<'a> {
workspace_root: &'a Path,
metadata: &'a MultiTargetMetadata,
manifests: &'a ManifestAnalyzer,
target_cfg_sets: &'a HashMap<String, TargetCfgSet>,
unreachable_features: HashMap<String, BTreeSet<String>>,
workspace_is_consumer_scope: bool,
compiler_cache_identity: &'a CompilerCacheIdentity,
target_preflight: Option<&'a crate::compiler::target_preflight::TargetPreflight>,
compiler_artifact_budget: CompilerArtifactBudget,
compiler_targets: Vec<&'a str>,
}
struct DependencyDeclaration<'a> {
member: &'a ParsedManifest,
usages: &'a [DepUsage],
resolved: Option<ResolvedDependency>,
}
impl<'a> UnusedDepFinder<'a> {
pub fn new(
workspace_root: &'a Path,
metadata: &'a MultiTargetMetadata,
manifests: &'a ManifestAnalyzer,
target_cfg_sets: &'a HashMap<String, TargetCfgSet>,
pruned_features: &[crate::cargo::unify_types::PrunedFeature],
config: &UnifyConfig,
compiler_cache_identity: &'a CompilerCacheIdentity,
) -> Self {
let mut unreachable_features: HashMap<String, BTreeSet<String>> = HashMap::new();
for feature in pruned_features {
unreachable_features
.entry(feature.crate_name.to_string())
.or_default()
.insert(feature.feature_name.to_string());
}
Self {
workspace_root,
metadata,
manifests,
target_cfg_sets,
unreachable_features,
workspace_is_consumer_scope: config.consumer_scope == ConsumerScope::Workspace,
compiler_cache_identity,
target_preflight: None,
compiler_artifact_budget: CompilerArtifactBudget::new(
config.compiler_artifact_soft_limit_bytes,
config.compiler_artifact_hard_limit_bytes,
),
compiler_targets: config.effective_compiler_targets(&metadata.targets()),
}
}
pub(crate) fn with_target_preflight(
mut self,
preflight: &'a crate::compiler::target_preflight::TargetPreflight,
) -> Self {
self.target_preflight = Some(preflight);
self
}
pub fn find(&self) -> RailResult<Vec<UnusedDep>> {
let mut unused = Vec::new();
let declarations = self.dependency_declarations();
let configured_targets: Vec<&str> = self.metadata.targets();
let compiler_targets = &self.compiler_targets;
let source_unused = self.detect_source_unused_deps(&declarations, compiler_targets, self.target_cfg_sets)?;
for declaration in declarations {
if let Some(resolved) = declaration.resolved {
let required_features = planned_feature_selections(declaration.member);
for usage in declaration.usages {
if self.should_skip_usage(usage, &configured_targets, self.target_cfg_sets)
|| (usage.kind == DepKind::Build && self.metadata.package_has_links(&resolved.package_ids))
{
continue;
}
let required_targets = usage_required_targets(usage, &configured_targets, self.target_cfg_sets);
if required_targets.is_empty() {
continue;
}
let Some(member_evidence) = source_unused.get(&declaration.member.package_id) else {
continue;
};
let dependency = DependencyIdentity {
member: declaration.member.package_id.clone(),
declaration_key: usage.cargo_toml_key.to_string(),
resolved_packages: resolved.package_ids.clone(),
crate_names: resolved.crate_names.clone(),
kind: usage.kind,
target: usage.target.clone(),
optional: usage.optional,
};
if member_evidence.dependency_state_for_features(&dependency, &required_targets, &required_features)
!= DependencyEvidenceState::Unused
{
continue;
}
let mut proof = compiler_proof(&dependency, member_evidence, &required_targets, &required_features);
let mut dev_dependency = dependency.clone();
dev_dependency.kind = DepKind::Dev;
let dev_state = member_evidence.dependency_state_for_features(
&dev_dependency,
&required_targets,
&required_features,
);
let reason = if usage.kind == DepKind::Normal
&& dev_state == DependencyEvidenceState::Used
&& declaration_has_only_development_retention(declaration.member, &resolved)
{
if declaration.usages.iter().any(|other| {
other.kind == DepKind::Dev
&& other.target == usage.target
&& other.cargo_toml_key == usage.cargo_toml_key
}) {
continue;
}
let dev_summary = member_evidence.dependency_summary_for_features(
&dev_dependency,
&required_targets,
&required_features,
);
proof.evidence_source = "compiler_dependency_domains_and_source_retention";
proof.applicable_configurations += dev_summary.applicable;
proof.complete_configurations += dev_summary.complete;
proof.used_observations += dev_summary.used;
proof.unused_observations += dev_summary.unused;
proof.incomplete_observations += dev_summary.incomplete;
UnusedReason::DevelopmentOnly
} else {
if declaration_has_retention_evidence(declaration.member, &resolved) {
continue;
}
UnusedReason::NotUsedInSource
};
unused.push(UnusedDep {
member: Arc::from(declaration.member.package_name.as_str()),
dep_name: Arc::clone(&usage.cargo_toml_key),
kind: usage.kind,
target: usage.target.as_deref().map(Arc::from),
reason,
proof,
});
}
continue;
}
for usage in declaration.usages {
if usage.kind != DepKind::Normal {
continue;
}
if usage.optional {
if !self.optional_dependency_is_removable(declaration.member, usage) {
continue;
}
} else if self.should_skip_usage(usage, &configured_targets, self.target_cfg_sets) {
continue;
}
let applicable_targets = usage_required_targets(usage, &configured_targets, self.target_cfg_sets);
let optional_proof = if usage.optional {
let Some(member_evidence) = source_unused.get(&declaration.member.package_id) else {
continue;
};
let dependency = DependencyIdentity {
member: declaration.member.package_id.clone(),
declaration_key: usage.cargo_toml_key.to_string(),
resolved_packages: BTreeSet::new(),
crate_names: BTreeSet::from([usage.cargo_toml_key.replace('-', "_")]),
kind: usage.kind,
target: usage.target.clone(),
optional: true,
};
let required_features = [FeatureSelection::AllFeatures];
if member_evidence.dependency_state_for_features(
&dependency,
&applicable_targets,
&required_features,
) != DependencyEvidenceState::Unused
{
continue;
}
Some(compiler_feature_proof(
&dependency,
member_evidence,
&applicable_targets,
&required_features,
))
} else {
None
};
if let Some(target_cfg) = &usage.target {
unused.push(UnusedDep {
member: Arc::from(declaration.member.package_name.as_str()),
dep_name: Arc::clone(&usage.cargo_toml_key),
kind: usage.kind,
target: usage.target.as_deref().map(Arc::from),
reason: UnusedReason::TargetConfiguredButNotResolved {
target_cfg: Arc::from(target_cfg.as_str()),
},
proof: optional_proof.unwrap_or_else(|| graph_proof(applicable_targets.len())),
});
continue;
}
unused.push(UnusedDep {
member: Arc::from(declaration.member.package_name.as_str()),
dep_name: Arc::clone(&usage.cargo_toml_key),
kind: usage.kind,
target: None,
reason: UnusedReason::NotInResolvedGraph,
proof: optional_proof.unwrap_or_else(|| graph_proof(applicable_targets.len())),
});
}
}
if !unused.is_empty() {
progress!(" Found {} potentially unused dependencies", unused.len());
}
Ok(unused)
}
pub fn uncertainty_issues(&self) -> Vec<UnifyIssue> {
let mut seen = BTreeSet::new();
let mut issues = Vec::new();
for member in &self.manifests.members {
for (dependency, usages) in &member.dependencies {
let has_links = self
.metadata
.resolve_dependency(
&member.package_id,
&dependency.name,
dependency.is_renamed().then(|| dependency.alias()),
)
.is_some_and(|resolved| self.metadata.package_has_links(&resolved.package_ids));
for usage in usages {
let reason = if usage.referenced_in_features {
Some("a manifest feature edge references this declaration")
} else if usage.optional && self.optional_dependency_is_removable(member, usage) {
None
} else if usage.optional {
Some("optional activation reachability and compiler evidence are incomplete")
} else {
match usage.kind {
DepKind::Normal => None,
DepKind::Dev => None,
DepKind::Build if has_links => {
Some("the dependency declares a native links contract with side effects")
}
DepKind::Build => None,
}
};
let Some(reason) = reason else {
continue;
};
let key = (
member.package_name.as_str(),
usage.cargo_toml_key.as_ref(),
usage.kind.as_str(),
usage.target.as_deref(),
);
if !seen.insert(key) {
continue;
}
issues.push(UnifyIssue {
kind: UnifyIssueKind::Preserved,
dep_name: Arc::clone(&usage.cargo_toml_key),
severity: IssueSeverity::Info,
message: Arc::from(format!(
"preserved {} dependency `{}` in `{}`: {reason}",
usage.kind.as_str(),
usage.cargo_toml_key,
member.package_name
)),
});
}
}
}
issues.sort_by(|left, right| {
left.dep_name
.cmp(&right.dep_name)
.then_with(|| left.message.cmp(&right.message))
});
issues
}
fn dependency_declarations(&self) -> Vec<DependencyDeclaration<'_>> {
let workspace_member_names: HashSet<&str> = self
.metadata
.workspace_packages()
.map(|package| package.name.as_ref())
.collect();
let capacity = self
.manifests
.members
.iter()
.map(|member| member.dependencies.len())
.sum();
let mut declarations = Vec::with_capacity(capacity);
for member in &self.manifests.members {
for (key, usages) in &member.dependencies {
if workspace_member_names.contains(key.name.as_ref()) {
continue;
}
declarations.push(DependencyDeclaration {
member,
usages,
resolved: self.metadata.resolve_dependency(
&member.package_id,
&key.name,
key.is_renamed().then(|| key.alias()),
),
});
}
}
declarations
}
fn should_skip_usage(
&self,
usage: &DepUsage,
configured_targets: &[&str],
cfg_sets: &HashMap<String, TargetCfgSet>,
) -> bool {
if usage.optional || usage.referenced_in_features {
return true;
}
if let Some(target_cfg) = &usage.target {
return !target_constraint_matches_any(target_cfg, configured_targets, cfg_sets);
}
false
}
fn optional_dependency_is_removable(&self, member: &ParsedManifest, usage: &DepUsage) -> bool {
let private_package = self
.metadata
.workspace_packages()
.find(|package| package.id == member.package_id)
.and_then(|package| package.publish.as_ref())
.is_some_and(Vec::is_empty);
if !private_package || !self.workspace_is_consumer_scope {
return false;
}
if usage.referenced_in_features && !self.only_referenced_by_unreachable_features(member, usage) {
return false;
}
let crate_name = usage.cargo_toml_key.replace('-', "_");
!member.source_cfg_features.contains(usage.cargo_toml_key.as_ref())
&& !member.retention_identifiers.contains(&crate_name)
}
fn only_referenced_by_unreachable_features(&self, member: &ParsedManifest, usage: &DepUsage) -> bool {
let Some(unreachable) = self.unreachable_features.get(&member.package_name) else {
return false;
};
let Some(package) = self
.metadata
.workspace_packages()
.find(|package| package.id == member.package_id)
else {
return false;
};
let dependency = usage.cargo_toml_key.as_ref();
let providers: Vec<_> = package
.features
.iter()
.filter(|(_, edges)| {
edges
.iter()
.any(|edge| crate::cargo::feature_scanner::feature_edge_references_dependency(edge, dependency))
})
.map(|(feature, _)| feature)
.collect();
!providers.is_empty() && providers.iter().all(|feature| unreachable.contains(*feature))
}
fn detect_source_unused_deps(
&self,
declarations: &[DependencyDeclaration<'_>],
configured_targets: &[&str],
cfg_sets: &HashMap<String, TargetCfgSet>,
) -> RailResult<HashMap<PackageId, MemberEvidence>> {
if configured_targets.is_empty() {
return Ok(HashMap::new());
}
let candidates = self.compiler_candidates(declarations, configured_targets, cfg_sets);
if candidates.is_empty() {
return Ok(HashMap::new());
}
let collector = CompilerDiagnosticsCollector::with_identity(
self.workspace_root,
self.manifests,
configured_targets.to_vec(),
self.compiler_cache_identity,
)
.with_artifact_budget(self.compiler_artifact_budget);
let collector = match self.target_preflight {
Some(preflight) => collector.with_target_preflight(preflight.clone()),
None => collector,
};
collector.collect_for_candidates(&candidates)
}
fn compiler_candidates(
&self,
declarations: &[DependencyDeclaration<'_>],
configured_targets: &[&str],
cfg_sets: &HashMap<String, TargetCfgSet>,
) -> Vec<CompilerCandidate> {
let mut candidates = BTreeSet::new();
for declaration in declarations {
if let Some(resolved) = &declaration.resolved
&& declaration_has_retention_evidence(declaration.member, resolved)
&& !(declaration.usages.iter().any(|usage| usage.kind == DepKind::Normal)
&& declaration_has_only_development_retention(declaration.member, resolved))
{
continue;
}
for usage in declaration.usages {
if declaration.resolved.is_none() {
if usage.kind != DepKind::Normal
|| !usage.optional
|| !self.optional_dependency_is_removable(declaration.member, usage)
{
continue;
}
let applicable_targets: BTreeSet<String> =
usage_required_targets(usage, configured_targets, cfg_sets)
.into_iter()
.map(str::to_string)
.collect();
candidates.insert(CompilerCandidate {
member: declaration.member.package_name.clone(),
crate_name: usage.cargo_toml_key.replace('-', "_"),
kind: usage.kind,
applicable_targets,
required_features: Some(FeatureSelection::AllFeatures),
});
continue;
}
let Some(resolved) = declaration.resolved.as_ref() else {
continue;
};
if self.should_skip_usage(usage, configured_targets, cfg_sets)
|| (usage.kind == DepKind::Build && self.metadata.package_has_links(&resolved.package_ids))
{
continue;
}
let applicable_targets: BTreeSet<String> = usage_required_targets(usage, configured_targets, cfg_sets)
.into_iter()
.map(str::to_string)
.collect();
for crate_name in &resolved.crate_names {
candidates.insert(CompilerCandidate {
member: declaration.member.package_name.clone(),
crate_name: crate_name.clone(),
kind: usage.kind,
applicable_targets: applicable_targets.clone(),
required_features: None,
});
}
}
}
candidates.into_iter().collect()
}
pub fn generate_removal_edits(&self, unused: &[UnusedDep]) -> HashMap<String, Vec<MemberEdit>> {
let mut edits: HashMap<String, Vec<MemberEdit>> = HashMap::new();
for dep in unused {
edits.entry(dep.member.to_string()).or_default().push(
if matches!(dep.reason, UnusedReason::DevelopmentOnly) {
MemberEdit::MoveToDev {
dep_name: Arc::clone(&dep.dep_name),
target: dep.target.clone(),
}
} else {
MemberEdit::RemoveDep {
dep_name: Arc::clone(&dep.dep_name),
dep_kind: dep.kind,
target: dep.target.clone(),
}
},
);
}
edits
}
}
fn declaration_has_retention_evidence(member: &ParsedManifest, resolved: &ResolvedDependency) -> bool {
resolved
.crate_names
.iter()
.any(|crate_name| member.retention_identifiers.contains(crate_name))
}
fn declaration_has_only_development_retention(member: &ParsedManifest, resolved: &ResolvedDependency) -> bool {
declaration_has_retention_evidence(member, resolved)
&& resolved.crate_names.iter().all(|crate_name| {
!member.retention_identifiers.contains(crate_name)
|| member.development_only_identifiers.contains(crate_name)
})
}
fn usage_required_targets<'a>(
usage: &DepUsage,
configured_targets: &'a [&'a str],
cfg_sets: &HashMap<String, TargetCfgSet>,
) -> Vec<&'a str> {
match &usage.target {
Some(target_cfg) => configured_targets
.iter()
.copied()
.filter(|target| target_constraint_matches_target(target_cfg, target, cfg_sets.get(*target)))
.collect(),
None => configured_targets.to_vec(),
}
}
fn compiler_proof(
dependency: &DependencyIdentity,
evidence: &MemberEvidence,
required_targets: &[&str],
required_features: &[FeatureSelection],
) -> DependencyProof {
let summary = evidence.dependency_summary_for_features(dependency, required_targets, required_features);
DependencyProof {
schema_version: 1,
package_ids: dependency
.resolved_packages
.iter()
.map(|package| Arc::from(package.to_string()))
.collect(),
crate_names: dependency
.crate_names
.iter()
.map(|name| Arc::from(name.as_str()))
.collect(),
evidence_source: "compiler",
applicable_configurations: summary.applicable,
complete_configurations: summary.complete,
used_observations: summary.used,
unused_observations: summary.unused,
incomplete_observations: summary.incomplete,
cache_hits: evidence.cache.hits,
cache_misses: evidence.cache.misses,
cache_miss_reasons: evidence
.cache
.miss_reasons
.iter()
.map(|(reason, count)| Arc::from(format!("{reason}={count}")))
.collect(),
cache_publication_bypasses: evidence
.cache
.publication_bypasses
.iter()
.map(|(reason, count)| Arc::from(format!("{reason}={count}")))
.collect(),
uncertainties: Vec::new(),
}
}
fn compiler_feature_proof(
dependency: &DependencyIdentity,
evidence: &MemberEvidence,
required_targets: &[&str],
required_features: &[FeatureSelection],
) -> DependencyProof {
let summary = evidence.dependency_summary_for_features(dependency, required_targets, required_features);
DependencyProof {
schema_version: 1,
package_ids: Vec::new(),
crate_names: dependency
.crate_names
.iter()
.map(|name| Arc::from(name.as_str()))
.collect(),
evidence_source: "compiler_optional_activation",
applicable_configurations: summary.applicable,
complete_configurations: summary.complete,
used_observations: summary.used,
unused_observations: summary.unused,
incomplete_observations: summary.incomplete,
cache_hits: evidence.cache.hits,
cache_misses: evidence.cache.misses,
cache_miss_reasons: evidence
.cache
.miss_reasons
.iter()
.map(|(reason, count)| Arc::from(format!("{reason}={count}")))
.collect(),
cache_publication_bypasses: evidence
.cache
.publication_bypasses
.iter()
.map(|(reason, count)| Arc::from(format!("{reason}={count}")))
.collect(),
uncertainties: Vec::new(),
}
}
fn graph_proof(applicable_targets: usize) -> DependencyProof {
DependencyProof {
schema_version: 1,
package_ids: Vec::new(),
crate_names: Vec::new(),
evidence_source: "resolved_graph",
applicable_configurations: applicable_targets,
complete_configurations: applicable_targets,
used_observations: 0,
unused_observations: applicable_targets,
incomplete_observations: 0,
cache_hits: 0,
cache_misses: 0,
cache_miss_reasons: Vec::new(),
cache_publication_bypasses: Vec::new(),
uncertainties: Vec::new(),
}
}
fn target_constraint_matches_any(
cfg: &str,
configured_targets: &[&str],
cfg_sets: &HashMap<String, TargetCfgSet>,
) -> bool {
configured_targets
.iter()
.copied()
.any(|target| target_constraint_matches_target(cfg, target, cfg_sets.get(target)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_target_constraint_matches_windows() {
let targets = ["x86_64-pc-windows-msvc"];
let mut cfg_sets = HashMap::new();
cfg_sets.insert(
"x86_64-pc-windows-msvc".to_string(),
TargetCfgSet::from_rustc_output("windows\ntarget_os=\"windows\""),
);
assert!(target_constraint_matches_any("cfg(windows)", &targets, &cfg_sets));
assert!(!target_constraint_matches_any("cfg(unix)", &targets, &cfg_sets));
}
#[test]
fn test_target_constraint_matches_unix() {
let targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"];
let mut cfg_sets = HashMap::new();
cfg_sets.insert(
"x86_64-unknown-linux-gnu".to_string(),
TargetCfgSet::from_rustc_output("unix\ntarget_os=\"linux\""),
);
cfg_sets.insert(
"aarch64-apple-darwin".to_string(),
TargetCfgSet::from_rustc_output("unix\ntarget_os=\"macos\""),
);
assert!(target_constraint_matches_any("cfg(unix)", &targets, &cfg_sets));
assert!(!target_constraint_matches_any("cfg(windows)", &targets, &cfg_sets));
}
#[test]
fn test_target_constraint_matches_specific_os() {
let targets = ["x86_64-unknown-linux-gnu"];
let mut cfg_sets = HashMap::new();
cfg_sets.insert(
"x86_64-unknown-linux-gnu".to_string(),
TargetCfgSet::from_rustc_output("unix\ntarget_os=\"linux\""),
);
assert!(target_constraint_matches_any(
"cfg(target_os = \"linux\")",
&targets,
&cfg_sets
));
assert!(!target_constraint_matches_any(
"cfg(target_os = \"macos\")",
&targets,
&cfg_sets
));
}
#[test]
fn test_usage_required_targets_filters_by_cfg() {
let usage = DepUsage {
unconditional_features: std::collections::BTreeSet::new(),
local_features: std::collections::BTreeSet::new(),
conditional_features: std::collections::BTreeSet::new(),
default_features: true,
kind: DepKind::Normal,
target: Some("cfg(windows)".to_string()),
used_by: Arc::from("crate"),
optional: false,
path: None,
declared_version: Some("1".to_string()),
manifest_path: None,
cargo_toml_key: Arc::from("dep"),
referenced_in_features: false,
workspace_inherited: false,
};
let configured = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"];
let mut cfg_sets = HashMap::new();
cfg_sets.insert(
"x86_64-unknown-linux-gnu".to_string(),
TargetCfgSet::from_rustc_output("unix\ntarget_os=\"linux\""),
);
cfg_sets.insert(
"x86_64-pc-windows-msvc".to_string(),
TargetCfgSet::from_rustc_output("windows\ntarget_os=\"windows\""),
);
let required = usage_required_targets(&usage, &configured, &cfg_sets);
assert_eq!(required, vec!["x86_64-pc-windows-msvc"]);
}
}