1use std::collections::HashMap;
2use std::collections::HashSet;
3
4#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize)]
5#[serde(rename_all = "snake_case")]
6#[non_exhaustive]
7pub struct Id {
8 pub name: &'static str,
9 pub explanation: &'static str,
10 pub category: Category,
11 pub default_severity: Severity,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub struct Diff {
18 pub severity: Severity,
19 pub id: Id,
20 pub before: Option<Location>,
21 pub after: Option<Location>,
22}
23
24#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum Category {
27 Unknown,
28 Added,
29 Removed,
30 Changed,
31}
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum Severity {
36 Allow,
37 Report,
38 Warn,
39}
40
41#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, serde::Serialize)]
42#[serde(rename_all = "snake_case")]
43#[non_exhaustive]
44pub struct Location {
45 pub crate_id: Option<crate::CrateId>,
46 pub path_id: Option<crate::PathId>,
47 pub item_id: Option<crate::ItemId>,
48}
49
50pub fn diff(before: &crate::Api, after: &crate::Api, changes: &mut Vec<Diff>) {
51 public_dependencies(before, after, changes);
52}
53
54pub const ALL_IDS: &[Id] = &[
55 DEPENDENCY_REMOVED,
56 DEPENDENCY_ADDED,
57 DEPENDENCY_AMBIGUOUS,
58 DEPENDENCY_REQUIREMENT,
59];
60
61pub const DEPENDENCY_REMOVED: Id = Id {
62 name: "dependency-removed",
63 explanation: "Public dependency removed because of an API change",
64 category: Category::Removed,
65 default_severity: Severity::Allow,
67};
68
69pub const DEPENDENCY_ADDED: Id = Id {
70 name: "dependency-added",
71 explanation: "Public dependency removed because of an API change",
72 category: Category::Added,
73 default_severity: Severity::Report,
75};
76
77pub const DEPENDENCY_AMBIGUOUS: Id = Id {
78 name: "dependency-ambiguous",
79 explanation: "Could not determine the dependency version to check it",
80 category: Category::Unknown,
81 default_severity: Severity::Allow,
83};
84
85pub const DEPENDENCY_REQUIREMENT: Id = Id {
86 name: "dependency-requirement",
87 explanation: "Changing the major version requirements breaks compatibility",
88 category: Category::Changed,
89 default_severity: Severity::Warn,
90};
91
92pub fn public_dependencies(before: &crate::Api, after: &crate::Api, changes: &mut Vec<Diff>) {
93 let before_by_name: HashMap<_, _> = before
94 .crates
95 .iter()
96 .map(|(id, crate_)| (crate_.name.as_str(), id))
97 .collect();
98 let after_by_name: HashMap<_, _> = after
99 .crates
100 .iter()
101 .map(|(id, crate_)| (crate_.name.as_str(), id))
102 .collect();
103
104 let before_names: HashSet<_> = before_by_name.keys().collect();
105 let after_names: HashSet<_> = after_by_name.keys().collect();
106
107 for removed_name in before_names.difference(&after_names) {
108 let before_crate_id = *before_by_name.get(*removed_name).unwrap();
109 changes.push(Diff {
110 severity: DEPENDENCY_REMOVED.default_severity,
111 id: DEPENDENCY_REMOVED,
112 before: Some(Location {
113 crate_id: Some(before_crate_id),
114 ..Default::default()
115 }),
116 after: None,
117 });
118 }
119
120 for added_name in after_names.difference(&before_names) {
121 let after_crate_id = *after_by_name.get(*added_name).unwrap();
122 changes.push(Diff {
123 severity: DEPENDENCY_ADDED.default_severity,
124 id: DEPENDENCY_ADDED,
125 before: None,
126 after: Some(Location {
127 crate_id: Some(after_crate_id),
128 ..Default::default()
129 }),
130 });
131 }
132
133 for common_name in after_names.intersection(&before_names) {
134 let before_crate_id = *before_by_name.get(*common_name).unwrap();
135 let before_crate = before.crates.get(before_crate_id).unwrap();
136 let after_crate_id = *after_by_name.get(*common_name).unwrap();
137 let after_crate = after.crates.get(after_crate_id).unwrap();
138
139 if before_crate.version.is_none() || after_crate.version.is_none() {
140 changes.push(Diff {
141 severity: DEPENDENCY_AMBIGUOUS.default_severity,
142 id: DEPENDENCY_AMBIGUOUS,
143 before: Some(Location {
144 crate_id: Some(before_crate_id),
145 ..Default::default()
146 }),
147 after: Some(Location {
148 crate_id: Some(after_crate_id),
149 ..Default::default()
150 }),
151 });
152 } else if before_crate.version == after_crate.version {
153 } else {
154 let (before_lower, before_upper) = breaking(before_crate.version.as_ref().unwrap());
155 let before_lower = before_lower.unwrap_or((0, 0, 0));
156 let before_upper = before_upper.unwrap_or((u64::MAX, u64::MAX, u64::MAX));
157
158 let (after_lower, after_upper) = breaking(after_crate.version.as_ref().unwrap());
159 let after_lower = after_lower.unwrap_or((0, 0, 0));
160 let after_upper = after_upper.unwrap_or((u64::MAX, u64::MAX, u64::MAX));
161
162 if before_lower < after_lower || after_upper < before_upper {
163 changes.push(Diff {
164 severity: DEPENDENCY_REQUIREMENT.default_severity,
165 id: DEPENDENCY_REQUIREMENT,
166 before: Some(Location {
167 crate_id: Some(before_crate_id),
168 ..Default::default()
169 }),
170 after: Some(Location {
171 crate_id: Some(after_crate_id),
172 ..Default::default()
173 }),
174 });
175 }
176 }
177 }
178}
179
180fn breaking(version: &semver::VersionReq) -> VersionRange {
181 if *version == semver::VersionReq::STAR {
182 return (None, None);
183 }
184
185 let mut lower = None;
186 let mut upper = None;
187 for comparator in &version.comparators {
188 let (current_lower, current_upper) = breaking_comparator(comparator);
189 if let Some(current_lower) = current_lower {
190 lower.get_or_insert(current_lower);
191 lower = Some(lower.unwrap().max(current_lower));
192 }
193 if let Some(current_upper) = current_upper {
194 upper.get_or_insert(current_upper);
195 upper = Some(upper.unwrap().min(current_upper));
196 }
197 }
198
199 (lower, upper)
200}
201
202type VersionParts = (u64, u64, u64);
203
204type VersionRange = (Option<VersionParts>, Option<VersionParts>);
205
206fn breaking_comparator(comparator: &semver::Comparator) -> VersionRange {
207 match comparator.op {
208 semver::Op::Exact => {
209 if let Some(major) = exact_break(comparator) {
210 (Some(major), Some(major))
211 } else {
212 (None, None)
213 }
214 }
215 semver::Op::Greater => {
216 let major = if 1 <= comparator.major {
217 let major = comparator.major;
218 let major = if comparator.minor.is_none() {
219 major + 1
220 } else {
221 major
222 };
223 (major, 0, 0)
224 } else if comparator.minor.is_none() {
225 return (None, None);
226 } else if 1 <= comparator.minor.unwrap() {
227 let major = comparator.minor.unwrap();
228 let major = if comparator.patch.is_none() {
229 major + 1
230 } else {
231 major
232 };
233 (0, major, 0)
234 } else if comparator.patch.is_none() {
235 return (None, None);
236 } else {
237 let major = comparator.patch.unwrap() + 1;
238 (0, 0, major)
239 };
240 (Some(major), None)
241 }
242 semver::Op::GreaterEq => {
243 if let Some(major) = exact_break(comparator) {
244 (Some(major), None)
245 } else {
246 (None, None)
247 }
248 }
249 semver::Op::Less => {
250 let major = if 1 <= comparator.major {
251 let major = comparator.major;
252 let major = if comparator.minor.is_none() {
253 major - 1
254 } else {
255 major
256 };
257 (major, 0, 0)
258 } else if comparator.minor.is_none() {
259 return (None, None);
260 } else if 1 <= comparator.minor.unwrap() {
261 let major = comparator.minor.unwrap();
262 let major = if comparator.patch.is_none() {
263 major - 1
264 } else {
265 major
266 };
267 (0, major, 0)
268 } else if comparator.patch.is_none() {
269 return (None, None);
270 } else {
271 let major = comparator.patch.unwrap();
272 let major = if major == 0 { major } else { major - 1 };
273 (0, 0, major)
274 };
275 (None, Some(major))
276 }
277 semver::Op::LessEq => {
278 if let Some(major) = exact_break(comparator) {
279 (None, Some(major))
280 } else {
281 (None, None)
282 }
283 }
284 semver::Op::Tilde => {
285 if let Some(major) = exact_break(comparator) {
286 (Some(major), Some(major))
287 } else {
288 (None, None)
289 }
290 }
291 semver::Op::Caret => {
292 if let Some(major) = exact_break(comparator) {
293 (Some(major), Some(major))
294 } else {
295 (None, None)
296 }
297 }
298 semver::Op::Wildcard => {
299 if let Some(major) = exact_break(comparator) {
300 (Some(major), Some(major))
301 } else {
302 (None, None)
303 }
304 }
305 _ => (None, None),
306 }
307}
308
309fn exact_break(comparator: &semver::Comparator) -> Option<(u64, u64, u64)> {
310 if 1 <= comparator.major {
311 let major = comparator.major;
312 Some((major, 0, 0))
313 } else if comparator.minor.is_none() {
314 None
315 } else if 1 <= comparator.minor.unwrap() {
316 let major = comparator.minor.unwrap();
317 Some((0, major, 0))
318 } else if comparator.patch.is_none() {
319 None
320 } else {
321 let major = comparator.patch.unwrap();
322 Some((0, 0, major))
323 }
324}