use std::collections::HashMap;
use std::path::PathBuf;
use cabin_core::standard_compatibility::{DependencyAttributes, ReqOfSource, Requirement};
use cabin_core::{
LanguageStandardSettings, LanguageStandardSource, ResolvedLanguageStandards, SourceLanguage,
StandardDeclaration, Target, classify_source, effective_c, effective_cxx,
};
use cabin_workspace::PackageKind;
use cabin_workspace::standards::{
DeclarationSite, DeclarationSites, Provenance, TargetEdge, TargetNode, effective_requirements,
provenance_c, provenance_cxx,
};
use crate::error::BuildError;
use crate::planner::{PlanRequest, TargetDepEdge, TargetId, format_target_id, lookup_target};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeclScope {
Package,
Target(String),
Workspace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclSite {
pub manifest_path: PathBuf,
pub scope: DeclScope,
pub field: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequirementOrigin {
Declared { site: DeclSite },
DeclaredNone { site: DeclSite },
HeaderOnlyInference { site: DeclSite },
CrossLanguageDefault,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeRequirement {
Min(&'static str),
Forbidden,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StandardCompatViolation {
pub consumer: String,
pub language: SourceLanguage,
pub consumer_standard: &'static str,
pub consumer_site: DeclSite,
pub dependency: String,
pub dependency_package: String,
pub dependency_version: String,
pub dependency_is_registry: bool,
pub requirement: EdgeRequirement,
pub origin_target: String,
pub origin: RequirementOrigin,
pub chain: Vec<String>,
pub consumer_manifest_path: PathBuf,
pub ignored: bool,
pub override_section: Option<String>,
}
struct NodeSites {
decl_c: Option<DeclSite>,
decl_cxx: Option<DeclSite>,
impl_c: Option<DeclSite>,
impl_cxx: Option<DeclSite>,
}
pub(crate) fn edge_violations(
topo: &[TargetId],
resolved_deps: &HashMap<TargetId, Vec<TargetDepEdge>>,
req: &PlanRequest<'_>,
) -> Result<Vec<StandardCompatViolation>, BuildError> {
let index_of: HashMap<&TargetId, usize> = topo
.iter()
.enumerate()
.map(|(index, tid)| (tid, index))
.collect();
let mut nodes: Vec<TargetNode> = Vec::with_capacity(topo.len());
let mut sites: Vec<NodeSites> = Vec::with_capacity(topo.len());
for tid in topo {
let target = lookup_target(tid, req.graph)?;
let pkg = &req.graph.packages[tid.0];
let pkg_standards = req
.language_standards
.get(&tid.0)
.copied()
.unwrap_or_default();
let (attributes, node_sites) =
dependency_attributes(tid, target, pkg_standards, &pkg.package.language, req);
let deps = resolved_deps
.get(tid)
.map(|edges| {
edges
.iter()
.map(|edge| TargetEdge {
to: index_of[&edge.to],
public: edge.public,
})
.collect()
})
.unwrap_or_default();
nodes.push(TargetNode {
name: format_target_id(tid, req.graph),
manifest_path: pkg.manifest_path.clone(),
attributes,
sites: declaration_sites(&node_sites),
deps,
});
sites.push(node_sites);
}
let effective = effective_requirements(&nodes);
let mut violations = Vec::new();
for (consumer_index, tid) in topo.iter().enumerate() {
let consumer_target = lookup_target(tid, req.graph)?;
if consumer_target.kind.is_header_only() {
continue;
}
let pkg_standards = req
.language_standards
.get(&tid.0)
.copied()
.unwrap_or_default();
let Some(edges) = resolved_deps.get(tid) else {
continue;
};
let compiles_c = has_sources_of(consumer_target, SourceLanguage::C);
let compiles_cxx = has_sources_of(consumer_target, SourceLanguage::Cxx);
let dev_edges_visible = consumer_target.kind.is_dev_only();
let selected_pkg_edge = |dep_pkg: usize| -> Option<&cabin_workspace::DependencyEdge> {
let pkg_edges = &req.graph.packages[tid.0].deps;
pkg_edges
.iter()
.find(|pkg_edge| {
pkg_edge.kind == cabin_core::DependencyKind::Normal && pkg_edge.index == dep_pkg
})
.or_else(|| {
dev_edges_visible
.then(|| {
pkg_edges.iter().find(|pkg_edge| {
pkg_edge.kind == cabin_core::DependencyKind::Dev
&& pkg_edge.index == dep_pkg
})
})
.flatten()
})
};
for edge in edges {
let dep_index = index_of[&edge.to];
let selected = if tid.0 == edge.to.0 {
None
} else {
selected_pkg_edge(edge.to.0)
};
let edge_ignored = selected.is_some_and(|pkg_edge| pkg_edge.ignore_interface_standard);
let override_section = selected.map(section_of);
if compiles_c
&& let Some(consumer) = effective_c(&pkg_standards, consumer_target)
&& !effective[dep_index]
.c
.requirement
.satisfied_by(consumer.standard)
{
let provenance = provenance_c(&effective, dep_index);
if !interface_less_default_origin(&provenance, topo, req)? {
violations.push(violation(
SourceLanguage::C,
consumer.standard.as_str(),
consumer_site(consumer.source, "c-standard", consumer_index, &nodes, req),
requirement_of(effective[dep_index].c.requirement),
&provenance,
tid,
edge,
edge_ignored,
override_section.clone(),
&nodes,
&sites,
req,
));
}
}
if compiles_cxx
&& let Some(consumer) = effective_cxx(&pkg_standards, consumer_target)
&& !effective[dep_index]
.cxx
.requirement
.satisfied_by(consumer.standard)
{
let provenance = provenance_cxx(&effective, dep_index);
if !interface_less_default_origin(&provenance, topo, req)? {
violations.push(violation(
SourceLanguage::Cxx,
consumer.standard.as_str(),
consumer_site(consumer.source, "cxx-standard", consumer_index, &nodes, req),
requirement_of(effective[dep_index].cxx.requirement),
&provenance,
tid,
edge,
edge_ignored,
override_section.clone(),
&nodes,
&sites,
req,
));
}
}
}
}
violations.sort_by(|a, b| {
(&a.consumer, &a.dependency, a.language.human_label()).cmp(&(
&b.consumer,
&b.dependency,
b.language.human_label(),
))
});
Ok(violations)
}
fn section_of(pkg_edge: &cabin_workspace::DependencyEdge) -> String {
let table = match pkg_edge.kind {
cabin_core::DependencyKind::Normal => "dependencies",
cabin_core::DependencyKind::Dev => "dev-dependencies",
};
match &pkg_edge.condition {
Some(condition) => format!("[target.'cfg({condition})'.{table}]"),
None => format!("[{table}]"),
}
}
fn interface_less_default_origin(
provenance: &Provenance<'_>,
topo: &[TargetId],
req: &PlanRequest<'_>,
) -> Result<bool, BuildError> {
if provenance.origin.source != ReqOfSource::CrossLanguageDefault {
return Ok(false);
}
let origin_tid = &topo[*provenance
.path
.last()
.expect("a provenance chain is never empty")];
let origin = lookup_target(origin_tid, req.graph)?;
Ok(!origin.kind.is_library_like())
}
fn dependency_attributes(
tid: &TargetId,
target: &Target,
pkg_standards: ResolvedLanguageStandards,
pkg_settings: &LanguageStandardSettings,
req: &PlanRequest<'_>,
) -> (DependencyAttributes, NodeSites) {
let attributes = cabin_core::standard_compatibility::dependency_attributes(
target,
&pkg_standards,
pkg_settings,
);
let header_only = target.kind.is_header_only();
let pkg = &req.graph.packages[tid.0];
let target_name = target.name.as_str();
let node_sites = NodeSites {
decl_c: attributes.decl_c.is_some().then(|| {
interface_decl_site(
"interface-c-standard",
target.language.interface_c_standard.is_some(),
matches!(
pkg_settings.interface_c_standard,
Some(StandardDeclaration::Inherited(_))
),
target_name,
pkg,
req,
)
}),
decl_cxx: attributes.decl_cxx.is_some().then(|| {
interface_decl_site(
"interface-cxx-standard",
target.language.interface_cxx_standard.is_some(),
matches!(
pkg_settings.interface_cxx_standard,
Some(StandardDeclaration::Inherited(_))
),
target_name,
pkg,
req,
)
}),
impl_c: (header_only && attributes.impl_c.is_some()).then(|| DeclSite {
manifest_path: pkg.manifest_path.clone(),
scope: DeclScope::Target(target_name.to_owned()),
field: "c-standard",
}),
impl_cxx: (header_only && attributes.impl_cxx.is_some()).then(|| DeclSite {
manifest_path: pkg.manifest_path.clone(),
scope: DeclScope::Target(target_name.to_owned()),
field: "cxx-standard",
}),
};
(attributes, node_sites)
}
fn interface_decl_site(
field: &'static str,
target_declares: bool,
pkg_inherited: bool,
target_name: &str,
pkg: &cabin_workspace::WorkspacePackage,
req: &PlanRequest<'_>,
) -> DeclSite {
if target_declares {
DeclSite {
manifest_path: pkg.manifest_path.clone(),
scope: DeclScope::Target(target_name.to_owned()),
field,
}
} else if pkg_inherited {
DeclSite {
manifest_path: req.graph.root_manifest_path.clone(),
scope: DeclScope::Workspace,
field,
}
} else {
DeclSite {
manifest_path: pkg.manifest_path.clone(),
scope: DeclScope::Package,
field,
}
}
}
fn declaration_sites(sites: &NodeSites) -> DeclarationSites {
let site = |decl: &Option<DeclSite>| {
decl.as_ref().map(|s| DeclarationSite {
manifest_path: s.manifest_path.clone(),
span: None,
})
};
DeclarationSites {
decl_c: site(&sites.decl_c),
decl_cxx: site(&sites.decl_cxx),
impl_c: site(&sites.impl_c),
impl_cxx: site(&sites.impl_cxx),
}
}
fn consumer_site(
source: LanguageStandardSource,
field: &'static str,
consumer_index: usize,
nodes: &[TargetNode],
req: &PlanRequest<'_>,
) -> DeclSite {
let manifest_path = nodes[consumer_index].manifest_path.clone();
match source {
LanguageStandardSource::Target => DeclSite {
manifest_path,
scope: DeclScope::Target(target_name_of(&nodes[consumer_index].name)),
field,
},
LanguageStandardSource::Package => DeclSite {
manifest_path,
scope: DeclScope::Package,
field,
},
LanguageStandardSource::Workspace => DeclSite {
manifest_path: req.graph.root_manifest_path.clone(),
scope: DeclScope::Workspace,
field,
},
}
}
fn target_name_of(display: &str) -> String {
display
.split_once(':')
.map_or(display, |(_, target)| target)
.to_owned()
}
fn requirement_of<S: Copy + Ord + AsStandardStr>(requirement: Requirement<S>) -> EdgeRequirement {
match requirement {
Requirement::Min(min) => EdgeRequirement::Min(min.standard_str()),
Requirement::Forbidden => EdgeRequirement::Forbidden,
Requirement::Unconstrained => {
unreachable!("an unconstrained requirement cannot be violated (spec D11)")
}
}
}
trait AsStandardStr {
fn standard_str(self) -> &'static str;
}
impl AsStandardStr for cabin_core::CStandard {
fn standard_str(self) -> &'static str {
self.as_str()
}
}
impl AsStandardStr for cabin_core::CxxStandard {
fn standard_str(self) -> &'static str {
self.as_str()
}
}
#[allow(clippy::too_many_arguments)]
fn violation(
language: SourceLanguage,
consumer_standard: &'static str,
consumer_site: DeclSite,
requirement: EdgeRequirement,
provenance: &Provenance<'_>,
consumer_tid: &TargetId,
edge: &TargetDepEdge,
ignored: bool,
override_section: Option<String>,
nodes: &[TargetNode],
sites: &[NodeSites],
req: &PlanRequest<'_>,
) -> StandardCompatViolation {
let origin_index = *provenance
.path
.last()
.expect("a provenance chain is never empty");
let origin_sites = &sites[origin_index];
let origin = match provenance.origin.source {
ReqOfSource::Declared => RequirementOrigin::Declared {
site: decl_site_for(origin_sites, language),
},
ReqOfSource::DeclaredNone => RequirementOrigin::DeclaredNone {
site: decl_site_for(origin_sites, language),
},
ReqOfSource::HeaderOnlyInference => RequirementOrigin::HeaderOnlyInference {
site: impl_site_for(origin_sites, language),
},
ReqOfSource::CrossLanguageDefault => RequirementOrigin::CrossLanguageDefault,
ReqOfSource::CompiledNoDeclaration => unreachable!(
"a compiled target without a declaration imposes no constraint (spec D9 row 4)"
),
};
let dep_pkg = &req.graph.packages[edge.to.0];
StandardCompatViolation {
consumer: format_target_id(consumer_tid, req.graph),
language,
consumer_standard,
consumer_site,
dependency: nodes[*provenance
.path
.first()
.expect("a provenance chain is never empty")]
.name
.clone(),
dependency_package: dep_pkg.package.name.as_str().to_owned(),
dependency_version: dep_pkg.package.version.to_string(),
dependency_is_registry: matches!(dep_pkg.kind, PackageKind::Registry),
requirement,
origin_target: nodes[origin_index].name.clone(),
origin,
chain: provenance
.path
.iter()
.map(|&index| nodes[index].name.clone())
.collect(),
consumer_manifest_path: req.graph.packages[consumer_tid.0].manifest_path.clone(),
ignored,
override_section,
}
}
fn decl_site_for(sites: &NodeSites, language: SourceLanguage) -> DeclSite {
match language {
SourceLanguage::C => sites.decl_c.clone(),
SourceLanguage::Cxx => sites.decl_cxx.clone(),
}
.expect("a declared requirement records its declaration site")
}
fn impl_site_for(sites: &NodeSites, language: SourceLanguage) -> DeclSite {
match language {
SourceLanguage::C => sites.impl_c.clone(),
SourceLanguage::Cxx => sites.impl_cxx.clone(),
}
.expect("header-only inference records its implementation site")
}
fn has_sources_of(target: &Target, language: SourceLanguage) -> bool {
target
.sources
.iter()
.any(|source| classify_source(source) == Some(language))
}