use cpd_core::detect::PathLabel;
use cpd_core::models::{CloneKind, CpdClone, Fragment, Location};
use cpd_core::paths::clean_source_id;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
pub const MIN_Z: f32 = 3.0;
const TRIM: usize = TOP;
const TOP: usize = 8;
pub const MIN_BACKGROUND: usize = 8;
const MIN_CALLEE_NAME: usize = 5;
const ROW_BLOCK: usize = 32;
pub trait Embedder: Send + Sync {
fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String>;
}
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticUnit {
pub grammar: &'static str,
pub name: String,
pub start: Location,
pub end: Location,
pub range: [u32; 2],
pub token_count: u32,
pub text: String,
pub test: bool,
}
impl SemanticUnit {
pub fn build(
grammar: &'static str,
name: String,
start: Location,
end: Location,
text: String,
spans: &[(Location, Location)],
) -> Option<Self> {
let (first, last) = cpd_core::similarity::token_range(spans, &start, &end)?;
if text.trim().is_empty() {
return None;
}
Some(Self {
grammar,
name,
start,
end,
range: [first as u32, (last - 1) as u32],
token_count: (last - first) as u32,
text,
test: false,
})
}
pub fn line_span(&self) -> u32 {
self.end.line.saturating_sub(self.start.line)
}
}
#[derive(Debug, Clone)]
pub struct UnitSource {
pub id: String,
pub format: String,
pub units: Vec<SemanticUnit>,
pub path_label: PathLabel,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Thresholds {
pub across: f32,
pub within: f32,
pub near_best: f32,
pub group_floor: f32,
}
impl Thresholds {
pub const REFERENCE: Self = Self {
across: 0.6,
within: 0.75,
near_best: 0.05,
group_floor: 0.8,
};
pub fn for_pair(&self, same_language: bool) -> f32 {
match same_language {
true => self.within,
false => self.across,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SemanticParams {
pub thresholds: Thresholds,
pub min_tokens: usize,
pub min_lines: usize,
pub scope: SemanticScope,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SemanticScope {
#[default]
All,
Same,
Cross,
}
impl SemanticScope {
pub const NAMES: &'static str = "all, same, cross";
pub fn as_str(self) -> &'static str {
match self {
SemanticScope::All => "all",
SemanticScope::Same => "same",
SemanticScope::Cross => "cross",
}
}
fn allows(self, same_language: bool) -> bool {
match self {
SemanticScope::All => true,
SemanticScope::Same => same_language,
SemanticScope::Cross => !same_language,
}
}
}
impl std::str::FromStr for SemanticScope {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"all" => Ok(SemanticScope::All),
"same" => Ok(SemanticScope::Same),
"cross" => Ok(SemanticScope::Cross),
other => Err(format!(
"unknown scope '{other}': must be one of: {}",
SemanticScope::NAMES
)),
}
}
}
pub fn find_semantic_clones(
sources: &[UnitSource],
embedder: &dyn Embedder,
params: &SemanticParams,
existing: &[CpdClone],
) -> Result<Vec<CpdClone>, String> {
let items = eligible_items(sources, params);
if items.len() < 2 {
return Ok(Vec::new());
}
let unit = |item: &Item| &sources[item.source].units[item.unit];
let texts: Vec<&str> = items.iter().map(|item| unit(item).text.as_str()).collect();
let vectors = embedder.embed(&texts)?;
let space = VectorSpace::new(&vectors, texts.len())?;
let grammars = grammar_ids(&items, |item| unit(item).grammar);
let mut related = call_pairs(&items, |item| unit(item));
for (list, covered) in related
.iter_mut()
.zip(covered_pairs(&items, sources, existing))
{
if !covered.is_empty() {
list.extend(covered);
list.sort_unstable();
list.dedup();
}
}
let labels: Vec<&PathLabel> = items
.iter()
.map(|item| &sources[item.source].path_label)
.collect();
let rows = space.scan(
&items,
&grammars.of_item,
grammars.count,
&related,
|i, j| !labels[i].skips(labels[j]),
);
let clones = matched_pairs(&rows, &grammars.of_item, ¶ms.thresholds, params.scope)
.into_iter()
.map(|(i, j, similarity)| {
let (a, b) = (&items[i], &items[j]);
let (src_a, src_b) = (&sources[a.source], &sources[b.source]);
make_clone(
src_a,
&src_a.units[a.unit],
src_b,
&src_b.units[b.unit],
similarity,
)
})
.collect();
let mut clones = drop_nested(clones);
clones.sort_by(|x, y| x.position_key().cmp(&y.position_key()));
Ok(clones)
}
pub(crate) fn matched_pairs(
rows: &[Vec<Background>],
grammar_of: &[usize],
bars: &Thresholds,
scope: SemanticScope,
) -> Vec<(usize, usize, f32)> {
let mut pairs = Vec::new();
for (i, row) in rows.iter().enumerate() {
let own = grammar_of[i];
let targets = row
.iter()
.enumerate()
.filter(|&(grammar, _)| scope.allows(grammar == own));
for (grammar, background) in targets {
let threshold = bars.for_pair(grammar == own);
for (j, similarity) in background.near_best(bars.near_best) {
let other = &rows[j][own];
if j <= i || !other.is_near_best(i, bars.near_best) {
continue;
}
let mutual_best = background.best() == Some(j) && other.best() == Some(i);
let floor = match mutual_best {
true => threshold,
false => threshold.max(bars.group_floor),
};
if similarity < floor {
continue;
}
let z = background.z(similarity).into_iter();
if z.chain(other.z(similarity)).any(|z| z < MIN_Z) {
continue;
}
pairs.push((i, j, similarity));
}
}
}
pairs
}
fn drop_nested(mut clones: Vec<CpdClone>) -> Vec<CpdClone> {
let lines = |f: &Fragment| f.end.line - f.start.line;
clones.sort_by_key(|c| std::cmp::Reverse(lines(&c.fragment_a) + lines(&c.fragment_b)));
let inside = |inner: &Fragment, outer: &Fragment| {
inner.source_id == outer.source_id
&& outer.start.line <= inner.start.line
&& inner.end.line <= outer.end.line
};
let mut kept: Vec<CpdClone> = Vec::with_capacity(clones.len());
for clone in clones {
let nested = kept.iter().any(|outer| {
(inside(&clone.fragment_a, &outer.fragment_a)
&& inside(&clone.fragment_b, &outer.fragment_b))
|| (inside(&clone.fragment_a, &outer.fragment_b)
&& inside(&clone.fragment_b, &outer.fragment_a))
});
if !nested {
kept.push(clone);
}
}
kept
}
pub(crate) struct Item {
pub(crate) source: usize,
pub(crate) unit: usize,
pub(crate) file: u32,
}
fn eligible_items(sources: &[UnitSource], params: &SemanticParams) -> Vec<Item> {
let mut files: FxHashMap<&str, u32> = FxHashMap::default();
let mut items = Vec::new();
for (si, src) in sources.iter().enumerate() {
let next = files.len() as u32;
let file = *files.entry(clean_source_id(&src.id)).or_insert(next);
for (ui, unit) in src.units.iter().enumerate() {
if (unit.token_count as usize) < params.min_tokens
|| (unit.line_span() as usize) < params.min_lines
{
continue;
}
items.push(Item {
source: si,
unit: ui,
file,
});
}
}
items
}
pub(crate) struct Grammars {
pub(crate) of_item: Vec<usize>,
pub(crate) count: usize,
}
pub(crate) fn grammar_ids(items: &[Item], grammar: impl Fn(&Item) -> &'static str) -> Grammars {
let mut ids: Vec<&'static str> = Vec::new();
let of_item = items
.iter()
.map(|item| {
let g = grammar(item);
ids.iter().position(|&known| known == g).unwrap_or_else(|| {
ids.push(g);
ids.len() - 1
})
})
.collect();
Grammars {
of_item,
count: ids.len(),
}
}
pub(crate) fn call_pairs<'u>(
items: &[Item],
unit: impl Fn(&Item) -> &'u SemanticUnit,
) -> Vec<Vec<usize>> {
let mut by_name: FxHashMap<&str, Vec<usize>> = FxHashMap::default();
for (i, item) in items.iter().enumerate() {
let name = unit(item).name.as_str();
if name.chars().count() >= MIN_CALLEE_NAME && !name.starts_with('<') && !unit(item).test {
by_name.entry(name).or_default().push(i);
}
}
let mut related: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
if by_name.is_empty() {
return related;
}
for (i, item) in items.iter().enumerate() {
let own = unit(item);
let mut seen: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
for callee in called_names(&own.text) {
if (callee == own.name && !own.test) || !seen.insert(callee) {
continue;
}
for &j in by_name.get(callee).map(Vec::as_slice).unwrap_or_default() {
if j != i && call_family(unit(&items[j]).grammar) == call_family(own.grammar) {
related[i].push(j);
related[j].push(i);
}
}
}
}
for list in &mut related {
list.sort_unstable();
list.dedup();
}
related
}
pub(crate) fn call_family(grammar: &str) -> &str {
match grammar {
"cpp" => "c",
"kotlin" | "scala" => "java",
other => other,
}
}
pub(crate) fn called_names(text: &str) -> impl Iterator<Item = &str> {
let bytes = text.as_bytes();
let mut i = 0;
std::iter::from_fn(move || {
while i < bytes.len() {
let c = bytes[i];
if !(c.is_ascii_alphabetic() || c == b'_' || c == b'$') {
i += 1;
continue;
}
let start = i;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'_' | b'$'))
{
i += 1;
}
let end = i;
let mut k = i;
while k < bytes.len() && matches!(bytes[k], b' ' | b'\t') {
k += 1;
}
if bytes.get(k) == Some(&b'(') {
return Some(&text[start..end]);
}
}
None
})
}
pub(crate) struct VectorSpace {
dims: usize,
data: Vec<f32>,
}
impl VectorSpace {
pub(crate) fn new(vectors: &[Vec<f32>], expected: usize) -> Result<Self, String> {
if vectors.len() != expected {
return Err(format!(
"the embedding model returned {} vectors for {} functions",
vectors.len(),
expected
));
}
let dims = vectors.first().map_or(0, Vec::len);
if dims == 0 {
return Err("the embedding model returned empty vectors".to_string());
}
let mut data = Vec::with_capacity(dims * vectors.len());
for v in vectors {
if v.len() != dims {
return Err(format!(
"the embedding model returned vectors of {} and {} dimensions",
dims,
v.len()
));
}
let norm = v
.iter()
.map(|x| f64::from(*x) * f64::from(*x))
.sum::<f64>()
.sqrt();
let scale = if norm.is_finite() && norm > 0.0 {
(1.0 / norm) as f32
} else {
0.0
};
data.extend(
v.iter()
.map(|x| if x.is_finite() { x * scale } else { 0.0 }),
);
}
Ok(Self { dims, data })
}
pub(crate) fn row(&self, i: usize) -> &[f32] {
&self.data[i * self.dims..(i + 1) * self.dims]
}
pub(crate) fn scan(
&self,
items: &[Item],
grammar_of: &[usize],
grammars: usize,
related: &[Vec<usize>],
may_pair: impl Fn(usize, usize) -> bool + Sync,
) -> Vec<Vec<Background>> {
let n = items.len();
(0..n.div_ceil(ROW_BLOCK))
.into_par_iter()
.flat_map_iter(|block| {
let rows = block * ROW_BLOCK..((block + 1) * ROW_BLOCK).min(n);
let mut out = vec![vec![Background::EMPTY; grammars]; rows.len()];
for j in 0..n {
let column = self.row(j);
for (r, i) in rows.clone().enumerate() {
if items[i].file == items[j].file
|| related[i].binary_search(&j).is_ok()
|| !may_pair(i, j)
{
continue;
}
let sim = dot(self.row(i), column);
out[r][grammar_of[j]].add(sim, j);
}
}
out
})
.collect()
}
}
#[inline]
pub(crate) fn dot(a: &[f32], b: &[f32]) -> f32 {
let mut lanes = [0f32; 8];
let (chunks_a, chunks_b) = (a.chunks_exact(8), b.chunks_exact(8));
let tail: f32 = chunks_a
.remainder()
.iter()
.zip(chunks_b.remainder())
.map(|(x, y)| x * y)
.sum();
for (x, y) in chunks_a.zip(chunks_b) {
for k in 0..8 {
lanes[k] += x[k] * y[k];
}
}
lanes.iter().sum::<f32>() + tail
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Background {
count: u32,
sum: f64,
sum_sq: f64,
top: [(f32, u32); TOP],
len: u8,
}
impl Background {
const EMPTY: Self = Self {
count: 0,
sum: 0.0,
sum_sq: 0.0,
top: [(f32::NEG_INFINITY, u32::MAX); TOP],
len: 0,
};
fn add(&mut self, sim: f32, item: usize) {
self.count += 1;
self.sum += f64::from(sim);
self.sum_sq += f64::from(sim) * f64::from(sim);
let len = self.len as usize;
let at = self.top[..len].partition_point(|&(s, _)| s >= sim);
if at == TOP {
return;
}
let end = len.min(TOP - 1);
self.top.copy_within(at..end, at + 1);
self.top[at] = (sim, item as u32);
self.len = (len + 1).min(TOP) as u8;
}
fn near_best(&self, margin: f32) -> impl Iterator<Item = (usize, f32)> + '_ {
let floor = self.top[0].0 - margin;
self.top[..self.len as usize]
.iter()
.take_while(move |&&(s, _)| s >= floor)
.map(|&(s, item)| (item as usize, s))
}
fn best(&self) -> Option<usize> {
(self.len > 0).then_some(self.top[0].1 as usize)
}
fn is_near_best(&self, item: usize, margin: f32) -> bool {
self.near_best(margin).any(|(i, _)| i == item)
}
fn z(&self, sim: f32) -> Option<f32> {
let trim = TRIM.min(self.len as usize);
let count = self.count as usize - trim;
if count < MIN_BACKGROUND {
return None;
}
let top = &self.top[..trim];
let sum = self.sum - top.iter().map(|&(s, _)| f64::from(s)).sum::<f64>();
let sum_sq = self.sum_sq
- top
.iter()
.map(|&(s, _)| f64::from(s) * f64::from(s))
.sum::<f64>();
let n = count as f64;
let mean = sum / n;
let std = (sum_sq / n - mean * mean).max(0.0).sqrt();
(std > 1e-6).then(|| ((f64::from(sim) - mean) / std) as f32)
}
}
fn covered_pairs(items: &[Item], sources: &[UnitSource], existing: &[CpdClone]) -> Vec<Vec<usize>> {
let mut covered: Vec<Vec<usize>> = vec![Vec::new(); items.len()];
if existing.is_empty() {
return covered;
}
let source_of: FxHashMap<&str, usize> = sources
.iter()
.enumerate()
.map(|(i, s)| (s.id.as_str(), i))
.collect();
let mut items_of: Vec<Vec<usize>> = vec![Vec::new(); sources.len()];
for (i, item) in items.iter().enumerate() {
items_of[item.source].push(i);
}
let mut between: FxHashMap<(usize, usize), Vec<&CpdClone>> = FxHashMap::default();
for clone in existing {
let a = source_of.get(clone.fragment_a.source_id.as_str());
let b = source_of.get(clone.fragment_b.source_id.as_str());
if let (Some(&a), Some(&b)) = (a, b)
&& a != b
{
between.entry((a.min(b), a.max(b))).or_default().push(clone);
}
}
let unit = |i: usize| &sources[items[i].source].units[items[i].unit];
let met = |source: usize, clones: &[&CpdClone]| -> Vec<usize> {
let id = sources[source].id.as_str();
items_of[source]
.iter()
.copied()
.filter(|&i| {
clones.iter().any(|c| {
[&c.fragment_a, &c.fragment_b]
.into_iter()
.any(|f| f.source_id == id && meets(f, unit(i)))
})
})
.collect()
};
for (&(sa, sb), clones) in &between {
for i in met(sa, clones) {
for j in met(sb, clones) {
if items[i].file != items[j].file
&& covered_by(&sources[sa].id, unit(i), &sources[sb].id, unit(j), clones)
{
covered[i].push(j);
covered[j].push(i);
}
}
}
}
for list in &mut covered {
list.sort_unstable();
list.dedup();
}
covered
}
fn meets(frag: &Fragment, f: &SemanticUnit) -> bool {
frag.start.line <= f.end.line && f.start.line <= frag.end.line
}
fn covered_by(
id_a: &str,
a: &SemanticUnit,
id_b: &str,
b: &SemanticUnit,
clones: &[&CpdClone],
) -> bool {
let lines = |f: &SemanticUnit| vec![false; (f.end.line - f.start.line + 1) as usize];
let (mut in_a, mut in_b) = (lines(a), lines(b));
let mark = |covered: &mut [bool], frag: &Fragment, f: &SemanticUnit| {
let lo = frag.start.line.max(f.start.line);
let hi = frag.end.line.min(f.end.line);
for line in lo..=hi {
covered[(line - f.start.line) as usize] = true;
}
};
for c in clones {
let (frag_a, frag_b) = if c.fragment_a.source_id == id_a && c.fragment_b.source_id == id_b {
(&c.fragment_a, &c.fragment_b)
} else if c.fragment_a.source_id == id_b && c.fragment_b.source_id == id_a {
(&c.fragment_b, &c.fragment_a)
} else {
continue;
};
if meets(frag_a, a) && meets(frag_b, b) {
mark(&mut in_a, frag_a, a);
mark(&mut in_b, frag_b, b);
}
}
let share =
|covered: &[bool]| covered.iter().filter(|&&c| c).count() as f32 / covered.len() as f32;
share(&in_a) >= 0.9 && share(&in_b) >= 0.9
}
fn make_clone(
src_a: &UnitSource,
a: &SemanticUnit,
src_b: &UnitSource,
b: &SemanticUnit,
similarity: f32,
) -> CpdClone {
let frag = |src: &UnitSource, f: &SemanticUnit| {
Fragment::new(src.id.clone(), f.start.clone(), f.end.clone(), f.range)
};
let a_first = (src_a.id.as_str(), a.start.line) <= (src_b.id.as_str(), b.start.line);
let ((first_src, first), (second_src, second)) = if a_first {
((src_a, a), (src_b, b))
} else {
((src_b, b), (src_a, a))
};
CpdClone {
format: first_src.format.clone(),
fragment_a: frag(first_src, first),
fragment_b: frag(second_src, second),
token_count: first.token_count.min(second.token_count),
is_new: false,
kind: CloneKind::Semantic,
similarity: Some(similarity.min(1.0)),
similarity_method: None,
unmatched_lines: [0, 0],
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn loc(line: u32, offset: u32) -> Location {
Location::new(line, 0, offset)
}
fn unit(grammar: &'static str, name: &str, line: u32, text: &str) -> SemanticUnit {
SemanticUnit {
grammar,
name: name.to_string(),
start: loc(line, line * 100),
end: loc(line + 9, line * 100 + 90),
range: [line * 10, line * 10 + 59],
token_count: 60,
text: text.to_string(),
test: false,
}
}
fn source(id: &str, format: &str, units: Vec<SemanticUnit>) -> UnitSource {
UnitSource {
id: id.to_string(),
format: format.to_string(),
units,
path_label: PathLabel::default(),
}
}
fn with_bars(thresholds: Thresholds) -> SemanticParams {
SemanticParams {
thresholds,
..PARAMS
}
}
const PARAMS: SemanticParams = SemanticParams {
thresholds: Thresholds::REFERENCE,
min_tokens: 50,
min_lines: 5,
scope: SemanticScope::All,
};
struct Table(HashMap<String, Vec<f32>>);
impl Embedder for Table {
fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
texts
.iter()
.map(|t| {
let key = t.split_whitespace().next().unwrap_or_default();
self.0
.get(key)
.cloned()
.ok_or(format!("no vector for {key}"))
})
.collect()
}
}
fn pairs(found: &[CpdClone]) -> Vec<(&str, &str)> {
found
.iter()
.map(|c| {
(
c.fragment_a.source_id.as_str(),
c.fragment_b.source_id.as_str(),
)
})
.collect()
}
fn vec_on(axis: usize, noise_axis: usize, noise: f32) -> Vec<f32> {
let mut v = vec![0.0; 128];
v[axis] = 1.0;
v[noise_axis] += noise;
v
}
const FILLERS: usize = 30;
fn with_backgrounds(mut sources: Vec<UnitSource>) -> Vec<UnitSource> {
sources.extend(filler("back", "rust", "rust"));
sources.extend(filler("front", "oxc", "typescript"));
sources
}
fn filler(prefix: &str, grammar: &'static str, format: &str) -> Vec<UnitSource> {
(0..FILLERS)
.map(|k| {
let text = format!("{prefix}{k} body");
source(
&format!("{prefix}/filler{k}.x"),
format,
vec![unit(grammar, &format!("filler{k}"), 1, &text)],
)
})
.collect()
}
fn a_and_two_bs() -> Vec<UnitSource> {
with_backgrounds(vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
source("b1.ts", "typescript", vec![unit("oxc", "b1", 1, "pb1")]),
source("b2.ts", "typescript", vec![unit("oxc", "b2", 1, "pb2")]),
])
}
fn embedder(named: &[(&str, Vec<f32>)]) -> Table {
let mut table: HashMap<String, Vec<f32>> = named
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
for (prefix, offset) in [("back", 64), ("front", 96)] {
for k in 0..FILLERS {
let mut v = vec![0.0; 128];
v[offset + k] = 1.0;
v[0] = 0.15;
v[1] = 0.15;
table.insert(format!("{prefix}{k}"), v);
}
}
Table(table)
}
#[test]
fn a_mutual_best_match_across_languages_is_a_semantic_clone() {
let sources = with_backgrounds(vec![
source(
"backend/src/pricing.rs",
"rust",
vec![unit("rust", "cart_totals", 10, "totals-rs fn cart_totals")],
),
source(
"frontend/src/Cart.svelte:typescript",
"typescript",
vec![unit(
"oxc",
"computeTotals",
20,
"totals-ts function computeTotals",
)],
),
]);
let embedder = embedder(&[
("totals-rs", vec_on(0, 2, 0.5)),
("totals-ts", vec_on(0, 3, 0.6)),
]);
let clones = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
assert_eq!(clones.len(), 1, "{clones:#?}");
let c = &clones[0];
assert_eq!(c.kind, CloneKind::Semantic);
assert_eq!(c.format, "rust");
assert_eq!(c.fragment_a.source_id, "backend/src/pricing.rs");
assert_eq!(
c.fragment_b.source_id,
"frontend/src/Cart.svelte:typescript"
);
assert_eq!((c.fragment_a.start.line, c.fragment_b.start.line), (10, 20));
let sim = c.similarity.unwrap();
assert!((0.7..0.8).contains(&sim), "{sim}");
assert_eq!(c.token_count, 60);
}
#[test]
fn the_threshold_is_a_cosine_floor() {
let sources = with_backgrounds(vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa x")]),
source("b.ts", "typescript", vec![unit("oxc", "b", 1, "pb x")]),
]);
let embedder = embedder(&[("pa", vec_on(4, 5, 0.75)), ("pb", vec_on(4, 6, 0.75))]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
assert_eq!(found.len(), 1);
let sim = found[0].similarity.unwrap();
let strict = with_bars(Thresholds {
across: sim + 0.01,
..Thresholds::REFERENCE
});
assert!(
find_semantic_clones(&sources, &embedder, &strict, &[])
.unwrap()
.is_empty()
);
}
#[test]
fn a_pair_within_one_language_needs_a_higher_threshold() {
let with_b = |id: &str, grammar: &'static str, format: &str| {
with_backgrounds(vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa x")]),
source(id, format, vec![unit(grammar, "b", 1, "pb x")]),
])
};
let across = with_b("b.ts", "oxc", "typescript");
let within = with_b("b.rs", "rust", "rust");
let vectors = embedder(&[("pa", vec_on(4, 5, 0.75)), ("pb", vec_on(4, 6, 0.75))]);
let found = |sources: &[UnitSource], params: &SemanticParams| {
find_semantic_clones(sources, &vectors, params, &[])
.unwrap()
.len()
};
assert_eq!(found(&across, &PARAMS), 1);
assert_eq!(found(&within, &PARAMS), 0);
let own = |within: f32| {
with_bars(Thresholds {
within,
..Thresholds::REFERENCE
})
};
assert_eq!(found(&within, &own(0.6)), 1);
assert_eq!(found(&across, &own(0.9)), 1);
}
#[test]
fn only_the_best_match_of_a_function_is_paired() {
let sources = a_and_two_bs();
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.3)),
("pb1", vec_on(4, 6, 0.3)),
("pb2", vec_on(4, 7, 0.5)),
]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
let pairs = pairs(&found);
assert!(pairs.contains(&("a.rs", "b1.ts")), "{pairs:?}");
assert!(
!pairs.iter().any(|(x, y)| *x == "b2.ts" && *y == "a.rs"),
"{pairs:?}"
);
assert!(!pairs.contains(&("a.rs", "b2.ts")), "{pairs:?}");
}
#[test]
fn functions_of_one_file_are_never_paired() {
let mut sources = vec![
source(
"cart.svelte:typescript",
"typescript",
vec![unit("oxc", "load", 1, "pa"), unit("oxc", "save", 20, "pb")],
),
source(
"cart.svelte:javascript",
"javascript",
vec![unit("oxc", "other", 40, "pc")],
),
];
sources.extend(filler("front", "oxc", "typescript"));
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.1)),
("pb", vec_on(4, 6, 0.1)),
("pc", vec_on(4, 7, 0.1)),
]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
assert!(found.is_empty(), "{found:#?}");
}
#[test]
fn a_function_and_the_helper_it_calls_are_not_a_clone() {
let mut sources = vec![
source(
"routes.rs",
"rust",
vec![unit(
"rust",
"list_articles",
1,
"pa fn list_articles() { db.articles_page (1) }",
)],
),
source(
"db.rs",
"rust",
vec![unit("rust", "articles_page", 1, "pb fn articles_page() {}")],
),
source(
"db2.rs",
"rust",
vec![unit("rust", "fetch_page", 1, "pc fn fetch_page() {}")],
),
];
sources.extend(filler("back", "rust", "rust"));
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.2)),
("pb", vec_on(4, 6, 0.2)),
("pc", vec_on(4, 7, 0.4)),
]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
let pairs = pairs(&found);
assert!(pairs.contains(&("db2.rs", "routes.rs")), "{pairs:?}");
assert!(
!pairs.contains(&("db.rs", "routes.rs")),
"a function never pairs with the helper it calls: {pairs:?}"
);
}
#[test]
fn a_path_filtered_best_match_leaves_room_for_the_allowed_one() {
let roots = [
std::path::PathBuf::from("app"),
std::path::PathBuf::from("web"),
];
let filters = cpd_core::detect::PathFilters {
skip_local: true,
scan_roots: &roots,
isolated_groups: &[],
};
let mut sources = vec![
source("app/a.ts", "typescript", vec![unit("oxc", "a", 1, "pa")]),
source("app/n.ts", "typescript", vec![unit("oxc", "n", 1, "pn")]),
source("web/b.ts", "typescript", vec![unit("oxc", "b", 1, "pb")]),
];
sources.extend(
filler("front", "oxc", "typescript")
.into_iter()
.map(|mut src| {
src.id = format!("web/{}", src.id);
src
}),
);
for src in &mut sources {
src.path_label = filters.label(&src.id);
}
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.1)),
("pn", vec_on(4, 6, 0.1)),
("pb", vec_on(4, 7, 0.4)),
]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
let pairs = pairs(&found);
assert!(pairs.contains(&("app/a.ts", "web/b.ts")), "{pairs:?}");
assert!(!pairs.contains(&("app/a.ts", "app/n.ts")), "{pairs:?}");
}
#[test]
fn pairs_nested_in_a_reported_pair_are_dropped() {
let pair = |a: (u32, u32), b: (u32, u32)| {
let mut c = CpdClone::exact(
"tsx",
Fragment::new("Add.tsx", loc(a.0, 0), loc(a.1, 0), [0, 1]),
Fragment::new("Edit.tsx", loc(b.0, 0), loc(b.1, 0), [0, 1]),
50,
);
c.kind = CloneKind::Semantic;
c
};
let kept = drop_nested(vec![
pair((20, 30), (25, 35)),
pair((1, 100), (1, 110)),
pair((120, 130), (20, 30)),
]);
let spans: Vec<(u32, u32)> = kept
.iter()
.map(|c| (c.fragment_a.start.line, c.fragment_b.start.line))
.collect();
assert_eq!(
spans,
vec![(1, 1), (120, 20)],
"the inner pair goes, an unrelated one stays"
);
}
#[test]
fn three_implementations_of_one_feature_make_three_pairs() {
let mut sources = vec![
source("a.ts", "typescript", vec![unit("oxc", "a", 1, "pa")]),
source("b.ts", "typescript", vec![unit("oxc", "b", 1, "pb")]),
source("c.ts", "typescript", vec![unit("oxc", "c", 1, "pc")]),
];
sources.extend(filler("front", "oxc", "typescript"));
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.30)),
("pb", vec_on(4, 6, 0.30)),
("pc", vec_on(4, 7, 0.35)),
]);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[]).unwrap();
assert_eq!(
pairs(&found),
vec![("a.ts", "b.ts"), ("a.ts", "c.ts"), ("b.ts", "c.ts")]
);
}
#[test]
fn the_group_floor_does_not_follow_the_threshold() {
let sources = a_and_two_bs();
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.0)),
("pb1", vec_on(4, 6, 0.75)),
("pb2", vec_on(4, 7, 0.83)),
]);
let loose = with_bars(Thresholds {
across: 0.5,
..Thresholds::REFERENCE
});
let found = find_semantic_clones(&sources, &embedder, &loose, &[]).unwrap();
assert_eq!(pairs(&found), vec![("a.rs", "b1.ts")], "{found:#?}");
let lower = with_bars(Thresholds {
across: 0.5,
near_best: 0.08,
group_floor: 0.7,
..Thresholds::REFERENCE
});
let found = find_semantic_clones(&sources, &embedder, &lower, &[]).unwrap();
assert_eq!(
pairs(&found),
vec![("a.rs", "b1.ts"), ("a.rs", "b2.ts")],
"{found:#?}"
);
}
#[test]
fn scope_keeps_pairs_within_or_across_languages() {
let sources = with_backgrounds(vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
source("c.ts", "typescript", vec![unit("oxc", "c", 1, "pc")]),
]);
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.2)),
("pb", vec_on(4, 6, 0.2)),
("pc", vec_on(4, 7, 0.3)),
]);
let with = |scope| {
let params = SemanticParams { scope, ..PARAMS };
let found = find_semantic_clones(&sources, &embedder, ¶ms, &[]).unwrap();
pairs(&found)
.into_iter()
.map(|(x, y)| format!("{x}~{y}"))
.collect::<Vec<_>>()
};
assert_eq!(
with(SemanticScope::All),
["a.rs~b.rs", "a.rs~c.ts", "b.rs~c.ts"]
);
assert_eq!(with(SemanticScope::Same), ["a.rs~b.rs"]);
assert_eq!(with(SemanticScope::Cross), ["a.rs~c.ts", "b.rs~c.ts"]);
assert_eq!("cross".parse::<SemanticScope>(), Ok(SemanticScope::Cross));
assert!(
"both"
.parse::<SemanticScope>()
.unwrap_err()
.contains("all, same, cross")
);
}
#[test]
fn a_test_titled_after_its_subject_still_calls_it() {
let items: Vec<Item> = (0..3)
.map(|k| Item {
source: 0,
unit: k,
file: k as u32,
})
.collect();
let units = [
unit("oxc", "compute", 1, "function compute(x) { return x * 2 }"),
SemanticUnit {
test: true,
..unit("oxc", "compute", 1, "it('compute', () => { compute (1) })")
},
unit(
"oxc",
"caller",
1,
"function caller() { return compute(2) }",
),
];
let related = call_pairs(&items, |item| &units[item.unit]);
assert_eq!(related[1], vec![0], "the test calls compute");
assert_eq!(
related[2],
vec![0],
"a caller of compute is not related to the test"
);
}
#[test]
fn namesakes_are_not_mistaken_for_caller_and_callee() {
let related = call_pairs(
&[
Item {
source: 0,
unit: 0,
file: 0,
},
Item {
source: 0,
unit: 1,
file: 1,
},
],
|item| {
static UNITS: std::sync::OnceLock<Vec<SemanticUnit>> = std::sync::OnceLock::new();
&UNITS.get_or_init(|| {
vec![
unit(
"rust",
"segment",
1,
"pub fn segment(&self) { segment_inner(1) }",
),
unit(
"oxc",
"segment",
1,
"async segment(n) { return segment (n - 1) }",
),
]
})[item.unit]
},
);
assert!(related.iter().all(Vec::is_empty), "{related:?}");
}
#[test]
fn a_call_counts_only_within_one_language() {
static UNITS: std::sync::OnceLock<Vec<SemanticUnit>> = std::sync::OnceLock::new();
let units = UNITS.get_or_init(|| {
vec![
unit("python", "parse", 1, "def parse(text): return dict(text)"),
unit(
"oxc",
"readEnv",
1,
"function readEnv(v) { return JSON.parse(v) || JSON.parse(v) }",
),
unit("oxc", "parse", 1, "function parse(s) { return s }"),
unit(
"java",
"loadUser",
1,
"User loadUser(int id) { return db.find(id); }",
),
unit(
"kotlin",
"showUser",
1,
"fun showUser(id: Int) = render(loadUser(id))",
),
]
});
let items: Vec<Item> = (0..5)
.map(|k| Item {
source: k,
unit: k,
file: k as u32,
})
.collect();
let related = call_pairs(&items, |item| &units[item.unit]);
assert_eq!(
related,
vec![vec![], vec![2], vec![1], vec![4], vec![3]],
"a TypeScript call reaches the TypeScript parse only, once; Kotlin calls into Java"
);
}
#[test]
fn called_names_finds_calls_not_mentions() {
let names: Vec<&str> =
called_names("fn f(x) { g (x); let y = h; obj.method(1); $ref(2) }").collect();
assert_eq!(names, vec!["f", "g", "method", "$ref"]);
}
#[test]
fn a_pair_already_reported_as_a_clone_is_skipped() {
let mut sources = vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
];
sources.extend(filler("back", "rust", "rust"));
let embedder = embedder(&[("pa", vec_on(4, 5, 0.1)), ("pb", vec_on(4, 6, 0.1))]);
assert_eq!(
find_semantic_clones(&sources, &embedder, &PARAMS, &[])
.unwrap()
.len(),
1
);
let exact = CpdClone::exact(
"rust",
Fragment::new("a.rs", loc(1, 0), loc(10, 0), [0, 50]),
Fragment::new("b.rs", loc(2, 0), loc(10, 0), [0, 50]),
50,
);
assert!(
find_semantic_clones(&sources, &embedder, &PARAMS, &[exact])
.unwrap()
.is_empty()
);
let half = |from: u32, to: u32| {
CpdClone::exact(
"rust",
Fragment::new("b.rs", loc(from, 0), loc(to, 0), [0, 20]),
Fragment::new("a.rs", loc(from, 0), loc(to, 0), [0, 20]),
20,
)
};
let first = half(1, 5);
let second = half(6, 10);
assert_eq!(
find_semantic_clones(&sources, &embedder, &PARAMS, std::slice::from_ref(&first))
.unwrap()
.len(),
1
);
assert!(
find_semantic_clones(&sources, &embedder, &PARAMS, &[first, second])
.unwrap()
.is_empty()
);
}
#[test]
fn a_copy_already_found_does_not_hide_the_real_semantic_match() {
let mut sources = vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
source("copy.rs", "rust", vec![unit("rust", "copy", 1, "pcopy")]),
source("c.rs", "rust", vec![unit("rust", "c", 1, "pc")]),
];
sources.extend(filler("back", "rust", "rust"));
let embedder = embedder(&[
("pa", vec_on(4, 5, 0.1)),
("pcopy", vec_on(4, 5, 0.1)),
("pc", vec_on(4, 6, 0.8)),
]);
let copy = CpdClone::exact(
"rust",
Fragment::new("a.rs", loc(1, 0), loc(10, 0), [0, 50]),
Fragment::new("copy.rs", loc(1, 0), loc(10, 0), [0, 50]),
50,
);
let found = find_semantic_clones(&sources, &embedder, &PARAMS, &[copy]).unwrap();
assert_eq!(pairs(&found), vec![("a.rs", "c.rs")], "{found:#?}");
}
#[test]
fn small_functions_are_not_embedded() {
struct Refuse;
impl Embedder for Refuse {
fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String> {
Err(format!("asked to embed {}", texts.len()))
}
}
let mut tiny = unit("rust", "a", 1, "pa");
tiny.token_count = 10;
let sources = vec![
source("a.rs", "rust", vec![tiny]),
source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
];
assert!(
find_semantic_clones(&sources, &Refuse, &PARAMS, &[])
.unwrap()
.is_empty()
);
}
#[test]
fn embedder_errors_and_bad_vectors_are_reported() {
struct Fixed(Vec<Vec<f32>>);
impl Embedder for Fixed {
fn embed(&self, _: &[&str]) -> Result<Vec<Vec<f32>>, String> {
Ok(self.0.clone())
}
}
let sources = vec![
source("a.rs", "rust", vec![unit("rust", "a", 1, "pa")]),
source("b.rs", "rust", vec![unit("rust", "b", 1, "pb")]),
];
let short = find_semantic_clones(&sources, &Fixed(vec![vec![1.0]]), &PARAMS, &[]);
assert!(short.unwrap_err().contains("1 vectors for 2 functions"));
let ragged = Fixed(vec![vec![1.0, 0.0], vec![1.0]]);
let err = find_semantic_clones(&sources, &ragged, &PARAMS, &[]).unwrap_err();
assert!(err.contains("2 and 1 dimensions"), "{err}");
}
#[test]
fn z_leaves_out_the_closest_matches_and_needs_eight_more() {
let mut bg = Background::EMPTY;
for k in 0..TOP {
bg.add(0.9 - 0.01 * k as f32, k);
}
for k in 0..7 {
bg.add(0.1 + 0.01 * k as f32, TOP + k);
}
assert_eq!(bg.z(0.9), None);
bg.add(0.12, TOP + 7);
let z = bg.z(0.9).unwrap();
assert!(z > 20.0, "{z}");
let near: Vec<usize> = bg.near_best(0.05).map(|(item, _)| item).collect();
assert_eq!(near, vec![0, 1, 2, 3, 4, 5], "within 0.05 of 0.9");
}
#[test]
fn dot_matches_a_plain_sum() {
let a: Vec<f32> = (0..19).map(|i| i as f32 * 0.5).collect();
let b: Vec<f32> = (0..19).map(|i| 1.0 - i as f32 * 0.25).collect();
let plain: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum();
assert!((dot(&a, &b) - plain).abs() < 1e-3);
}
#[test]
fn unit_build_maps_bytes_to_tokens() {
let spans: Vec<(Location, Location)> = (0..10)
.map(|i| (loc(i + 1, i * 10), loc(i + 1, i * 10 + 5)))
.collect();
let u = SemanticUnit::build(
"rust",
"f".into(),
loc(3, 20),
loc(8, 75),
"fn f() {}".into(),
&spans,
)
.unwrap();
assert_eq!((u.range, u.token_count, u.line_span()), ([2, 7], 6, 5));
assert!(
SemanticUnit::build(
"rust",
"g".into(),
loc(3, 20),
loc(8, 75),
" ".into(),
&spans
)
.is_none()
);
}
}