Skip to main content

weavatrix_clone/
detector.rs

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