use super::{
BTreeMap, BTreeSet, BodyMateriality, Boilerplate, BuildVariant, CloneGroupFingerprint,
FileFeatures, FragmentFingerprint, GroupDetail, Lexeme, SourceTokenSpan, StructuralConfig,
SyntaxIrFile, Token, TokenKind, Unit, UnitEvidence, features, grouping, stable_id,
substitution, test_code, verify, view,
};
pub(super) fn group_detail(
group: &grouping::StructuralGroup,
units: &[Unit],
files: &[SyntaxIrFile],
feature_files: &[FileFeatures],
evidence: &UnitEvidence,
variant: &BuildVariant,
config: &StructuralConfig,
) -> GroupDetail {
let medoid_view = view(group.canonical, units, files, feature_files, evidence);
let member_breakdowns = group
.members
.iter()
.map(|&member| {
verify::verify(
&medoid_view,
&view(member, units, files, feature_files, evidence),
&config.verify,
)
.breakdown
})
.collect();
let cohesion_breakdown = group
.members
.iter()
.enumerate()
.flat_map(|(left, &a)| group.members[left + 1..].iter().map(move |&b| (a, b)))
.map(|(a, b)| {
verify::verify(
&view(a, units, files, feature_files, evidence),
&view(b, units, files, feature_files, evidence),
&config.verify,
)
.breakdown
})
.min_by(|left, right| left.composite.total_cmp(&right.composite))
.unwrap_or_else(|| verify::verify(&medoid_view, &medoid_view, &config.verify).breakdown);
let fingerprint = group_fingerprint(group, units, variant);
GroupDetail {
fingerprint,
member_breakdowns,
cohesion_breakdown,
identifier_jaccard: group_identifier_jaccard(group, units, files),
body_materiality: group_body_materiality(group, units, feature_files),
boilerplate: dominant_boilerplate(group, units),
test_code: group.members.iter().all(|&member| units[member].test_code),
test_code_evidence: test_code::aggregate_evidence(
group
.members
.iter()
.map(|&member| units[member].test_code_evidence),
),
width_family: written_once_per_width(group, units, files),
}
}
pub(super) fn group_fingerprint(
group: &grouping::StructuralGroup,
units: &[Unit],
variant: &BuildVariant,
) -> CloneGroupFingerprint {
let member_contents: Vec<FragmentFingerprint> = group
.members
.iter()
.map(|&member| units[member].group_content(group.clone_type))
.collect();
stable_id::structural_clone_group_fingerprint(
variant,
group.clone_type,
&units[group.canonical].group_content(group.clone_type),
&member_contents,
)
}
pub(super) fn group_body_materiality(
group: &grouping::StructuralGroup,
units: &[Unit],
feature_files: &[FileFeatures],
) -> BodyMateriality {
let members: Vec<&features::UnitFeatures> = group
.members
.iter()
.map(|&member| {
let unit = &units[member];
&feature_files[unit.file].units[unit.local]
})
.collect();
BodyMateriality {
has_loop: members
.iter()
.all(|features| features.cfg.max_loop_depth > 0),
has_dynamic_allocation: members
.iter()
.all(|features| features.api.names.iter().any(is_allocation_api)),
call_count: members
.iter()
.map(|features| u64::try_from(features.api.names.len()).unwrap_or(u64::MAX))
.min()
.unwrap_or(0),
}
}
pub(super) fn is_allocation_api(name: &Lexeme) -> bool {
matches!(
name.as_str(),
"aligned_alloc"
| "calloc"
| "make_shared"
| "make_unique"
| "malloc"
| "realloc"
| "reserve"
| "reserve_exact"
| "try_reserve"
| "try_reserve_exact"
| "with_capacity"
| "with_capacity_and_hasher"
)
}
#[must_use]
pub fn span_identifier_jaccard(
files: &[SyntaxIrFile],
canonical: SourceTokenSpan,
corresponding: impl IntoIterator<Item = SourceTokenSpan>,
) -> f64 {
let canonical = identifier_set(files, canonical);
corresponding
.into_iter()
.map(|span| set_jaccard(&canonical, &identifier_set(files, span)))
.min_by(f64::total_cmp)
.unwrap_or(1.0)
}
fn group_identifier_jaccard(
group: &grouping::StructuralGroup,
units: &[Unit],
files: &[SyntaxIrFile],
) -> f64 {
span_identifier_jaccard(
files,
unit_token_span(&units[group.canonical]),
group
.members
.iter()
.filter(|&&member| member != group.canonical)
.map(|&member| unit_token_span(&units[member])),
)
}
const fn unit_token_span(unit: &Unit) -> SourceTokenSpan {
SourceTokenSpan::new(unit.file, unit.tokens.0, unit.tokens.1)
}
fn identifier_set(files: &[SyntaxIrFile], span: SourceTokenSpan) -> BTreeSet<&str> {
let tokens = files
.get(span.file)
.and_then(|file| file.tokens.get(span.token_start..span.token_end))
.unwrap_or(&[]);
tokens
.iter()
.filter(|token| matches!(token.kind, TokenKind::Identifier))
.map(|token| token.text.as_str())
.collect()
}
#[allow(clippy::cast_precision_loss)]
pub(super) fn set_jaccard(left: &BTreeSet<&str>, right: &BTreeSet<&str>) -> f64 {
let union = left.union(right).count();
if union == 0 {
return 1.0;
}
left.intersection(right).count() as f64 / union as f64
}
pub(super) fn written_once_per_width(
group: &grouping::StructuralGroup,
units: &[Unit],
files: &[SyntaxIrFile],
) -> bool {
written_once_per_width_members(group.canonical, &group.members, units, files)
}
pub(super) fn written_once_per_width_members(
canonical: usize,
members: &[usize],
units: &[Unit],
files: &[SyntaxIrFile],
) -> bool {
let medoid = unit_tokens(&units[canonical], files);
let mut compared = 0usize;
for &member in members {
if member == canonical {
continue;
}
compared += 1;
let alike = substitution::witness(medoid, unit_tokens(&units[member], files))
.is_some_and(|witness| witness.written_once_per_width());
if !alike {
return false;
}
}
compared > 0
}
fn unit_tokens<'a>(unit: &Unit, files: &'a [SyntaxIrFile]) -> &'a [Token] {
&files[unit.file].tokens[unit.tokens.0..unit.tokens.1]
}
pub(super) fn dominant_boilerplate(
group: &grouping::StructuralGroup,
units: &[Unit],
) -> Option<Boilerplate> {
dominant_boilerplate_members(&group.members, units)
}
pub(super) fn dominant_boilerplate_members(
members: &[usize],
units: &[Unit],
) -> Option<Boilerplate> {
let mut counts = BTreeMap::new();
for &member in members {
if let Some(category) = units[member].boilerplate {
*counts.entry(category).or_insert(0usize) += 1;
}
}
let (category, count) = counts
.into_iter()
.max_by_key(|(category, count)| (*count, *category))?;
(count.saturating_mul(5) >= members.len().saturating_mul(4)).then_some(category)
}