Skip to main content

cpd_semantic/
search.rs

1//! Semantic clones (Type-4, `--semantic`, experimental).
2//!
3//! Two functions are a semantic clone when they do the same thing but are
4//! written differently — renamed, restructured, or in another language, like
5//! a validation rule implemented once in a Rust backend and again in a Svelte
6//! frontend. Token-based detection cannot see that; embeddings can: an
7//! [`Embedder`] turns the code of every function into a vector, and functions
8//! whose vectors point the same way are candidates.
9//!
10//! A pair of functions `a` and `b` is reported when
11//!
12//! 1. they live in different files, neither calls the other by name (a
13//!    call counts within languages that call each other only: `JSON.parse(`
14//!    in TypeScript does not call a Python `parse`), the clones already
15//!    found do not cover both (90% of the lines of each),
16//!    and the path filters (`--skip-local`, `--skip-isolated`) allow the
17//!    pair. A pair ruled out here is left out of each function's matches
18//!    altogether, so a copy that token detection already reported does not
19//!    stand in the way of a function's real semantic match;
20//! 2. `b` is the closest match of `a` among the functions of `b`'s grammar,
21//!    or within [`Thresholds::near_best`] of it, and the same holds for `a`
22//!    among the functions of `a`'s grammar (a mutual near-best match): three
23//!    implementations of one feature make three pairs. A pair that is not
24//!    each other's very best must also reach [`Thresholds::group_floor`], so
25//!    a function's weaker neighbours stay out;
26//! 3. the cosine similarity of their vectors reaches
27//!    [`Thresholds::across`] for a pair across languages, or the higher
28//!    [`Thresholds::within`] for two functions of one language; and
29//! 4. the similarity stands out: it is at least [`MIN_Z`] standard
30//!    deviations above the mean similarity of `a` to the functions of `b`'s
31//!    grammar, and of `b` to the functions of `a`'s grammar, each background
32//!    leaving out the function's closest matches (see `TRIM`).
33//!
34//! Rule 2 keeps a function that resembles many others (a request handler, a
35//! getter) from pairing with each of them. Rules 3 and 4 deal with language:
36//! two functions in different languages score lower than two in one language
37//! whatever they do, so a cosine cut-off low enough for a Rust/TypeScript
38//! pair lets through unrelated pairs within one language. The distance from
39//! each function's own background does not depend on the language pair, and
40//! the margin of rule 3 drops the weaker same-language pairs that pass rule
41//! 4, which mostly share only their shape: constructors, handlers,
42//! implementations of one interface. Rule 1 drops pairs that are related
43//! rather than duplicated: a function and a helper it calls, or two
44//! functions of one file, which share names and context.
45
46use 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
52/// How many standard deviations above a function's mean similarity to a
53/// grammar a pair must score (rule 4 of the module docs).
54pub const MIN_Z: f32 = 3.0;
55/// Best matches left out of a function's background when its z-score is
56/// computed (rule 4 of the module docs).
57const TRIM: usize = TOP;
58/// Matches remembered per function and grammar; a feature implemented more
59/// often than this in one language reports its closest copies only.
60const TOP: usize = 8;
61/// A background of fewer functions than this (outside the function's own
62/// file) is too thin for a z-score; rule 4 is then not applied for it.
63pub const MIN_BACKGROUND: usize = 8;
64/// A callee name shorter than this is too common to tell a call from a
65/// namesake (`new`, `get`, `run`), so it does not exclude a pair.
66const MIN_CALLEE_NAME: usize = 5;
67/// Rows of the similarity matrix computed together, so each column vector
68/// is read from memory once per block instead of once per row.
69const ROW_BLOCK: usize = 32;
70
71/// Turns function source code into vectors.
72pub trait Embedder: Send + Sync {
73    /// One vector per text, in input order. Every vector must have the same
74    /// length; it need not be normalized.
75    fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, String>;
76}
77
78/// One function of a source, ready to embed.
79#[derive(Debug, Clone, PartialEq)]
80pub struct SemanticUnit {
81    /// Grammar of the extractor that found the function (`oxc`, `rust`, ...).
82    /// Mutual best matches are taken per grammar, and z-scores against the
83    /// functions of one grammar.
84    pub grammar: &'static str,
85    /// Declared or inferred name (`<arrow>` / `<anonymous>` when none).
86    pub name: String,
87    pub start: Location,
88    pub end: Location,
89    /// Inclusive detection-token index range inside the owning source.
90    pub range: [u32; 2],
91    /// Detection tokens covered by the function.
92    pub token_count: u32,
93    /// The text given to the embedder: the function's code without comments.
94    pub text: String,
95    /// A test rather than code: `--compare` measures the two apart and
96    /// pairs a test only with a test. `--semantic` does not look at it.
97    pub test: bool,
98}
99
100impl SemanticUnit {
101    /// Attach the token range from the owning source's token spans. Returns
102    /// `None` when no detection token lies inside the function (a body of
103    /// comments or type declarations only).
104    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    /// Lines spanned, in jscpd's `end - start` convention.
129    pub fn line_span(&self) -> u32 {
130        self.end.line.saturating_sub(self.start.line)
131    }
132}
133
134/// The functions of one source, as the semantic search needs them.
135#[derive(Debug, Clone)]
136pub struct UnitSource {
137    /// Source id; an embedded block keeps its `path:format` form.
138    pub id: String,
139    pub format: String,
140    pub units: Vec<SemanticUnit>,
141    /// The source's place for the path filters; pairs whose labels skip
142    /// each other are never compared. Default: filters off.
143    pub path_label: PathLabel,
144}
145
146/// The similarities the rules compare against. Models score similarity on
147/// scales of their own, so each model has its own set: see
148/// `embed::catalog`.
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct Thresholds {
151    /// Lowest cosine similarity of a pair across languages (rule 3 of the
152    /// module docs).
153    pub across: f32,
154    /// Lowest cosine similarity of a pair within one language (rule 3).
155    /// Code of one language resembles itself whatever it does, so this one
156    /// is higher: below it, most same-language pairs are related code, not
157    /// duplicates.
158    pub within: f32,
159    /// How far below a function's best match another match may score and
160    /// still count as a best match (rule 2).
161    pub near_best: f32,
162    /// The similarity a pair needs when its two functions are near-best but
163    /// not best matches of each other (rule 2), so a third copy of a feature
164    /// is reported while the weaker neighbours of a function stay out. It
165    /// does not move with `across` and `within`: a floor derived from
166    /// `within` would drop real duplicates just above it.
167    pub group_floor: f32,
168}
169
170impl Thresholds {
171    /// The set tuned with jina-embeddings-v2-base-code on a demo, open-source
172    /// projects, Rosetta Code and CodeNet; the other models' sets are
173    /// measured against it.
174    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    /// The threshold of a pair of functions of one language or of two.
182    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/// Settings of the semantic pass.
191#[derive(Debug, Clone, Copy)]
192pub struct SemanticParams {
193    pub thresholds: Thresholds,
194    /// Functions with fewer detection tokens are not embedded.
195    pub min_tokens: usize,
196    /// Functions spanning fewer lines are not embedded.
197    pub min_lines: usize,
198    /// Which pairs to look for: within one language, across languages, or
199    /// both.
200    pub scope: SemanticScope,
201}
202
203/// Which pairs `--semantic` reports. A language is a grammar: a Svelte
204/// component's script and a `.ts` file are one language.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
206pub enum SemanticScope {
207    /// Every pair.
208    #[default]
209    All,
210    /// Pairs within one language: similar implementations of one feature.
211    Same,
212    /// Pairs across languages: a rule written once per side.
213    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
252/// Find semantic clones among the functions of `sources`. Pairs already
253/// covered by a clone in `existing` (an exact, renamed or similar match
254/// spanning both functions) are left out before the best matches are taken,
255/// so nothing is reported twice and a copy never hides a real match.
256///
257/// Fails only when the embedder does.
258pub 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, &params.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
315/// The pairs that rules 2 to 4 of the module docs accept among the scanned
316/// `rows`, as `(i, j, similarity)` with `i < j`; `scope` says which
317/// grammars' matches are looked at.
318pub(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                // Each mutual pair is seen from both ends; keep one.
335                let other = &rows[j][own];
336                if j <= i || !other.is_near_best(i, bars.near_best) {
337                    continue;
338                }
339                // Two functions that are each other's best match need the
340                // threshold; a further member of a group needs more.
341                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
360/// Drop a pair whose functions both sit inside the functions of another
361/// reported pair of the same two sources — helpers nested in two copies of
362/// a component pair up too, and the outer pair already says it all.
363fn 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
386/// One eligible function: where it lives and which file it belongs to.
387pub(crate) struct Item {
388    pub(crate) source: usize,
389    pub(crate) unit: usize,
390    /// Index of the host file: an embedded block counts as its host file.
391    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
439/// For every item, the sorted items it calls or is called by (rule 1). A
440/// call is the callee's name followed by `(`; a function's own name is never
441/// a call, so the header `fn name(` and recursion do not count, and two
442/// namesakes (a port keeps the name) are not mistaken for caller and callee.
443/// Only a callee the caller's language can call counts (see
444/// [`call_family`]): a method of a library (`JSON.parse`, `schema.validate`)
445/// must not rule out the other side of a port that happens to share its
446/// name.
447pub(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        // A test's name is a title (`it('add', …)`), never called.
455        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            // A function's own name in its header or a recursive call is
468            // not a call; a test titled after the function it calls is.
469            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
487/// Grammars whose code calls into each other: C and C++, and the languages
488/// of the JVM. Every other grammar calls only into itself (the JavaScript
489/// and TypeScript of components and modules are one grammar already).
490pub(crate) fn call_family(grammar: &str) -> &str {
491    match grammar {
492        "cpp" => "c",
493        "kotlin" | "scala" => "java",
494        other => other,
495    }
496}
497
498/// Identifiers directly followed by `(` (spaces allowed in between).
499pub(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
528/// Unit-normalized vectors stored row-major, one row per item.
529pub(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            // A zero or non-finite vector matches nothing: leave it zero.
562            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    /// Every item's background per grammar: similarity statistics over the
580    /// items of other files that it neither calls nor is called by and that
581    /// `may_pair` lets it pair with (the path filters, the two sides of a
582    /// comparison).
583    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    // Eight independent lanes let the compiler vectorize the loop.
619    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/// Similarity statistics of one item against the items of one grammar,
636/// with its [`TOP`] closest matches, best first.
637#[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        // Items arrive in ascending order, and a tie ranks after the
660        // earlier item: results do not depend on how rows were computed.
661        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    /// The matches within `margin` of the best one, with their scores.
673    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    /// Standard score of `sim` against this background, leaving out the
690    /// [`TRIM`] best matches: they are the candidates being judged, and a
691    /// feature implemented three times must not hide each copy behind the
692    /// others. `None` when what remains is too small or flat to judge.
693    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
713/// True when the clones already found between the two sources cover at
714/// least 90% of the lines of both functions, together: a function copied
715/// with one edited line is two exact clones with a gap, and that pair is
716/// already reported.
717/// For every item, the sorted items whose pair the clones in `existing`
718/// already cover (rule 1). Only functions that some clone between their two
719/// sources meets are tried, so the cost follows the clones, not the items.
720fn 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    // The clones between two different sources, keyed lower index first.
735    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    // The items of `source` that one of `clones` meets on that source's side.
747    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
780/// Whether the fragment and the function share a line.
781fn meets(frag: &Fragment, f: &SemanticUnit) -> bool {
782    frag.start.line <= f.end.line && f.start.line <= frag.end.line
783}
784
785/// Whether `clones` cover 90% of the lines of both `a` (in source `id_a`)
786/// and `b` (in `id_b`), counting only clones that meet both functions.
787fn 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    // Mark the lines of `f` inside `frag`; false when they do not meet.
797    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        // A clone of `a` with some other function of `b`'s file says
813        // nothing about this pair.
814        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    // Deterministic fragment order: by source id, then position.
835    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    /// A function on lines `line..line+9` whose text is `text`.
864    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    /// [`PARAMS`] with other thresholds.
887    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    /// Embeds a text as the vector registered for its first word.
902    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    /// The two files of every clone found.
920    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    /// A unit vector along `axis` blended with `noise` of axis `noise_axis`.
933    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    /// Unrelated functions in files of their own, as many as a small
941    /// project has, so every background is big enough for a z-score.
942    const FILLERS: usize = 30;
943
944    /// `sources` with a Rust and a TypeScript background around them, so
945    /// every function has enough others to be judged against.
946    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    /// A Rust function `a` and two TypeScript functions, `b1` and `b2`, that
966    /// may resemble it, among the backgrounds.
967    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    /// An embedder knowing `named` and the filler vectors, which lie mostly
976    /// on axes of their own.
977    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        // cos = 1 / (1 + 0.75^2) = 0.64
1040        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        // cos = 1 / (1 + 0.75^2) = 0.64: enough across languages at 0.6, not
1066        // within one language, which needs 0.75.
1067        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        // The bar within one language leaves pairs across languages alone.
1076        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        // `b1` and `b2` both resemble `a`; `b1` more. `b2` finds `a` as its
1089        // best match, but `a` does not return the favour.
1090        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            // The host file's markup is another source of the same file.
1115            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            // Its real counterpart, a little less similar than the callee.
1150            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        // `a` resembles its neighbour `n` most, but `--skip-local` forbids
1174        // that pair: `a` must still pair with `b` from the other scan root.
1175        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        // `b1` implements `a` at 0.80, `b2` at 0.77: a near-best match that
1262        // is not the best one, so it needs the group floor, 0.8, even when
1263        // the threshold is as low as 0.5.
1264        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        // A model whose scores run lower has a lower floor, and a wider
1277        // margin for near-best matches: then `b2` belongs to the group.
1278        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, &params, &[]).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        // `it('compute', () => { compute(1) })`: the title is no function
1330        // name, so the call to `compute(` relates the test to `compute`,
1331        // and the title is nothing another function can call.
1332        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        // Two clones with a gap cover the pair together; one alone does not.
1474        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        // `copy` is `a` word for word, and the token passes reported it; `c`
1500        // does what `a` does in its own way. With the copy in the running,
1501        // `a`'s best match would be the copy, the covered pair would then be
1502        // dropped, and `c` would be lost with it.
1503        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        // `c` scores 0.78 against both: above the same-language threshold,
1510        // below the group floor, so it pairs with its best match only.
1511        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        // One eligible function cannot pair with anything: no request at all.
1541        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        // Eight close matches, the part of the background that is trimmed.
1571        for k in 0..TOP {
1572            bg.add(0.9 - 0.01 * k as f32, k);
1573        }
1574        // Seven unrelated functions: too few to judge by.
1575        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        // Against the unrelated functions only, 0.9 stands out by far; the
1582        // close matches alone would have hidden it.
1583        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}