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 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}