1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::{Display, Formatter};
4use std::sync::Arc;
5
6use indexmap::IndexSet;
7use petgraph::{
8 Directed, Direction,
9 graph::{Graph, NodeIndex},
10};
11use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
12
13use uv_configuration::{Constraints, Overrides};
14use uv_distribution::Metadata;
15use uv_distribution_types::{
16 Dist, DistributionId, Edge, Identifier, IndexUrl, Name, Node, Requirement, RequiresPython,
17 ResolutionDiagnostic, ResolvedDist,
18};
19use uv_git::GitResolver;
20use uv_normalize::{ExtraName, GroupName, PackageName};
21use uv_pep440::{Version, VersionSpecifier};
22use uv_pep508::{MarkerEnvironment, MarkerTree, MarkerTreeKind};
23use uv_pypi_types::{Conflicts, HashDigests, ParsedUrlError, VerbatimParsedUrl, Yanked};
24
25use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
26use crate::pins::FilePins;
27use crate::preferences::Preferences;
28use crate::redirect::url_to_precise;
29use crate::resolution::AnnotatedDist;
30use crate::resolution_mode::ResolutionStrategy;
31use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
32use crate::universal_marker::{ConflictMarker, UniversalMarker};
33use crate::{
34 InMemoryIndex, MetadataResponse, Options, PythonRequirement, ResolveError, VersionsResponse,
35};
36
37#[derive(Debug)]
42pub struct ResolverOutput {
43 pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
45 pub(crate) requires_python: RequiresPython,
47 pub(crate) fork_markers: Vec<UniversalMarker>,
50 pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
52 pub(crate) requirements: Vec<Requirement>,
54 pub(crate) constraints: Constraints,
56 pub(crate) overrides: Overrides,
58 pub(crate) options: Options,
60}
61
62#[derive(Debug, Clone)]
63#[expect(clippy::large_enum_variant)]
64pub(crate) enum ResolutionGraphNode {
65 Root,
66 Dist(AnnotatedDist),
67}
68
69impl ResolutionGraphNode {
70 pub(crate) fn marker(&self) -> &UniversalMarker {
71 match self {
72 Self::Root => &UniversalMarker::TRUE,
73 Self::Dist(dist) => &dist.marker,
74 }
75 }
76
77 pub(crate) fn package_extra_names(&self) -> Option<(&PackageName, &ExtraName)> {
78 match self {
79 Self::Root => None,
80 Self::Dist(dist) => {
81 let extra = dist.extra.as_ref()?;
82 Some((&dist.name, extra))
83 }
84 }
85 }
86
87 pub(crate) fn package_group_names(&self) -> Option<(&PackageName, &GroupName)> {
88 match self {
89 Self::Root => None,
90 Self::Dist(dist) => {
91 let group = dist.group.as_ref()?;
92 Some((&dist.name, group))
93 }
94 }
95 }
96
97 pub(crate) fn package_name(&self) -> Option<&PackageName> {
98 match self {
99 Self::Root => None,
100 Self::Dist(dist) => Some(&dist.name),
101 }
102 }
103}
104
105impl Display for ResolutionGraphNode {
106 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107 match self {
108 Self::Root => f.write_str("root"),
109 Self::Dist(dist) => Display::fmt(dist, f),
110 }
111 }
112}
113
114#[derive(Debug, Eq, PartialEq, Hash)]
115struct PackageRef<'a> {
116 package_name: &'a PackageName,
117 version: &'a Version,
118 url: Option<&'a VerbatimParsedUrl>,
119 index: Option<&'a IndexUrl>,
120 extra: Option<&'a ExtraName>,
121 group: Option<&'a GroupName>,
122}
123
124impl ResolverOutput {
125 pub(crate) fn from_state(
127 resolutions: &[Resolution],
128 requirements: &[Requirement],
129 constraints: &Constraints,
130 overrides: &Overrides,
131 preferences: &Preferences,
132 index: &InMemoryIndex,
133 git: &GitResolver,
134 python: &PythonRequirement,
135 conflicts: &Conflicts,
136 resolution_strategy: &ResolutionStrategy,
137 options: Options,
138 ) -> Result<Self, ResolveError> {
139 let size_guess = resolutions[0].nodes.len();
140 let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
141 Graph::with_capacity(size_guess, size_guess);
142 let mut inverse: FxHashMap<PackageRef, NodeIndex<u32>> =
143 FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
144 let mut diagnostics = Vec::new();
145
146 let root_index = graph.add_node(ResolutionGraphNode::Root);
148
149 let mut seen = FxHashSet::default();
150 for resolution in resolutions {
151 for (package, version) in &resolution.nodes {
153 if !seen.insert((package, version)) {
154 continue;
156 }
157 Self::add_version(
158 &mut graph,
159 &mut inverse,
160 &mut diagnostics,
161 preferences,
162 &resolution.pins,
163 index,
164 git,
165 package,
166 version,
167 )?;
168 }
169 }
170
171 let mut seen = FxHashSet::default();
172 for resolution in resolutions {
173 let marker = resolution.env.try_universal_markers().unwrap_or_default();
174
175 for edge in &resolution.edges {
178 if !seen.insert((edge, marker)) {
179 continue;
181 }
182
183 Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
184 }
185 }
186
187 let requires_python = python.target().clone();
189
190 let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
191 resolution
195 .env
196 .try_universal_markers()
197 .into_iter()
198 .filter(|marker| !marker.is_true())
199 .collect()
200 } else {
201 resolutions
202 .iter()
203 .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
204 .collect()
205 };
206
207 let mut reachability = marker_reachability(&graph, &fork_markers);
209
210 let conflict_marker = ConflictMarker::from_conflicts(conflicts);
213 for index in graph.node_indices() {
214 if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
215 dist.marker = reachability.remove(&index).unwrap_or_default();
216 dist.marker.imbibe(conflict_marker);
217 }
218 }
219 for weight in graph.edge_weights_mut() {
220 weight.imbibe(conflict_marker);
221 }
222
223 simplify_conflict_markers(conflicts, &mut graph);
224
225 graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
227
228 if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
229 report_missing_lower_bounds(&graph, &mut diagnostics, constraints, overrides);
230 }
231
232 let output = Self {
233 graph,
234 requires_python,
235 diagnostics,
236 requirements: requirements.to_vec(),
237 constraints: constraints.clone(),
238 overrides: overrides.clone(),
239 options,
240 fork_markers,
241 };
242
243 if conflicts.is_empty() {
258 #[allow(unused_mut, reason = "Used in debug_assertions below")]
259 let mut conflicting = output.find_conflicting_distributions();
260 if !conflicting.is_empty() {
261 tracing::warn!(
262 "found {} conflicting distributions in resolution, \
263 please report this as a bug at \
264 https://github.com/astral-sh/uv/issues/new",
265 conflicting.len()
266 );
267 }
268 #[cfg(debug_assertions)]
277 if let Some(err) = conflicting.pop() {
278 return Err(ResolveError::ConflictingDistribution(err));
279 }
280 }
281 Ok(output)
282 }
283
284 fn add_edge(
285 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
286 inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
287 root_index: NodeIndex,
288 edge: &ResolutionDependencyEdge,
289 marker: UniversalMarker,
290 ) {
291 let from_index = edge.from.as_ref().map_or(root_index, |from| {
292 inverse[&PackageRef {
293 package_name: from,
294 version: &edge.from_version,
295 url: edge.from_url.as_ref(),
296 index: edge.from_index.as_ref(),
297 extra: edge.from_extra.as_ref(),
298 group: edge.from_group.as_ref(),
299 }]
300 });
301 let to_index = inverse[&PackageRef {
302 package_name: &edge.to,
303 version: &edge.to_version,
304 url: edge.to_url.as_ref(),
305 index: edge.to_index.as_ref(),
306 extra: edge.to_extra.as_ref(),
307 group: edge.to_group.as_ref(),
308 }];
309
310 let edge_marker = {
311 let mut edge_marker = edge.universal_marker();
312 edge_marker.and(marker);
313 edge_marker
314 };
315
316 if let Some(weight) = graph
317 .find_edge(from_index, to_index)
318 .and_then(|edge| graph.edge_weight_mut(edge))
319 {
320 weight.or(edge_marker);
323 } else {
324 graph.update_edge(from_index, to_index, edge_marker);
325 }
326 }
327
328 fn add_version<'a>(
329 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
330 inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
331 diagnostics: &mut Vec<ResolutionDiagnostic>,
332 preferences: &Preferences,
333 pins: &FilePins,
334 in_memory: &InMemoryIndex,
335 git: &GitResolver,
336 package: &'a ResolutionPackage,
337 version: &'a Version,
338 ) -> Result<(), ResolveError> {
339 let ResolutionPackage {
340 name,
341 extra,
342 dev: group,
343 url,
344 index,
345 } = &package;
346 let (dist, hashes, metadata) = Self::parse_dist(
348 name,
349 index.as_ref(),
350 url.as_ref(),
351 version,
352 pins,
353 diagnostics,
354 preferences,
355 in_memory,
356 git,
357 )?;
358
359 if let Some(metadata) = metadata.as_ref() {
360 if let Some(extra) = extra {
362 if !metadata.provides_extra.contains(extra) {
363 diagnostics.push(ResolutionDiagnostic::MissingExtra {
364 dist: dist.clone(),
365 extra: extra.clone(),
366 });
367 }
368 }
369
370 if let Some(dev) = group {
372 if !metadata.dependency_groups.contains_key(dev) {
373 diagnostics.push(ResolutionDiagnostic::MissingGroup {
374 dist: dist.clone(),
375 group: dev.clone(),
376 });
377 }
378 }
379 }
380
381 let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
383 dist,
384 name: name.clone(),
385 version: version.clone(),
386 extra: extra.clone(),
387 group: group.clone(),
388 hashes,
389 metadata,
390 marker: UniversalMarker::TRUE,
391 }));
392 inverse.insert(
393 PackageRef {
394 package_name: name,
395 version,
396 url: url.as_ref(),
397 index: index.as_ref(),
398 extra: extra.as_ref(),
399 group: group.as_ref(),
400 },
401 node,
402 );
403 Ok(())
404 }
405
406 fn parse_dist(
407 name: &PackageName,
408 index: Option<&IndexUrl>,
409 url: Option<&VerbatimParsedUrl>,
410 version: &Version,
411 pins: &FilePins,
412 diagnostics: &mut Vec<ResolutionDiagnostic>,
413 preferences: &Preferences,
414 in_memory: &InMemoryIndex,
415 git: &GitResolver,
416 ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
417 Ok(if let Some(url) = url {
418 let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
421 let metadata_id = Dist::from_url(name.clone(), url.clone())?.distribution_id();
422
423 let hashes = Self::get_hashes(
425 name,
426 index,
427 Some(url),
428 &metadata_id,
429 version,
430 preferences,
431 in_memory,
432 );
433
434 let metadata = {
436 let response = in_memory
437 .distributions()
438 .get(&metadata_id)
439 .unwrap_or_else(|| {
440 panic!("Every URL distribution should have metadata: {metadata_id:?}")
441 });
442
443 let MetadataResponse::Found(archive) = &*response else {
444 panic!("Every URL distribution should have metadata: {metadata_id:?}")
445 };
446
447 archive.metadata.clone()
448 };
449
450 (
451 ResolvedDist::Installable {
452 dist: Arc::new(dist),
453 version: Some(version.clone()),
454 },
455 hashes,
456 Some(metadata),
457 )
458 } else {
459 let (dist, metadata_id) = pins
460 .dist_and_id(name, version)
461 .expect("Every package should be pinned");
462 let dist = dist.clone();
463 let hashes_id = dist.distribution_id();
464
465 match dist.yanked() {
467 None | Some(Yanked::Bool(false)) => {}
468 Some(Yanked::Bool(true)) => {
469 diagnostics.push(ResolutionDiagnostic::YankedVersion {
470 dist: dist.clone(),
471 reason: None,
472 });
473 }
474 Some(Yanked::Reason(reason)) => {
475 diagnostics.push(ResolutionDiagnostic::YankedVersion {
476 dist: dist.clone(),
477 reason: Some(reason.to_string()),
478 });
479 }
480 }
481
482 let hashes = Self::get_hashes(
484 name,
485 index,
486 None,
487 &hashes_id,
488 version,
489 preferences,
490 in_memory,
491 );
492
493 let metadata = {
495 in_memory
496 .distributions()
497 .get(metadata_id)
498 .and_then(|response| {
499 if let MetadataResponse::Found(archive) = &*response {
500 Some(archive.metadata.clone())
501 } else {
502 None
503 }
504 })
505 };
506
507 (dist, hashes, metadata)
508 })
509 }
510
511 fn get_hashes(
514 name: &PackageName,
515 index: Option<&IndexUrl>,
516 url: Option<&VerbatimParsedUrl>,
517 metadata_id: &DistributionId,
518 version: &Version,
519 preferences: &Preferences,
520 in_memory: &InMemoryIndex,
521 ) -> HashDigests {
522 if let Some(digests) = preferences.match_hashes(name, version) {
524 if !digests.is_empty() {
525 return HashDigests::from(digests);
526 }
527 }
528
529 if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
531 if let MetadataResponse::Found(ref archive) = *metadata_response {
532 let mut digests = archive.hashes.clone();
533 digests.sort_unstable();
534 if !digests.is_empty() {
535 return digests;
536 }
537 }
538 }
539
540 if url.is_none() {
542 let implicit_response = in_memory.implicit().get(name);
544 let mut explicit_response = None;
545
546 let hashes = implicit_response
548 .as_ref()
549 .and_then(|response| {
550 if let VersionsResponse::Found(version_maps) = &**response {
551 Some(version_maps)
552 } else {
553 None
554 }
555 })
556 .into_iter()
557 .flatten()
558 .filter(|version_map| version_map.index() == index)
559 .find_map(|version_map| version_map.hashes(version))
560 .or_else(|| {
561 explicit_response = index
563 .and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
564 explicit_response
565 .as_ref()
566 .and_then(|response| {
567 if let VersionsResponse::Found(version_maps) = &**response {
568 Some(version_maps)
569 } else {
570 None
571 }
572 })
573 .into_iter()
574 .flatten()
575 .filter(|version_map| version_map.index() == index)
576 .find_map(|version_map| version_map.hashes(version))
577 });
578
579 if let Some(hashes) = hashes {
580 let mut digests = HashDigests::from(hashes);
581 digests.sort_unstable();
582 if !digests.is_empty() {
583 return digests;
584 }
585 }
586 }
587
588 HashDigests::empty()
589 }
590
591 fn dists(&self) -> impl Iterator<Item = &AnnotatedDist> {
593 self.graph
594 .node_indices()
595 .filter_map(move |index| match &self.graph[index] {
596 ResolutionGraphNode::Root => None,
597 ResolutionGraphNode::Dist(dist) => Some(dist),
598 })
599 }
600
601 pub(crate) fn base_dists(&self) -> impl Iterator<Item = (NodeIndex, &AnnotatedDist)> {
603 self.graph
604 .node_indices()
605 .filter_map(move |node_index| match &self.graph[node_index] {
606 ResolutionGraphNode::Root => None,
607 ResolutionGraphNode::Dist(dist) => dist.is_base().then_some((node_index, dist)),
608 })
609 }
610
611 pub fn len(&self) -> usize {
613 self.base_dists().count()
614 }
615
616 pub fn is_empty(&self) -> bool {
618 self.base_dists().next().is_none()
619 }
620
621 pub fn contains(&self, name: &PackageName) -> bool {
623 self.dists().any(|dist| dist.name() == name)
624 }
625
626 pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
628 &self.diagnostics
629 }
630
631 pub fn marker_tree(
652 &self,
653 index: &InMemoryIndex,
654 marker_env: &MarkerEnvironment,
655 ) -> Result<MarkerTree, Box<ParsedUrlError>> {
656 use uv_pep508::{
657 CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
658 MarkerOperator, MarkerTree,
659 };
660
661 #[derive(Debug, Eq, Hash, PartialEq)]
667 enum MarkerParam {
668 Version(CanonicalMarkerValueVersion),
669 String(CanonicalMarkerValueString),
670 }
671
672 fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
674 match marker_tree.kind() {
675 MarkerTreeKind::True => {}
676 MarkerTreeKind::False => {}
677 MarkerTreeKind::Version(marker) => {
678 set.insert(MarkerParam::Version(marker.key()));
679 for (_, tree) in marker.edges() {
680 add_marker_params_from_tree(tree, set);
681 }
682 }
683 MarkerTreeKind::String(marker) => {
684 set.insert(MarkerParam::String(marker.key()));
685 for (_, tree) in marker.children() {
686 add_marker_params_from_tree(tree, set);
687 }
688 }
689 MarkerTreeKind::In(marker) => {
690 set.insert(MarkerParam::String(marker.key()));
691 for (_, tree) in marker.children() {
692 add_marker_params_from_tree(tree, set);
693 }
694 }
695 MarkerTreeKind::Contains(marker) => {
696 set.insert(MarkerParam::String(marker.key()));
697 for (_, tree) in marker.children() {
698 add_marker_params_from_tree(tree, set);
699 }
700 }
701 MarkerTreeKind::Extra(marker) => {
707 for (_, tree) in marker.children() {
708 add_marker_params_from_tree(tree, set);
709 }
710 }
711 MarkerTreeKind::List(marker) => {
712 for (_, tree) in marker.children() {
713 add_marker_params_from_tree(tree, set);
714 }
715 }
716 }
717 }
718
719 let mut seen_marker_values = IndexSet::default();
720 for i in self.graph.node_indices() {
721 let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
722 continue;
723 };
724 let metadata_id = dist.dist.distribution_id();
725 let res = index
726 .distributions()
727 .get(&metadata_id)
728 .expect("every package in resolution graph has metadata");
729 let MetadataResponse::Found(archive, ..) = &*res else {
730 panic!("Every package should have metadata: {metadata_id:?}")
731 };
732 for req in self.constraints.apply(self.overrides.apply_for(
733 &dist.name,
734 &dist.version,
735 archive.metadata.requires_dist.iter(),
736 )) {
737 add_marker_params_from_tree(req.marker, &mut seen_marker_values);
738 }
739 }
740
741 for direct_req in self
743 .constraints
744 .apply(self.overrides.apply(self.requirements.iter()))
745 {
746 add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
747 }
748
749 let mut conjunction = MarkerTree::TRUE;
752 for marker_param in seen_marker_values {
753 let expr = match marker_param {
754 MarkerParam::Version(value_version) => {
755 let from_env = marker_env.get_version(value_version);
756 MarkerExpression::Version {
757 key: value_version.into(),
758 specifier: VersionSpecifier::equals_version(from_env.clone()),
759 }
760 }
761 MarkerParam::String(value_string) => {
762 let from_env = marker_env.get_string(value_string);
763 MarkerExpression::String {
764 key: value_string.into(),
765 operator: MarkerOperator::Equal,
766 value: from_env.into(),
767 }
768 }
769 };
770 conjunction.and(MarkerTree::expression(expr));
771 }
772 Ok(conjunction)
773 }
774
775 fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
784 let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
785 BTreeMap::new();
786 for node in self.graph.node_weights() {
787 let annotated_dist = match node {
788 ResolutionGraphNode::Root => continue,
789 ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
790 };
791 name_to_markers
792 .entry(&annotated_dist.name)
793 .or_default()
794 .push((&annotated_dist.version, &annotated_dist.marker));
795 }
796 let mut dupes = vec![];
797 for (name, marker_trees) in name_to_markers {
798 for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
799 for (version2, marker2) in &marker_trees[i + 1..] {
800 if version1 == version2 {
801 continue;
802 }
803 if !marker1.is_disjoint(**marker2) {
804 dupes.push(ConflictingDistributionError {
805 name: name.clone(),
806 version1: (*version1).clone(),
807 version2: (*version2).clone(),
808 marker1: **marker1,
809 marker2: **marker2,
810 });
811 }
812 }
813 }
814 }
815 dupes
816 }
817}
818
819#[derive(Debug)]
826pub struct ConflictingDistributionError {
827 name: PackageName,
828 version1: Version,
829 version2: Version,
830 marker1: UniversalMarker,
831 marker2: UniversalMarker,
832}
833
834impl std::error::Error for ConflictingDistributionError {}
835
836impl Display for ConflictingDistributionError {
837 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
838 let Self {
839 ref name,
840 ref version1,
841 ref version2,
842 ref marker1,
843 ref marker2,
844 } = *self;
845 write!(
846 f,
847 "found conflicting versions for package `{name}`:
848 `{marker1:?}` (for version `{version1}`) is not disjoint with \
849 `{marker2:?}` (for version `{version2}`)",
850 )
851 }
852}
853
854impl From<ResolverOutput> for uv_distribution_types::Resolution {
866 fn from(output: ResolverOutput) -> Self {
867 let ResolverOutput {
868 graph,
869 diagnostics,
870 fork_markers,
871 ..
872 } = output;
873
874 assert!(
875 fork_markers.is_empty(),
876 "universal resolutions are not supported"
877 );
878
879 let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
880 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
881
882 let root = transformed.add_node(Node::Root);
884
885 for index in graph.node_indices() {
887 let ResolutionGraphNode::Dist(dist) = &graph[index] else {
888 continue;
889 };
890 if dist.is_base() {
891 inverse.insert(
892 &dist.name,
893 transformed.add_node(Node::Dist {
894 dist: dist.dist.clone(),
895 hashes: dist.hashes.clone(),
896 install: true,
897 }),
898 );
899 }
900 }
901
902 for edge in graph.edge_indices() {
904 let (source, target) = graph.edge_endpoints(edge).unwrap();
905
906 match (&graph[source], &graph[target]) {
907 (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
908 let target = inverse[&target_dist.name()];
909 transformed.update_edge(root, target, Edge::Prod);
910 }
911 (
912 ResolutionGraphNode::Dist(source_dist),
913 ResolutionGraphNode::Dist(target_dist),
914 ) => {
915 let source = inverse[&source_dist.name()];
916 let target = inverse[&target_dist.name()];
917
918 let edge = if let Some(extra) = source_dist.extra.as_ref() {
919 Edge::Optional(extra.clone())
920 } else if let Some(group) = source_dist.group.as_ref() {
921 Edge::Dev(group.clone())
922 } else {
923 Edge::Prod
924 };
925
926 transformed.add_edge(source, target, edge);
927 }
928 _ => {
929 unreachable!("root should not contain incoming edges");
930 }
931 }
932 }
933
934 Self::new(transformed).with_diagnostics(diagnostics)
935 }
936}
937
938fn report_missing_lower_bounds(
940 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
941 diagnostics: &mut Vec<ResolutionDiagnostic>,
942 constraints: &Constraints,
943 overrides: &Overrides,
944) {
945 for node_index in graph.node_indices() {
946 let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
947 continue;
949 };
950 if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
951 diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
952 package_name: dist.name().clone(),
953 });
954 }
955 }
956}
957
958fn has_lower_bound(
960 node_index: NodeIndex,
961 package_name: &PackageName,
962 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
963 constraints: &Constraints,
964 overrides: &Overrides,
965) -> bool {
966 for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
967 let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
968 ResolutionGraphNode::Root => {
969 return true;
972 }
973 ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
974 };
975
976 if neighbor_dist.name() == package_name {
977 return true;
979 }
980
981 let Some(metadata) = neighbor_dist.metadata.as_ref() else {
982 return true;
984 };
985
986 for requirement in overrides
989 .apply_for(
990 neighbor_dist.name(),
991 &neighbor_dist.version,
992 metadata.requires_dist.iter(),
993 )
994 .chain(overrides.apply(metadata.dependency_groups.values().flatten()))
995 .chain(constraints.requirements().map(Cow::Borrowed))
997 {
998 if requirement.name != *package_name {
999 continue;
1000 }
1001 let Some(specifiers) = requirement.source.version_specifiers() else {
1002 return true;
1004 };
1005 if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1006 return true;
1007 }
1008 }
1009 }
1010 false
1011}