1use std::collections::HashMap;
7
8use crate::code_unit::CodeUnit;
9use crate::fingerprint::Fingerprint;
10use crate::near_duplicate::near_duplicate_finder::NearDuplicateFinder;
11
12#[derive(Debug, Clone)]
14pub struct DuplicateGroup {
15 pub fingerprint: Fingerprint,
18 pub members: Vec<CodeUnit>,
20 pub similarity: f64,
22}
23
24#[derive(Debug, Clone, serde::Serialize)]
26pub struct DuplicationStats {
27 pub total_code_units: usize,
28 pub total_lines: usize,
29 pub exact_duplicate_groups: usize,
30 pub exact_duplicate_units: usize,
31 pub near_duplicate_groups: usize,
32 pub near_duplicate_units: usize,
33 pub exact_duplicate_lines: usize,
34 pub near_duplicate_lines: usize,
35 pub sub_exact_groups: usize,
37 pub sub_exact_units: usize,
38 pub sub_near_groups: usize,
39 pub sub_near_units: usize,
40 pub baseline_suppressed: Option<usize>,
44}
45
46impl DuplicationStats {
47 #[must_use]
49 pub const fn with_baseline_suppressed(self, suppressed: Option<usize>) -> Self {
50 Self {
51 baseline_suppressed: suppressed,
52 ..self
53 }
54 }
55
56 fn percent_of_total(&self, lines: usize) -> f64 {
57 if self.total_lines == 0 {
58 0.0
59 } else {
60 lines as f64 / self.total_lines as f64 * 100.0
61 }
62 }
63
64 #[must_use]
66 pub fn exact_duplicate_percent(&self) -> f64 {
67 self.percent_of_total(self.exact_duplicate_lines)
68 }
69
70 #[must_use]
72 pub fn near_duplicate_percent(&self) -> f64 {
73 self.percent_of_total(self.near_duplicate_lines)
74 }
75}
76
77#[must_use]
79pub fn group_exact_duplicates(units: &[CodeUnit]) -> Vec<DuplicateGroup> {
80 let mut groups: HashMap<Fingerprint, Vec<CodeUnit>> = HashMap::new();
81
82 for unit in units {
83 groups
84 .entry(unit.fingerprint)
85 .or_default()
86 .push(unit.clone());
87 }
88
89 let mut result: Vec<DuplicateGroup> = groups
90 .into_iter()
91 .filter(|(_, members)| members.len() > 1)
92 .map(|(fp, members)| DuplicateGroup {
93 fingerprint: fp,
94 members,
95 similarity: 1.0,
96 })
97 .collect();
98
99 result.sort_by(|a, b| {
101 b.members
102 .len()
103 .cmp(&a.members.len())
104 .then_with(|| a.fingerprint.cmp(&b.fingerprint))
105 });
106
107 result
108}
109
110#[must_use]
113pub fn find_near_duplicates(
114 units: &[CodeUnit],
115 threshold: f64,
116 exact_fingerprints: &[Fingerprint],
117) -> Vec<DuplicateGroup> {
118 NearDuplicateFinder::new(threshold).find(units, exact_fingerprints)
119}
120
121fn group_line_count(group: &DuplicateGroup) -> usize {
123 group
124 .members
125 .iter()
126 .map(|m| m.line_end.saturating_sub(m.line_start) + 1)
127 .sum()
128}
129
130pub fn compute_stats(
132 units: &[CodeUnit],
133 exact_groups: &[DuplicateGroup],
134 near_groups: &[DuplicateGroup],
135) -> DuplicationStats {
136 let total_lines: usize = units
137 .iter()
138 .map(|u| u.line_end.saturating_sub(u.line_start) + 1)
139 .sum();
140
141 DuplicationStats {
142 total_code_units: units.len(),
143 total_lines,
144 exact_duplicate_groups: exact_groups.len(),
145 exact_duplicate_units: exact_groups.iter().map(|g| g.members.len()).sum(),
146 near_duplicate_groups: near_groups.len(),
147 near_duplicate_units: near_groups.iter().map(|g| g.members.len()).sum(),
148 exact_duplicate_lines: exact_groups.iter().map(group_line_count).sum(),
149 near_duplicate_lines: near_groups.iter().map(group_line_count).sum(),
150 sub_exact_groups: 0,
151 sub_exact_units: 0,
152 sub_near_groups: 0,
153 sub_near_units: 0,
154 baseline_suppressed: None,
155 }
156}
157
158#[must_use]
160pub fn compute_stats_with_sub(
161 units: &[CodeUnit],
162 exact_groups: &[DuplicateGroup],
163 near_groups: &[DuplicateGroup],
164 sub_exact_groups: &[DuplicateGroup],
165 sub_near_groups: &[DuplicateGroup],
166) -> DuplicationStats {
167 let mut stats = compute_stats(units, exact_groups, near_groups);
168 stats.sub_exact_groups = sub_exact_groups.len();
169 stats.sub_exact_units = sub_exact_groups.iter().map(|g| g.members.len()).sum();
170 stats.sub_near_groups = sub_near_groups.len();
171 stats.sub_near_units = sub_near_groups.iter().map(|g| g.members.len()).sum();
172 stats
173}