Skip to main content

weavatrix_clone/model/
mod.rs

1use crate::error::{CloneError, Result};
2use std::{cmp::Ordering, path::Path};
3
4#[cfg(test)]
5mod tests;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
8pub enum Language {
9    Rust,
10    Go,
11    C,
12    Cpp,
13    Bash,
14    Sql,
15    JavaScript,
16    TypeScript,
17    Python,
18    Java,
19    CSharp,
20    Markup,
21    Text,
22}
23
24impl Language {
25    #[must_use]
26    pub fn from_path(path: impl AsRef<Path>) -> Self {
27        let path = path.as_ref();
28        let extension = path
29            .extension()
30            .and_then(|value| value.to_str())
31            .unwrap_or_default()
32            .to_ascii_lowercase();
33        match extension.as_str() {
34            "rs" => Self::Rust,
35            "go" => Self::Go,
36            "c" | "h" => Self::C,
37            "cc" | "cpp" | "cxx" | "hh" | "hpp" | "hxx" => Self::Cpp,
38            "sh" | "bash" | "zsh" => Self::Bash,
39            "sql" | "psql" => Self::Sql,
40            "js" | "jsx" | "mjs" | "cjs" => Self::JavaScript,
41            "ts" | "tsx" | "mts" | "cts" => Self::TypeScript,
42            "py" | "pyi" => Self::Python,
43            "java" => Self::Java,
44            "cs" => Self::CSharp,
45            "html" | "htm" | "xml" | "vue" | "svelte" | "md" | "mdx" => Self::Markup,
46            _ => Self::Text,
47        }
48    }
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct SourceSpan {
53    pub start_byte: usize,
54    pub end_byte: usize,
55    pub start_line: u32,
56    pub end_line: u32,
57}
58
59impl SourceSpan {
60    #[must_use]
61    pub fn whole(text: &str) -> Self {
62        Self {
63            start_byte: 0,
64            end_byte: text.len(),
65            start_line: 1,
66            end_line: u32::try_from(text.lines().count().max(1)).unwrap_or(u32::MAX),
67        }
68    }
69
70    #[must_use]
71    pub const fn overlaps(self, other: Self) -> bool {
72        self.start_byte < other.end_byte && other.start_byte < self.end_byte
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct SourceFragment {
78    pub id: String,
79    pub path: String,
80    pub language: Language,
81    pub span: SourceSpan,
82    pub text: String,
83}
84
85impl SourceFragment {
86    /// Creates one independently comparable source fragment.
87    ///
88    /// # Errors
89    ///
90    /// Rejects empty identifiers/paths and invalid source spans.
91    pub fn new(
92        id: impl Into<String>,
93        path: impl Into<String>,
94        language: Language,
95        span: SourceSpan,
96        text: impl Into<String>,
97    ) -> Result<Self> {
98        let fragment = Self {
99            id: id.into(),
100            path: path.into().replace('\\', "/"),
101            language,
102            span,
103            text: text.into(),
104        };
105        fragment.validate()?;
106        Ok(fragment)
107    }
108
109    pub(crate) fn validate(&self) -> Result<()> {
110        let reason = if self.id.trim().is_empty() {
111            Some("id must not be empty")
112        } else if self.path.trim().is_empty() {
113            Some("path must not be empty")
114        } else if self.span.start_byte > self.span.end_byte {
115            Some("start_byte must not exceed end_byte")
116        } else if self.span.start_line == 0 || self.span.start_line > self.span.end_line {
117            Some("line range must be one-based and ordered")
118        } else {
119            None
120        };
121        if let Some(reason) = reason {
122            return Err(CloneError::InvalidFragment {
123                id: self.id.clone(),
124                reason,
125            });
126        }
127        Ok(())
128    }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
132pub struct Similarity(u16);
133
134impl Similarity {
135    pub const PERFECT: Self = Self(1_000);
136
137    #[must_use]
138    pub const fn from_permille(value: u16) -> Self {
139        Self(if value > 1_000 { 1_000 } else { value })
140    }
141
142    #[must_use]
143    pub fn from_ratio(numerator: usize, denominator: usize) -> Self {
144        if denominator == 0 {
145            return Self::from_permille(0);
146        }
147        let scaled = numerator.saturating_mul(1_000) / denominator;
148        Self::from_permille(u16::try_from(scaled).unwrap_or(1_000))
149    }
150
151    #[must_use]
152    pub const fn permille(self) -> u16 {
153        self.0
154    }
155
156    #[must_use]
157    pub fn percent(self) -> f32 {
158        f32::from(self.0) / 10.0
159    }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
163pub enum CloneKind {
164    Type1,
165    Type2,
166    Type3,
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
170pub enum DetectionMode {
171    Exact,
172    Renamed,
173    #[default]
174    NearMiss,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Hash)]
178pub struct CloneLocation {
179    pub fragment_id: String,
180    pub path: String,
181    pub span: SourceSpan,
182}
183
184impl CloneLocation {
185    pub(crate) fn from_fragment(fragment: &SourceFragment) -> Self {
186        Self {
187            fragment_id: fragment.id.clone(),
188            path: fragment.path.clone(),
189            span: fragment.span,
190        }
191    }
192}
193
194impl Ord for CloneLocation {
195    fn cmp(&self, other: &Self) -> Ordering {
196        (&self.path, self.span, &self.fragment_id).cmp(&(
197            &other.path,
198            other.span,
199            &other.fragment_id,
200        ))
201    }
202}
203
204impl PartialOrd for CloneLocation {
205    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
206        Some(self.cmp(other))
207    }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct CloneEvidence {
212    pub strict_equal: bool,
213    pub renamed_equal: bool,
214    pub shared_fingerprints: usize,
215    pub fingerprint_jaccard: Similarity,
216    pub fingerprint_containment: Similarity,
217    pub edit_distance: usize,
218    pub compared_tokens: usize,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct ClonePair {
223    pub id: String,
224    pub left: CloneLocation,
225    pub right: CloneLocation,
226    pub kind: CloneKind,
227    pub similarity: Similarity,
228    pub evidence: CloneEvidence,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct CloneFamily {
233    pub id: String,
234    pub members: Vec<CloneLocation>,
235    pub pair_ids: Vec<String>,
236}
237
238#[derive(Debug, Clone, Default, PartialEq, Eq)]
239pub struct CloneStatistics {
240    pub source_files: usize,
241    pub source_tokens: usize,
242    pub input_fragments: usize,
243    pub analyzed_fragments: usize,
244    pub skipped_small_fragments: usize,
245    pub tokens: usize,
246    pub fingerprints: usize,
247    pub candidate_pairs: usize,
248    pub exact_block_candidates: usize,
249    pub verified_pairs: usize,
250    pub suppressed_buckets: usize,
251    pub suppressed_exact_buckets: usize,
252}
253
254#[derive(Debug, Clone, Default, PartialEq, Eq)]
255pub struct CloneReport {
256    pub pairs: Vec<ClonePair>,
257    pub families: Vec<CloneFamily>,
258    pub statistics: CloneStatistics,
259}