uv_pypi_types/
conflicts.rs

1use petgraph::{
2    algo::toposort,
3    graph::{DiGraph, NodeIndex},
4};
5use rustc_hash::{FxHashMap, FxHashSet};
6#[cfg(feature = "schemars")]
7use std::borrow::Cow;
8use std::{collections::BTreeSet, hash::Hash, rc::Rc};
9use uv_normalize::{ExtraName, GroupName, PackageName};
10
11use crate::dependency_groups::{DependencyGroupSpecifier, DependencyGroups};
12
13/// A list of conflicting sets of extras/groups pre-defined by an end user.
14///
15/// This is useful to force the resolver to fork according to extras that have
16/// unavoidable conflicts with each other. (The alternative is that resolution
17/// will fail.)
18#[derive(Debug, Default, Clone, Eq, PartialEq, serde::Deserialize)]
19pub struct Conflicts(Vec<ConflictSet>);
20
21impl Conflicts {
22    /// Returns no conflicts.
23    ///
24    /// This results in no effect on resolution.
25    pub fn empty() -> Self {
26        Self::default()
27    }
28
29    /// Push a single set of conflicts.
30    pub fn push(&mut self, set: ConflictSet) {
31        self.0.push(set);
32    }
33
34    /// Returns an iterator over all sets of conflicting sets.
35    pub fn iter(&self) -> impl Iterator<Item = &'_ ConflictSet> + Clone + '_ {
36        self.0.iter()
37    }
38
39    /// Returns true if these conflicts contain any set that contains the given
40    /// package and extra name pair.
41    pub fn contains<'a>(
42        &self,
43        package: &PackageName,
44        kind: impl Into<ConflictKindRef<'a>>,
45    ) -> bool {
46        let kind = kind.into();
47        self.iter().any(|set| set.contains(package, kind))
48    }
49
50    /// Returns true if there are no conflicts.
51    pub fn is_empty(&self) -> bool {
52        self.0.is_empty()
53    }
54
55    /// Appends the given conflicts to this one. This drains all sets from the
56    /// conflicts given, such that after this call, it is empty.
57    pub fn append(&mut self, other: &mut Self) {
58        self.0.append(&mut other.0);
59    }
60
61    /// Expand [`Conflicts`]s to include all [`ConflictSet`]s that can
62    /// be transitively inferred from group conflicts directly defined
63    /// in configuration.
64    ///
65    /// A directed acyclic graph (DAG) is created representing all
66    /// transitive group includes, with nodes corresponding to group conflict
67    /// items. For every conflict item directly mentioned in configuration,
68    /// its node starts with a set of canonical items with itself as the only
69    /// member.
70    ///
71    /// The graph is traversed one node at a time in topological order and
72    /// canonical items are propagated to each neighbor. We also update our
73    /// substitutions at each neighbor to reflect that this neighbor transitively
74    /// includes all canonical items visited so far to reach it.
75    ///
76    /// Finally, we apply the substitutions to the conflict sets that were
77    /// directly defined in configuration to generate all transitively inferable
78    /// [`ConflictSet`]s.
79    ///
80    /// There is an assumption that inclusion graphs will not be very large
81    /// or complex. This algorithm creates all combinations of substitutions.
82    /// Each resulting [`ConflictSet`] would also later correspond to a separate
83    /// resolver fork during resolution.
84    pub fn expand_transitive_group_includes(
85        &mut self,
86        package: &PackageName,
87        groups: &DependencyGroups,
88    ) {
89        let mut graph = DiGraph::new();
90        let mut group_node_idxs: FxHashMap<&GroupName, NodeIndex> = FxHashMap::default();
91        let mut node_conflict_items: FxHashMap<NodeIndex, Rc<ConflictItem>> = FxHashMap::default();
92        // Used for transitively deriving new conflict sets with substitutions.
93        // The keys are canonical items (mentioned directly in configured conflicts).
94        // The values correspond to groups that transitively include them.
95        let mut substitutions: FxHashMap<Rc<ConflictItem>, FxHashSet<Rc<ConflictItem>>> =
96            FxHashMap::default();
97
98        // Track all existing conflict sets to avoid duplicates.
99        let mut conflict_sets: FxHashSet<ConflictSet> = FxHashSet::default();
100
101        // Add groups in directly defined conflict sets to the graph.
102        let mut seen: FxHashSet<&GroupName> = FxHashSet::default();
103
104        for set in &self.0 {
105            conflict_sets.insert(set.clone());
106            for item in set.iter() {
107                let ConflictKind::Group(group) = &item.kind else {
108                    // TODO(john): Do we also want to handle extras here?
109                    continue;
110                };
111                if !seen.insert(group) {
112                    continue;
113                }
114                let item = Rc::new(item.clone());
115                let mut canonical_items = FxHashSet::default();
116                canonical_items.insert(item.clone());
117                let node_id = graph.add_node(canonical_items);
118                group_node_idxs.insert(group, node_id);
119                node_conflict_items.insert(node_id, item.clone());
120            }
121        }
122
123        // Create conflict items for remaining groups and add them to the graph.
124        for group in groups.keys() {
125            if !seen.insert(group) {
126                continue;
127            }
128            let group_conflict_item = ConflictItem {
129                package: package.clone(),
130                kind: ConflictKind::Group(group.clone()),
131            };
132            let node_id = graph.add_node(FxHashSet::default());
133            group_node_idxs.insert(group, node_id);
134            node_conflict_items.insert(node_id, Rc::new(group_conflict_item));
135        }
136
137        // Create edges representing group inclusion (with edges reversed so that
138        // included groups point to including groups).
139        for (group, specifiers) in groups {
140            if let Some(includer) = group_node_idxs.get(group) {
141                for specifier in specifiers {
142                    if let DependencyGroupSpecifier::IncludeGroup { include_group } = specifier {
143                        if let Some(included) = group_node_idxs.get(include_group) {
144                            graph.add_edge(*included, *includer, ());
145                        }
146                    }
147                }
148            }
149        }
150
151        let Ok(topo_nodes) = toposort(&graph, None) else {
152            return;
153        };
154        // Propagate canonical items through the graph and populate substitutions.
155        for node in topo_nodes {
156            for neighbor_idx in graph.neighbors(node).collect::<Vec<_>>() {
157                let mut neighbor_canonical_items = Vec::new();
158                if let Some(canonical_items) = graph.node_weight(node) {
159                    let neighbor_item = node_conflict_items
160                        .get(&neighbor_idx)
161                        .expect("ConflictItem should already be in graph")
162                        .clone();
163                    for canonical_item in canonical_items {
164                        neighbor_canonical_items.push(canonical_item.clone());
165                        substitutions
166                            .entry(canonical_item.clone())
167                            .or_default()
168                            .insert(neighbor_item.clone());
169                    }
170                }
171                graph
172                    .node_weight_mut(neighbor_idx)
173                    .expect("Graph node should have weight")
174                    .extend(neighbor_canonical_items.into_iter());
175            }
176        }
177
178        // Create new conflict sets for all possible replacements of canonical
179        // items by substitution items.
180        // Note that new sets are (potentially) added to transitive_conflict_sets
181        // at the end of each iteration.
182        for (canonical_item, subs) in substitutions {
183            let mut new_conflict_sets = FxHashSet::default();
184            for conflict_set in conflict_sets
185                .iter()
186                .filter(|set| set.contains_item(&canonical_item))
187                .cloned()
188                .collect::<Vec<_>>()
189            {
190                for sub in &subs {
191                    let new_set = conflict_set
192                        .replaced_item(&canonical_item, (**sub).clone())
193                        .expect("`ConflictItem` should be in `ConflictSet`");
194                    if !conflict_sets.contains(&new_set) {
195                        new_conflict_sets.insert(new_set);
196                    }
197                }
198            }
199            conflict_sets.extend(new_conflict_sets.into_iter());
200        }
201
202        // Add all newly discovered conflict sets (excluding the originals already in self.0)
203        for set in conflict_sets {
204            if !self.0.contains(&set) {
205                self.0.push(set);
206            }
207        }
208    }
209}
210
211/// A single set of package-extra pairs that conflict with one another.
212///
213/// Within each set of conflicts, the resolver should isolate the requirements
214/// corresponding to each extra from the requirements of other extras in
215/// this set. That is, the resolver should put each set of requirements in a
216/// different fork.
217///
218/// A `TryFrom<Vec<ConflictItem>>` impl may be used to build a set from a
219/// sequence. Note though that at least 2 items are required.
220#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
221pub struct ConflictSet {
222    set: BTreeSet<ConflictItem>,
223}
224
225impl ConflictSet {
226    /// Create a pair of items that conflict with one another.
227    pub fn pair(item1: ConflictItem, item2: ConflictItem) -> Self {
228        Self {
229            set: BTreeSet::from_iter(vec![item1, item2]),
230        }
231    }
232
233    /// Returns an iterator over all conflicting items.
234    pub fn iter(&self) -> impl Iterator<Item = &'_ ConflictItem> + Clone + '_ {
235        self.set.iter()
236    }
237
238    /// Returns true if this conflicting item contains the given package and
239    /// extra name pair.
240    pub fn contains<'a>(
241        &self,
242        package: &PackageName,
243        kind: impl Into<ConflictKindRef<'a>>,
244    ) -> bool {
245        let kind = kind.into();
246        self.iter()
247            .any(|set| set.package() == package && *set.kind() == kind)
248    }
249
250    /// Returns true if these conflicts contain any set that contains the given
251    /// [`ConflictItem`].
252    pub fn contains_item(&self, conflict_item: &ConflictItem) -> bool {
253        self.set.contains(conflict_item)
254    }
255
256    /// Replace an old [`ConflictItem`] with a new one.
257    pub fn replaced_item(
258        &self,
259        old: &ConflictItem,
260        new: ConflictItem,
261    ) -> Result<Self, ConflictError> {
262        let mut new_set = self.set.clone();
263        if !new_set.contains(old) {
264            return Err(ConflictError::ReplaceMissingConflictItem);
265        }
266        new_set.remove(old);
267        new_set.insert(new);
268        Ok(Self { set: new_set })
269    }
270}
271
272impl<'de> serde::Deserialize<'de> for ConflictSet {
273    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
274    where
275        D: serde::Deserializer<'de>,
276    {
277        let set = Vec::<ConflictItem>::deserialize(deserializer)?;
278        Self::try_from(set).map_err(serde::de::Error::custom)
279    }
280}
281
282impl TryFrom<Vec<ConflictItem>> for ConflictSet {
283    type Error = ConflictError;
284
285    fn try_from(items: Vec<ConflictItem>) -> Result<Self, ConflictError> {
286        match items.len() {
287            0 => return Err(ConflictError::ZeroItems),
288            1 => return Err(ConflictError::OneItem),
289            _ => {}
290        }
291        Ok(Self {
292            set: BTreeSet::from_iter(items),
293        })
294    }
295}
296
297/// A single item in a conflicting set.
298///
299/// Each item is a pair of a package and a corresponding extra or group name
300/// for that package.
301#[derive(
302    Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize, serde::Serialize,
303)]
304#[serde(
305    deny_unknown_fields,
306    try_from = "ConflictItemWire",
307    into = "ConflictItemWire"
308)]
309pub struct ConflictItem {
310    package: PackageName,
311    kind: ConflictKind,
312}
313
314impl ConflictItem {
315    /// Returns the package name of this conflicting item.
316    pub fn package(&self) -> &PackageName {
317        &self.package
318    }
319
320    /// Returns the package-specific conflict.
321    ///
322    /// i.e., Either an extra or a group name.
323    pub fn kind(&self) -> &ConflictKind {
324        &self.kind
325    }
326
327    /// Returns the extra name of this conflicting item.
328    pub fn extra(&self) -> Option<&ExtraName> {
329        self.kind.extra()
330    }
331
332    /// Returns the group name of this conflicting item.
333    pub fn group(&self) -> Option<&GroupName> {
334        self.kind.group()
335    }
336
337    /// Returns this item as a new type with its fields borrowed.
338    pub fn as_ref(&self) -> ConflictItemRef<'_> {
339        ConflictItemRef {
340            package: self.package(),
341            kind: self.kind.as_ref(),
342        }
343    }
344}
345
346impl From<PackageName> for ConflictItem {
347    fn from(package: PackageName) -> Self {
348        let kind = ConflictKind::Project;
349        Self { package, kind }
350    }
351}
352
353impl From<(PackageName, ExtraName)> for ConflictItem {
354    fn from((package, extra): (PackageName, ExtraName)) -> Self {
355        let kind = ConflictKind::Extra(extra);
356        Self { package, kind }
357    }
358}
359
360impl From<(PackageName, GroupName)> for ConflictItem {
361    fn from((package, group): (PackageName, GroupName)) -> Self {
362        let kind = ConflictKind::Group(group);
363        Self { package, kind }
364    }
365}
366
367/// A single item in a conflicting set, by reference.
368///
369/// Each item is a pair of a package and a corresponding extra name for that
370/// package.
371#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
372pub struct ConflictItemRef<'a> {
373    package: &'a PackageName,
374    kind: ConflictKindRef<'a>,
375}
376
377impl<'a> ConflictItemRef<'a> {
378    /// Returns the package name of this conflicting item.
379    pub fn package(&self) -> &'a PackageName {
380        self.package
381    }
382
383    /// Returns the package-specific conflict.
384    ///
385    /// i.e., Either an extra or a group name.
386    pub fn kind(&self) -> ConflictKindRef<'a> {
387        self.kind
388    }
389
390    /// Returns the extra name of this conflicting item.
391    pub fn extra(&self) -> Option<&'a ExtraName> {
392        self.kind.extra()
393    }
394
395    /// Returns the group name of this conflicting item.
396    pub fn group(&self) -> Option<&'a GroupName> {
397        self.kind.group()
398    }
399
400    /// Converts this borrowed conflicting item to its owned variant.
401    pub fn to_owned(&self) -> ConflictItem {
402        ConflictItem {
403            package: self.package().clone(),
404            kind: self.kind.to_owned(),
405        }
406    }
407}
408
409impl<'a> From<&'a PackageName> for ConflictItemRef<'a> {
410    fn from(package: &'a PackageName) -> Self {
411        let kind = ConflictKindRef::Project;
412        Self { package, kind }
413    }
414}
415
416impl<'a> From<(&'a PackageName, &'a ExtraName)> for ConflictItemRef<'a> {
417    fn from((package, extra): (&'a PackageName, &'a ExtraName)) -> Self {
418        let kind = ConflictKindRef::Extra(extra);
419        ConflictItemRef { package, kind }
420    }
421}
422
423impl<'a> From<(&'a PackageName, &'a GroupName)> for ConflictItemRef<'a> {
424    fn from((package, group): (&'a PackageName, &'a GroupName)) -> Self {
425        let kind = ConflictKindRef::Group(group);
426        ConflictItemRef { package, kind }
427    }
428}
429
430impl hashbrown::Equivalent<ConflictItem> for ConflictItemRef<'_> {
431    fn equivalent(&self, key: &ConflictItem) -> bool {
432        key.as_ref() == *self
433    }
434}
435
436/// The actual conflicting data for a package.
437///
438/// That is, either an extra or a group name, or the entire project itself.
439#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
440#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
441pub enum ConflictKind {
442    Extra(ExtraName),
443    Group(GroupName),
444    Project,
445}
446
447impl ConflictKind {
448    /// If this conflict corresponds to an extra, then return the
449    /// extra name.
450    pub fn extra(&self) -> Option<&ExtraName> {
451        match self {
452            Self::Extra(extra) => Some(extra),
453            Self::Group(_) | Self::Project => None,
454        }
455    }
456
457    /// If this conflict corresponds to a group, then return the
458    /// group name.
459    pub fn group(&self) -> Option<&GroupName> {
460        match self {
461            Self::Group(group) => Some(group),
462            Self::Extra(_) | Self::Project => None,
463        }
464    }
465
466    /// Returns this conflict as a new type with its fields borrowed.
467    pub fn as_ref(&self) -> ConflictKindRef<'_> {
468        match self {
469            Self::Extra(extra) => ConflictKindRef::Extra(extra),
470            Self::Group(group) => ConflictKindRef::Group(group),
471            Self::Project => ConflictKindRef::Project,
472        }
473    }
474}
475
476/// The actual conflicting data for a package, by reference.
477///
478/// That is, either a borrowed extra name or a borrowed group name.
479#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
480pub enum ConflictKindRef<'a> {
481    Extra(&'a ExtraName),
482    Group(&'a GroupName),
483    Project,
484}
485
486impl<'a> ConflictKindRef<'a> {
487    /// If this conflict corresponds to an extra, then return the
488    /// extra name.
489    pub fn extra(&self) -> Option<&'a ExtraName> {
490        match self {
491            Self::Extra(extra) => Some(extra),
492            Self::Group(_) | Self::Project => None,
493        }
494    }
495
496    /// If this conflict corresponds to a group, then return the
497    /// group name.
498    pub fn group(&self) -> Option<&'a GroupName> {
499        match self {
500            Self::Group(group) => Some(group),
501            Self::Extra(_) | Self::Project => None,
502        }
503    }
504
505    /// Converts this borrowed conflict to its owned variant.
506    pub fn to_owned(&self) -> ConflictKind {
507        match self {
508            Self::Extra(extra) => ConflictKind::Extra((*extra).clone()),
509            Self::Group(group) => ConflictKind::Group((*group).clone()),
510            Self::Project => ConflictKind::Project,
511        }
512    }
513}
514
515impl<'a> From<&'a ExtraName> for ConflictKindRef<'a> {
516    fn from(extra: &'a ExtraName) -> Self {
517        Self::Extra(extra)
518    }
519}
520
521impl<'a> From<&'a GroupName> for ConflictKindRef<'a> {
522    fn from(group: &'a GroupName) -> Self {
523        Self::Group(group)
524    }
525}
526
527impl PartialEq<ConflictKind> for ConflictKindRef<'_> {
528    fn eq(&self, other: &ConflictKind) -> bool {
529        other.as_ref() == *self
530    }
531}
532
533impl<'a> PartialEq<ConflictKindRef<'a>> for ConflictKind {
534    fn eq(&self, other: &ConflictKindRef<'a>) -> bool {
535        self.as_ref() == *other
536    }
537}
538
539impl hashbrown::Equivalent<ConflictKind> for ConflictKindRef<'_> {
540    fn equivalent(&self, key: &ConflictKind) -> bool {
541        key.as_ref() == *self
542    }
543}
544
545/// An error that occurs when the given conflicting set is invalid somehow.
546#[derive(Debug, thiserror::Error)]
547pub enum ConflictError {
548    /// An error for when there are zero conflicting items.
549    #[error("Each set of conflicts must have at least two entries, but found none")]
550    ZeroItems,
551    /// An error for when there is one conflicting items.
552    #[error("Each set of conflicts must have at least two entries, but found only one")]
553    OneItem,
554    /// An error that occurs when the `package` field is missing.
555    ///
556    /// (This is only applicable when deserializing from the lock file.
557    /// When deserializing from `pyproject.toml`, the `package` field is
558    /// optional.)
559    #[error("Expected `package` field in conflicting entry")]
560    MissingPackage,
561    /// An error that occurs when all of `package`, `extra` and `group` are missing.
562    #[error("Expected `package`, `extra` or `group` field in conflicting entry")]
563    MissingPackageAndExtraAndGroup,
564    /// An error that occurs when both `extra` and `group` are present.
565    #[error("Expected one of `extra` or `group` in conflicting entry, but found both")]
566    FoundExtraAndGroup,
567    #[error("Expected `ConflictSet` to contain `ConflictItem` to replace")]
568    ReplaceMissingConflictItem,
569}
570
571/// Like [`Conflicts`], but for deserialization in `pyproject.toml`.
572///
573/// The schema format is different from the in-memory format. Specifically, the
574/// schema format does not allow specifying the package name (or will make it
575/// optional in the future), where as the in-memory format needs the package
576/// name.
577///
578/// N.B. `Conflicts` is still used for (de)serialization. Specifically, in the
579/// lock file, where the package name is required.
580#[derive(Debug, Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
581#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
582pub struct SchemaConflicts(Vec<SchemaConflictSet>);
583
584impl SchemaConflicts {
585    /// Convert the public schema "conflicting" type to our internal fully
586    /// resolved type. Effectively, this pairs the corresponding package name
587    /// with each conflict.
588    ///
589    /// If a conflict has an explicit package name (written by the end user),
590    /// then that takes precedence over the given package name, which is only
591    /// used when there is no explicit package name written.
592    pub fn to_conflicts_with_package_name(&self, package: &PackageName) -> Conflicts {
593        let mut conflicting = Conflicts::empty();
594        for tool_uv_set in &self.0 {
595            let mut set = vec![];
596            for item in &tool_uv_set.0 {
597                let package = item.package.clone().unwrap_or_else(|| package.clone());
598                set.push(ConflictItem {
599                    package: package.clone(),
600                    kind: item.kind.clone(),
601                });
602            }
603            // OK because we guarantee that
604            // `SchemaConflictingGroupList` is valid and there aren't
605            // any new errors that can occur here.
606            let set = ConflictSet::try_from(set).unwrap();
607            conflicting.push(set);
608        }
609        conflicting
610    }
611}
612
613/// Like [`ConflictSet`], but for deserialization in `pyproject.toml`.
614///
615/// The schema format is different from the in-memory format. Specifically, the
616/// schema format does not allow specifying the package name (or will make it
617/// optional in the future), where as the in-memory format needs the package
618/// name.
619#[derive(Debug, Default, Clone, Eq, PartialEq, serde::Serialize)]
620#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
621pub struct SchemaConflictSet(Vec<SchemaConflictItem>);
622
623/// Like [`ConflictItem`], but for deserialization in `pyproject.toml`.
624///
625/// The schema format is different from the in-memory format. Specifically, the
626/// schema format does not allow specifying the package name (or will make it
627/// optional in the future), where as the in-memory format needs the package
628/// name.
629#[derive(
630    Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize, serde::Serialize,
631)]
632#[serde(
633    deny_unknown_fields,
634    try_from = "ConflictItemWire",
635    into = "ConflictItemWire"
636)]
637pub struct SchemaConflictItem {
638    package: Option<PackageName>,
639    kind: ConflictKind,
640}
641
642#[cfg(feature = "schemars")]
643impl schemars::JsonSchema for SchemaConflictItem {
644    fn schema_name() -> Cow<'static, str> {
645        Cow::Borrowed("SchemaConflictItem")
646    }
647
648    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
649        <ConflictItemWire as schemars::JsonSchema>::json_schema(generator)
650    }
651}
652
653impl<'de> serde::Deserialize<'de> for SchemaConflictSet {
654    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
655    where
656        D: serde::Deserializer<'de>,
657    {
658        let items = Vec::<SchemaConflictItem>::deserialize(deserializer)?;
659        Self::try_from(items).map_err(serde::de::Error::custom)
660    }
661}
662
663impl TryFrom<Vec<SchemaConflictItem>> for SchemaConflictSet {
664    type Error = ConflictError;
665
666    fn try_from(items: Vec<SchemaConflictItem>) -> Result<Self, ConflictError> {
667        match items.len() {
668            0 => return Err(ConflictError::ZeroItems),
669            1 => return Err(ConflictError::OneItem),
670            _ => {}
671        }
672        Ok(Self(items))
673    }
674}
675
676/// A single item in a conflicting set.
677///
678/// Each item is a pair of an (optional) package and a corresponding extra or group name for that
679/// package.
680#[derive(Debug, serde::Deserialize, serde::Serialize)]
681#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
682struct ConflictItemWire {
683    #[serde(default)]
684    package: Option<PackageName>,
685    #[serde(default)]
686    extra: Option<ExtraName>,
687    #[serde(default)]
688    group: Option<GroupName>,
689}
690
691impl TryFrom<ConflictItemWire> for ConflictItem {
692    type Error = ConflictError;
693
694    fn try_from(wire: ConflictItemWire) -> Result<Self, ConflictError> {
695        let Some(package) = wire.package else {
696            return Err(ConflictError::MissingPackage);
697        };
698        match (wire.extra, wire.group) {
699            (Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup),
700            (None, None) => Ok(Self::from(package)),
701            (Some(extra), None) => Ok(Self::from((package, extra))),
702            (None, Some(group)) => Ok(Self::from((package, group))),
703        }
704    }
705}
706
707impl From<ConflictItem> for ConflictItemWire {
708    fn from(item: ConflictItem) -> Self {
709        match item.kind {
710            ConflictKind::Extra(extra) => Self {
711                package: Some(item.package),
712                extra: Some(extra),
713                group: None,
714            },
715            ConflictKind::Group(group) => Self {
716                package: Some(item.package),
717                extra: None,
718                group: Some(group),
719            },
720            ConflictKind::Project => Self {
721                package: Some(item.package),
722                extra: None,
723                group: None,
724            },
725        }
726    }
727}
728
729impl TryFrom<ConflictItemWire> for SchemaConflictItem {
730    type Error = ConflictError;
731
732    fn try_from(wire: ConflictItemWire) -> Result<Self, ConflictError> {
733        let package = wire.package;
734        match (wire.extra, wire.group) {
735            (Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup),
736            (None, None) => {
737                let Some(package) = package else {
738                    return Err(ConflictError::MissingPackageAndExtraAndGroup);
739                };
740                Ok(Self {
741                    package: Some(package),
742                    kind: ConflictKind::Project,
743                })
744            }
745            (Some(extra), None) => Ok(Self {
746                package,
747                kind: ConflictKind::Extra(extra),
748            }),
749            (None, Some(group)) => Ok(Self {
750                package,
751                kind: ConflictKind::Group(group),
752            }),
753        }
754    }
755}
756
757impl From<SchemaConflictItem> for ConflictItemWire {
758    fn from(item: SchemaConflictItem) -> Self {
759        match item.kind {
760            ConflictKind::Extra(extra) => Self {
761                package: item.package,
762                extra: Some(extra),
763                group: None,
764            },
765            ConflictKind::Group(group) => Self {
766                package: item.package,
767                extra: None,
768                group: Some(group),
769            },
770            ConflictKind::Project => Self {
771                package: item.package,
772                extra: None,
773                group: None,
774            },
775        }
776    }
777}
778
779/// An inference about whether a conflicting item is always included or
780/// excluded.
781///
782/// We collect these for each node in the graph after determining which
783/// extras/groups are activated for each node. Once we know what's
784/// activated, we can infer what must also be *inactivated* based on what's
785/// conflicting with it. So for example, if we have a conflict marker like
786/// `extra == 'foo' and extra != 'bar'`, and `foo` and `bar` have been
787/// declared as conflicting, and we are in a part of the graph where we
788/// know `foo` must be activated, then it follows that `extra != 'bar'`
789/// must always be true. Because if it were false, it would imply both
790/// `foo` and `bar` were activated simultaneously, which uv guarantees
791/// won't happen.
792///
793/// We then use these inferences to simplify the conflict markers.
794#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
795pub struct Inference {
796    pub included: bool,
797    pub item: ConflictItem,
798}