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