use std::collections::BTreeSet;
use std::sync::Arc;
use thiserror::Error;
use crate::check_config::{GateConfig, ModuleBoundaryConfig};
use super::catalog::{MODULE_BOUNDARY_RULES, ModuleBoundaryRule, resolve_severity};
use super::verdict::{
GateAnchor, GateEvidence, GateLocation, GateSeverity, GateVerdict, ModuleBoundaryEntity,
ModuleBoundaryEvidence, ModuleBoundaryMeasurement,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum ModuleBoundaryInputError {
#[error("a gate location requires a non-empty path")]
EmptyLocationPath,
#[error("a gate location requires a one-based line")]
ZeroLocationLine,
#[error("a gate location requires a one-based column")]
ZeroLocationColumn,
#[error("entity {ordinal} requires a non-empty label")]
EmptyEntityLabel {
ordinal: u32,
},
#[error("a component requires at least one member")]
EmptyComponentMembers,
#[error("component member ordinal {ordinal} is repeated")]
DuplicateComponentMember {
ordinal: u32,
},
#[error("a component requires at least one declared partition")]
EmptyComponentPartitions,
#[error("component partition ordinal {ordinal} is repeated")]
DuplicateComponentPartition {
ordinal: u32,
},
#[error("a component with {members} members cannot occupy {partitions} partitions")]
ComponentPartitionsExceedMembers {
members: usize,
partitions: usize,
},
#[error("an acyclic component cannot hold {members} members")]
AcyclicComponentHasMultipleMembers {
members: usize,
},
#[error("a misplaced symbol cannot prefer its declared partition {ordinal}")]
MisplacedSymbolPartitionsAreEqual {
ordinal: u32,
},
#[error("symbol {ordinal} has no outgoing selected edge to attribute")]
MisplacedSymbolHasNoOutgoingEdges {
ordinal: u32,
},
#[error("{foreign_edges} foreign edges exceed {total_outgoing_edges} outgoing edges")]
MisplacedSymbolForeignEdgesExceedTotal {
foreign_edges: u32,
total_outgoing_edges: u32,
},
#[error("a module-boundary input requires a non-empty target")]
EmptyInputTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleBoundaryFact {
subject: ModuleBoundaryEntity,
measurement: ModuleBoundaryMeasurement,
}
impl ModuleBoundaryFact {
pub fn component(
cyclic: bool,
members: Arc<[ModuleBoundaryEntity]>,
partitions: Arc<[ModuleBoundaryEntity]>,
) -> Result<Self, ModuleBoundaryInputError> {
let first = validate_component(cyclic, &members, &partitions)?;
let subject = component_subject(first, &members)?;
Ok(Self {
subject,
measurement: ModuleBoundaryMeasurement::Component {
cyclic,
members,
partitions,
},
})
}
pub fn misplaced_symbol(
symbol: ModuleBoundaryEntity,
declared_partition: ModuleBoundaryEntity,
candidate_partition: ModuleBoundaryEntity,
foreign_edges: u32,
total_outgoing_edges: u32,
) -> Result<Self, ModuleBoundaryInputError> {
validate_misplacement(
&symbol,
&declared_partition,
&candidate_partition,
foreign_edges,
total_outgoing_edges,
)?;
Ok(Self {
subject: symbol,
measurement: ModuleBoundaryMeasurement::MisplacedSymbol {
declared_partition,
candidate_partition,
foreign_edges,
total_outgoing_edges,
},
})
}
pub fn low_module_cohesion(
partition: ModuleBoundaryEntity,
internal_edges: u32,
boundary_edges: u32,
) -> Self {
Self {
subject: partition,
measurement: ModuleBoundaryMeasurement::LowModuleCohesion {
internal_edges,
boundary_edges,
},
}
}
pub fn subject(&self) -> &ModuleBoundaryEntity {
&self.subject
}
pub fn measurement(&self) -> &ModuleBoundaryMeasurement {
&self.measurement
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleBoundaryInput {
target: Arc<str>,
facts: Box<[ModuleBoundaryFact]>,
}
impl ModuleBoundaryInput {
pub fn try_new(
target: impl Into<Arc<str>>,
facts: Box<[ModuleBoundaryFact]>,
) -> Result<Self, ModuleBoundaryInputError> {
let target = target.into();
match target.is_empty() {
true => Err(ModuleBoundaryInputError::EmptyInputTarget),
false => Ok(Self { target, facts }),
}
}
pub fn target(&self) -> &str {
&self.target
}
pub fn facts(&self) -> &[ModuleBoundaryFact] {
&self.facts
}
}
fn validate_component<'m>(
cyclic: bool,
members: &'m [ModuleBoundaryEntity],
partitions: &[ModuleBoundaryEntity],
) -> Result<&'m ModuleBoundaryEntity, ModuleBoundaryInputError> {
let first = members
.first()
.ok_or(ModuleBoundaryInputError::EmptyComponentMembers)?;
require_unique_ordinals(members, |ordinal| {
ModuleBoundaryInputError::DuplicateComponentMember { ordinal }
})?;
if partitions.is_empty() {
return Err(ModuleBoundaryInputError::EmptyComponentPartitions);
}
require_unique_ordinals(partitions, |ordinal| {
ModuleBoundaryInputError::DuplicateComponentPartition { ordinal }
})?;
if partitions.len() > members.len() {
return Err(ModuleBoundaryInputError::ComponentPartitionsExceedMembers {
members: members.len(),
partitions: partitions.len(),
});
}
if !cyclic && members.len() > 1 {
return Err(
ModuleBoundaryInputError::AcyclicComponentHasMultipleMembers {
members: members.len(),
},
);
}
Ok(first)
}
fn require_unique_ordinals(
entities: &[ModuleBoundaryEntity],
repeated: impl Fn(u32) -> ModuleBoundaryInputError,
) -> Result<(), ModuleBoundaryInputError> {
if entities.len() < 2 {
return Ok(());
}
let mut seen = BTreeSet::new();
match entities
.iter()
.map(ModuleBoundaryEntity::ordinal)
.find(|ordinal| !seen.insert(*ordinal))
{
Some(ordinal) => Err(repeated(ordinal)),
None => Ok(()),
}
}
fn component_subject(
first: &ModuleBoundaryEntity,
members: &[ModuleBoundaryEntity],
) -> Result<ModuleBoundaryEntity, ModuleBoundaryInputError> {
let annotation = first_anchored(members, GateAnchor::Span)
.or_else(|| first_anchored(members, GateAnchor::File));
ModuleBoundaryEntity::try_new(
first.ordinal(),
format!("scc::{}", first.label()),
annotation,
)
}
fn first_anchored(members: &[ModuleBoundaryEntity], anchor: GateAnchor) -> Option<GateLocation> {
members
.iter()
.filter_map(ModuleBoundaryEntity::location)
.find(|location| location.anchor() == anchor)
.cloned()
}
fn validate_misplacement(
symbol: &ModuleBoundaryEntity,
declared_partition: &ModuleBoundaryEntity,
candidate_partition: &ModuleBoundaryEntity,
foreign_edges: u32,
total_outgoing_edges: u32,
) -> Result<(), ModuleBoundaryInputError> {
if declared_partition.ordinal() == candidate_partition.ordinal() {
return Err(
ModuleBoundaryInputError::MisplacedSymbolPartitionsAreEqual {
ordinal: declared_partition.ordinal(),
},
);
}
if total_outgoing_edges == 0 {
return Err(
ModuleBoundaryInputError::MisplacedSymbolHasNoOutgoingEdges {
ordinal: symbol.ordinal(),
},
);
}
if foreign_edges > total_outgoing_edges {
return Err(
ModuleBoundaryInputError::MisplacedSymbolForeignEdgesExceedTotal {
foreign_edges,
total_outgoing_edges,
},
);
}
Ok(())
}
pub(super) fn is_large_component(
measurement: &ModuleBoundaryMeasurement,
config: &ModuleBoundaryConfig,
) -> bool {
match measurement {
ModuleBoundaryMeasurement::Component {
cyclic: true,
members,
..
} => members.len() as u64 > u64::from(config.max_scc_members()),
_ => false,
}
}
pub(super) fn is_boundary_crossing_component(measurement: &ModuleBoundaryMeasurement) -> bool {
match measurement {
ModuleBoundaryMeasurement::Component {
cyclic: true,
partitions,
..
} => partitions.len() > 1,
_ => false,
}
}
pub(super) fn is_misplaced_symbol(
measurement: &ModuleBoundaryMeasurement,
config: &ModuleBoundaryConfig,
) -> bool {
match measurement {
ModuleBoundaryMeasurement::MisplacedSymbol {
foreign_edges,
total_outgoing_edges,
..
} => {
*foreign_edges >= config.misplaced_symbol_min_foreign_edges()
&& at_least_percent(
u64::from(*foreign_edges),
u64::from(*total_outgoing_edges),
config.misplaced_symbol_min_affinity_percent(),
)
}
_ => false,
}
}
pub(super) fn is_low_cohesion_module(
measurement: &ModuleBoundaryMeasurement,
config: &ModuleBoundaryConfig,
) -> bool {
match measurement {
ModuleBoundaryMeasurement::LowModuleCohesion {
internal_edges,
boundary_edges,
} => {
let total = u64::from(*internal_edges) + u64::from(*boundary_edges);
total >= u64::from(config.low_module_cohesion_min_outgoing_edges())
&& below_percent(
u64::from(*internal_edges),
total,
config.low_module_cohesion_min_percent(),
)
}
_ => false,
}
}
fn at_least_percent(part: u64, total: u64, percent: u8) -> bool {
part * 100 >= total * u64::from(percent)
}
fn below_percent(part: u64, total: u64, percent: u8) -> bool {
part * 100 < total * u64::from(percent)
}
pub fn evaluate_module_boundary_rules(
input: &ModuleBoundaryInput,
config: &GateConfig,
) -> Box<[GateVerdict]> {
if !config.enabled || !config.module_boundary.enabled() {
return Box::new([]);
}
MODULE_BOUNDARY_RULES
.iter()
.flat_map(|rule| rule_verdicts(rule, input, &config.module_boundary))
.collect()
}
fn rule_verdicts<'i>(
rule: &'static ModuleBoundaryRule,
input: &'i ModuleBoundaryInput,
config: &'i ModuleBoundaryConfig,
) -> impl Iterator<Item = GateVerdict> + 'i {
resolve_severity(rule.name, rule.default_severity, config.rule_overrides())
.into_iter()
.flat_map(move |severity| {
input
.facts()
.iter()
.filter(move |fact| (rule.predicate)(fact.measurement(), config))
.map(move |fact| verdict(rule, severity, &input.target, fact))
})
}
fn verdict(
rule: &'static ModuleBoundaryRule,
severity: GateSeverity,
target: &Arc<str>,
fact: &ModuleBoundaryFact,
) -> GateVerdict {
let evidence = ModuleBoundaryEvidence::new(
Arc::clone(target),
fact.subject().clone(),
fact.measurement().clone(),
);
rule.verdict(severity, Some(GateEvidence::ModuleBoundary(evidence)))
}