1use std::path::{Path, PathBuf};
4
5use fallow_config::DuplicatesConfig;
6use fallow_types::discover::DiscoveredFile;
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use crate::results::DuplicationAnalysis;
10
11#[path = "duplication_detector/mod.rs"]
12mod detector;
13
14#[cfg(test)]
15pub(crate) use detector::token_types;
16pub(crate) use detector::types;
17
18#[doc(hidden)]
21pub use detector::{detect, normalize, tokenize};
22
23pub type CloneGroup = fallow_types::duplicates::CloneGroup;
25pub type CloneGroupKind = fallow_types::duplicates::CloneGroupKind;
27pub type CloneInstance = fallow_types::duplicates::CloneInstance;
29pub type DefaultIgnoreSkips = fallow_types::duplicates::DefaultIgnoreSkips;
31pub type DuplicationReport = fallow_types::duplicates::DuplicationReport;
33pub type DuplicationStats = fallow_types::duplicates::DuplicationStats;
35pub type RefactoringKind = fallow_types::duplicates::RefactoringKind;
37pub type RefactoringSuggestion = fallow_types::duplicates::RefactoringSuggestion;
39
40pub use detector::{
41 CloneFingerprintKey, CloneFingerprintSet, FINGERPRINT_PREFIX, clone_fingerprint,
42 dominant_identifier, group_refactoring_suggestion,
43};
44
45pub fn refresh_clone_families(report: &mut DuplicationReport, root: &Path) {
47 report.clone_families = detector::families::group_into_families(&report.clone_groups, root);
48 report.mirrored_directories =
49 detector::families::detect_mirrored_directories(&report.clone_families, root);
50}
51
52pub fn refresh_scoped_report(report: &mut DuplicationReport, root: &Path) {
59 refresh_clone_families(report, root);
60 report.stats = recompute_stats(report);
61 report.sort();
62}
63
64#[derive(Debug, Clone, Copy)]
68pub struct DuplicationScope<'a> {
69 pub changed_files: Option<&'a FxHashSet<PathBuf>>,
71 pub diff: Option<&'a fallow_output::DiffIndex>,
73 pub workspace_roots: Option<&'a [PathBuf]>,
76}
77
78pub fn apply_scope(report: &mut DuplicationReport, scope: &DuplicationScope<'_>, root: &Path) {
86 if let Some(changed_files) = scope.changed_files {
87 crate::changed_files::filter_duplication_by_changed_files(report, changed_files, root);
88 }
89 if let Some(diff) = scope.diff {
90 crate::diff_scope::filter_duplication_by_diff(report, diff, root);
91 }
92 if let Some(roots) = scope.workspace_roots {
93 filter_to_workspaces(report, roots, root);
94 }
95}
96
97pub fn filter_to_workspaces(report: &mut DuplicationReport, roots: &[PathBuf], root: &Path) {
106 report.clone_groups.retain(|group| {
107 group
108 .instances
109 .iter()
110 .any(|instance| roots.iter().any(|scope| instance.file.starts_with(scope)))
111 });
112 refresh_scoped_report(report, root);
113}
114
115pub fn apply_top(report: &mut DuplicationReport, n: usize, root: &Path) {
124 report.sort();
125 report.clone_groups.truncate(n);
126 refresh_clone_families(report, root);
127 report.sort();
128}
129
130#[must_use]
140pub fn recompute_stats(report: &DuplicationReport) -> DuplicationStats {
141 let mut files_with_clones: FxHashSet<&Path> = FxHashSet::default();
142 let mut file_dup_lines: FxHashMap<&Path, FxHashSet<usize>> = FxHashMap::default();
143 let mut duplicated_tokens = 0usize;
144 let mut clone_instances = 0usize;
145
146 for group in &report.clone_groups {
147 for instance in &group.instances {
148 files_with_clones.insert(&instance.file);
149 clone_instances += 1;
150 let lines = file_dup_lines.entry(&instance.file).or_default();
151 for line in instance.start_line..=instance.end_line {
152 lines.insert(line);
153 }
154 }
155 duplicated_tokens += group.token_count * group.instances.len().saturating_sub(1);
156 }
157
158 let duplicated_lines: usize = file_dup_lines.values().map(FxHashSet::len).sum();
159
160 DuplicationStats {
161 total_files: report.stats.total_files,
162 files_with_clones: files_with_clones.len(),
163 total_lines: report.stats.total_lines,
164 duplicated_lines,
165 total_tokens: report.stats.total_tokens,
166 duplicated_tokens: duplicated_tokens.min(report.stats.total_tokens),
167 clone_groups: report.clone_groups.len(),
168 clone_families: report.clone_families.len(),
169 clone_instances,
170 duplication_percentage: if report.stats.total_lines > 0 {
171 (duplicated_lines as f64 / report.stats.total_lines as f64) * 100.0
172 } else {
173 0.0
174 },
175 clone_groups_below_min_occurrences: report.stats.clone_groups_below_min_occurrences,
176 clone_groups_ignored: report.stats.clone_groups_ignored,
177 near_candidates_skipped: report.stats.near_candidates_skipped,
178 }
179}
180
181#[must_use]
186pub fn source_token_kinds_equivalent(
187 path: &Path,
188 current: &str,
189 base: &str,
190 cross_language: bool,
191) -> bool {
192 let current_tokens = detector::tokenize::tokenize_file(path, current, cross_language);
193 let base_tokens = detector::tokenize::tokenize_file(path, base, cross_language);
194 current_tokens
195 .tokens
196 .iter()
197 .map(|token| &token.kind)
198 .eq(base_tokens.tokens.iter().map(|token| &token.kind))
199}
200
201#[must_use]
203pub fn find_duplicates(
204 root: &Path,
205 files: &[DiscoveredFile],
206 config: &DuplicatesConfig,
207) -> DuplicationReport {
208 detector::find_duplicates(root, files, config)
209}
210
211#[must_use]
213pub fn find_duplicates_with_defaults(
214 root: &Path,
215 files: &[DiscoveredFile],
216 config: &DuplicatesConfig,
217 cache_dir: Option<&Path>,
218) -> DuplicationAnalysis {
219 detector::detect_duplicates(root, files, config, None, cache_dir)
220}
221
222#[must_use]
224pub fn find_duplicates_touching_files_with_defaults(
225 root: &Path,
226 files: &[DiscoveredFile],
227 config: &DuplicatesConfig,
228 changed_files: &[PathBuf],
229 cache_dir: Option<&Path>,
230) -> DuplicationAnalysis {
231 let changed_files = changed_files.iter().cloned().collect::<FxHashSet<_>>();
232 detector::detect_duplicates(root, files, config, Some(&changed_files), cache_dir)
233}
234
235#[cfg(test)]
236mod tests {
237 use std::path::PathBuf;
238
239 use super::*;
240
241 fn instance(file: &str, start_line: usize, end_line: usize) -> CloneInstance {
242 CloneInstance {
243 file: PathBuf::from(file),
244 start_line,
245 end_line,
246 start_col: 0,
247 end_col: 0,
248 fragment: String::new(),
249 }
250 }
251
252 fn report(clone_groups: Vec<CloneGroup>) -> DuplicationReport {
253 DuplicationReport {
254 clone_groups,
255 clone_families: Vec::new(),
256 mirrored_directories: Vec::new(),
257 stats: DuplicationStats {
258 total_files: 3,
259 total_lines: 100,
260 total_tokens: 1_000,
261 clone_groups_below_min_occurrences: 4,
262 ..DuplicationStats::default()
263 },
264 }
265 }
266
267 #[test]
268 fn recompute_stats_deduplicates_overlapping_lines_per_file() {
269 let report = report(vec![
270 CloneGroup {
271 instances: vec![instance("src/a.ts", 1, 10), instance("src/b.ts", 20, 24)],
272 token_count: 30,
273 line_count: 10,
274 similarity: None,
275 },
276 CloneGroup {
277 instances: vec![instance("src/a.ts", 5, 12), instance("src/c.ts", 40, 44)],
278 token_count: 20,
279 line_count: 8,
280 similarity: None,
281 },
282 ]);
283
284 let stats = recompute_stats(&report);
285
286 assert_eq!(stats.total_files, 3);
287 assert_eq!(stats.files_with_clones, 3);
288 assert_eq!(stats.total_lines, 100);
289 assert_eq!(stats.duplicated_lines, 22);
290 assert_eq!(stats.total_tokens, 1_000);
291 assert_eq!(stats.duplicated_tokens, 50);
292 assert_eq!(stats.clone_groups, 2);
293 assert_eq!(stats.clone_instances, 4);
294 assert!((stats.duplication_percentage - 22.0).abs() < f64::EPSILON);
295 assert_eq!(stats.clone_groups_below_min_occurrences, 4);
296 }
297
298 #[test]
299 fn recompute_stats_handles_zero_total_lines() {
300 let mut report = report(vec![CloneGroup {
301 instances: vec![instance("src/a.ts", 1, 1)],
302 token_count: 5,
303 line_count: 1,
304 similarity: None,
305 }]);
306 report.stats.total_lines = 0;
307
308 let stats = recompute_stats(&report);
309
310 assert_eq!(stats.duplicated_lines, 1);
311 assert!(stats.duplication_percentage.abs() < f64::EPSILON);
312 }
313
314 #[test]
315 fn clone_fingerprint_set_delegates_without_leaking_core_type() {
316 let groups = vec![CloneGroup {
317 instances: vec![
318 CloneInstance {
319 fragment: "const value = 1;".to_string(),
320 ..instance("src/a.ts", 1, 1)
321 },
322 CloneInstance {
323 fragment: "const value = 1;".to_string(),
324 ..instance("src/b.ts", 2, 2)
325 },
326 ],
327 token_count: 5,
328 line_count: 1,
329 similarity: None,
330 }];
331 let fingerprints = CloneFingerprintSet::from_groups(&groups);
332 let fingerprint = fingerprints.fingerprint_for_group(&groups[0]);
333
334 assert!(fingerprint.starts_with(FINGERPRINT_PREFIX));
335 assert!(fingerprints.find_group(&groups, &fingerprint).is_some());
336 }
337}