use crate::cargo::manifest_analyzer::DepKind;
use cargo_metadata::PackageId;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const COLLECTOR_VERSION: u32 = 15;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PlatformTarget(pub String);
impl PlatformTarget {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for PlatformTarget {
fn from(target: &str) -> Self {
Self(target.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum FeatureSelection {
Default,
DefaultWith(Vec<String>),
Selected(Vec<String>),
NoDefaultFeatures,
AllFeatures,
}
impl FeatureSelection {
pub const BASELINES: [Self; 3] = [Self::Default, Self::NoDefaultFeatures, Self::AllFeatures];
#[must_use]
pub fn label(&self) -> String {
match self {
Self::Default => "default-features".to_string(),
Self::DefaultWith(features) => format!("default-features+selected:{}", features.join(",")),
Self::NoDefaultFeatures => "no-default-features".to_string(),
Self::AllFeatures => "all-features".to_string(),
Self::Selected(features) => format!("selected:{}", features.join(",")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CoverageView {
pub target: PlatformTarget,
pub features: FeatureSelection,
pub packages: Vec<String>,
}
impl CoverageView {
pub(crate) fn new(target: PlatformTarget, features: FeatureSelection, packages: BTreeSet<String>) -> Self {
Self {
target,
features,
packages: packages.into_iter().collect(),
}
}
pub fn identity(&self) -> crate::error::RailResult<String> {
let bytes = serde_json::to_vec(self)?;
Ok(format!(
"coverage:v1-sha256-{}",
crate::source::ContentDigest::sha256(&bytes)
))
}
#[must_use]
pub fn cargo_arguments(&self) -> Vec<String> {
let mut arguments = vec!["check".to_string(), "--locked".to_string(), "--all-targets".to_string()];
self.extend_feature_arguments(&mut arguments);
self.extend_package_and_target_arguments(&mut arguments);
arguments
}
#[must_use]
pub fn nextest_arguments(&self) -> Vec<String> {
let mut arguments = vec!["nextest".to_string(), "run".to_string(), "--locked".to_string()];
self.extend_feature_arguments(&mut arguments);
self.extend_package_and_target_arguments(&mut arguments);
arguments
}
fn extend_feature_arguments(&self, arguments: &mut Vec<String>) {
match &self.features {
FeatureSelection::Default => {}
FeatureSelection::DefaultWith(features) => {
for package in &self.packages {
for feature in features {
arguments.push("--features".to_string());
arguments.push(format!("{package}/{feature}"));
}
}
}
FeatureSelection::NoDefaultFeatures => arguments.push("--no-default-features".to_string()),
FeatureSelection::AllFeatures => arguments.push("--all-features".to_string()),
FeatureSelection::Selected(features) => {
arguments.push("--no-default-features".to_string());
for package in &self.packages {
for feature in features {
arguments.push("--features".to_string());
arguments.push(format!("{package}/{feature}"));
}
}
}
}
}
fn extend_package_and_target_arguments(&self, arguments: &mut Vec<String>) {
for package in &self.packages {
arguments.push("--package".to_string());
arguments.push(package.clone());
}
if self.target.as_str() != "default" {
arguments.push("--target".to_string());
arguments.push(self.target.as_str().to_string());
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum CargoTargetKind {
Library,
Binary,
Test,
Example,
Benchmark,
ProcMacro,
CustomBuild,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct CompilationUnitId {
pub kind: CargoTargetKind,
pub name: String,
pub source: Option<String>,
pub test_mode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilationUnitEvidence {
pub unit: CompilationUnitId,
pub unused_crates: BTreeSet<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DependencyEvidenceState {
Used,
Unused,
Inapplicable,
Incomplete,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceSummary {
pub applicable: usize,
pub complete: usize,
pub used: usize,
pub unused: usize,
pub incomplete: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceCacheSummary {
pub hits: usize,
pub misses: usize,
pub miss_reasons: BTreeMap<String, usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DependencyIdentity {
pub member: PackageId,
pub declaration_key: String,
pub resolved_packages: BTreeSet<PackageId>,
pub crate_names: BTreeSet<String>,
pub kind: DepKind,
pub target: Option<String>,
pub optional: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetEvidence {
pub platform: PlatformTarget,
pub features: FeatureSelection,
pub compiled_units: BTreeSet<CompilationUnitId>,
pub unused_crates: BTreeSet<String>,
pub unit_evidence: Vec<CompilationUnitEvidence>,
pub completeness: DiagnosticsCompleteness,
}
impl TargetEvidence {
#[must_use]
pub fn dependency_state(&self, crate_name: &str) -> DependencyEvidenceState {
if self.completeness != DiagnosticsCompleteness::Complete || self.compiled_units.is_empty() {
return DependencyEvidenceState::Incomplete;
}
if self.unused_crates.contains(crate_name) {
DependencyEvidenceState::Unused
} else {
DependencyEvidenceState::Used
}
}
#[must_use]
pub fn dependency_state_for_kind(&self, crate_name: &str, kind: DepKind) -> DependencyEvidenceState {
if self.completeness != DiagnosticsCompleteness::Complete {
return DependencyEvidenceState::Incomplete;
}
let relevant: Vec<_> = self
.compiled_units
.iter()
.filter(|unit| match kind {
DepKind::Normal => !unit.test_mode && unit.kind != CargoTargetKind::CustomBuild,
DepKind::Dev => {
unit.test_mode
|| matches!(
unit.kind,
CargoTargetKind::Test | CargoTargetKind::Example | CargoTargetKind::Benchmark
)
}
DepKind::Build => unit.kind == CargoTargetKind::CustomBuild,
})
.collect();
if relevant.is_empty() {
return DependencyEvidenceState::Incomplete;
}
if relevant.iter().all(|unit| {
self.unit_evidence
.iter()
.find(|evidence| &evidence.unit == *unit)
.is_some_and(|evidence| evidence.unused_crates.contains(crate_name))
}) {
DependencyEvidenceState::Unused
} else {
DependencyEvidenceState::Used
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberEvidence {
pub package_id: PackageId,
pub configurations: BTreeMap<PlatformTarget, BTreeMap<FeatureSelection, TargetEvidence>>,
pub cache: EvidenceCacheSummary,
}
impl MemberEvidence {
#[must_use]
pub fn new(package_id: PackageId) -> Self {
Self {
package_id,
configurations: BTreeMap::new(),
cache: EvidenceCacheSummary::default(),
}
}
#[must_use]
pub fn dependency_state(
&self,
dependency: &DependencyIdentity,
required_targets: &[&str],
) -> DependencyEvidenceState {
if dependency.member != self.package_id || dependency.crate_names.is_empty() {
return DependencyEvidenceState::Incomplete;
}
let mut incomplete = false;
for crate_name in &dependency.crate_names {
match self.crate_state(crate_name, dependency.kind, required_targets) {
DependencyEvidenceState::Used => return DependencyEvidenceState::Used,
DependencyEvidenceState::Incomplete | DependencyEvidenceState::Inapplicable => incomplete = true,
DependencyEvidenceState::Unused => {}
}
}
if incomplete {
DependencyEvidenceState::Incomplete
} else {
DependencyEvidenceState::Unused
}
}
#[must_use]
pub fn dependency_summary(&self, dependency: &DependencyIdentity, required_targets: &[&str]) -> EvidenceSummary {
let mut summary = EvidenceSummary::default();
for target in required_targets {
let target = PlatformTarget::from(*target);
let configurations = self.configurations.get(&target);
for features in FeatureSelection::BASELINES {
if configurations.is_none_or(|items| !items.contains_key(&features)) {
summary.applicable += 1;
summary.incomplete += 1;
}
}
for evidence in configurations.into_iter().flat_map(BTreeMap::values) {
summary.applicable += 1;
match dependency_state_in_configuration(evidence, dependency) {
DependencyEvidenceState::Used => {
summary.complete += 1;
summary.used += 1;
}
DependencyEvidenceState::Unused => {
summary.complete += 1;
summary.unused += 1;
}
DependencyEvidenceState::Incomplete | DependencyEvidenceState::Inapplicable => {
summary.incomplete += 1
}
}
}
}
summary
}
#[must_use]
pub fn dependency_state_for_features(
&self,
dependency: &DependencyIdentity,
required_targets: &[&str],
required_features: &[FeatureSelection],
) -> DependencyEvidenceState {
if dependency.member != self.package_id || dependency.crate_names.is_empty() || required_targets.is_empty() {
return DependencyEvidenceState::Incomplete;
}
let mut incomplete = false;
for target in required_targets {
let configurations = self.configurations.get(&PlatformTarget::from(*target));
for features in required_features {
let Some(evidence) = configurations.and_then(|items| items.get(features)) else {
incomplete = true;
continue;
};
match dependency_state_in_configuration(evidence, dependency) {
DependencyEvidenceState::Used => return DependencyEvidenceState::Used,
DependencyEvidenceState::Incomplete | DependencyEvidenceState::Inapplicable => incomplete = true,
DependencyEvidenceState::Unused => {}
}
}
}
if incomplete {
DependencyEvidenceState::Incomplete
} else {
DependencyEvidenceState::Unused
}
}
#[must_use]
pub fn dependency_summary_for_features(
&self,
dependency: &DependencyIdentity,
required_targets: &[&str],
required_features: &[FeatureSelection],
) -> EvidenceSummary {
let mut summary = EvidenceSummary::default();
for target in required_targets {
let configurations = self.configurations.get(&PlatformTarget::from(*target));
for features in required_features {
summary.applicable += 1;
let Some(evidence) = configurations.and_then(|items| items.get(features)) else {
summary.incomplete += 1;
continue;
};
match dependency_state_in_configuration(evidence, dependency) {
DependencyEvidenceState::Used => {
summary.complete += 1;
summary.used += 1;
}
DependencyEvidenceState::Unused => {
summary.complete += 1;
summary.unused += 1;
}
DependencyEvidenceState::Incomplete | DependencyEvidenceState::Inapplicable => {
summary.incomplete += 1
}
}
}
}
summary
}
fn crate_state(&self, crate_name: &str, kind: DepKind, required_targets: &[&str]) -> DependencyEvidenceState {
if required_targets.is_empty() {
return DependencyEvidenceState::Inapplicable;
}
let mut incomplete = false;
for target in required_targets {
let configurations = self.configurations.get(&PlatformTarget::from(*target));
for features in FeatureSelection::BASELINES {
if configurations.is_none_or(|items| !items.contains_key(&features)) {
incomplete = true;
}
}
for evidence in configurations.into_iter().flat_map(BTreeMap::values) {
match evidence.dependency_state_for_kind(crate_name, kind) {
DependencyEvidenceState::Used => return DependencyEvidenceState::Used,
DependencyEvidenceState::Incomplete => incomplete = true,
DependencyEvidenceState::Unused => {}
DependencyEvidenceState::Inapplicable => incomplete = true,
}
}
}
if incomplete {
DependencyEvidenceState::Incomplete
} else {
DependencyEvidenceState::Unused
}
}
}
fn dependency_state_in_configuration(
evidence: &TargetEvidence,
dependency: &DependencyIdentity,
) -> DependencyEvidenceState {
let mut state = DependencyEvidenceState::Unused;
for crate_name in &dependency.crate_names {
match evidence.dependency_state_for_kind(crate_name, dependency.kind) {
DependencyEvidenceState::Used => return DependencyEvidenceState::Used,
DependencyEvidenceState::Incomplete | DependencyEvidenceState::Inapplicable => {
state = DependencyEvidenceState::Incomplete;
}
DependencyEvidenceState::Unused => {}
}
}
state
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CompilerDiagKey {
pub package_id: PackageId,
#[serde(default)]
pub package_name: String,
pub target: PlatformTarget,
pub features: FeatureSelection,
pub rustc_version: String,
pub cargo_version: String,
pub host_triple: String,
pub toolchain_fingerprint: String,
pub target_fingerprint: String,
pub lock_fingerprint: String,
pub manifest_fingerprint: String,
pub source_fingerprint: String,
pub compiler_env_fingerprint: String,
pub cargo_config_fingerprint: String,
}
impl CompilerDiagKey {
#[must_use]
pub fn stable_id(&self) -> String {
format!(
"{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
self.package_id,
self.target.as_str(),
self.features.label(),
self.rustc_version,
self.cargo_version,
self.host_triple,
self.toolchain_fingerprint,
self.target_fingerprint,
self.lock_fingerprint,
self.manifest_fingerprint,
self.source_fingerprint,
self.compiler_env_fingerprint,
self.cargo_config_fingerprint
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticsCompleteness {
Complete,
Incomplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerDiagEntry {
pub key: CompilerDiagKey,
pub evidence: TargetEvidence,
pub generated_at_unix_ms: u64,
pub collector_version: u32,
pub(crate) observations: Vec<crate::compiler::observation::CompilationObservationManifest>,
}
#[cfg(test)]
mod tests {
use super::*;
fn package_id(name: &str) -> PackageId {
PackageId {
repr: format!("path+file:///workspace#{name}@0.1.0"),
}
}
fn target_evidence(
target: &str,
features: FeatureSelection,
unused: &[&str],
completeness: DiagnosticsCompleteness,
) -> TargetEvidence {
let unit = CompilationUnitId {
kind: CargoTargetKind::Library,
name: "member".to_string(),
source: Some("src/lib.rs".to_string()),
test_mode: false,
};
TargetEvidence {
platform: PlatformTarget::from(target),
features,
compiled_units: BTreeSet::from([unit.clone()]),
unused_crates: unused.iter().map(|name| (*name).to_string()).collect(),
unit_evidence: vec![CompilationUnitEvidence {
unit,
unused_crates: unused.iter().map(|name| (*name).to_string()).collect(),
}],
completeness,
}
}
fn insert_target(
evidence: &mut MemberEvidence,
target: &str,
features: FeatureSelection,
unused: &[&str],
completeness: DiagnosticsCompleteness,
) {
let target_evidence = target_evidence(target, features.clone(), unused, completeness);
evidence
.configurations
.entry(target_evidence.platform.clone())
.or_default()
.insert(features, target_evidence);
}
fn dependency(member: &PackageId, crate_name: &str) -> DependencyIdentity {
DependencyIdentity {
member: member.clone(),
declaration_key: crate_name.to_string(),
resolved_packages: BTreeSet::from([package_id(crate_name)]),
crate_names: BTreeSet::from([crate_name.to_string()]),
kind: DepKind::Normal,
target: None,
optional: false,
}
}
#[test]
fn test_member_evidence_positive_usage_disproves_removal_with_missing_targets() {
let mut evidence = MemberEvidence::new(package_id("member"));
let dependency = dependency(&evidence.package_id, "serde");
insert_target(
&mut evidence,
"aarch64-apple-darwin",
FeatureSelection::Default,
&[],
DiagnosticsCompleteness::Complete,
);
assert_eq!(
evidence.dependency_state(&dependency, &["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]),
DependencyEvidenceState::Used
);
}
#[test]
fn test_member_evidence_requires_complete_unused_observations() {
let mut evidence = MemberEvidence::new(package_id("member"));
let dependency = dependency(&evidence.package_id, "serde");
for target in ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"] {
for features in FeatureSelection::BASELINES {
insert_target(
&mut evidence,
target,
features,
&["serde"],
DiagnosticsCompleteness::Complete,
);
}
}
assert_eq!(
evidence.dependency_state(&dependency, &["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]),
DependencyEvidenceState::Unused
);
assert_eq!(
evidence.dependency_state(&dependency, &["aarch64-apple-darwin", "x86_64-pc-windows-msvc"]),
DependencyEvidenceState::Incomplete
);
}
#[test]
fn test_member_evidence_keeps_package_identity_and_target_order() {
let package_id = package_id("member");
let mut evidence = MemberEvidence::new(package_id.clone());
for target in ["z-target", "a-target"] {
insert_target(
&mut evidence,
target,
FeatureSelection::Default,
&[],
DiagnosticsCompleteness::Complete,
);
}
assert_eq!(evidence.package_id, package_id);
assert_eq!(
evidence
.configurations
.keys()
.map(PlatformTarget::as_str)
.collect::<Vec<_>>(),
vec!["a-target", "z-target"]
);
let serialized = serde_json::to_string(&evidence).expect("serialize evidence");
assert!(
serialized.find("a-target") < serialized.find("z-target"),
"serialized evidence must retain deterministic platform order: {serialized}"
);
assert!(serialized.contains(&package_id.repr));
}
#[test]
fn test_dependency_kinds_use_independent_compilation_domains() {
let normal = CompilationUnitId {
kind: CargoTargetKind::Library,
name: "member".to_string(),
source: Some("src/lib.rs".to_string()),
test_mode: false,
};
let test = CompilationUnitId {
test_mode: true,
..normal.clone()
};
let build = CompilationUnitId {
kind: CargoTargetKind::CustomBuild,
name: "build-script-build".to_string(),
source: Some("build.rs".to_string()),
test_mode: false,
};
let evidence = TargetEvidence {
platform: PlatformTarget::from("default"),
features: FeatureSelection::Default,
compiled_units: BTreeSet::from([normal.clone(), test.clone(), build.clone()]),
unused_crates: BTreeSet::new(),
unit_evidence: vec![
CompilationUnitEvidence {
unit: normal,
unused_crates: BTreeSet::from(["normal_unused".to_string()]),
},
CompilationUnitEvidence {
unit: test,
unused_crates: BTreeSet::from(["dev_unused".to_string()]),
},
CompilationUnitEvidence {
unit: build,
unused_crates: BTreeSet::from(["build_unused".to_string()]),
},
],
completeness: DiagnosticsCompleteness::Complete,
};
assert_eq!(
evidence.dependency_state_for_kind("normal_unused", DepKind::Normal),
DependencyEvidenceState::Unused
);
assert_eq!(
evidence.dependency_state_for_kind("normal_unused", DepKind::Dev),
DependencyEvidenceState::Used
);
assert_eq!(
evidence.dependency_state_for_kind("dev_unused", DepKind::Dev),
DependencyEvidenceState::Unused
);
assert_eq!(
evidence.dependency_state_for_kind("build_unused", DepKind::Build),
DependencyEvidenceState::Unused
);
}
}