1use crate::error::{CloneError, Result};
2use crate::model::{CloneKind, CloneLocation, ClonePair, CloneReport, Similarity};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct OracleLocation {
7 pub path: String,
8 pub start_line: u32,
9 pub end_line: u32,
10}
11
12impl OracleLocation {
13 #[must_use]
14 pub fn new(path: impl Into<String>, start_line: u32, end_line: u32) -> Self {
15 Self {
16 path: path.into().replace('\\', "/"),
17 start_line,
18 end_line,
19 }
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct OraclePair {
25 pub id: String,
26 pub left: OracleLocation,
27 pub right: OracleLocation,
28 pub expected: bool,
29 pub kind: Option<CloneKind>,
30}
31
32impl OraclePair {
33 #[must_use]
34 pub fn positive(
35 id: impl Into<String>,
36 kind: CloneKind,
37 left: OracleLocation,
38 right: OracleLocation,
39 ) -> Self {
40 Self {
41 id: id.into(),
42 left,
43 right,
44 expected: true,
45 kind: Some(kind),
46 }
47 }
48
49 #[must_use]
50 pub fn negative(id: impl Into<String>, left: OracleLocation, right: OracleLocation) -> Self {
51 Self {
52 id: id.into(),
53 left,
54 right,
55 expected: false,
56 kind: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62pub struct AccuracyCounts {
63 pub true_positives: usize,
64 pub false_positives: usize,
65 pub true_negatives: usize,
66 pub false_negatives: usize,
67}
68
69impl AccuracyCounts {
70 #[must_use]
71 pub fn precision(self) -> Similarity {
72 ratio(
73 self.true_positives,
74 self.true_positives.saturating_add(self.false_positives),
75 )
76 }
77
78 #[must_use]
79 pub fn recall(self) -> Similarity {
80 ratio(
81 self.true_positives,
82 self.true_positives.saturating_add(self.false_negatives),
83 )
84 }
85
86 #[must_use]
87 pub fn f1(self) -> Similarity {
88 let precision = usize::from(self.precision().permille());
89 let recall = usize::from(self.recall().permille());
90 if precision + recall == 0 {
91 return Similarity::from_permille(0);
92 }
93 Similarity::from_permille(
94 u16::try_from(2 * precision * recall / (precision + recall)).unwrap_or(1_000),
95 )
96 }
97
98 fn record(&mut self, expected: bool, detected: bool) {
99 match (expected, detected) {
100 (true, true) => self.true_positives += 1,
101 (false, true) => self.false_positives += 1,
102 (false, false) => self.true_negatives += 1,
103 (true, false) => self.false_negatives += 1,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
109pub struct AccuracyReport {
110 pub overall: AccuracyCounts,
111 pub type1: AccuracyCounts,
112 pub type2: AccuracyCounts,
113 pub type3: AccuracyCounts,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct AccuracyGate {
118 pub coverage: Similarity,
119 pub min_precision: Similarity,
120 pub min_recall: Similarity,
121}
122
123impl Default for AccuracyGate {
124 fn default() -> Self {
125 Self {
126 coverage: Similarity::from_permille(700),
127 min_precision: Similarity::PERFECT,
128 min_recall: Similarity::PERFECT,
129 }
130 }
131}
132
133impl AccuracyGate {
134 #[must_use]
135 pub fn evaluate(self, report: &CloneReport, oracle: &[OraclePair]) -> AccuracyReport {
136 let mut accuracy = AccuracyReport::default();
137 let mut by_paths = HashMap::<(&str, &str), Vec<&ClonePair>>::new();
138 for candidate in &report.pairs {
139 by_paths
140 .entry(path_key(&candidate.left.path, &candidate.right.path))
141 .or_default()
142 .push(candidate);
143 }
144 for expected in oracle {
145 let detected = by_paths
146 .get(&path_key(&expected.left.path, &expected.right.path))
147 .into_iter()
148 .flatten()
149 .any(|candidate| pair_matches(candidate, expected, self.coverage));
150 accuracy.overall.record(expected.expected, detected);
151 let kind_counts = match expected.kind {
152 Some(CloneKind::Type1) => Some(&mut accuracy.type1),
153 Some(CloneKind::Type2) => Some(&mut accuracy.type2),
154 Some(CloneKind::Type3) => Some(&mut accuracy.type3),
155 None => None,
156 };
157 if let Some(counts) = kind_counts {
158 counts.record(expected.expected, detected);
159 }
160 }
161 accuracy
162 }
163
164 pub fn check(self, report: &CloneReport, oracle: &[OraclePair]) -> Result<AccuracyReport> {
170 let accuracy = self.evaluate(report, oracle);
171 require(
172 "precision",
173 accuracy.overall.precision(),
174 self.min_precision,
175 )?;
176 require("recall", accuracy.overall.recall(), self.min_recall)?;
177 Ok(accuracy)
178 }
179}
180
181fn path_key<'a>(left: &'a str, right: &'a str) -> (&'a str, &'a str) {
182 if left <= right {
183 (left, right)
184 } else {
185 (right, left)
186 }
187}
188
189fn pair_matches(candidate: &ClonePair, expected: &OraclePair, threshold: Similarity) -> bool {
190 (covers(&candidate.left, &expected.left, threshold)
191 && covers(&candidate.right, &expected.right, threshold))
192 || (covers(&candidate.left, &expected.right, threshold)
193 && covers(&candidate.right, &expected.left, threshold))
194}
195
196fn covers(candidate: &CloneLocation, expected: &OracleLocation, threshold: Similarity) -> bool {
197 if candidate.path.replace('\\', "/") != expected.path || expected.start_line > expected.end_line
198 {
199 return false;
200 }
201 let start = candidate.span.start_line.max(expected.start_line);
202 let end = candidate.span.end_line.min(expected.end_line);
203 let intersection = end
204 .saturating_sub(start)
205 .saturating_add(u32::from(end >= start));
206 let expected_lines = expected
207 .end_line
208 .saturating_sub(expected.start_line)
209 .saturating_add(1);
210 u64::from(intersection) * 1_000 >= u64::from(expected_lines) * u64::from(threshold.permille())
211}
212
213fn ratio(numerator: usize, denominator: usize) -> Similarity {
214 if denominator == 0 {
215 Similarity::PERFECT
216 } else {
217 Similarity::from_ratio(numerator, denominator)
218 }
219}
220
221fn require(metric: &'static str, actual: Similarity, required: Similarity) -> Result<()> {
222 if actual < required {
223 return Err(CloneError::AccuracyGate {
224 metric,
225 actual: actual.permille(),
226 required: required.permille(),
227 });
228 }
229 Ok(())
230}