crate_api/
diff.rs

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    // This is a side effect of an API change but not an API change in of itself
66    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    // In case people weren't aware they added a dependency to their public API
74    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    // In case people weren't aware they added a dependency to their public API
82    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}