Skip to main content

weavatrix_clone/
detector.rs

1use crate::canonical::suppress_contained;
2use crate::cluster::{families_for_pairs, pair_id};
3use crate::config::CloneConfig;
4use crate::error::{CloneError, Result};
5use crate::fingerprint::winnow;
6use crate::index::candidates;
7use crate::model::{
8    CloneLocation, ClonePair, CloneReport, CloneStatistics, DetectionMode, SourceFragment,
9};
10use crate::token::{Interner, Tokenized, tokenize};
11use crate::verify::{Verifier, evidence};
12use std::collections::HashSet;
13
14#[derive(Debug, Clone, Copy, Default)]
15pub struct CloneDetector {
16    config: CloneConfig,
17}
18
19impl CloneDetector {
20    /// Creates a detector after validating all safety bounds.
21    ///
22    /// # Errors
23    ///
24    /// Returns an error for inconsistent thresholds or zero limits.
25    pub fn new(config: CloneConfig) -> Result<Self> {
26        Ok(Self {
27            config: config.validate()?,
28        })
29    }
30
31    #[must_use]
32    pub const fn config(&self) -> CloneConfig {
33        self.config
34    }
35
36    /// Detects deterministic Type-1, Type-2, and bounded near-miss Type-3
37    /// clones over caller-provided fragments.
38    ///
39    /// # Errors
40    ///
41    /// Rejects malformed or duplicate fragments and configured capacity
42    /// limits without returning partial output.
43    pub fn detect(&self, fragments: &[SourceFragment]) -> Result<CloneReport> {
44        if fragments.len() > self.config.max_fragments {
45            return Err(CloneError::CapacityExceeded {
46                resource: "input fragments",
47                limit: self.config.max_fragments,
48            });
49        }
50        validate_fragments(fragments)?;
51        let (prepared, mut statistics) = prepare_fragments(self.config, fragments)?;
52        let fingerprint_sets = prepared
53            .iter()
54            .map(|item| item.fingerprints.clone())
55            .collect::<Vec<_>>();
56        let index = candidates(&fingerprint_sets, self.config)?;
57        statistics.candidate_pairs = index.candidates.len();
58        statistics.suppressed_buckets = index.suppressed_buckets;
59        let locations = prepared
60            .iter()
61            .map(|item| CloneLocation::from_fragment(&fragments[item.source_index]))
62            .collect::<Vec<_>>();
63        let pairs = verify_pairs(
64            self.config,
65            fragments,
66            &prepared,
67            &locations,
68            index.candidates,
69        );
70        let pairs = suppress_contained(pairs);
71        statistics.verified_pairs = pairs.len();
72        Ok(CloneReport {
73            families: families_for_pairs(&pairs),
74            pairs,
75            statistics,
76        })
77    }
78}
79
80fn prepare_fragments(
81    config: CloneConfig,
82    fragments: &[SourceFragment],
83) -> Result<(Vec<Prepared>, CloneStatistics)> {
84    let mut order = (0..fragments.len()).collect::<Vec<_>>();
85    order.sort_unstable_by(|left, right| {
86        let left = &fragments[*left];
87        let right = &fragments[*right];
88        (&left.path, left.span, &left.id).cmp(&(&right.path, right.span, &right.id))
89    });
90    let mut interner = Interner::default();
91    let mut prepared = Vec::with_capacity(fragments.len());
92    let mut statistics = CloneStatistics {
93        input_fragments: fragments.len(),
94        ..CloneStatistics::default()
95    };
96    for index in order {
97        let fragment = &fragments[index];
98        let tokens = tokenize(&fragment.text, fragment.language, config, &mut interner)?;
99        if tokens.strict.len() < config.min_tokens {
100            statistics.skipped_small_fragments += 1;
101            continue;
102        }
103        statistics.tokens = statistics.tokens.saturating_add(tokens.strict.len());
104        let fingerprint_tokens = if config.mode == DetectionMode::Exact {
105            &tokens.strict
106        } else {
107            &tokens.renamed
108        };
109        let fingerprints = winnow(fingerprint_tokens, config.k_gram, config.winnowing_window);
110        statistics.fingerprints = statistics.fingerprints.saturating_add(fingerprints.len());
111        prepared.push(Prepared {
112            source_index: index,
113            tokens,
114            fingerprints,
115        });
116    }
117    statistics.analyzed_fragments = prepared.len();
118    Ok((prepared, statistics))
119}
120
121fn verify_pairs(
122    config: CloneConfig,
123    fragments: &[SourceFragment],
124    prepared: &[Prepared],
125    locations: &[CloneLocation],
126    candidates: Vec<crate::index::Candidate>,
127) -> Vec<ClonePair> {
128    let mut pairs = Vec::new();
129    let mut verifier = Verifier::default();
130    for candidate in candidates {
131        let left = &prepared[candidate.left];
132        let right = &prepared[candidate.right];
133        let left_fragment = &fragments[left.source_index];
134        let right_fragment = &fragments[right.source_index];
135        if !config.compare_overlapping_fragments
136            && left_fragment.path == right_fragment.path
137            && left_fragment.span.overlaps(right_fragment.span)
138        {
139            continue;
140        }
141        let Some(match_result) = verifier.verify(
142            &left.tokens.strict,
143            &right.tokens.strict,
144            &left.tokens.renamed,
145            &right.tokens.renamed,
146            config,
147        ) else {
148            continue;
149        };
150        let left_location = &locations[candidate.left];
151        let right_location = &locations[candidate.right];
152        let id = pair_id(left_location, right_location);
153        pairs.push(ClonePair {
154            id,
155            left: left_location.clone(),
156            right: right_location.clone(),
157            kind: match_result.kind,
158            similarity: match_result.similarity,
159            evidence: evidence(
160                &match_result,
161                candidate.shared,
162                candidate.jaccard,
163                candidate.containment,
164                left.tokens.renamed.len().max(right.tokens.renamed.len()),
165            ),
166        });
167    }
168    pairs
169}
170
171struct Prepared {
172    source_index: usize,
173    tokens: Tokenized,
174    fingerprints: Vec<u64>,
175}
176
177fn validate_fragments(fragments: &[SourceFragment]) -> Result<()> {
178    let mut ids = HashSet::<&str>::with_capacity(fragments.len());
179    for fragment in fragments {
180        fragment.validate()?;
181        if !ids.insert(&fragment.id) {
182            return Err(CloneError::DuplicateFragment(fragment.id.clone()));
183        }
184    }
185    Ok(())
186}