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, fingerprint_for_fragment, 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
52#[doc(hidden)]
54pub fn refresh_clone_group_metrics(group: &mut CloneGroup) {
55 detector::refresh_near_group_metrics(group);
56}
57
58#[must_use]
63pub fn recompute_stats(report: &DuplicationReport) -> DuplicationStats {
64 let mut files_with_clones: FxHashSet<&Path> = FxHashSet::default();
65 let mut file_dup_lines: FxHashMap<&Path, FxHashSet<usize>> = FxHashMap::default();
66 let mut duplicated_tokens = 0usize;
67 let mut clone_instances = 0usize;
68
69 for group in &report.clone_groups {
70 for instance in &group.instances {
71 files_with_clones.insert(&instance.file);
72 clone_instances += 1;
73 let lines = file_dup_lines.entry(&instance.file).or_default();
74 for line in instance.start_line..=instance.end_line {
75 lines.insert(line);
76 }
77 }
78 duplicated_tokens += group.token_count * group.instances.len().saturating_sub(1);
79 }
80
81 let duplicated_lines: usize = file_dup_lines.values().map(FxHashSet::len).sum();
82
83 DuplicationStats {
84 total_files: report.stats.total_files,
85 files_with_clones: files_with_clones.len(),
86 total_lines: report.stats.total_lines,
87 duplicated_lines,
88 total_tokens: report.stats.total_tokens,
89 duplicated_tokens: duplicated_tokens.min(report.stats.total_tokens),
90 clone_groups: report.clone_groups.len(),
91 clone_instances,
92 duplication_percentage: if report.stats.total_lines > 0 {
93 (duplicated_lines as f64 / report.stats.total_lines as f64) * 100.0
94 } else {
95 0.0
96 },
97 clone_groups_below_min_occurrences: report.stats.clone_groups_below_min_occurrences,
98 clone_groups_ignored: report.stats.clone_groups_ignored,
99 near_candidates_skipped: report.stats.near_candidates_skipped,
100 }
101}
102
103#[must_use]
108pub fn source_token_kinds_equivalent(
109 path: &Path,
110 current: &str,
111 base: &str,
112 cross_language: bool,
113) -> bool {
114 let current_tokens = detector::tokenize::tokenize_file(path, current, cross_language);
115 let base_tokens = detector::tokenize::tokenize_file(path, base, cross_language);
116 current_tokens
117 .tokens
118 .iter()
119 .map(|token| &token.kind)
120 .eq(base_tokens.tokens.iter().map(|token| &token.kind))
121}
122
123#[must_use]
125pub fn find_duplicates(
126 root: &Path,
127 files: &[DiscoveredFile],
128 config: &DuplicatesConfig,
129) -> DuplicationReport {
130 detector::find_duplicates(root, files, config)
131}
132
133#[must_use]
135pub(crate) fn find_duplicates_cached(
136 root: &Path,
137 files: &[DiscoveredFile],
138 config: &DuplicatesConfig,
139 cache_dir: &Path,
140) -> DuplicationReport {
141 detector::find_duplicates_cached(root, files, config, cache_dir)
142}
143
144#[must_use]
146pub fn find_duplicates_with_defaults(
147 root: &Path,
148 files: &[DiscoveredFile],
149 config: &DuplicatesConfig,
150 cache_dir: Option<&Path>,
151) -> DuplicationAnalysis {
152 let (report, default_ignore_skips) = if let Some(cache_dir) = cache_dir {
153 detector::find_duplicates_cached_with_default_ignore_skips(root, files, config, cache_dir)
154 } else {
155 detector::find_duplicates_with_default_ignore_skips(root, files, config)
156 };
157 DuplicationAnalysis {
158 report,
159 default_ignore_skips,
160 }
161}
162
163#[must_use]
165pub fn find_duplicates_touching_files_with_defaults(
166 root: &Path,
167 files: &[DiscoveredFile],
168 config: &DuplicatesConfig,
169 changed_files: &[PathBuf],
170 cache_dir: Option<&Path>,
171) -> DuplicationAnalysis {
172 let changed_files = changed_files.iter().cloned().collect::<FxHashSet<_>>();
173 let (report, default_ignore_skips) = if let Some(cache_dir) = cache_dir {
174 detector::find_duplicates_touching_files_cached_with_default_ignore_skips(
175 root,
176 files,
177 config,
178 &changed_files,
179 cache_dir,
180 )
181 } else {
182 detector::find_duplicates_touching_files_with_default_ignore_skips(
183 root,
184 files,
185 config,
186 &changed_files,
187 )
188 };
189 DuplicationAnalysis {
190 report,
191 default_ignore_skips,
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use std::path::PathBuf;
198
199 use super::*;
200
201 fn instance(file: &str, start_line: usize, end_line: usize) -> CloneInstance {
202 CloneInstance {
203 file: PathBuf::from(file),
204 start_line,
205 end_line,
206 start_col: 0,
207 end_col: 0,
208 fragment: String::new(),
209 }
210 }
211
212 fn report(clone_groups: Vec<CloneGroup>) -> DuplicationReport {
213 DuplicationReport {
214 clone_groups,
215 clone_families: Vec::new(),
216 mirrored_directories: Vec::new(),
217 stats: DuplicationStats {
218 total_files: 3,
219 total_lines: 100,
220 total_tokens: 1_000,
221 clone_groups_below_min_occurrences: 4,
222 ..DuplicationStats::default()
223 },
224 }
225 }
226
227 #[test]
228 fn recompute_stats_deduplicates_overlapping_lines_per_file() {
229 let report = report(vec![
230 CloneGroup {
231 instances: vec![instance("src/a.ts", 1, 10), instance("src/b.ts", 20, 24)],
232 token_count: 30,
233 line_count: 10,
234 similarity: None,
235 },
236 CloneGroup {
237 instances: vec![instance("src/a.ts", 5, 12), instance("src/c.ts", 40, 44)],
238 token_count: 20,
239 line_count: 8,
240 similarity: None,
241 },
242 ]);
243
244 let stats = recompute_stats(&report);
245
246 assert_eq!(stats.total_files, 3);
247 assert_eq!(stats.files_with_clones, 3);
248 assert_eq!(stats.total_lines, 100);
249 assert_eq!(stats.duplicated_lines, 22);
250 assert_eq!(stats.total_tokens, 1_000);
251 assert_eq!(stats.duplicated_tokens, 50);
252 assert_eq!(stats.clone_groups, 2);
253 assert_eq!(stats.clone_instances, 4);
254 assert!((stats.duplication_percentage - 22.0).abs() < f64::EPSILON);
255 assert_eq!(stats.clone_groups_below_min_occurrences, 4);
256 }
257
258 #[test]
259 fn recompute_stats_handles_zero_total_lines() {
260 let mut report = report(vec![CloneGroup {
261 instances: vec![instance("src/a.ts", 1, 1)],
262 token_count: 5,
263 line_count: 1,
264 similarity: None,
265 }]);
266 report.stats.total_lines = 0;
267
268 let stats = recompute_stats(&report);
269
270 assert_eq!(stats.duplicated_lines, 1);
271 assert!(stats.duplication_percentage.abs() < f64::EPSILON);
272 }
273
274 #[test]
275 fn clone_fingerprint_set_delegates_without_leaking_core_type() {
276 let groups = vec![CloneGroup {
277 instances: vec![
278 CloneInstance {
279 fragment: "const value = 1;".to_string(),
280 ..instance("src/a.ts", 1, 1)
281 },
282 CloneInstance {
283 fragment: "const value = 1;".to_string(),
284 ..instance("src/b.ts", 2, 2)
285 },
286 ],
287 token_count: 5,
288 line_count: 1,
289 similarity: None,
290 }];
291 let fingerprints = CloneFingerprintSet::from_groups(&groups);
292 let fingerprint = fingerprints.fingerprint_for_group(&groups[0]);
293
294 assert!(fingerprint.starts_with(FINGERPRINT_PREFIX));
295 assert!(fingerprints.find_group(&groups, &fingerprint).is_some());
296 }
297}