1use crate::{
7 PublicApi,
8 public_item::{PublicItem, PublicItemPath},
9};
10use hashbag::HashBag;
11use std::collections::HashMap;
12
13type ItemsWithPath = HashMap<PublicItemPath, Vec<PublicItem>>;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct ChangedPublicItem {
19 pub old: PublicItem,
21
22 pub new: PublicItem,
24}
25
26impl ChangedPublicItem {
27 #[must_use]
29 pub fn grouping_cmp(&self, other: &Self) -> std::cmp::Ordering {
30 match PublicItem::grouping_cmp(&self.old, &other.old) {
31 std::cmp::Ordering::Equal => PublicItem::grouping_cmp(&self.new, &other.new),
32 ordering => ordering,
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct PublicApiDiff {
44 pub removed: Vec<PublicItem>,
47
48 pub changed: Vec<ChangedPublicItem>,
53
54 pub added: Vec<PublicItem>,
57}
58
59impl PublicApiDiff {
60 #[must_use]
65 pub fn between(old: PublicApi, new: PublicApi) -> Self {
66 let old = old.into_items().collect::<HashBag<_>>();
70 let new = new.into_items().collect::<HashBag<_>>();
71
72 let all_removed = old.difference(&new);
77 let all_added = new.difference(&old);
78
79 let mut removed_paths: ItemsWithPath = bag_to_path_map(all_removed);
81 let mut added_paths: ItemsWithPath = bag_to_path_map(all_added);
82
83 let mut removed: Vec<PublicItem> = vec![];
85 let mut changed: Vec<ChangedPublicItem> = vec![];
86 let mut added: Vec<PublicItem> = vec![];
87
88 let mut touched_paths: Vec<PublicItemPath> = vec![];
92 touched_paths.extend::<Vec<_>>(removed_paths.keys().cloned().collect());
93 touched_paths.extend::<Vec<_>>(added_paths.keys().cloned().collect());
94
95 for path in touched_paths {
99 let mut removed_items = removed_paths.remove(&path).unwrap_or_default();
100 let mut added_items = added_paths.remove(&path).unwrap_or_default();
101 loop {
102 match (removed_items.pop(), added_items.pop()) {
103 (Some(old), Some(new)) => changed.push(ChangedPublicItem { old, new }),
104 (Some(old), None) => removed.push(old),
105 (None, Some(new)) => added.push(new),
106 (None, None) => break,
107 }
108 }
109 }
110
111 removed.sort_by(PublicItem::grouping_cmp);
113 changed.sort_by(ChangedPublicItem::grouping_cmp);
114 added.sort_by(PublicItem::grouping_cmp);
115
116 Self {
117 removed,
118 changed,
119 added,
120 }
121 }
122
123 #[must_use]
125 pub fn is_empty(&self) -> bool {
126 self.removed.is_empty() && self.changed.is_empty() && self.added.is_empty()
127 }
128}
129
130fn bag_to_path_map<'a>(difference: impl Iterator<Item = (&'a PublicItem, usize)>) -> ItemsWithPath {
133 let mut map: ItemsWithPath = HashMap::new();
134 for (item, occurrences) in difference {
135 let items = map.entry(item.sortable_path.clone()).or_default();
136 for _ in 0..occurrences {
137 items.push(item.clone());
138 }
139 }
140 map
141}
142
143#[cfg(test)]
144mod tests {
145 use rustdoc_types::Id;
146
147 use crate::tokens::Token;
148
149 use super::*;
150
151 const DUMMY_ID: Id = Id(1234);
152
153 #[test]
154 fn single_and_only_item_removed() {
155 let old = api([item_with_path("foo")]);
156 let new = api([]);
157
158 let actual = PublicApiDiff::between(old, new);
159 let expected = PublicApiDiff {
160 removed: vec![item_with_path("foo")],
161 changed: vec![],
162 added: vec![],
163 };
164 assert_eq!(actual, expected);
165 assert!(!actual.is_empty());
166 }
167
168 #[test]
169 fn single_and_only_item_added() {
170 let old = api([]);
171 let new = api([item_with_path("foo")]);
172
173 let actual = PublicApiDiff::between(old, new);
174 let expected = PublicApiDiff {
175 removed: vec![],
176 changed: vec![],
177 added: vec![item_with_path("foo")],
178 };
179 assert_eq!(actual, expected);
180 assert!(!actual.is_empty());
181 }
182
183 #[test]
184 fn middle_item_added() {
185 let old = api([item_with_path("1"), item_with_path("3")]);
186 let new = api([
187 item_with_path("1"),
188 item_with_path("2"),
189 item_with_path("3"),
190 ]);
191
192 let actual = PublicApiDiff::between(old, new);
193 let expected = PublicApiDiff {
194 removed: vec![],
195 changed: vec![],
196 added: vec![item_with_path("2")],
197 };
198 assert_eq!(actual, expected);
199 assert!(!actual.is_empty());
200 }
201
202 #[test]
203 fn middle_item_removed() {
204 let old = api([
205 item_with_path("1"),
206 item_with_path("2"),
207 item_with_path("3"),
208 ]);
209 let new = api([item_with_path("1"), item_with_path("3")]);
210
211 let actual = PublicApiDiff::between(old, new);
212 let expected = PublicApiDiff {
213 removed: vec![item_with_path("2")],
214 changed: vec![],
215 added: vec![],
216 };
217 assert_eq!(actual, expected);
218 assert!(!actual.is_empty());
219 }
220
221 #[test]
222 fn many_identical_items() {
223 let old = api([
224 item_with_path("1"),
225 item_with_path("2"),
226 item_with_path("2"),
227 item_with_path("3"),
228 item_with_path("3"),
229 item_with_path("3"),
230 fn_with_param_type(&["a", "b"], "i32"),
231 fn_with_param_type(&["a", "b"], "i32"),
232 ]);
233 let new = api([
234 item_with_path("1"),
235 item_with_path("2"),
236 item_with_path("3"),
237 item_with_path("4"),
238 item_with_path("4"),
239 fn_with_param_type(&["a", "b"], "i64"),
240 fn_with_param_type(&["a", "b"], "i64"),
241 ]);
242
243 let actual = PublicApiDiff::between(old, new);
244 let expected = PublicApiDiff {
245 removed: vec![
246 item_with_path("2"),
247 item_with_path("3"),
248 item_with_path("3"),
249 ],
250 changed: vec![
251 ChangedPublicItem {
252 old: fn_with_param_type(&["a", "b"], "i32"),
253 new: fn_with_param_type(&["a", "b"], "i64"),
254 },
255 ChangedPublicItem {
256 old: fn_with_param_type(&["a", "b"], "i32"),
257 new: fn_with_param_type(&["a", "b"], "i64"),
258 },
259 ],
260 added: vec![item_with_path("4"), item_with_path("4")],
261 };
262 assert_eq!(actual, expected);
263 assert!(!actual.is_empty());
264 }
265
266 #[test]
269 fn no_off_by_one_diff_skewing() {
270 let old = api([
271 fn_with_param_type(&["a", "b"], "i8"),
272 fn_with_param_type(&["a", "b"], "i32"),
273 fn_with_param_type(&["a", "b"], "i64"),
274 ]);
275 let new = api([
279 fn_with_param_type(&["a", "b"], "u8"), fn_with_param_type(&["a", "b"], "i8"),
281 fn_with_param_type(&["a", "b"], "i32"),
282 fn_with_param_type(&["a", "b"], "i64"),
283 ]);
284 let expected = PublicApiDiff {
285 removed: vec![],
286 changed: vec![],
287 added: vec![fn_with_param_type(&["a", "b"], "u8")],
288 };
289 let actual = PublicApiDiff::between(old, new);
290 assert_eq!(actual, expected);
291 assert!(!actual.is_empty());
292 }
293
294 #[test]
295 fn no_diff_means_empty_diff() {
296 let old = api([item_with_path("foo")]);
297 let new = api([item_with_path("foo")]);
298
299 let actual = PublicApiDiff::between(old, new);
300 let expected = PublicApiDiff {
301 removed: vec![],
302 changed: vec![],
303 added: vec![],
304 };
305 assert_eq!(actual, expected);
306 assert!(actual.is_empty());
307 }
308
309 fn item_with_path(path_str: &str) -> PublicItem {
310 new_public_item(
311 path_str
312 .split("::")
313 .map(std::string::ToString::to_string)
314 .collect(),
315 vec![crate::tokens::Token::identifier(path_str)],
316 )
317 }
318
319 fn api(items: impl IntoIterator<Item = PublicItem>) -> PublicApi {
320 PublicApi {
321 items: items.into_iter().collect(),
322 missing_item_ids: vec![],
323 }
324 }
325
326 fn fn_with_param_type(path_str: &[&str], type_: &str) -> PublicItem {
327 let path: Vec<_> = path_str
328 .iter()
329 .map(std::string::ToString::to_string)
330 .collect();
331
332 let mut tokens = vec![q("pub"), w(), k("fn"), w()];
334
335 tokens.extend(itertools::intersperse(
337 path.iter().cloned().map(Token::identifier),
338 Token::symbol("::"),
339 ));
340
341 tokens.extend(vec![q("("), i("x"), s(":"), w(), t(type_), q(")")]);
343
344 new_public_item(path, tokens)
346 }
347
348 fn new_public_item(path: PublicItemPath, tokens: Vec<Token>) -> PublicItem {
349 PublicItem {
350 sortable_path: path,
351 tokens,
352 parent_id: None,
353 id: DUMMY_ID,
354 }
355 }
356
357 fn s(s: &str) -> Token {
358 Token::symbol(s)
359 }
360
361 fn t(s: &str) -> Token {
362 Token::type_(s)
363 }
364
365 fn q(s: &str) -> Token {
366 Token::qualifier(s)
367 }
368
369 fn k(s: &str) -> Token {
370 Token::kind(s)
371 }
372
373 fn i(s: &str) -> Token {
374 Token::identifier(s)
375 }
376
377 fn w() -> Token {
378 Token::Whitespace
379 }
380}