use super::{
BTreeMap, BTreeSet, Boilerplate, ByteRange, CandidateConfig, CandidateStats, CloneClass,
CloneGroupFingerprint, ControlFlowConfig, ControlFlowStats, CrossVariantComparisonId,
CrossVariantGroupId, FragmentFingerprint, GroupingConfig, GroupingSet, GroupingStats, Language,
Lexeme, LiteralNorm, MaximalConfig, NearMatchConfig, NearMatchStats, RegionStats,
SimilarityBreakdown, TestCodeEvidence, Token, UnitFingerprint, UnitKind, VerifyConfig,
stable_id, test_code, verify,
};
pub const DEFAULT_MAX_SHAPE_DIVERGENCE: f64 = 0.5;
pub const DEFAULT_VERIFICATION_BUDGET: usize = 2_000_000;
#[derive(Debug, Clone, PartialEq)]
pub struct StructuralConfig {
pub min_clone_tokens: u32,
pub candidate: CandidateConfig,
pub near_match: NearMatchConfig,
pub control_flow: ControlFlowConfig,
pub maximal: MaximalConfig,
pub literals: LiteralNorm,
pub verify: VerifyConfig,
pub verification_budget: usize,
pub max_shape_divergence: f64,
pub grouping: GroupingConfig,
pub siblings: SiblingConfig,
}
impl Default for StructuralConfig {
fn default() -> Self {
Self {
min_clone_tokens: 20,
candidate: CandidateConfig::default(),
near_match: NearMatchConfig::default(),
control_flow: ControlFlowConfig::default(),
maximal: MaximalConfig::default(),
literals: LiteralNorm::default(),
verify: VerifyConfig::default(),
verification_budget: DEFAULT_VERIFICATION_BUDGET,
max_shape_divergence: DEFAULT_MAX_SHAPE_DIVERGENCE,
grouping: GroupingConfig::default(),
siblings: SiblingConfig::default(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SiblingConfig {
pub similarity_delta: f64,
pub candidate_budget: usize,
pub per_group_cap: usize,
pub total_cap: usize,
}
impl Default for SiblingConfig {
fn default() -> Self {
Self {
similarity_delta: 0.10,
candidate_budget: 50_000,
per_group_cap: 8,
total_cap: 1_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuralUnit {
pub file: usize,
pub kind: UnitKind,
pub range: ByteRange,
pub start_line: u32,
pub end_line: u32,
pub token_start: usize,
pub token_end: usize,
pub name: Option<Lexeme>,
pub boilerplate: Option<Boilerplate>,
pub test_code: bool,
pub test_code_evidence: Option<TestCodeEvidence>,
pub fingerprint: UnitFingerprint,
pub content: FragmentFingerprint,
pub normalized_content: FragmentFingerprint,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VerifiedPair {
pub members: Vec<usize>,
pub canonical: usize,
pub fingerprint: CloneGroupFingerprint,
pub similarity: f64,
pub breakdown: Option<SimilarityBreakdown>,
pub class: CloneClass,
pub confidence: verify::Confidence,
pub boilerplate: Option<Boilerplate>,
pub width_family: bool,
}
impl VerifiedPair {
#[must_use]
pub fn holds(&self, unit: usize) -> bool {
self.members.binary_search(&unit).is_ok()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroupDetail {
pub fingerprint: CloneGroupFingerprint,
pub member_breakdowns: Vec<SimilarityBreakdown>,
pub cohesion_breakdown: SimilarityBreakdown,
pub identifier_jaccard: f64,
pub body_materiality: BodyMateriality,
pub boilerplate: Option<Boilerplate>,
pub test_code: bool,
pub test_code_evidence: Option<TestCodeEvidence>,
pub width_family: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BodyMateriality {
pub has_loop: bool,
pub has_dynamic_allocation: bool,
pub call_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegionOccurrence {
pub file: usize,
pub unit: usize,
pub range: ByteRange,
pub start_line: u32,
pub end_line: u32,
pub token_start: usize,
pub token_end: usize,
pub content: FragmentFingerprint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceTokenSpan {
pub file: usize,
pub token_start: usize,
pub token_end: usize,
}
impl SourceTokenSpan {
#[must_use]
pub const fn new(file: usize, token_start: usize, token_end: usize) -> Self {
Self {
file,
token_start,
token_end,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuralRegion {
pub fingerprint: CloneGroupFingerprint,
pub clone_type: CloneClass,
pub statements: u32,
pub occurrences: Vec<RegionOccurrence>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StructuralStats {
pub files: usize,
pub units: usize,
pub candidate: CandidateStats,
pub near_match: NearMatchStats,
pub control_flow: ControlFlowStats,
pub maximal: RegionStats,
pub regions: usize,
pub region_singletons: usize,
pub region_overlapping: usize,
pub region_adjoining: usize,
pub region_subsumed: usize,
pub region_merged: usize,
pub below_min_clone_token_regions: usize,
pub nested_pairs: usize,
pub alternative_pairs: usize,
pub divergent_shape_pairs: usize,
pub below_min_clone_token_pairs: usize,
pub unit_pairs: usize,
pub verification_budget_dropped: usize,
pub verified_pairs: usize,
pub unrepresented_pairs: usize,
pub described_pairs: usize,
pub severed_pairs: usize,
pub grouping: GroupingStats,
pub siblings: SiblingSweepStats,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SiblingSweepStats {
pub groups_considered: usize,
pub eligible_candidates: usize,
pub candidates_examined: usize,
pub accepted: usize,
pub candidate_budget_dropped: usize,
pub per_group_cap_dropped: usize,
pub total_cap_dropped: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructuralSibling {
pub unit: usize,
pub clone_type: CloneClass,
pub confidence: verify::Confidence,
pub breakdown: SimilarityBreakdown,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroupSiblings {
pub group: usize,
pub siblings: Vec<StructuralSibling>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StructuralNearMiss {
pub a: usize,
pub b: usize,
pub estimated_jaccard: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructuralReport {
pub units: Vec<StructuralUnit>,
pub groups: GroupingSet,
pub regions: Vec<StructuralRegion>,
pub details: Vec<GroupDetail>,
pub unrepresented: Vec<VerifiedPair>,
pub siblings: Vec<GroupSiblings>,
pub near_misses: Vec<StructuralNearMiss>,
pub stats: StructuralStats,
}
impl StructuralReport {
pub fn apply_test_path_evidence(&mut self, test_files: &[bool]) {
for unit in &mut self.units {
if unit.test_code_evidence.is_none()
&& test_files.get(unit.file).copied().unwrap_or(false)
{
unit.test_code_evidence = Some(TestCodeEvidence::Path);
}
unit.test_code = unit.test_code_evidence.is_some();
}
for (group, detail) in self.groups.groups.iter().zip(&mut self.details) {
let evidence = test_code::aggregate_evidence(
group
.members
.iter()
.map(|&member| self.units[member].test_code_evidence),
);
detail.test_code = evidence.is_some();
detail.test_code_evidence = evidence;
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CrossVariantUnit<'a> {
pub origin_variant: &'a str,
pub language: Language,
pub file_path: &'a str,
pub start_line: u32,
pub end_line: u32,
pub name: Option<&'a str>,
pub tokens: &'a [Token],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrossVariantMember {
pub id: stable_id::CrossVariantMemberId,
pub origin_variant: String,
pub language: Language,
pub file_path: String,
pub start_line: u32,
pub end_line: u32,
pub name: Option<String>,
pub token_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrossVariantGroup {
pub id: CrossVariantGroupId,
pub clone_type: CloneClass,
pub members: Vec<CrossVariantMember>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrossVariantComparison {
pub id: CrossVariantComparisonId,
pub origin_variants: Vec<String>,
pub groups: Vec<CrossVariantGroup>,
}
#[must_use]
pub fn compare_build_variants(units: &[CrossVariantUnit<'_>]) -> Option<CrossVariantComparison> {
let mut origins: Vec<String> = units
.iter()
.map(|unit| unit.origin_variant.to_string())
.collect();
origins.sort_unstable();
origins.dedup();
if origins.len() < 2 {
return None;
}
let id = stable_id::cross_variant_comparison_id(&origins);
let mut classes: BTreeMap<(String, [u8; 16]), Vec<&CrossVariantUnit<'_>>> = BTreeMap::new();
for unit in units {
let mut content = blake3::Hasher::new();
content.update(b"cross-variant-raw-unit-v1");
for token in unit.tokens {
content.update(&[token.kind.tag()]);
let length = u32::try_from(token.text.len()).unwrap_or(u32::MAX);
content.update(&length.to_le_bytes());
content.update(token.text.as_bytes());
}
let mut digest = [0_u8; 16];
digest.copy_from_slice(&content.finalize().as_bytes()[..16]);
classes
.entry((unit.language.name().to_string(), digest))
.or_default()
.push(unit);
}
let mut groups = Vec::new();
for ((language, content), members) in classes {
let origins_in_group: BTreeSet<&str> =
members.iter().map(|member| member.origin_variant).collect();
if origins_in_group.len() < 2 {
continue;
}
let mut members: Vec<CrossVariantMember> = members
.into_iter()
.map(|member| CrossVariantMember {
id: stable_id::CrossVariantMemberId::from_bytes([0; 16]),
origin_variant: member.origin_variant.to_string(),
language: member.language,
file_path: member.file_path.to_string(),
start_line: member.start_line,
end_line: member.end_line,
name: member.name.map(ToString::to_string),
token_count: member.tokens.len(),
})
.collect();
members.sort_by(|left, right| {
left.origin_variant
.cmp(&right.origin_variant)
.then_with(|| left.file_path.cmp(&right.file_path))
.then_with(|| left.start_line.cmp(&right.start_line))
.then_with(|| left.end_line.cmp(&right.end_line))
.then_with(|| left.name.cmp(&right.name))
});
let language = match language.as_str() {
"c" => Language::C,
"cpp" => Language::Cpp,
_ => Language::Rust,
};
let group_id =
stable_id::cross_variant_group_id(&id, CloneClass::Type1, language, &content);
let mut origin_ranks = BTreeMap::<(&str, &str), u32>::new();
for member in &mut members {
let rank = origin_ranks
.entry((&member.origin_variant, member.language.name()))
.or_default();
member.id = stable_id::cross_variant_member_id(
&group_id,
&member.origin_variant,
member.language,
*rank,
);
*rank = rank.saturating_add(1);
}
groups.push(CrossVariantGroup {
id: group_id,
clone_type: CloneClass::Type1,
members,
});
}
groups.sort_by_key(|group| group.id);
Some(CrossVariantComparison {
id,
origin_variants: origins,
groups,
})
}