use crate::error::{RailResult, ResultExt};
use cargo_metadata::DependencyKind as MetadataDepKind;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use toml_edit::{DocumentMut, Item, Value};
#[derive(Debug, Clone)]
pub struct DepKey {
pub name: Arc<str>,
pub renamed_from: Option<Arc<str>>,
}
impl DepKey {
pub fn new(name: impl Into<Arc<str>>) -> Self {
Self {
name: name.into(),
renamed_from: None,
}
}
pub fn is_renamed(&self) -> bool {
self.renamed_from.is_some()
}
pub fn alias(&self) -> &str {
self.renamed_from.as_deref().unwrap_or(&self.name)
}
}
impl PartialEq for DepKey {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.renamed_from == other.renamed_from
}
}
impl Eq for DepKey {}
impl std::hash::Hash for DepKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.renamed_from.hash(state);
}
}
impl PartialOrd for DepKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DepKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.name.cmp(&other.name) {
std::cmp::Ordering::Equal => self.renamed_from.cmp(&other.renamed_from),
other => other,
}
}
}
#[derive(Debug, Clone)]
pub struct DepUsage {
pub unconditional_features: BTreeSet<String>,
pub conditional_features: BTreeSet<String>,
pub default_features: bool,
pub kind: DepKind,
pub target: Option<String>,
pub used_by: Arc<str>,
pub optional: bool,
pub path: Option<String>,
pub declared_version: Option<String>,
pub manifest_path: Option<PathBuf>,
pub cargo_toml_key: Arc<str>,
pub referenced_in_features: bool,
}
#[derive(Debug, Default)]
struct ParsedDepTable {
renamed_from: Option<String>,
actual_name: Option<String>,
unconditional_features: BTreeSet<String>,
default_features: bool,
optional: bool,
path: Option<String>,
declared_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DepKind {
Normal,
Dev,
Build,
}
impl From<MetadataDepKind> for DepKind {
fn from(kind: MetadataDepKind) -> Self {
match kind {
MetadataDepKind::Normal => DepKind::Normal,
MetadataDepKind::Development => DepKind::Dev,
MetadataDepKind::Build => DepKind::Build,
_ => DepKind::Normal,
}
}
}
#[derive(Debug)]
pub struct ParsedManifest {
pub path: PathBuf,
pub package_name: String,
pub dependencies: HashMap<DepKey, Vec<DepUsage>>,
}
struct ParseContext<'a> {
package_name: &'a str,
manifest_path: &'a Path,
dependencies: &'a mut HashMap<DepKey, Vec<DepUsage>>,
}
pub struct ManifestAnalyzer {
pub members: Vec<ParsedManifest>,
usage_index: HashMap<DepKey, Vec<DepUsage>>,
usage_counts: HashMap<DepKey, usize>,
package_index: HashMap<Arc<str>, Vec<DepKey>>,
}
impl ManifestAnalyzer {
fn unconditional_usages(&self, dep: &DepKey) -> Vec<&DepUsage> {
self
.usage_index
.get(dep)
.map(|usages| usages.iter().filter(|u| u.target.is_none()).collect())
.unwrap_or_default()
}
pub fn parse_workspace(_workspace_root: &Path, members: &[&cargo_metadata::Package]) -> RailResult<Self> {
let results: Vec<RailResult<ParsedManifest>> = members
.par_iter()
.map(|pkg| {
let manifest_path = pkg.manifest_path.as_std_path();
Self::parse_single_manifest(manifest_path, &pkg.name)
})
.collect();
let mut parsed_members = Vec::with_capacity(results.len());
for result in results {
parsed_members.push(result?);
}
let mut usage_index: HashMap<DepKey, Vec<DepUsage>> = HashMap::new();
for member in &parsed_members {
for (dep_key, usages) in &member.dependencies {
usage_index
.entry(dep_key.clone())
.or_default()
.extend(usages.iter().cloned());
}
}
let usage_counts: HashMap<DepKey, usize> = usage_index
.iter()
.map(|(key, usages)| {
let unique_users: HashSet<_> = usages.iter().map(|u| &u.used_by).collect();
(key.clone(), unique_users.len())
})
.collect();
let mut package_index: HashMap<Arc<str>, Vec<DepKey>> = HashMap::new();
for dep_key in usage_index.keys() {
package_index
.entry(Arc::clone(&dep_key.name))
.or_default()
.push(dep_key.clone());
}
Ok(Self {
members: parsed_members,
usage_index,
usage_counts,
package_index,
})
}
fn parse_single_manifest(manifest_path: &Path, package_name: &str) -> RailResult<ParsedManifest> {
let content =
std::fs::read_to_string(manifest_path).with_context(|| format!("Failed to read {}", manifest_path.display()))?;
let doc: DocumentMut = content
.parse()
.with_context(|| format!("Failed to parse {}", manifest_path.display()))?;
let mut dependencies = HashMap::new();
let mut ctx = ParseContext {
package_name,
manifest_path,
dependencies: &mut dependencies,
};
Self::parse_dep_section(&mut ctx, doc.as_item(), "dependencies", DepKind::Normal, None)?;
Self::parse_dep_section(&mut ctx, doc.as_item(), "dev-dependencies", DepKind::Dev, None)?;
Self::parse_dep_section(&mut ctx, doc.as_item(), "build-dependencies", DepKind::Build, None)?;
if let Some(target_table) = doc.get("target").and_then(|t| t.as_table()) {
for (target_cfg, target_value) in target_table {
if let Some(target_deps) = target_value.as_table() {
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if target_deps.contains_key(section) {
let kind = match section {
"dependencies" => DepKind::Normal,
"dev-dependencies" => DepKind::Dev,
"build-dependencies" => DepKind::Build,
_ => continue,
};
Self::parse_dep_section(&mut ctx, target_value, section, kind, Some(target_cfg))?;
}
}
}
}
}
if let Some(features_table) = doc.get("features").and_then(|f| f.as_table()) {
for (_feature_name, feature_value) in features_table {
if let Some(feature_list) = feature_value.as_array() {
for item in feature_list {
if let Some(s) = item.as_str() {
if let Some(dep_name) = s.strip_prefix("dep:") {
let dep_key = DepKey::new(dep_name);
if let Some(usages) = dependencies.get_mut(&dep_key) {
for usage in usages {
usage.referenced_in_features = true;
}
}
}
else if let Some((dep, feat)) = s.split_once('/') {
let dep_name = dep.strip_prefix("dep:").unwrap_or(dep);
let dep_key = DepKey::new(dep_name);
if let Some(usages) = dependencies.get_mut(&dep_key) {
for usage in usages {
usage.conditional_features.insert(feat.to_string());
usage.referenced_in_features = true;
}
}
}
else {
let dep_key = DepKey::new(s);
if let Some(usages) = dependencies.get_mut(&dep_key) {
for usage in usages {
if usage.optional {
usage.referenced_in_features = true;
}
}
}
}
}
}
}
}
}
Ok(ParsedManifest {
path: manifest_path.to_path_buf(),
package_name: package_name.to_string(),
dependencies,
})
}
fn parse_dep_section(
ctx: &mut ParseContext<'_>,
parent: &Item,
section: &str,
kind: DepKind,
target: Option<&str>,
) -> RailResult<()> {
let Some(deps_table) = parent.get(section).and_then(|d| d.as_table_like()) else {
return Ok(()); };
for (dep_name, dep_value) in deps_table.iter() {
let parsed = match dep_value {
Item::Value(Value::String(version_str)) => {
Some(ParsedDepTable {
declared_version: Some(version_str.value().to_string()),
default_features: true,
..Default::default()
})
}
Item::Value(Value::InlineTable(inline_table)) => Self::parse_dep_table(inline_table, dep_name),
Item::Table(table) => Self::parse_dep_table(table, dep_name),
_ => None,
};
if let Some(p) = parsed {
let dep_key = if let Some(actual) = p.actual_name {
DepKey {
name: actual.into(),
renamed_from: p.renamed_from.map(Into::into),
}
} else {
DepKey::new(dep_name)
};
let usage = DepUsage {
unconditional_features: p.unconditional_features,
conditional_features: BTreeSet::new(), default_features: p.default_features,
kind,
target: target.map(String::from),
used_by: Arc::from(ctx.package_name),
optional: p.optional,
path: p.path,
declared_version: p.declared_version,
manifest_path: Some(ctx.manifest_path.to_path_buf()),
cargo_toml_key: Arc::from(dep_name),
referenced_in_features: false, };
ctx.dependencies.entry(dep_key).or_default().push(usage);
}
}
Ok(())
}
fn parse_dep_table<T: toml_edit::TableLike>(table: &T, dep_name: &str) -> Option<ParsedDepTable> {
if table.get("workspace").and_then(|v| v.as_bool()) == Some(true) {
return None;
}
let mut parsed = ParsedDepTable {
default_features: true, ..Default::default()
};
if let Some(pkg) = table.get("package").and_then(|v| v.as_str()) {
if pkg != dep_name {
parsed.renamed_from = Some(dep_name.to_string());
parsed.actual_name = Some(pkg.to_string());
}
}
parsed.declared_version = table.get("version").and_then(|v| v.as_str()).map(String::from);
parsed.path = table.get("path").and_then(|v| v.as_str()).map(String::from);
if let Some(features) = table.get("features").and_then(|f| f.as_array()) {
for feat in features {
if let Some(s) = feat.as_str() {
parsed.unconditional_features.insert(s.to_string());
}
}
}
if let Some(df) = table.get("default-features").and_then(|v| v.as_bool()) {
parsed.default_features = df;
}
if let Some(opt) = table.get("optional").and_then(|v| v.as_bool()) {
parsed.optional = opt;
}
Some(parsed)
}
pub fn compute_union(&self, dep: &DepKey) -> BTreeSet<String> {
let Some(usages) = self.usage_index.get(dep) else {
return BTreeSet::new();
};
if usages.is_empty() {
return BTreeSet::new();
}
let mut union = BTreeSet::new();
for usage in usages {
if usage.target.is_none() {
union.extend(usage.unconditional_features.iter().cloned());
}
}
union
}
pub fn compute_target_local_features(&self, dep: &DepKey) -> BTreeSet<String> {
let Some(usages) = self.usage_index.get(dep) else {
return BTreeSet::new();
};
let mut target_features = BTreeSet::new();
for usage in usages {
if usage.target.is_some() {
target_features.extend(usage.unconditional_features.iter().cloned());
}
}
let unconditional = self.compute_union(dep);
target_features.difference(&unconditional).cloned().collect()
}
pub fn compute_intersection(&self, dep: &DepKey) -> BTreeSet<String> {
let unconditional_usages = self.unconditional_usages(dep);
if unconditional_usages.len() < 2 {
return BTreeSet::new(); }
let mut intersection = unconditional_usages[0].unconditional_features.clone();
for usage in &unconditional_usages[1..] {
intersection = intersection
.intersection(&usage.unconditional_features)
.cloned()
.collect();
}
intersection
}
pub fn get_usage_sites(&self, dep: &DepKey) -> Vec<&DepUsage> {
self
.usage_index
.get(dep)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn all_dependencies(&self) -> Vec<&DepKey> {
self.usage_index.keys().collect()
}
pub fn has_mixed_defaults(&self, dep: &DepKey) -> bool {
let unconditional_usages = self.unconditional_usages(dep);
if unconditional_usages.len() < 2 {
return false;
}
let first_default = unconditional_usages[0].default_features;
!unconditional_usages.iter().all(|u| u.default_features == first_default)
}
pub fn default_features_policy(&self, dep: &DepKey) -> Option<bool> {
let unconditional_usages = self.unconditional_usages(dep);
if unconditional_usages.is_empty() {
return None;
}
if unconditional_usages.iter().any(|u| !u.default_features) {
Some(false)
} else {
Some(true)
}
}
pub fn usage_count(&self, dep: &DepKey) -> usize {
self.usage_counts.get(dep).copied().unwrap_or(0)
}
pub fn package_usage_count(&self, package_name: &str) -> usize {
let Some(dep_keys) = self.package_index.get(package_name) else {
return 0;
};
let mut unique_users: HashSet<&str> = HashSet::new();
for dep_key in dep_keys {
if let Some(usages) = self.usage_index.get(dep_key) {
for usage in usages {
unique_users.insert(&usage.used_by);
}
}
}
unique_users.len()
}
pub fn dep_keys_for_package(&self, package_name: &str) -> Vec<&DepKey> {
self
.package_index
.get(package_name)
.map(|keys| keys.iter().collect())
.unwrap_or_default()
}
pub fn get_package_usage_sites(&self, package_name: &str) -> Vec<&DepUsage> {
let Some(dep_keys) = self.package_index.get(package_name) else {
return Vec::new();
};
let mut all_usages = Vec::new();
for dep_key in dep_keys {
if let Some(usages) = self.usage_index.get(dep_key) {
all_usages.extend(usages.iter());
}
}
all_usages
}
pub fn compute_package_union(&self, package_name: &str) -> BTreeSet<String> {
let mut union = BTreeSet::new();
for usage in self.get_package_usage_sites(package_name) {
if usage.target.is_none() {
union.extend(usage.unconditional_features.iter().cloned());
}
}
union
}
pub fn package_has_mixed_defaults(&self, package_name: &str) -> bool {
let usages: Vec<_> = self
.get_package_usage_sites(package_name)
.into_iter()
.filter(|u| u.target.is_none())
.collect();
if usages.len() < 2 {
return false;
}
let first_default = usages[0].default_features;
!usages.iter().all(|u| u.default_features == first_default)
}
pub fn package_default_features_policy(&self, package_name: &str) -> Option<bool> {
let usages: Vec<_> = self
.get_package_usage_sites(package_name)
.into_iter()
.filter(|u| u.target.is_none())
.collect();
if usages.is_empty() {
return None;
}
if usages.iter().any(|u| !u.default_features) {
Some(false)
} else {
Some(true)
}
}
pub fn unique_packages(&self) -> HashSet<Arc<str>> {
self.usage_index.keys().map(|k| Arc::clone(&k.name)).collect()
}
}
#[derive(Debug, Clone)]
pub struct ExistingWorkspaceDep {
pub name: String,
pub version: Option<String>,
pub features: Vec<String>,
pub default_features: bool,
pub path: Option<String>,
}
pub fn parse_existing_workspace_deps(workspace_root: &Path) -> RailResult<FxHashMap<String, ExistingWorkspaceDep>> {
let workspace_toml = workspace_root.join("Cargo.toml");
let content = match std::fs::read_to_string(&workspace_toml) {
Ok(c) => c,
Err(_) => return Ok(FxHashMap::default()), };
let doc: DocumentMut = content
.parse()
.with_context(|| format!("Failed to parse {}", workspace_toml.display()))?;
let mut existing = FxHashMap::default();
let Some(workspace) = doc.get("workspace").and_then(|w| w.as_table()) else {
return Ok(existing); };
let Some(deps) = workspace.get("dependencies").and_then(|d| d.as_table_like()) else {
return Ok(existing); };
for (name, value) in deps.iter() {
let dep = parse_workspace_dep_entry(name, value);
existing.insert(name.to_string(), dep);
}
Ok(existing)
}
fn parse_workspace_dep_entry(name: &str, value: &Item) -> ExistingWorkspaceDep {
let mut dep = ExistingWorkspaceDep {
name: name.to_string(),
version: None,
features: Vec::new(),
default_features: true,
path: None,
};
if let Some(version) = value.as_str() {
dep.version = Some(version.to_string());
return dep;
}
if let Some(table) = value.as_inline_table() {
if let Some(v) = table.get("version").and_then(|v| v.as_str()) {
dep.version = Some(v.to_string());
}
if let Some(p) = table.get("path").and_then(|v| v.as_str()) {
dep.path = Some(p.to_string());
}
if let Some(df) = table.get("default-features").and_then(|v| v.as_bool()) {
dep.default_features = df;
}
if let Some(features) = table.get("features").and_then(|f| f.as_array()) {
dep.features = features.iter().filter_map(|v| v.as_str()).map(String::from).collect();
}
return dep;
}
if let Some(table) = value.as_table() {
if let Some(v) = table.get("version").and_then(|i| i.as_value()).and_then(|v| v.as_str()) {
dep.version = Some(v.to_string());
}
if let Some(p) = table.get("path").and_then(|i| i.as_value()).and_then(|v| v.as_str()) {
dep.path = Some(p.to_string());
}
if let Some(df) = table
.get("default-features")
.and_then(|i| i.as_value())
.and_then(|v| v.as_bool())
{
dep.default_features = df;
}
if let Some(features) = table
.get("features")
.and_then(|i| i.as_value())
.and_then(|v| v.as_array())
{
dep.features = features.iter().filter_map(|v| v.as_str()).map(String::from).collect();
}
}
dep
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_workspace(workspace_toml_content: &str) -> TempDir {
let dir = TempDir::new().unwrap();
let mut file = std::fs::File::create(dir.path().join("Cargo.toml")).unwrap();
file.write_all(workspace_toml_content.as_bytes()).unwrap();
dir
}
#[test]
fn test_parse_existing_workspace_deps_empty() {
let dir = create_test_workspace(
r#"
[workspace]
members = ["crate-a"]
"#,
);
let result = parse_existing_workspace_deps(dir.path()).unwrap();
assert!(
result.is_empty(),
"Should return empty map when no workspace.dependencies"
);
}
#[test]
fn test_parse_existing_workspace_deps_simple_version() {
let dir = create_test_workspace(
r#"
[workspace]
members = ["crate-a"]
[workspace.dependencies]
serde = "1.0"
anyhow = "1.0.50"
"#,
);
let result = parse_existing_workspace_deps(dir.path()).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result["serde"].version.as_ref().unwrap(), "1.0");
assert_eq!(result["anyhow"].version.as_ref().unwrap(), "1.0.50");
assert!(result["serde"].features.is_empty());
assert!(result["serde"].default_features);
}
#[test]
fn test_parse_existing_workspace_deps_inline_table() {
let dir = create_test_workspace(
r#"
[workspace]
members = ["crate-a"]
[workspace.dependencies]
serde = { version = "1.0", features = ["derive", "rc"], default-features = false }
tokio = { version = "1.0", features = ["full"] }
"#,
);
let result = parse_existing_workspace_deps(dir.path()).unwrap();
assert_eq!(result.len(), 2);
let serde = &result["serde"];
assert_eq!(serde.version.as_ref().unwrap(), "1.0");
assert_eq!(serde.features, vec!["derive", "rc"]);
assert!(!serde.default_features);
let tokio = &result["tokio"];
assert_eq!(tokio.version.as_ref().unwrap(), "1.0");
assert_eq!(tokio.features, vec!["full"]);
assert!(tokio.default_features);
}
#[test]
fn test_parse_existing_workspace_deps_with_path() {
let dir = create_test_workspace(
r#"
[workspace]
members = ["crate-a", "crate-b"]
[workspace.dependencies]
crate-a = { path = "crate-a", version = "0.1.0" }
external = "1.0"
"#,
);
let result = parse_existing_workspace_deps(dir.path()).unwrap();
assert_eq!(result.len(), 2);
let crate_a = &result["crate-a"];
assert_eq!(crate_a.path.as_ref().unwrap(), "crate-a");
assert_eq!(crate_a.version.as_ref().unwrap(), "0.1.0");
let external = &result["external"];
assert!(external.path.is_none());
}
}