use std::collections::{BTreeSet, HashSet};
use cargo_metadata::{DependencyKind, PackageId};
use petgraph::Direction;
use petgraph::visit::EdgeRef as _;
use super::core::WorkspaceGraph;
use crate::error::{RailError, RailResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ImpactDomain {
Build,
Development,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ImpactFallback {
HistoricalResolutionUnavailable,
TargetUnfiltered,
UnknownDependencyKind,
DeclarationUnknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ImpactStep {
pub(crate) dependency: PackageId,
pub(crate) dependent: PackageId,
pub(crate) alias: String,
pub(crate) kind: DependencyKind,
pub(crate) target: Option<String>,
pub(crate) optional: Option<bool>,
pub(crate) uses_default_features: Option<bool>,
pub(crate) features: Vec<String>,
pub(crate) proc_macro: bool,
pub(crate) host: bool,
pub(crate) domain: ImpactDomain,
pub(crate) fallbacks: Vec<ImpactFallback>,
}
#[derive(Debug, Default)]
pub(crate) struct ImpactPropagation {
pub(crate) build: BTreeSet<PackageId>,
pub(crate) development: BTreeSet<PackageId>,
pub(crate) steps: Vec<ImpactStep>,
}
impl ImpactPropagation {
pub(crate) fn merge(&mut self, mut other: Self) {
self.build.append(&mut other.build);
self.development.append(&mut other.development);
self.development.retain(|package| !self.build.contains(package));
self.steps.append(&mut other.steps);
sort_steps(&mut self.steps);
}
}
pub(super) fn propagate(
graph: &WorkspaceGraph,
seeds: &HashSet<PackageId>,
target_filtered: bool,
) -> RailResult<ImpactPropagation> {
if seeds.is_empty() {
return Ok(ImpactPropagation::default());
}
let seed_nodes = seeds
.iter()
.map(|seed| {
graph
.id_to_node
.get(seed)
.copied()
.ok_or_else(|| RailError::message(format!("Package ID '{}' not found in the resolved graph", seed)))
})
.collect::<RailResult<Vec<_>>>()?;
let seed_node_set = seed_nodes.iter().copied().collect::<HashSet<_>>();
let mut visited = seed_node_set.clone();
let mut stack = seed_nodes;
let mut impact = ImpactPropagation::default();
let mut node_visits = 0;
let mut edge_visits = 0;
while let Some(dependency) = stack.pop() {
node_visits += 1;
for edge in graph.graph.edges_directed(dependency, Direction::Incoming) {
edge_visits += 1;
let dependent = edge.source();
let semantics = edge.weight();
let domain = match semantics.kind {
DependencyKind::Development => ImpactDomain::Development,
DependencyKind::Normal | DependencyKind::Build | DependencyKind::Unknown => ImpactDomain::Build,
};
if graph.graph[dependent].is_workspace_member && !seed_node_set.contains(&dependent) {
let dependent_id = graph.graph[dependent].id.clone();
match domain {
ImpactDomain::Build => {
impact.build.insert(dependent_id.clone());
}
ImpactDomain::Development => {
impact.development.insert(dependent_id.clone());
}
}
impact.steps.push(ImpactStep {
dependency: graph.graph[dependency].id.clone(),
dependent: dependent_id,
alias: semantics.name.clone(),
kind: semantics.kind,
target: semantics.target.as_ref().map(ToString::to_string),
optional: semantics.optional,
uses_default_features: semantics.uses_default_features,
features: semantics.features.clone(),
proc_macro: graph.graph[dependency].is_proc_macro,
host: semantics.kind == DependencyKind::Build || graph.graph[dependency].is_proc_macro,
domain,
fallbacks: fallbacks(
semantics.kind,
semantics.target.is_some(),
semantics.optional,
target_filtered,
),
});
}
if domain == ImpactDomain::Build && visited.insert(dependent) {
stack.push(dependent);
}
}
}
crate::instrumentation::record_graph_traversal(node_visits, edge_visits);
impact.development.retain(|package| !impact.build.contains(package));
sort_steps(&mut impact.steps);
Ok(impact)
}
fn sort_steps(steps: &mut Vec<ImpactStep>) {
steps.sort_by(|left, right| {
(
&left.dependent,
left.domain,
&left.dependency,
&left.alias,
dependency_kind_rank(left.kind),
&left.target,
&left.features,
)
.cmp(&(
&right.dependent,
right.domain,
&right.dependency,
&right.alias,
dependency_kind_rank(right.kind),
&right.target,
&right.features,
))
});
steps.dedup();
}
const fn dependency_kind_rank(kind: DependencyKind) -> u8 {
match kind {
DependencyKind::Normal => 0,
DependencyKind::Development => 1,
DependencyKind::Build => 2,
DependencyKind::Unknown => 3,
}
}
fn fallbacks(
kind: DependencyKind,
target_conditioned: bool,
optional: Option<bool>,
target_filtered: bool,
) -> Vec<ImpactFallback> {
let mut fallbacks = Vec::with_capacity(3);
if target_conditioned && !target_filtered {
fallbacks.push(ImpactFallback::TargetUnfiltered);
}
if kind == DependencyKind::Unknown {
fallbacks.push(ImpactFallback::UnknownDependencyKind);
}
if optional.is_none() {
fallbacks.push(ImpactFallback::DeclarationUnknown);
}
fallbacks
}