layer_conform_core/
deviation.rs1use compact_str::CompactString;
4
5use crate::rule::GoldenSelector;
6use crate::similarity::SimilarityScore;
7
8#[derive(Clone, Debug, Default, PartialEq)]
9pub struct Differences {
10 pub missing_calls: Vec<CompactString>,
11 pub extra_calls: Vec<CompactString>,
12 pub missing_imports: Vec<CompactString>,
13 pub extra_imports: Vec<CompactString>,
14}
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct GoldenMatch {
19 pub golden: GoldenSelector,
20 pub similarity: SimilarityScore,
21}
22
23#[derive(Clone, Debug, PartialEq)]
28pub struct Deviation {
29 pub rule_id: String,
30 pub file: String,
31 pub symbol: CompactString,
32 pub matched_golden: GoldenSelector,
33 pub all_golden_scores: Vec<GoldenMatch>,
34 pub similarity: SimilarityScore,
35 pub differences: Differences,
36}
37
38pub fn pick_best(scores: Vec<GoldenMatch>) -> (GoldenMatch, Vec<GoldenMatch>) {
47 assert!(!scores.is_empty(), "pick_best requires at least one golden");
48 let mut sorted = scores;
49 sorted.sort_by(|a, b| {
50 b.similarity
51 .overall
52 .partial_cmp(&a.similarity.overall)
53 .unwrap_or(std::cmp::Ordering::Equal)
54 });
55 let best = sorted[0].clone();
56 (best, sorted)
57}
58
59pub fn diff_sets(golden: &[CompactString], actual: &[CompactString]) -> (Vec<CompactString>, Vec<CompactString>) {
61 let (mut i, mut j) = (0_usize, 0_usize);
63 let (mut missing, mut extra) = (Vec::new(), Vec::new());
64 while i < golden.len() && j < actual.len() {
65 match golden[i].cmp(&actual[j]) {
66 std::cmp::Ordering::Equal => {
67 i += 1;
68 j += 1;
69 }
70 std::cmp::Ordering::Less => {
71 missing.push(golden[i].clone());
72 i += 1;
73 }
74 std::cmp::Ordering::Greater => {
75 extra.push(actual[j].clone());
76 j += 1;
77 }
78 }
79 }
80 while i < golden.len() {
81 missing.push(golden[i].clone());
82 i += 1;
83 }
84 while j < actual.len() {
85 extra.push(actual[j].clone());
86 j += 1;
87 }
88 (missing, extra)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn cs(items: &[&str]) -> Vec<CompactString> {
96 let mut v: Vec<CompactString> = items.iter().map(|s| (*s).into()).collect();
97 v.sort();
98 v
99 }
100
101 #[test]
102 fn identical_sets_have_no_diff() {
103 let g = cs(&["useSWR"]);
104 let a = cs(&["useSWR"]);
105 let (m, e) = diff_sets(&g, &a);
106 assert!(m.is_empty());
107 assert!(e.is_empty());
108 }
109
110 #[test]
111 fn missing_call_detected() {
112 let g = cs(&["useSWR", "axios"]);
113 let a = cs(&["useSWR"]);
114 let (m, e) = diff_sets(&g, &a);
115 assert_eq!(m, cs(&["axios"]));
116 assert!(e.is_empty());
117 }
118
119 #[test]
120 fn extra_call_detected() {
121 let g = cs(&["useSWR"]);
122 let a = cs(&["useSWR", "fetch"]);
123 let (m, e) = diff_sets(&g, &a);
124 assert!(m.is_empty());
125 assert_eq!(e, cs(&["fetch"]));
126 }
127
128 #[test]
129 fn both_missing_and_extra() {
130 let g = cs(&["useSWR"]);
131 let a = cs(&["fetch"]);
132 let (m, e) = diff_sets(&g, &a);
133 assert_eq!(m, cs(&["useSWR"]));
134 assert_eq!(e, cs(&["fetch"]));
135 }
136
137 fn golden_match(file: &str, sym: &str, overall: f64) -> GoldenMatch {
138 GoldenMatch {
139 golden: GoldenSelector { file: file.into(), symbol: sym.into() },
140 similarity: SimilarityScore {
141 overall,
142 shape: overall,
143 calls: overall,
144 imports: overall,
145 signature: overall,
146 },
147 }
148 }
149
150 #[test]
151 fn pick_best_single_golden_returns_it() {
152 let m = golden_match("a.ts", "a", 0.42);
153 let (best, sorted) = pick_best(vec![m.clone()]);
154 assert_eq!(best, m);
155 assert_eq!(sorted, vec![m]);
156 }
157
158 #[test]
159 fn pick_best_picks_higher_overall() {
160 let lo = golden_match("a.ts", "a", 0.3);
161 let hi = golden_match("b.ts", "b", 0.8);
162 let (best, sorted) = pick_best(vec![lo.clone(), hi.clone()]);
163 assert_eq!(best, hi);
164 assert_eq!(sorted, vec![hi, lo]);
165 }
166
167 #[test]
168 fn pick_best_sorts_three_descending() {
169 let a = golden_match("a.ts", "a", 0.3);
170 let b = golden_match("b.ts", "b", 0.8);
171 let c = golden_match("c.ts", "c", 0.5);
172 let (_, sorted) = pick_best(vec![a, b.clone(), c.clone()]);
173 assert_eq!(sorted[0].golden.file, "b.ts");
174 assert_eq!(sorted[1].golden.file, "c.ts");
175 assert_eq!(sorted[2].golden.file, "a.ts");
176 }
177
178 #[test]
179 #[should_panic(expected = "at least one golden")]
180 fn pick_best_panics_on_empty_input() {
181 let _ = pick_best(vec![]);
182 }
183}