1use cpd_core::detect::PathLabel;
47use cpd_core::models::{CloneKind, CpdClone, Fragment, Location};
48use cpd_core::paths::clean_source_id;
49use rayon::prelude::*;
50use rustc_hash::FxHashMap;
51
52pub const MIN_Z: f32 = 3.0;
55const TRIM: usize = TOP;
58const TOP: usize = 8;
61pub const MIN_BACKGROUND: usize = 8;
64const MIN_CALLEE_NAME: usize = 5;
67const ROW_BLOCK: usize = 32;
70
71pub trait Embedder: Send + Sync {
73 fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String>;
76}
77
78#[derive(Debug, Clone, PartialEq)]
80pub struct SemanticUnit {
81 pub grammar: &'static str,
85 pub name: String,
87 pub start: Location,
88 pub end: Location,
89 pub range: [u32; 2],
91 pub token_count: u32,
93 pub text: String,
95 pub test: bool,
98}
99
100impl SemanticUnit {
101 pub fn build(
105 grammar: &'static str,
106 name: String,
107 start: Location,
108 end: Location,
109 text: String,
110 spans: &[(Location, Location)],
111 ) -> Option<Self> {
112 let (first, last) = cpd_core::similarity::token_range(spans, &start, &end)?;
113 if text.trim().is_empty() {
114 return None;
115 }
116 Some(Self {
117 grammar,
118 name,
119 start,
120 end,
121 range: [first as u32, (last - 1) as u32],
122 token_count: (last - first) as u32,
123 text,
124 test: false,
125 })
126 }
127
128 pub fn line_span(&self) -> u32 {
130 self.end.line.saturating_sub(self.start.line)
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct UnitSource {
137 pub id: String,
139 pub format: String,
140 pub units: Vec<SemanticUnit>,
141 pub path_label: PathLabel,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct Thresholds {
151 pub across: f32,
154 pub within: f32,
159 pub near_best: f32,
162 pub group_floor: f32,
168}
169
170impl Thresholds {
171 pub const REFERENCE: Self = Self {
175 across: 0.6,
176 within: 0.75,
177 near_best: 0.05,
178 group_floor: 0.8,
179 };
180
181 pub fn for_pair(&self, same_language: bool) -> f32 {
183 match same_language {
184 true => self.within,
185 false => self.across,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Copy)]
192pub struct SemanticParams {
193 pub thresholds: Thresholds,
194 pub min_tokens: usize,
196 pub min_lines: usize,
198 pub scope: SemanticScope,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
206pub enum SemanticScope {
207 #[default]
209 All,
210 Same,
212 Cross,
214}
215
216impl SemanticScope {
217 pub const NAMES: &'static str = "all, same, cross";
218
219 pub fn as_str(self) -> &'static str {
220 match self {
221 SemanticScope::All => "all",
222 SemanticScope::Same => "same",
223 SemanticScope::Cross => "cross",
224 }
225 }
226
227 fn allows(self, same_language: bool) -> bool {
228 match self {
229 SemanticScope::All => true,
230 SemanticScope::Same => same_language,
231 SemanticScope::Cross => !same_language,
232 }
233 }
234}
235
236impl std::str::FromStr for SemanticScope {
237 type Err = String;
238
239 fn from_str(s: &str) -> Result<Self, Self::Err> {
240 match s.trim().to_ascii_lowercase().as_str() {
241 "all" => Ok(SemanticScope::All),
242 "same" => Ok(SemanticScope::Same),
243 "cross" => Ok(SemanticScope::Cross),
244 other => Err(format!(
245 "unknown scope '{other}': must be one of: {}",
246 SemanticScope::NAMES
247 )),
248 }
249 }
250}
251
252pub fn find_semantic_clones(
259 sources: &[UnitSource],
260 embedder: &dyn Embedder,
261 params: &SemanticParams,
262 existing: &[CpdClone],
263) -> Result<Vec<CpdClone>, String> {
264 let items = eligible_items(sources, params);
265 if items.len() < 2 {
266 return Ok(Vec::new());
267 }
268 let unit = |item: &Item| &sources[item.source].units[item.unit];
269 let texts: Vec<&str> = items.iter().map(|item| unit(item).text.as_str()).collect();
270 let vectors = embedder.embed(&texts)?;
271 let space = VectorSpace::new(&vectors, texts.len())?;
272
273 let grammars = grammar_ids(&items, |item| unit(item).grammar);
274 let mut related = call_pairs(&items, |item| unit(item));
275 for (list, covered) in related
276 .iter_mut()
277 .zip(covered_pairs(&items, sources, existing))
278 {
279 if !covered.is_empty() {
280 list.extend(covered);
281 list.sort_unstable();
282 list.dedup();
283 }
284 }
285 let labels: Vec<&PathLabel> = items
286 .iter()
287 .map(|item| &sources[item.source].path_label)
288 .collect();
289 let rows = space.scan(
290 &items,
291 &grammars.of_item,
292 grammars.count,
293 &related,
294 |i, j| !labels[i].skips(labels[j]),
295 );
296 let clones = matched_pairs(&rows, &grammars.of_item, ¶ms.thresholds, params.scope)
297 .into_iter()
298 .map(|(i, j, similarity)| {
299 let (a, b) = (&items[i], &items[j]);
300 let (src_a, src_b) = (&sources[a.source], &sources[b.source]);
301 make_clone(
302 src_a,
303 &src_a.units[a.unit],
304 src_b,
305 &src_b.units[b.unit],
306 similarity,
307 )
308 })
309 .collect();
310 let mut clones = drop_nested(clones);
311 clones.sort_by(|x, y| x.position_key().cmp(&y.position_key()));
312 Ok(clones)
313}
314
315pub(crate) fn matched_pairs(
319 rows: &[Vec<Background>],
320 grammar_of: &[usize],
321 bars: &Thresholds,
322 scope: SemanticScope,
323) -> Vec<(usize, usize, f32)> {
324 let mut pairs = Vec::new();
325 for (i, row) in rows.iter().enumerate() {
326 let own = grammar_of[i];
327 let targets = row
328 .iter()
329 .enumerate()
330 .filter(|&(grammar, _)| scope.allows(grammar == own));
331 for (grammar, background) in targets {
332 let threshold = bars.for_pair(grammar == own);
333 for (j, similarity) in background.near_best(bars.near_best) {
334 let other = &rows[j][own];
336 if j <= i || !other.is_near_best(i, bars.near_best) {
337 continue;
338 }
339 let mutual_best = background.best() == Some(j) && other.best() == Some(i);
342 let floor = match mutual_best {
343 true => threshold,
344 false => threshold.max(bars.group_floor),
345 };
346 if similarity < floor {
347 continue;
348 }
349 let z = background.z(similarity).into_iter();
350 if z.chain(other.z(similarity)).any(|z| z < MIN_Z) {
351 continue;
352 }
353 pairs.push((i, j, similarity));
354 }
355 }
356 }
357 pairs
358}
359
360fn drop_nested(mut clones: Vec<CpdClone>) -> Vec<CpdClone> {
364 let lines = |f: &Fragment| f.end.line - f.start.line;
365 clones.sort_by_key(|c| std::cmp::Reverse(lines(&c.fragment_a) + lines(&c.fragment_b)));
366 let inside = |inner: &Fragment, outer: &Fragment| {
367 inner.source_id == outer.source_id
368 && outer.start.line <= inner.start.line
369 && inner.end.line <= outer.end.line
370 };
371 let mut kept: Vec<CpdClone> = Vec::with_capacity(clones.len());
372 for clone in clones {
373 let nested = kept.iter().any(|outer| {
374 (inside(&clone.fragment_a, &outer.fragment_a)
375 && inside(&clone.fragment_b, &outer.fragment_b))
376 || (inside(&clone.fragment_a, &outer.fragment_b)
377 && inside(&clone.fragment_b, &outer.fragment_a))
378 });
379 if !nested {
380 kept.push(clone);
381 }
382 }
383 kept
384}
385
386pub(crate) struct Item {
388 pub(crate) source: usize,
389 pub(crate) unit: usize,
390 pub(crate) file: u32,
392}
393
394fn eligible_items(sources: &[UnitSource], params: &SemanticParams) -> Vec<Item> {
395 let mut files: FxHashMap<&str, u32> = FxHashMap::default();
396 let mut items = Vec::new();
397 for (si, src) in sources.iter().enumerate() {
398 let next = files.len() as u32;
399 let file = *files.entry(clean_source_id(&src.id)).or_insert(next);
400 for (ui, unit) in src.units.iter().enumerate() {
401 if (unit.token_count as usize) < params.min_tokens
402 || (unit.line_span() as usize) < params.min_lines
403 {
404 continue;
405 }
406 items.push(Item {
407 source: si,
408 unit: ui,
409 file,
410 });
411 }
412 }
413 items
414}
415
416pub(crate) struct Grammars {
417 pub(crate) of_item: Vec<usize>,
418 pub(crate) count: usize,
419}
420
421pub(crate) fn grammar_ids(items: &[Item], grammar: impl Fn(&Item) -> &'static str) -> Grammars {
422 let mut ids: Vec<&'static str> = Vec::new();
423 let of_item = items
424 .iter()
425 .map(|item| {
426 let g = grammar(item);
427 ids.iter().position(|&known| known == g).unwrap_or_else(|| {
428 ids.push(g);
429 ids.len() - 1
430 })
431 })
432 .collect();
433 Grammars {
434 of_item,
435 count: ids.len(),
436 }
437}
438
439pub(crate) fn call_pairs<'u>(
448 items: &[Item],
449 unit: impl Fn(&Item) -> &'u SemanticUnit,
450) -> Vec<Vec<usize>> {
451 let mut by_name: FxHashMap<&str, Vec<usize>> = FxHashMap::default();
452 for (i, item) in items.iter().enumerate() {
453 let name = unit(item).name.as_str();
454 if name.chars().count() >= MIN_CALLEE_NAME && !name.starts_with('<') && !unit(item).test {
456 by_name.entry(name).or_default().push(i);
457 }
458 }
459 let mut related: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
460 if by_name.is_empty() {
461 return related;
462 }
463 for (i, item) in items.iter().enumerate() {
464 let own = unit(item);
465 let mut seen: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
466 for callee in called_names(&own.text) {
467 if (callee == own.name && !own.test) || !seen.insert(callee) {
470 continue;
471 }
472 for &j in by_name.get(callee).map(Vec::as_slice).unwrap_or_default() {
473 if j != i && call_family(unit(&items[j]).grammar) == call_family(own.grammar) {
474 related[i].push(j);
475 related[j].push(i);
476 }
477 }
478 }
479 }
480 for list in &mut related {
481 list.sort_unstable();
482 list.dedup();
483 }
484 related
485}
486
487pub(crate) fn call_family(grammar: &str) -> &str {
491 match grammar {
492 "cpp" => "c",
493 "kotlin" | "scala" => "java",
494 other => other,
495 }
496}
497
498pub(crate) fn called_names(text: &str) -> impl Iterator<Item = &str> {
500 let bytes = text.as_bytes();
501 let mut i = 0;
502 std::iter::from_fn(move || {
503 while i < bytes.len() {
504 let c = bytes[i];
505 if !(c.is_ascii_alphabetic() || c == b'_' || c == b'$') {
506 i += 1;
507 continue;
508 }
509 let start = i;
510 while i < bytes.len()
511 && (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'_' | b'$'))
512 {
513 i += 1;
514 }
515 let end = i;
516 let mut k = i;
517 while k < bytes.len() && matches!(bytes[k], b' ' | b'\t') {
518 k += 1;
519 }
520 if bytes.get(k) == Some(&b'(') {
521 return Some(&text[start..end]);
522 }
523 }
524 None
525 })
526}
527
528pub(crate) struct VectorSpace {
530 dims: usize,
531 data: Vec<f32>,
532}
533
534impl VectorSpace {
535 pub(crate) fn new(vectors: &[Vec<f32>], expected: usize) -> Result<Self, String> {
536 if vectors.len() != expected {
537 return Err(format!(
538 "the embedding model returned {} vectors for {} functions",
539 vectors.len(),
540 expected
541 ));
542 }
543 let dims = vectors.first().map_or(0, Vec::len);
544 if dims == 0 {
545 return Err("the embedding model returned empty vectors".to_string());
546 }
547 let mut data = Vec::with_capacity(dims * vectors.len());
548 for v in vectors {
549 if v.len() != dims {
550 return Err(format!(
551 "the embedding model returned vectors of {} and {} dimensions",
552 dims,
553 v.len()
554 ));
555 }
556 let norm = v
557 .iter()
558 .map(|x| f64::from(*x) * f64::from(*x))
559 .sum::<f64>()
560 .sqrt();
561 let scale = if norm.is_finite() && norm > 0.0 {
563 (1.0 / norm) as f32
564 } else {
565 0.0
566 };
567 data.extend(
568 v.iter()
569 .map(|x| if x.is_finite() { x * scale } else { 0.0 }),
570 );
571 }
572 Ok(Self { dims, data })
573 }
574
575 pub(crate) fn row(&self, i: usize) -> &[f32] {
576 &self.data[i * self.dims..(i + 1) * self.dims]
577 }
578
579 pub(crate) fn scan(
584 &self,
585 items: &[Item],
586 grammar_of: &[usize],
587 grammars: usize,
588 related: &[Vec<usize>],
589 may_pair: impl Fn(usize, usize) -> bool + Sync,
590 ) -> Vec<Vec<Background>> {
591 let n = items.len();
592 (0..n.div_ceil(ROW_BLOCK))
593 .into_par_iter()
594 .flat_map_iter(|block| {
595 let rows = block * ROW_BLOCK..((block + 1) * ROW_BLOCK).min(n);
596 let mut out = vec![vec![Background::EMPTY; grammars]; rows.len()];
597 for j in 0..n {
598 let column = self.row(j);
599 for (r, i) in rows.clone().enumerate() {
600 if items[i].file == items[j].file
601 || related[i].binary_search(&j).is_ok()
602 || !may_pair(i, j)
603 {
604 continue;
605 }
606 let sim = dot(self.row(i), column);
607 out[r][grammar_of[j]].add(sim, j);
608 }
609 }
610 out
611 })
612 .collect()
613 }
614}
615
616#[inline]
617pub(crate) fn dot(a: &[f32], b: &[f32]) -> f32 {
618 let mut lanes = [0f32; 8];
620 let (chunks_a, chunks_b) = (a.chunks_exact(8), b.chunks_exact(8));
621 let tail: f32 = chunks_a
622 .remainder()
623 .iter()
624 .zip(chunks_b.remainder())
625 .map(|(x, y)| x * y)
626 .sum();
627 for (x, y) in chunks_a.zip(chunks_b) {
628 for k in 0..8 {
629 lanes[k] += x[k] * y[k];
630 }
631 }
632 lanes.iter().sum::<f32>() + tail
633}
634
635#[derive(Debug, Clone, Copy)]
638pub(crate) struct Background {
639 count: u32,
640 sum: f64,
641 sum_sq: f64,
642 top: [(f32, u32); TOP],
643 len: u8,
644}
645
646impl Background {
647 const EMPTY: Self = Self {
648 count: 0,
649 sum: 0.0,
650 sum_sq: 0.0,
651 top: [(f32::NEG_INFINITY, u32::MAX); TOP],
652 len: 0,
653 };
654
655 fn add(&mut self, sim: f32, item: usize) {
656 self.count += 1;
657 self.sum += f64::from(sim);
658 self.sum_sq += f64::from(sim) * f64::from(sim);
659 let len = self.len as usize;
662 let at = self.top[..len].partition_point(|&(s, _)| s >= sim);
663 if at == TOP {
664 return;
665 }
666 let end = len.min(TOP - 1);
667 self.top.copy_within(at..end, at + 1);
668 self.top[at] = (sim, item as u32);
669 self.len = (len + 1).min(TOP) as u8;
670 }
671
672 fn near_best(&self, margin: f32) -> impl Iterator<Item = (usize, f32)> + '_ {
674 let floor = self.top[0].0 - margin;
675 self.top[..self.len as usize]
676 .iter()
677 .take_while(move |&&(s, _)| s >= floor)
678 .map(|&(s, item)| (item as usize, s))
679 }
680
681 fn best(&self) -> Option<usize> {
682 (self.len > 0).then_some(self.top[0].1 as usize)
683 }
684
685 fn is_near_best(&self, item: usize, margin: f32) -> bool {
686 self.near_best(margin).any(|(i, _)| i == item)
687 }
688
689 fn z(&self, sim: f32) -> Option<f32> {
694 let trim = TRIM.min(self.len as usize);
695 let count = self.count as usize - trim;
696 if count < MIN_BACKGROUND {
697 return None;
698 }
699 let top = &self.top[..trim];
700 let sum = self.sum - top.iter().map(|&(s, _)| f64::from(s)).sum::<f64>();
701 let sum_sq = self.sum_sq
702 - top
703 .iter()
704 .map(|&(s, _)| f64::from(s) * f64::from(s))
705 .sum::<f64>();
706 let n = count as f64;
707 let mean = sum / n;
708 let std = (sum_sq / n - mean * mean).max(0.0).sqrt();
709 (std > 1e-6).then(|| ((f64::from(sim) - mean) / std) as f32)
710 }
711}
712
713fn covered_pairs(items: &[Item], sources: &[UnitSource], existing: &[CpdClone]) -> Vec<Vec<usize>> {
721 let mut covered: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
722 if existing.is_empty() {
723 return covered;
724 }
725 let source_of: FxHashMap<&str, usize> = sources
726 .iter()
727 .enumerate()
728 .map(|(i, s)| (s.id.as_str(), i))
729 .collect();
730 let mut items_of: Vec<Vec<usize>> = vec![Vec::new(); sources.len()];
731 for (i, item) in items.iter().enumerate() {
732 items_of[item.source].push(i);
733 }
734 let mut between: FxHashMap<(usize, usize), Vec<&CpdClone>> = FxHashMap::default();
736 for clone in existing {
737 let a = source_of.get(clone.fragment_a.source_id.as_str());
738 let b = source_of.get(clone.fragment_b.source_id.as_str());
739 if let (Some(&a), Some(&b)) = (a, b)
740 && a != b
741 {
742 between.entry((a.min(b), a.max(b))).or_default().push(clone);
743 }
744 }
745 let unit = |i: usize| &sources[items[i].source].units[items[i].unit];
746 let met = |source: usize, clones: &[&CpdClone]| -> Vec<usize> {
748 let id = sources[source].id.as_str();
749 items_of[source]
750 .iter()
751 .copied()
752 .filter(|&i| {
753 clones.iter().any(|c| {
754 [&c.fragment_a, &c.fragment_b]
755 .into_iter()
756 .any(|f| f.source_id == id && meets(f, unit(i)))
757 })
758 })
759 .collect()
760 };
761 for (&(sa, sb), clones) in &between {
762 for i in met(sa, clones) {
763 for j in met(sb, clones) {
764 if items[i].file != items[j].file
765 && covered_by(&sources[sa].id, unit(i), &sources[sb].id, unit(j), clones)
766 {
767 covered[i].push(j);
768 covered[j].push(i);
769 }
770 }
771 }
772 }
773 for list in &mut covered {
774 list.sort_unstable();
775 list.dedup();
776 }
777 covered
778}
779
780fn meets(frag: &Fragment, f: &SemanticUnit) -> bool {
782 frag.start.line <= f.end.line && f.start.line <= frag.end.line
783}
784
785fn covered_by(
788 id_a: &str,
789 a: &SemanticUnit,
790 id_b: &str,
791 b: &SemanticUnit,
792 clones: &[&CpdClone],
793) -> bool {
794 let lines = |f: &SemanticUnit| vec![false; (f.end.line - f.start.line + 1) as usize];
795 let (mut in_a, mut in_b) = (lines(a), lines(b));
796 let mark = |covered: &mut [bool], frag: &Fragment, f: &SemanticUnit| {
798 let lo = frag.start.line.max(f.start.line);
799 let hi = frag.end.line.min(f.end.line);
800 for line in lo..=hi {
801 covered[(line - f.start.line) as usize] = true;
802 }
803 };
804 for c in clones {
805 let (frag_a, frag_b) = if c.fragment_a.source_id == id_a && c.fragment_b.source_id == id_b {
806 (&c.fragment_a, &c.fragment_b)
807 } else if c.fragment_a.source_id == id_b && c.fragment_b.source_id == id_a {
808 (&c.fragment_b, &c.fragment_a)
809 } else {
810 continue;
811 };
812 if meets(frag_a, a) && meets(frag_b, b) {
815 mark(&mut in_a, frag_a, a);
816 mark(&mut in_b, frag_b, b);
817 }
818 }
819 let share =
820 |covered: &[bool]| covered.iter().filter(|&&c| c).count() as f32 / covered.len() as f32;
821 share(&in_a) >= 0.9 && share(&in_b) >= 0.9
822}
823
824fn make_clone(
825 src_a: &UnitSource,
826 a: &SemanticUnit,
827 src_b: &UnitSource,
828 b: &SemanticUnit,
829 similarity: f32,
830) -> CpdClone {
831 let frag = |src: &UnitSource, f: &SemanticUnit| {
832 Fragment::new(src.id.clone(), f.start.clone(), f.end.clone(), f.range)
833 };
834 let a_first = (src_a.id.as_str(), a.start.line) <= (src_b.id.as_str(), b.start.line);
836 let ((first_src, first), (second_src, second)) = if a_first {
837 ((src_a, a), (src_b, b))
838 } else {
839 ((src_b, b), (src_a, a))
840 };
841 CpdClone {
842 format: first_src.format.clone(),
843 fragment_a: frag(first_src, first),
844 fragment_b: frag(second_src, second),
845 token_count: first.token_count.min(second.token_count),
846 is_new: false,
847 kind: CloneKind::Semantic,
848 similarity: Some(similarity.min(1.0)),
849 similarity_method: None,
850 unmatched_lines: [0, 0],
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857 use std::collections::HashMap;
858
859 fn loc(line: u32, offset: u32) -> Location {
860 Location::new(line, 0, offset)
861 }
862
863 fn unit(grammar: &'static str, name: &str, line: u32, text: &str) -> SemanticUnit {
865 SemanticUnit {
866 grammar,
867 name: name.to_string(),
868 start: loc(line, line * 100),
869 end: loc(line + 9, line * 100 + 90),
870 range: [line * 10, line * 10 + 59],
871 token_count: 60,
872 text: text.to_string(),
873 test: false,
874 }
875 }
876
877 fn source(id: &str, format: &str, units: Vec<SemanticUnit>) -> UnitSource {
878 UnitSource {
879 id: id.to_string(),
880 format: format.to_string(),
881 units,
882 path_label: PathLabel::default(),
883 }
884 }
885
886 fn with_bars(thresholds: Thresholds) -> SemanticParams {
888 SemanticParams {
889 thresholds,
890 ..PARAMS
891 }
892 }
893
894 const PARAMS: SemanticParams = SemanticParams {
895 thresholds: Thresholds::REFERENCE,
896 min_tokens: 50,
897 min_lines: 5,
898 scope: SemanticScope::All,
899 };
900
901 struct Table(HashMap<String, Vec<f32>>);
903
904 impl Embedder for Table {
905 fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
906 texts
907 .iter()
908 .map(|t| {
909 let key = t.split_whitespace().next().unwrap_or_default();
910 self.0
911 .get(key)
912 .cloned()
913 .ok_or(format!("no vector for {key}"))
914 })
915 .collect()
916 }
917 }
918
919 fn pairs(found: &[CpdClone]) -> Vec<(&str, &str)> {
921 found
922 .iter()
923 .map(|c| {
924 (
925 c.fragment_a.source_id.as_str(),
926 c.fragment_b.source_id.as_str(),
927 )
928 })
929 .collect()
930 }
931
932 fn vec_on(axis: usize, noise_axis: usize, noise: f32) -> Vec<f32> {
934 let mut v = vec![0.0; 128];
935 v[axis] = 1.0;
936 v[noise_axis] += noise;
937 v
938 }
939
940 const FILLERS: usize = 30;
943
944 fn with_backgrounds(mut sources: Vec<UnitSource>) -> Vec<UnitSource> {
947 sources.extend(filler("back", "rust", "rust"));
948 sources.extend(filler("front", "oxc", "typescript"));
949 sources
950 }
951
952 fn filler(prefix: &str, grammar: &'static str, format: &str) -> Vec<UnitSource> {
953 (0..FILLERS)
954 .map(|k| {
955 let text = format!("{prefix}{k} body");
956 source(
957 &format!("{prefix}/filler{k}.x"),
958 format,
959 vec![unit(grammar, &format!("filler{k}"), 1, &text)],
960 )
961 })
962 .collect()
963 }
964
965 fn a_and_two_bs() -> Vec<UnitSource> {
968 with_backgrounds(vec![
969 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
970 source("b1.ts", "typescript", vec![unit("oxc", "b1", 1, "pb1")]),
971 source("b2.ts", "typescript", vec![unit("oxc", "b2", 1, "pb2")]),
972 ])
973 }
974
975 fn embedder(named: &[(&str, Vec<f32>)]) -> Table {
978 let mut table: HashMap<String, Vec<f32>> = named
979 .iter()
980 .map(|(k, v)| (k.to_string(), v.clone()))
981 .collect();
982 for (prefix, offset) in [("back", 64), ("front", 96)] {
983 for k in 0..FILLERS {
984 let mut v = vec![0.0; 128];
985 v[offset + k] = 1.0;
986 v[0] = 0.15;
987 v[1] = 0.15;
988 table.insert(format!("{prefix}{k}"), v);
989 }
990 }
991 Table(table)
992 }
993
994 #[test]
995 fn a_mutual_best_match_across_languages_is_a_semantic_clone() {
996 let sources = with_backgrounds(vec![
997 source(
998 "backend/src/pricing.rs",
999 "rust",
1000 vec![unit("rust", "cart_totals", 10, "totals-rs fn cart_totals")],
1001 ),
1002 source(
1003 "frontend/src/Cart.svelte:typescript",
1004 "typescript",
1005 vec![unit(
1006 "oxc",
1007 "computeTotals",
1008 20,
1009 "totals-ts function computeTotals",
1010 )],
1011 ),
1012 ]);
1013 let embedder = embedder(&[
1014 ("totals-rs", vec_on(0, 2, 0.5)),
1015 ("totals-ts", vec_on(0, 3, 0.6)),
1016 ]);
1017 let clones = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1018 assert_eq!(clones.len(), 1, "{clones:#?}");
1019 let c = &clones[0];
1020 assert_eq!(c.kind, CloneKind::Semantic);
1021 assert_eq!(c.format, "rust");
1022 assert_eq!(c.fragment_a.source_id, "backend/src/pricing.rs");
1023 assert_eq!(
1024 c.fragment_b.source_id,
1025 "frontend/src/Cart.svelte:typescript"
1026 );
1027 assert_eq!((c.fragment_a.start.line, c.fragment_b.start.line), (10, 20));
1028 let sim = c.similarity.unwrap();
1029 assert!((0.7..0.8).contains(&sim), "{sim}");
1030 assert_eq!(c.token_count, 60);
1031 }
1032
1033 #[test]
1034 fn the_threshold_is_a_cosine_floor() {
1035 let sources = with_backgrounds(vec![
1036 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa x")]),
1037 source("b.ts", "typescript", vec![unit("oxc", "b", 1, "pb x")]),
1038 ]);
1039 let embedder = embedder(&[("pa", vec_on(4, 5, 0.75)), ("pb", vec_on(4, 6, 0.75))]);
1041 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1042 assert_eq!(found.len(), 1);
1043 let sim = found[0].similarity.unwrap();
1044 let strict = with_bars(Thresholds {
1045 across: sim + 0.01,
1046 ..Thresholds::REFERENCE
1047 });
1048 assert!(
1049 find_semantic_clones(&sources, &embedder, &strict, &[])
1050 .unwrap()
1051 .is_empty()
1052 );
1053 }
1054
1055 #[test]
1056 fn a_pair_within_one_language_needs_a_higher_threshold() {
1057 let with_b = |id: &str, grammar: &'static str, format: &str| {
1058 with_backgrounds(vec![
1059 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa x")]),
1060 source(id, format, vec![unit(grammar, "b", 1, "pb x")]),
1061 ])
1062 };
1063 let across = with_b("b.ts", "oxc", "typescript");
1064 let within = with_b("b.rs", "rust", "rust");
1065 let vectors = embedder(&[("pa", vec_on(4, 5, 0.75)), ("pb", vec_on(4, 6, 0.75))]);
1068 let found = |sources: &[UnitSource], params: &SemanticParams| {
1069 find_semantic_clones(sources, &vectors, params, &[])
1070 .unwrap()
1071 .len()
1072 };
1073 assert_eq!(found(&across, &PARAMS), 1);
1074 assert_eq!(found(&within, &PARAMS), 0);
1075 let own = |within: f32| {
1077 with_bars(Thresholds {
1078 within,
1079 ..Thresholds::REFERENCE
1080 })
1081 };
1082 assert_eq!(found(&within, &own(0.6)), 1);
1083 assert_eq!(found(&across, &own(0.9)), 1);
1084 }
1085
1086 #[test]
1087 fn only_the_best_match_of_a_function_is_paired() {
1088 let sources = a_and_two_bs();
1091 let embedder = embedder(&[
1092 ("pa", vec_on(4, 5, 0.3)),
1093 ("pb1", vec_on(4, 6, 0.3)),
1094 ("pb2", vec_on(4, 7, 0.5)),
1095 ]);
1096 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1097 let pairs = pairs(&found);
1098 assert!(pairs.contains(&("a.rs", "b1.ts")), "{pairs:?}");
1099 assert!(
1100 !pairs.iter().any(|(x, y)| *x == "b2.ts" && *y == "a.rs"),
1101 "{pairs:?}"
1102 );
1103 assert!(!pairs.contains(&("a.rs", "b2.ts")), "{pairs:?}");
1104 }
1105
1106 #[test]
1107 fn functions_of_one_file_are_never_paired() {
1108 let mut sources = vec![
1109 source(
1110 "cart.svelte:typescript",
1111 "typescript",
1112 vec![unit("oxc", "load", 1, "pa"), unit("oxc", "save", 20, "pb")],
1113 ),
1114 source(
1116 "cart.svelte:javascript",
1117 "javascript",
1118 vec![unit("oxc", "other", 40, "pc")],
1119 ),
1120 ];
1121 sources.extend(filler("front", "oxc", "typescript"));
1122 let embedder = embedder(&[
1123 ("pa", vec_on(4, 5, 0.1)),
1124 ("pb", vec_on(4, 6, 0.1)),
1125 ("pc", vec_on(4, 7, 0.1)),
1126 ]);
1127 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1128 assert!(found.is_empty(), "{found:#?}");
1129 }
1130
1131 #[test]
1132 fn a_function_and_the_helper_it_calls_are_not_a_clone() {
1133 let mut sources = vec![
1134 source(
1135 "routes.rs",
1136 "rust",
1137 vec![unit(
1138 "rust",
1139 "list_articles",
1140 1,
1141 "pa fn list_articles() { db.articles_page (1) }",
1142 )],
1143 ),
1144 source(
1145 "db.rs",
1146 "rust",
1147 vec![unit("rust", "articles_page", 1, "pb fn articles_page() {}")],
1148 ),
1149 source(
1151 "db2.rs",
1152 "rust",
1153 vec![unit("rust", "fetch_page", 1, "pc fn fetch_page() {}")],
1154 ),
1155 ];
1156 sources.extend(filler("back", "rust", "rust"));
1157 let embedder = embedder(&[
1158 ("pa", vec_on(4, 5, 0.2)),
1159 ("pb", vec_on(4, 6, 0.2)),
1160 ("pc", vec_on(4, 7, 0.4)),
1161 ]);
1162 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1163 let pairs = pairs(&found);
1164 assert!(pairs.contains(&("db2.rs", "routes.rs")), "{pairs:?}");
1165 assert!(
1166 !pairs.contains(&("db.rs", "routes.rs")),
1167 "a function never pairs with the helper it calls: {pairs:?}"
1168 );
1169 }
1170
1171 #[test]
1172 fn a_path_filtered_best_match_leaves_room_for_the_allowed_one() {
1173 let roots = [
1176 std::path::PathBuf::from("app"),
1177 std::path::PathBuf::from("web"),
1178 ];
1179 let filters = cpd_core::detect::PathFilters {
1180 skip_local: true,
1181 scan_roots: &roots,
1182 isolated_groups: &[],
1183 };
1184 let mut sources = vec![
1185 source("app/a.ts", "typescript", vec![unit("oxc", "a", 1, "pa")]),
1186 source("app/n.ts", "typescript", vec![unit("oxc", "n", 1, "pn")]),
1187 source("web/b.ts", "typescript", vec![unit("oxc", "b", 1, "pb")]),
1188 ];
1189 sources.extend(
1190 filler("front", "oxc", "typescript")
1191 .into_iter()
1192 .map(|mut src| {
1193 src.id = format!("web/{}", src.id);
1194 src
1195 }),
1196 );
1197 for src in &mut sources {
1198 src.path_label = filters.label(&src.id);
1199 }
1200 let embedder = embedder(&[
1201 ("pa", vec_on(4, 5, 0.1)),
1202 ("pn", vec_on(4, 6, 0.1)),
1203 ("pb", vec_on(4, 7, 0.4)),
1204 ]);
1205 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1206 let pairs = pairs(&found);
1207 assert!(pairs.contains(&("app/a.ts", "web/b.ts")), "{pairs:?}");
1208 assert!(!pairs.contains(&("app/a.ts", "app/n.ts")), "{pairs:?}");
1209 }
1210
1211 #[test]
1212 fn pairs_nested_in_a_reported_pair_are_dropped() {
1213 let pair = |a: (u32, u32), b: (u32, u32)| {
1214 let mut c = CpdClone::exact(
1215 "tsx",
1216 Fragment::new("Add.tsx", loc(a.0, 0), loc(a.1, 0), [0, 1]),
1217 Fragment::new("Edit.tsx", loc(b.0, 0), loc(b.1, 0), [0, 1]),
1218 50,
1219 );
1220 c.kind = CloneKind::Semantic;
1221 c
1222 };
1223 let kept = drop_nested(vec![
1224 pair((20, 30), (25, 35)),
1225 pair((1, 100), (1, 110)),
1226 pair((120, 130), (20, 30)),
1227 ]);
1228 let spans: Vec<(u32, u32)> = kept
1229 .iter()
1230 .map(|c| (c.fragment_a.start.line, c.fragment_b.start.line))
1231 .collect();
1232 assert_eq!(
1233 spans,
1234 vec![(1, 1), (120, 20)],
1235 "the inner pair goes, an unrelated one stays"
1236 );
1237 }
1238
1239 #[test]
1240 fn three_implementations_of_one_feature_make_three_pairs() {
1241 let mut sources = vec![
1242 source("a.ts", "typescript", vec![unit("oxc", "a", 1, "pa")]),
1243 source("b.ts", "typescript", vec![unit("oxc", "b", 1, "pb")]),
1244 source("c.ts", "typescript", vec![unit("oxc", "c", 1, "pc")]),
1245 ];
1246 sources.extend(filler("front", "oxc", "typescript"));
1247 let embedder = embedder(&[
1248 ("pa", vec_on(4, 5, 0.30)),
1249 ("pb", vec_on(4, 6, 0.30)),
1250 ("pc", vec_on(4, 7, 0.35)),
1251 ]);
1252 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
1253 assert_eq!(
1254 pairs(&found),
1255 vec![("a.ts", "b.ts"), ("a.ts", "c.ts"), ("b.ts", "c.ts")]
1256 );
1257 }
1258
1259 #[test]
1260 fn the_group_floor_does_not_follow_the_threshold() {
1261 let sources = a_and_two_bs();
1265 let embedder = embedder(&[
1266 ("pa", vec_on(4, 5, 0.0)),
1267 ("pb1", vec_on(4, 6, 0.75)),
1268 ("pb2", vec_on(4, 7, 0.83)),
1269 ]);
1270 let loose = with_bars(Thresholds {
1271 across: 0.5,
1272 ..Thresholds::REFERENCE
1273 });
1274 let found = find_semantic_clones(&sources, &embedder, &loose, &[]).unwrap();
1275 assert_eq!(pairs(&found), vec![("a.rs", "b1.ts")], "{found:#?}");
1276 let lower = with_bars(Thresholds {
1279 across: 0.5,
1280 near_best: 0.08,
1281 group_floor: 0.7,
1282 ..Thresholds::REFERENCE
1283 });
1284 let found = find_semantic_clones(&sources, &embedder, &lower, &[]).unwrap();
1285 assert_eq!(
1286 pairs(&found),
1287 vec![("a.rs", "b1.ts"), ("a.rs", "b2.ts")],
1288 "{found:#?}"
1289 );
1290 }
1291
1292 #[test]
1293 fn scope_keeps_pairs_within_or_across_languages() {
1294 let sources = with_backgrounds(vec![
1295 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
1296 source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
1297 source("c.ts", "typescript", vec![unit("oxc", "c", 1, "pc")]),
1298 ]);
1299 let embedder = embedder(&[
1300 ("pa", vec_on(4, 5, 0.2)),
1301 ("pb", vec_on(4, 6, 0.2)),
1302 ("pc", vec_on(4, 7, 0.3)),
1303 ]);
1304 let with = |scope| {
1305 let params = SemanticParams { scope, ..PARAMS };
1306 let found = find_semantic_clones(&sources, &embedder, ¶ms, &[]).unwrap();
1307 pairs(&found)
1308 .into_iter()
1309 .map(|(x, y)| format!("{x}~{y}"))
1310 .collect::<Vec<_>>()
1311 };
1312 assert_eq!(
1313 with(SemanticScope::All),
1314 ["a.rs~b.rs", "a.rs~c.ts", "b.rs~c.ts"]
1315 );
1316 assert_eq!(with(SemanticScope::Same), ["a.rs~b.rs"]);
1317 assert_eq!(with(SemanticScope::Cross), ["a.rs~c.ts", "b.rs~c.ts"]);
1318 assert_eq!("cross".parse::<SemanticScope>(), Ok(SemanticScope::Cross));
1319 assert!(
1320 "both"
1321 .parse::<SemanticScope>()
1322 .unwrap_err()
1323 .contains("all, same, cross")
1324 );
1325 }
1326
1327 #[test]
1328 fn a_test_titled_after_its_subject_still_calls_it() {
1329 let items: Vec<Item> = (0..3)
1333 .map(|k| Item {
1334 source: 0,
1335 unit: k,
1336 file: k as u32,
1337 })
1338 .collect();
1339 let units = [
1340 unit("oxc", "compute", 1, "function compute(x) { return x * 2 }"),
1341 SemanticUnit {
1342 test: true,
1343 ..unit("oxc", "compute", 1, "it('compute', () => { compute (1) })")
1344 },
1345 unit(
1346 "oxc",
1347 "caller",
1348 1,
1349 "function caller() { return compute(2) }",
1350 ),
1351 ];
1352 let related = call_pairs(&items, |item| &units[item.unit]);
1353 assert_eq!(related[1], vec![0], "the test calls compute");
1354 assert_eq!(
1355 related[2],
1356 vec![0],
1357 "a caller of compute is not related to the test"
1358 );
1359 }
1360
1361 #[test]
1362 fn namesakes_are_not_mistaken_for_caller_and_callee() {
1363 let related = call_pairs(
1364 &[
1365 Item {
1366 source: 0,
1367 unit: 0,
1368 file: 0,
1369 },
1370 Item {
1371 source: 0,
1372 unit: 1,
1373 file: 1,
1374 },
1375 ],
1376 |item| {
1377 static UNITS: std::sync::OnceLock<Vec<SemanticUnit>> = std::sync::OnceLock::new();
1378 &UNITS.get_or_init(|| {
1379 vec![
1380 unit(
1381 "rust",
1382 "segment",
1383 1,
1384 "pub fn segment(&self) { segment_inner(1) }",
1385 ),
1386 unit(
1387 "oxc",
1388 "segment",
1389 1,
1390 "async segment(n) { return segment (n - 1) }",
1391 ),
1392 ]
1393 })[item.unit]
1394 },
1395 );
1396 assert!(related.iter().all(Vec::is_empty), "{related:?}");
1397 }
1398
1399 #[test]
1400 fn a_call_counts_only_within_one_language() {
1401 static UNITS: std::sync::OnceLock<Vec<SemanticUnit>> = std::sync::OnceLock::new();
1402 let units = UNITS.get_or_init(|| {
1403 vec![
1404 unit("python", "parse", 1, "def parse(text): return dict(text)"),
1405 unit(
1406 "oxc",
1407 "readEnv",
1408 1,
1409 "function readEnv(v) { return JSON.parse(v) || JSON.parse(v) }",
1410 ),
1411 unit("oxc", "parse", 1, "function parse(s) { return s }"),
1412 unit(
1413 "java",
1414 "loadUser",
1415 1,
1416 "User loadUser(int id) { return db.find(id); }",
1417 ),
1418 unit(
1419 "kotlin",
1420 "showUser",
1421 1,
1422 "fun showUser(id: Int) = render(loadUser(id))",
1423 ),
1424 ]
1425 });
1426 let items: Vec<Item> = (0..5)
1427 .map(|k| Item {
1428 source: k,
1429 unit: k,
1430 file: k as u32,
1431 })
1432 .collect();
1433 let related = call_pairs(&items, |item| &units[item.unit]);
1434 assert_eq!(
1435 related,
1436 vec![vec![], vec![2], vec![1], vec![4], vec![3]],
1437 "a TypeScript call reaches the TypeScript parse only, once; Kotlin calls into Java"
1438 );
1439 }
1440
1441 #[test]
1442 fn called_names_finds_calls_not_mentions() {
1443 let names: Vec<&str> =
1444 called_names("fn f(x) { g (x); let y = h; obj.method(1); $ref(2) }").collect();
1445 assert_eq!(names, vec!["f", "g", "method", "$ref"]);
1446 }
1447
1448 #[test]
1449 fn a_pair_already_reported_as_a_clone_is_skipped() {
1450 let mut sources = vec![
1451 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
1452 source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
1453 ];
1454 sources.extend(filler("back", "rust", "rust"));
1455 let embedder = embedder(&[("pa", vec_on(4, 5, 0.1)), ("pb", vec_on(4, 6, 0.1))]);
1456 assert_eq!(
1457 find_semantic_clones(&sources, &embedder, &PARAMS, &[])
1458 .unwrap()
1459 .len(),
1460 1
1461 );
1462 let exact = CpdClone::exact(
1463 "rust",
1464 Fragment::new("a.rs", loc(1, 0), loc(10, 0), [0, 50]),
1465 Fragment::new("b.rs", loc(2, 0), loc(10, 0), [0, 50]),
1466 50,
1467 );
1468 assert!(
1469 find_semantic_clones(&sources, &embedder, &PARAMS, &[exact])
1470 .unwrap()
1471 .is_empty()
1472 );
1473 let half = |from: u32, to: u32| {
1475 CpdClone::exact(
1476 "rust",
1477 Fragment::new("b.rs", loc(from, 0), loc(to, 0), [0, 20]),
1478 Fragment::new("a.rs", loc(from, 0), loc(to, 0), [0, 20]),
1479 20,
1480 )
1481 };
1482 let first = half(1, 5);
1483 let second = half(6, 10);
1484 assert_eq!(
1485 find_semantic_clones(&sources, &embedder, &PARAMS, std::slice::from_ref(&first))
1486 .unwrap()
1487 .len(),
1488 1
1489 );
1490 assert!(
1491 find_semantic_clones(&sources, &embedder, &PARAMS, &[first, second])
1492 .unwrap()
1493 .is_empty()
1494 );
1495 }
1496
1497 #[test]
1498 fn a_copy_already_found_does_not_hide_the_real_semantic_match() {
1499 let mut sources = vec![
1504 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
1505 source("copy.rs", "rust", vec![unit("rust", "copy", 1, "pcopy")]),
1506 source("c.rs", "rust", vec![unit("rust", "c", 1, "pc")]),
1507 ];
1508 sources.extend(filler("back", "rust", "rust"));
1509 let embedder = embedder(&[
1512 ("pa", vec_on(4, 5, 0.1)),
1513 ("pcopy", vec_on(4, 5, 0.1)),
1514 ("pc", vec_on(4, 6, 0.8)),
1515 ]);
1516 let copy = CpdClone::exact(
1517 "rust",
1518 Fragment::new("a.rs", loc(1, 0), loc(10, 0), [0, 50]),
1519 Fragment::new("copy.rs", loc(1, 0), loc(10, 0), [0, 50]),
1520 50,
1521 );
1522 let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[copy]).unwrap();
1523 assert_eq!(pairs(&found), vec![("a.rs", "c.rs")], "{found:#?}");
1524 }
1525
1526 #[test]
1527 fn small_functions_are_not_embedded() {
1528 struct Refuse;
1529 impl Embedder for Refuse {
1530 fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
1531 Err(format!("asked to embed {}", texts.len()))
1532 }
1533 }
1534 let mut tiny = unit("rust", "a", 1, "pa");
1535 tiny.token_count = 10;
1536 let sources = vec![
1537 source("a.rs", "rust", vec![tiny]),
1538 source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
1539 ];
1540 assert!(
1542 find_semantic_clones(&sources, &Refuse, &PARAMS, &[])
1543 .unwrap()
1544 .is_empty()
1545 );
1546 }
1547
1548 #[test]
1549 fn embedder_errors_and_bad_vectors_are_reported() {
1550 struct Fixed(Vec<Vec<f32>>);
1551 impl Embedder for Fixed {
1552 fn embed(&self, _: &[&str]) -> Result<Vec<Vec<f32>>, String> {
1553 Ok(self.0.clone())
1554 }
1555 }
1556 let sources = vec![
1557 source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
1558 source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
1559 ];
1560 let short = find_semantic_clones(&sources, &Fixed(vec![vec![1.0]]), &PARAMS, &[]);
1561 assert!(short.unwrap_err().contains("1 vectors for 2 functions"));
1562 let ragged = Fixed(vec![vec![1.0, 0.0], vec![1.0]]);
1563 let err = find_semantic_clones(&sources, &ragged, &PARAMS, &[]).unwrap_err();
1564 assert!(err.contains("2 and 1 dimensions"), "{err}");
1565 }
1566
1567 #[test]
1568 fn z_leaves_out_the_closest_matches_and_needs_eight_more() {
1569 let mut bg = Background::EMPTY;
1570 for k in 0..TOP {
1572 bg.add(0.9 - 0.01 * k as f32, k);
1573 }
1574 for k in 0..7 {
1576 bg.add(0.1 + 0.01 * k as f32, TOP + k);
1577 }
1578 assert_eq!(bg.z(0.9), None);
1579 bg.add(0.12, TOP + 7);
1580 let z = bg.z(0.9).unwrap();
1581 assert!(z > 20.0, "{z}");
1584 let near: Vec<usize> = bg.near_best(0.05).map(|(item, _)| item).collect();
1585 assert_eq!(near, vec![0, 1, 2, 3, 4, 5], "within 0.05 of 0.9");
1586 }
1587
1588 #[test]
1589 fn dot_matches_a_plain_sum() {
1590 let a: Vec<f32> = (0..19).map(|i| i as f32 * 0.5).collect();
1591 let b: Vec<f32> = (0..19).map(|i| 1.0 - i as f32 * 0.25).collect();
1592 let plain: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum();
1593 assert!((dot(&a, &b) - plain).abs() < 1e-3);
1594 }
1595
1596 #[test]
1597 fn unit_build_maps_bytes_to_tokens() {
1598 let spans: Vec<(Location, Location)> = (0..10)
1599 .map(|i| (loc(i + 1, i * 10), loc(i + 1, i * 10 + 5)))
1600 .collect();
1601 let u = SemanticUnit::build(
1602 "rust",
1603 "f".into(),
1604 loc(3, 20),
1605 loc(8, 75),
1606 "fn f() {}".into(),
1607 &spans,
1608 )
1609 .unwrap();
1610 assert_eq!((u.range, u.token_count, u.line_span()), ([2, 7], 6, 5));
1611 assert!(
1612 SemanticUnit::build(
1613 "rust",
1614 "g".into(),
1615 loc(3, 20),
1616 loc(8, 75),
1617 " ".into(),
1618 &spans
1619 )
1620 .is_none()
1621 );
1622 }
1623}