1mod clojure;
40mod join;
41mod prints;
42mod syntax;
43pub mod test_files;
44
45pub use join::jaccard;
46pub use prints::Names;
47pub use syntax::declared_name;
49
50use cpd_core::detect::{PathFilters, PreparedSource};
51use cpd_core::models::{
52 CloneKind, CpdClone, Fragment, Location, SimilarityMethod, StructuralMatch,
53};
54use cpd_tokenizer::line_index::LineIndex;
55use rustc_hash::FxHashMap;
56use std::collections::hash_map::Entry;
57use std::path::Path;
58
59pub const DEFAULT_THRESHOLD: f64 = 0.8;
61
62pub const DEFAULT_MIN_NODES: u32 = 20;
65
66const FORMATS: &[&str] = &[
68 "javascript",
69 "jsx",
70 "typescript",
71 "tsx",
72 "python",
73 "java",
74 "kotlin",
75 "scala",
76 "csharp",
77 "go",
78 "rust",
79 "c",
80 "c-header",
81 "cpp",
82 "cpp-header",
83 "php",
84 "ruby",
85 "swift",
86 "clojure",
87];
88
89pub fn supports(format: &str) -> bool {
91 FORMATS.contains(&format)
92}
93
94pub fn supported_formats() -> &'static [&'static str] {
96 FORMATS
97}
98
99pub fn embeds(format: &str) -> bool {
102 matches!(format, "markdown" | "md" | "vue" | "svelte" | "astro")
103}
104
105pub fn reads(path: &Path, format: &str) -> bool {
110 !is_test_file(path) && reads_with_tests(path, format)
111}
112
113pub fn reads_with_tests(path: &Path, format: &str) -> bool {
116 if embeds(format) {
117 return true;
118 }
119 let name = path
120 .file_name()
121 .and_then(|name| name.to_str())
122 .unwrap_or_default();
123 let extension = path
124 .extension()
125 .and_then(|e| e.to_str())
126 .unwrap_or_default()
127 .to_ascii_lowercase();
128 match format {
129 "python" => extension != "pyi",
132 "clojure" => extension != "edn",
134 "typescript" => !name.ends_with(".d.ts"),
135 _ => supports(format),
136 }
137}
138
139fn is_test_file(path: &Path) -> bool {
142 test_files::is_test_path(path)
143 || path.file_name().and_then(|name| name.to_str()) == Some("conftest.py")
144}
145
146fn language(format: &str) -> &'static str {
148 match format {
149 "javascript" | "jsx" => "javascript",
150 "typescript" | "tsx" => "typescript",
151 "c" | "c-header" => "c",
152 "cpp" | "cpp-header" => "cpp",
153 "python" => "python",
154 "java" => "java",
155 "kotlin" => "kotlin",
156 "scala" => "scala",
157 "csharp" => "csharp",
158 "go" => "go",
159 "rust" => "rust",
160 "php" => "php",
161 "ruby" => "ruby",
162 "swift" => "swift",
163 "clojure" => "clojure",
164 _ => "",
165 }
166}
167
168fn group(format: &str) -> &'static str {
171 match language(format) {
172 "javascript" => "typescript",
173 "c" => "cpp",
174 other => other,
175 }
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub struct Form {
181 pub name: String,
183 pub start: Location,
184 pub end: Location,
186 pub nodes: u32,
188 pub fingerprints: Vec<u64>,
190 pub range: [u32; 2],
193 pub tokens: u32,
194}
195
196impl Form {
197 pub fn line_span(&self) -> u32 {
200 self.end.line.saturating_sub(self.start.line)
201 }
202}
203
204pub fn forms(source: &str, format: &str, ignored: &[[usize; 2]], names: Names) -> Vec<Form> {
209 if source.is_empty() || !supports(format) {
210 return Vec::new();
211 }
212 let mut ranges = cpd_tokenizer::tokenizer::ignored_ranges(format, source);
213 ranges.extend_from_slice(ignored);
214 let ignored = cpd_tokenizer::tokenizer::merge_ranges(ranges);
215 let index = LineIndex::new(source.as_bytes());
216 let form = |name: String, start: usize, end: usize, nodes: u32, fingerprints: Vec<u64>| Form {
217 name,
218 start: index.location(start),
219 end: end_location(&index, source, start, end),
220 nodes,
221 fingerprints,
222 range: [0, 0],
223 tokens: 0,
224 };
225 if format == "clojure" {
226 return clojure::forms(source, &ignored, names)
227 .into_iter()
228 .map(|f| form(f.name, f.start_byte, f.end_byte, f.nodes, f.fingerprints))
229 .collect();
230 }
231 let Some(grammar) = syntax::Grammar::for_format(format) else {
232 return Vec::new();
233 };
234 syntax::forms(source, grammar, &ignored, names)
235 .into_iter()
236 .map(|f| form(f.name, f.start_byte, f.end_byte, f.nodes, f.fingerprints))
237 .collect()
238}
239
240fn end_location(index: &LineIndex, source: &str, start: usize, end: usize) -> Location {
243 match end > start && source.as_bytes().get(end - 1) == Some(&b'\n') {
244 true => index.location(end - 1),
245 false => index.location(end),
246 }
247}
248
249pub fn embedded_forms(
255 source: &str,
256 host_format: &str,
257 ignored: &[[usize; 2]],
258 names: Names,
259) -> Vec<(String, Form)> {
260 let blocks = match host_format {
261 "markdown" | "md" => cpd_tokenizer::markdown::code_blocks(source),
262 "vue" | "svelte" | "astro" => cpd_tokenizer::sfc::script_blocks(source, host_format),
263 _ => return Vec::new(),
264 };
265 let host = LineIndex::new(source.as_bytes());
266 let mut out = Vec::new();
267 for (format, range) in blocks {
268 if !supports(&format) {
269 continue;
270 }
271 let local = cpd_tokenizer::tokenizer::ranges_in(ignored, range.clone());
272 let place = |location: &Location| host.location(range.start + location.offset as usize);
273 for mut form in forms(&source[range.clone()], &format, &local, names) {
274 form.start = place(&form.start);
275 form.end = place(&form.end);
276 out.push((format.clone(), form));
277 }
278 }
279 out
280}
281
282pub fn token_range(
286 spans: &[(Location, Location)],
287 start: &Location,
288 end: &Location,
289) -> Option<(usize, usize)> {
290 let first = spans.partition_point(|(s, _)| s.offset < start.offset);
291 let last = spans.partition_point(|(_, e)| e.offset <= end.offset);
292 (first < last).then_some((first, last))
293}
294
295#[derive(Debug, Clone, PartialEq)]
297pub struct FormSource {
298 pub id: String,
299 pub format: String,
300 pub real_path: String,
303 pub forms: Vec<Form>,
304}
305
306impl FormSource {
307 pub fn new(prepared: &PreparedSource, forms: Vec<Form>) -> Self {
310 Self::with_spans(
311 prepared.id.clone(),
312 prepared.format.clone(),
313 prepared.real_path.clone(),
314 forms,
315 &prepared.spans,
316 )
317 }
318
319 pub fn with_spans(
321 id: String,
322 format: String,
323 real_path: String,
324 mut forms: Vec<Form>,
325 spans: &[(Location, Location)],
326 ) -> Self {
327 for form in &mut forms {
328 if let Some((first, last)) = token_range(spans, &form.start, &form.end) {
329 form.range = [first as u32, (last - 1) as u32];
330 form.tokens = (last - first) as u32;
331 }
332 }
333 Self {
334 id,
335 format,
336 real_path,
337 forms,
338 }
339 }
340}
341
342#[derive(Debug, Default)]
345pub struct SimilarPairs {
346 pub all: Vec<CpdClone>,
350 pub reported: Vec<CpdClone>,
356}
357
358const LINKS: usize = 8;
362
363#[derive(Debug, Clone, Copy)]
366struct Edge {
367 a: (usize, usize),
368 b: (usize, usize),
369 shared: u32,
370 total: u32,
371}
372
373pub struct SimilarityIndex {
377 sources: Vec<FormSource>,
378 groups: FxHashMap<&'static str, Vec<(usize, usize)>>,
380 min_nodes: u32,
381 min_lines: u32,
382}
383
384impl SimilarityIndex {
385 pub fn build(sources: Vec<FormSource>, min_nodes: u32, min_lines: u32) -> Self {
388 let mut groups: FxHashMap<&'static str, Vec<(usize, usize)>> = FxHashMap::default();
389 for (si, source) in sources.iter().enumerate() {
390 let group = group(&source.format);
391 for (fi, form) in source.forms.iter().enumerate() {
392 if eligible(form, min_nodes, min_lines) {
393 groups.entry(group).or_default().push((si, fi));
394 }
395 }
396 }
397 Self {
398 sources,
399 groups,
400 min_nodes,
401 min_lines,
402 }
403 }
404
405 pub fn sources(&self) -> &[FormSource] {
407 &self.sources
408 }
409
410 pub fn pairs(
417 &self,
418 threshold: f64,
419 existing: &[CpdClone],
420 filters: &PathFilters,
421 keep_all: bool,
422 ) -> SimilarPairs {
423 let mut groups: Vec<(&&str, &Vec<(usize, usize)>)> = self.groups.iter().collect();
424 groups.sort_by_key(|(group, _)| **group);
425 let mut edges = Vec::new();
426 for (_, items) in groups {
427 self.group_edges(items, threshold, filters, keep_all, &mut edges);
428 }
429 edges.sort_by(|x, y| self.edge_order(x, y));
430 let reported = self.links(&edges, &Coverage::new(existing));
431 let mut all: Vec<CpdClone> = match keep_all {
432 true => edges.iter().map(|edge| self.clone_of(edge)).collect(),
433 false => Vec::new(),
434 };
435 all.sort_by(report_order);
436 SimilarPairs { all, reported }
437 }
438
439 fn group_edges(
445 &self,
446 items: &[(usize, usize)],
447 threshold: f64,
448 filters: &PathFilters,
449 keep_all: bool,
450 edges: &mut Vec<Edge>,
451 ) {
452 let set = |i: usize| {
453 let (si, fi) = items[i];
454 self.sources[si].forms[fi].fingerprints.as_slice()
455 };
456 let mut first: FxHashMap<&[u64], usize> = FxHashMap::default();
457 let mut copies: Vec<Vec<usize>> = Vec::new();
458 for i in 0..items.len() {
459 match first.entry(set(i)) {
460 Entry::Occupied(entry) => copies[*entry.get()].push(i),
461 Entry::Vacant(entry) => {
462 entry.insert(copies.len());
463 copies.push(vec![i]);
464 }
465 }
466 }
467 let linked = |n: usize| match keep_all {
468 true => n,
469 false => n.min(LINKS),
470 };
471 let mut add = |x: usize, y: usize, shared: u32, total: u32| {
472 let (a, b) = (items[x], items[y]);
473 if self.apart(a, b, filters) {
474 edges.push(Edge {
475 a,
476 b,
477 shared,
478 total,
479 });
480 }
481 };
482 for same in &copies {
483 let size = set(same[0]).len() as u32;
484 for (i, &x) in same.iter().enumerate().skip(1) {
485 for &y in &same[..linked(i)] {
486 add(y, x, size, size);
487 }
488 }
489 }
490 let distinct: Vec<&[u64]> = copies.iter().map(|same| set(same[0])).collect();
491 for found in join::pairs(&distinct, threshold) {
492 let (xs, ys) = (&copies[found.a], &copies[found.b]);
493 for &x in xs {
494 for &y in &ys[..linked(ys.len())] {
495 add(x, y, found.shared, found.total);
496 }
497 }
498 }
499 }
500
501 fn apart(
504 &self,
505 (sa, fa): (usize, usize),
506 (sb, fb): (usize, usize),
507 filters: &PathFilters,
508 ) -> bool {
509 let (src_a, src_b) = (&self.sources[sa], &self.sources[sb]);
510 let (a, b) = (&src_a.forms[fa], &src_b.forms[fb]);
511 !same_span(src_a, a, src_b, b)
512 && !nested(src_a, a, src_b, b)
513 && !filters
514 .should_skip_sources((&src_a.id, &src_a.real_path), (&src_b.id, &src_b.real_path))
515 }
516
517 fn edge_order(&self, x: &Edge, y: &Edge) -> std::cmp::Ordering {
520 let product = |e: &Edge, f: &Edge| u64::from(e.shared) * u64::from(f.total);
521 product(y, x)
522 .cmp(&product(x, y))
523 .then_with(|| self.ends(x).cmp(&self.ends(y)))
524 }
525
526 fn ends(&self, edge: &Edge) -> ((&str, u32), (&str, u32)) {
528 let end = |(si, fi): (usize, usize)| {
529 let source = &self.sources[si];
530 (source.id.as_str(), source.forms[fi].start.offset)
531 };
532 let (a, b) = (end(edge.a), end(edge.b));
533 match a <= b {
534 true => (a, b),
535 false => (b, a),
536 }
537 }
538
539 fn links(&self, edges: &[Edge], coverage: &Coverage) -> Vec<CpdClone> {
543 let mut base = Vec::with_capacity(self.sources.len());
544 let mut units = 0;
545 for source in &self.sources {
546 base.push(units);
547 units += source.forms.len();
548 }
549 let id = |(si, fi): (usize, usize)| base[si] + fi;
550 let span = |(si, fi): (usize, usize)| {
551 let source = &self.sources[si];
552 let form = &source.forms[fi];
553 (source.id.as_str(), form.start.line, form.end.line)
554 };
555 let mut parent: Vec<usize> = (0..units).collect();
556 for edge in edges {
557 if coverage.covers(span(edge.a), span(edge.b)) {
558 join_sets(&mut parent, id(edge.a), id(edge.b));
559 }
560 }
561 let mut reported: Vec<CpdClone> = edges
562 .iter()
563 .filter(|edge| join_sets(&mut parent, id(edge.a), id(edge.b)))
564 .map(|edge| self.clone_of(edge))
565 .collect();
566 reported.sort_by(|x, y| x.position_key().cmp(&y.position_key()));
567 reported
568 }
569
570 fn clone_of(&self, edge: &Edge) -> CpdClone {
571 let (src_a, src_b) = (&self.sources[edge.a.0], &self.sources[edge.b.0]);
572 let (a, b) = (&src_a.forms[edge.a.1], &src_b.forms[edge.b.1]);
573 make_clone(src_a, a, src_b, b, edge.shared, edge.total)
574 }
575
576 pub fn query_clones(
580 &self,
581 source: &FormSource,
582 threshold: f64,
583 existing: &[CpdClone],
584 ) -> Vec<CpdClone> {
585 let coverage = Coverage::new(existing);
586 let mut clones = Vec::new();
587 let Some(items) = self.groups.get(group(&source.format)) else {
588 return clones;
589 };
590 for form in &source.forms {
591 if !eligible(form, self.min_nodes, self.min_lines) {
592 continue;
593 }
594 let size = form.fingerprints.len() as f64;
595 for &(si, fi) in items {
596 let other = &self.sources[si];
597 let found = &other.forms[fi];
598 let other_size = found.fingerprints.len() as f64;
599 if other_size < threshold * size - 1e-9 || size < threshold * other_size - 1e-9 {
600 continue;
601 }
602 let (shared, total) = join::overlap(&form.fingerprints, &found.fingerprints);
603 if (shared as f64 / total as f64) < threshold {
604 continue;
605 }
606 let clone = make_clone(source, form, other, found, shared, total);
607 if !coverage.covers_clone(&clone) {
608 clones.push(clone);
609 }
610 }
611 }
612 clones.sort_by(report_order);
613 clones
614 }
615}
616
617fn eligible(form: &Form, min_nodes: u32, min_lines: u32) -> bool {
618 form.nodes >= min_nodes && form.line_span() >= min_lines
619}
620
621fn same_span(src_a: &FormSource, a: &Form, src_b: &FormSource, b: &Form) -> bool {
623 src_a.id == src_b.id && a.start.line == b.start.line && a.end.line == b.end.line
624}
625
626fn nested(src_a: &FormSource, a: &Form, src_b: &FormSource, b: &Form) -> bool {
629 let holds =
630 |x: &Form, y: &Form| x.start.offset <= y.start.offset && y.end.offset <= x.end.offset;
631 src_a.id == src_b.id && (holds(a, b) || holds(b, a))
632}
633
634fn set_of(parent: &mut [usize], mut x: usize) -> usize {
636 while parent[x] != x {
637 parent[x] = parent[parent[x]];
638 x = parent[x];
639 }
640 x
641}
642
643fn join_sets(parent: &mut [usize], x: usize, y: usize) -> bool {
645 let (x, y) = (set_of(parent, x), set_of(parent, y));
646 if x == y {
647 return false;
648 }
649 parent[x.max(y)] = x.min(y);
650 true
651}
652
653pub fn find_similar(
657 sources: Vec<FormSource>,
658 threshold: f64,
659 min_nodes: u32,
660 min_lines: u32,
661 existing: &[CpdClone],
662 filters: &PathFilters,
663 keep_all: bool,
664) -> SimilarPairs {
665 if sources.is_empty() {
666 return SimilarPairs::default();
667 }
668 SimilarityIndex::build(sources, min_nodes, min_lines)
669 .pairs(threshold, existing, filters, keep_all)
670}
671
672fn report_order(x: &CpdClone, y: &CpdClone) -> std::cmp::Ordering {
675 let score = |c: &CpdClone| c.structure.as_ref().map_or(0.0, StructuralMatch::score);
676 let language = |c: &CpdClone| c.structure.as_ref().map(|s| s.language.clone());
677 score(y)
678 .total_cmp(&score(x))
679 .then_with(|| language(x).cmp(&language(y)))
680 .then_with(|| x.fragment_a.source_id.cmp(&y.fragment_a.source_id))
681 .then_with(|| x.fragment_a.start.line.cmp(&y.fragment_a.start.line))
682 .then_with(|| x.fragment_b.source_id.cmp(&y.fragment_b.source_id))
683 .then_with(|| x.fragment_b.start.line.cmp(&y.fragment_b.start.line))
684}
685
686fn make_clone(
687 src_a: &FormSource,
688 a: &Form,
689 src_b: &FormSource,
690 b: &Form,
691 shared: u32,
692 total: u32,
693) -> CpdClone {
694 let fragment = |src: &FormSource, f: &Form| Fragment {
695 source_id: src.id.clone(),
696 source_root: None,
697 start: f.start.clone(),
698 end: f.end.clone(),
699 range: f.range,
700 blame: None,
701 };
702 let a_first = (src_a.id.as_str(), a.start.offset) <= (src_b.id.as_str(), b.start.offset);
705 let ((src_a, a), (src_b, b)) = match a_first {
706 true => ((src_a, a), (src_b, b)),
707 false => ((src_b, b), (src_a, a)),
708 };
709 let languages = [language(&src_a.format), language(&src_b.format)];
712 let language = match languages {
713 [a, b] if a == b => a,
714 _ => group(&src_a.format),
715 };
716 CpdClone {
717 format: src_a.format.clone(),
718 fragment_a: fragment(src_a, a),
719 fragment_b: fragment(src_b, b),
720 token_count: a.tokens.min(b.tokens),
721 is_new: false,
722 kind: CloneKind::Similar,
723 similarity: Some((shared as f64 / total as f64) as f32),
724 similarity_method: Some(SimilarityMethod::Ast),
725 structure: Some(StructuralMatch {
726 language: language.to_string(),
727 nodes: [a.nodes, b.nodes],
728 shared,
729 total,
730 }),
731 unmatched_lines: [0, 0],
732 after_cut: false,
733 }
734}
735
736pub struct Coverage<'a> {
742 by_source: FxHashMap<&'a str, Vec<(&'a Fragment, &'a Fragment)>>,
745}
746
747impl<'a> Coverage<'a> {
748 pub fn new(existing: impl IntoIterator<Item = &'a CpdClone>) -> Self {
749 let mut by_source: FxHashMap<&'a str, Vec<(&'a Fragment, &'a Fragment)>> =
750 FxHashMap::default();
751 for clone in existing {
752 let (a, b) = (&clone.fragment_a, &clone.fragment_b);
753 by_source
754 .entry(a.source_id.as_str())
755 .or_default()
756 .push((a, b));
757 by_source
758 .entry(b.source_id.as_str())
759 .or_default()
760 .push((b, a));
761 }
762 Self { by_source }
763 }
764
765 pub fn covers(&self, a: (&str, u32, u32), b: (&str, u32, u32)) -> bool {
768 let partners = |(id, start, end): (&str, u32, u32)| {
771 self.by_source
772 .get(id)
773 .into_iter()
774 .flatten()
775 .filter(move |(here, _)| spans_lines(here, start, end))
776 .map(|(_, there)| *there)
777 };
778 let into_b: Vec<&Fragment> = partners(b).collect();
779 if into_b.is_empty() {
780 return false;
781 }
782 partners(a).any(|x| {
783 (x.source_id == b.0 && spans_lines(x, b.1, b.2))
784 || into_b
785 .iter()
786 .any(|y| x.source_id == y.source_id && overlap(x, y))
787 })
788 }
789
790 pub fn covers_clone(&self, pair: &CpdClone) -> bool {
793 let (a, b) = (&pair.fragment_a, &pair.fragment_b);
794 self.covers(
795 (&a.source_id, a.start.line, a.end.line),
796 (&b.source_id, b.start.line, b.end.line),
797 )
798 }
799}
800
801pub fn discount_token_lines(clones: &mut [CpdClone]) {
807 let token_clone =
808 |c: &CpdClone| c.similarity_method != Some(SimilarityMethod::Ast) && !c.kind.is_semantic();
809 let file = cpd_core::paths::resolve_fragment_path;
812 let mut held: FxHashMap<String, Vec<(u32, u32)>> = FxHashMap::default();
813 for clone in clones.iter().filter(|c| token_clone(c)) {
814 for f in [&clone.fragment_a, &clone.fragment_b] {
815 held.entry(file(f))
816 .or_default()
817 .push((f.start.line, f.end.line));
818 }
819 }
820 for ranges in held.values_mut() {
821 ranges.sort_unstable();
822 let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
823 for &(start, end) in ranges.iter() {
824 match merged.last_mut() {
825 Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
826 _ => merged.push((start, end)),
827 }
828 }
829 *ranges = merged;
830 }
831 let shared = |f: &Fragment| -> u32 {
832 held.get(&file(f)).map_or(0, |ranges| {
833 ranges
834 .iter()
835 .map(|&(start, end)| {
836 let (lo, hi) = (start.max(f.start.line), end.min(f.end.line));
837 if lo <= hi { hi - lo + 1 } else { 0 }
838 })
839 .sum()
840 })
841 };
842 for clone in clones
843 .iter_mut()
844 .filter(|c| c.similarity_method == Some(SimilarityMethod::Ast))
845 {
846 clone.unmatched_lines = [shared(&clone.fragment_a), shared(&clone.fragment_b)];
847 }
848}
849
850fn spans_lines(frag: &Fragment, start: u32, end: u32) -> bool {
852 let lo = frag.start.line.max(start);
853 let hi = frag.end.line.min(end);
854 if hi < lo {
855 return false;
856 }
857 let overlap = hi - lo + 1;
858 let span = end.saturating_sub(start) + 1;
859 overlap as f32 >= 0.9 * span as f32
860}
861
862fn overlap(x: &Fragment, y: &Fragment) -> bool {
865 let lo = x.start.line.max(y.start.line);
866 let hi = x.end.line.min(y.end.line);
867 if hi < lo {
868 return false;
869 }
870 let shorter = x
871 .end
872 .line
873 .saturating_sub(x.start.line)
874 .min(y.end.line.saturating_sub(y.start.line))
875 + 1;
876 (hi - lo + 1) as f32 >= 0.9 * shorter as f32
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 fn source(id: &str, format: &str, code: &str) -> FormSource {
884 FormSource {
885 id: id.to_string(),
886 format: format.to_string(),
887 real_path: String::new(),
888 forms: forms(code, format, &[], Names::Consistent),
889 }
890 }
891
892 const ALPHA: &str = "def alpha(xs):\n ys = filter(xs, 1)\n total = 0\n for y in ys:\n total += y * 2\n return map(total, inc)\n";
893 const BETA: &str = "def beta(items):\n kept = filter(items, 5)\n sum = 0\n for k in kept:\n sum += k * 2\n return map(sum, dec)\n";
894 const GAMMA: &str = "def gamma(items):\n kept = sorted(items, 5)\n sum = 0\n for k in kept:\n sum -= k * 2\n print(sum)\n return map(sum, dec)\n";
895
896 #[test]
897 fn renamed_copies_pair_and_the_score_counts_shared_fingerprints() {
898 let sources = vec![
899 source("a.py", "python", ALPHA),
900 source("b.py", "python", BETA),
901 source("c.py", "python", GAMMA),
902 ];
903 let found = find_similar(
904 sources,
905 DEFAULT_THRESHOLD,
906 20,
907 3,
908 &[],
909 &PathFilters::default(),
910 true,
911 );
912 assert_eq!(found.all.len(), 1);
913 let pair = &found.all[0];
914 assert_eq!(pair.similarity, Some(1.0));
915 assert_eq!(pair.fragment_a.source_id, "a.py");
916 let structure = pair.structure.as_ref().unwrap();
917 assert_eq!(structure.language, "python");
918 assert_eq!(structure.shared, structure.total);
919 assert_eq!(found.reported.len(), 1);
920 }
921
922 #[test]
923 fn small_units_and_units_of_other_languages_do_not_pair() {
924 let ts = "function alpha(xs) {\n const ys = filter(xs, 1);\n let total = 0;\n for (const y of ys) { total += y * 2; }\n return map(total, inc);\n}\n";
925 let sources = vec![
926 source("a.py", "python", ALPHA),
927 source("a.ts", "typescript", ts),
928 source("b.js", "javascript", &ts.replace("alpha", "beta")),
929 ];
930 let found = find_similar(
931 sources.clone(),
932 DEFAULT_THRESHOLD,
933 20,
934 3,
935 &[],
936 &PathFilters::default(),
937 true,
938 );
939 assert_eq!(found.all.len(), 1);
940 assert_eq!(
941 found.all[0].structure.as_ref().unwrap().language,
942 "typescript"
943 );
944 let none = find_similar(
945 sources,
946 DEFAULT_THRESHOLD,
947 1000,
948 3,
949 &[],
950 &PathFilters::default(),
951 true,
952 );
953 assert!(none.all.is_empty());
954 }
955
956 #[test]
957 fn a_pair_a_token_clone_covers_is_found_but_not_reported() {
958 let sources = vec![
959 source("a.py", "python", ALPHA),
960 source("b.py", "python", BETA),
961 ];
962 let token_clone = CpdClone {
963 format: "python".into(),
964 fragment_a: Fragment::new(
965 "a.py",
966 Location::new(1, 0, 0),
967 Location::new(6, 0, 0),
968 [0, 40],
969 ),
970 fragment_b: Fragment::new(
971 "b.py",
972 Location::new(1, 0, 0),
973 Location::new(6, 0, 0),
974 [0, 40],
975 ),
976 token_count: 40,
977 is_new: false,
978 kind: CloneKind::Renamed,
979 similarity: None,
980 similarity_method: None,
981 structure: None,
982 unmatched_lines: [0, 0],
983 after_cut: false,
984 };
985 let found = find_similar(
986 sources,
987 DEFAULT_THRESHOLD,
988 20,
989 3,
990 &[token_clone],
991 &PathFilters::default(),
992 true,
993 );
994 assert_eq!(found.all.len(), 1);
995 assert!(found.reported.is_empty());
996 }
997
998 #[test]
999 fn a_snippet_finds_the_units_like_its_own() {
1000 let index = SimilarityIndex::build(
1001 vec![
1002 source("a.py", "python", ALPHA),
1003 source("c.py", "python", GAMMA),
1004 ],
1005 20,
1006 3,
1007 );
1008 let snippet = source("<snippet>", "python", BETA);
1009 let found = index.query_clones(&snippet, DEFAULT_THRESHOLD, &[]);
1010 assert_eq!(found.len(), 1);
1011 assert_eq!(found[0].fragment_b.source_id, "a.py");
1012 }
1013
1014 #[test]
1015 fn markdown_blocks_hold_units_at_their_place_in_the_host() {
1016 let md = format!("# Title\n\n```python\n{ALPHA}```\n");
1017 let found = embedded_forms(&md, "markdown", &[], Names::Consistent);
1018 assert_eq!(found.len(), 1);
1019 assert_eq!(found[0].0, "python");
1020 assert_eq!(found[0].1.start.line, 4);
1021 assert_eq!(found[0].1.end.line, 9);
1022 }
1023
1024 #[test]
1025 fn test_files_and_data_files_are_not_read() {
1026 assert!(reads(Path::new("src/app.py"), "python"));
1027 assert!(!reads(Path::new("tests/test_app.py"), "python"));
1028 assert!(!reads(Path::new("src/conftest.py"), "python"));
1029 assert!(!reads(Path::new("src/types.pyi"), "python"));
1030 assert!(!reads(Path::new("config.edn"), "clojure"));
1031 assert!(reads(Path::new("src/core.cljc"), "clojure"));
1032 assert!(!reads(Path::new("src/index.d.ts"), "typescript"));
1033 assert!(!reads(Path::new("src/app.test.ts"), "typescript"));
1034 assert!(!reads(
1035 Path::new("src/MyApp.Tests.Integration/CartFlow.cs"),
1036 "csharp"
1037 ));
1038 assert!(reads(
1039 Path::new("src/MSTest.TestAdapter/Execution.cs"),
1040 "csharp"
1041 ));
1042 assert!(reads(Path::new("docs/guide.md"), "markdown"));
1043 assert!(
1044 reads(Path::new("bin/deploy"), "python"),
1045 "a script without an extension"
1046 );
1047 assert!(reads(Path::new("src/core.cljx"), "clojure"));
1048 }
1049
1050 #[test]
1051 fn with_tests_a_test_file_is_read_and_data_still_is_not() {
1052 assert!(reads_with_tests(Path::new("tests/test_app.py"), "python"));
1053 assert!(reads_with_tests(Path::new("src/app.test.ts"), "typescript"));
1054 assert!(!reads_with_tests(Path::new("src/types.pyi"), "python"));
1055 assert!(!reads_with_tests(Path::new("config.edn"), "clojure"));
1056 assert!(!reads_with_tests(Path::new("src/index.d.ts"), "typescript"));
1057 }
1058
1059 #[test]
1060 fn a_unit_does_not_pair_with_one_nested_in_it() {
1061 let code = "def make_handler(config):\n class Handler:\n def handle(self, request):\n check(request)\n return send(config, request)\n return Handler\n";
1062 let found = find_similar(
1063 vec![source("factory.py", "python", code)],
1064 0.01,
1065 1,
1066 0,
1067 &[],
1068 &PathFilters::default(),
1069 true,
1070 );
1071 assert!(found.all.is_empty(), "{:?}", found.all);
1072 }
1073
1074 #[test]
1075 fn a_group_of_look_alikes_is_reported_as_the_pairs_that_link_it() {
1076 let sources = || -> Vec<FormSource> {
1077 (0..5)
1078 .map(|i| {
1079 let code = format!(
1080 "def f{i}(order):\n total = price(order) * 2\n return round(total, 2)\n"
1081 );
1082 source(&format!("{i}.py"), "python", &code)
1083 })
1084 .collect()
1085 };
1086 let found = find_similar(
1087 sources(),
1088 DEFAULT_THRESHOLD,
1089 1,
1090 0,
1091 &[],
1092 &PathFilters::default(),
1093 true,
1094 );
1095 assert_eq!(found.all.len(), 10, "every pair of the five copies");
1096 assert_eq!(found.reported.len(), 4, "each copy linked once");
1097 let found = find_similar(
1098 sources(),
1099 DEFAULT_THRESHOLD,
1100 1,
1101 0,
1102 &[],
1103 &PathFilters::default(),
1104 false,
1105 );
1106 assert!(found.all.is_empty());
1107 assert_eq!(found.reported.len(), 4);
1108 }
1109}