Skip to main content

uv_resolver/lock/
tree.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::fmt::Write;
4use std::path::Path;
5
6use either::Either;
7use itertools::Itertools;
8use owo_colors::OwoColorize;
9use petgraph::graph::{EdgeIndex, NodeIndex};
10use petgraph::prelude::EdgeRef;
11use petgraph::{Direction, Graph};
12use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
13use serde::Serialize;
14
15use uv_configuration::DependencyGroupsWithDefaults;
16use uv_console::human_readable_bytes;
17use uv_fs::PortablePathBuf;
18use uv_normalize::{ExtraName, GroupName, PackageName};
19use uv_pep440::Version;
20use uv_pep508::MarkerTree;
21use uv_pypi_types::ResolverMarkerEnvironment;
22
23use crate::lock::export::{
24    MetadataNode, MetadataNodeId, MetadataNodeKind, MetadataScript, MetadataWorkspace,
25    MetadataWorkspaceMember,
26};
27use crate::lock::{Package, PackageId};
28use crate::{ConflictMarker, Lock, PackageMap, UniversalMarker};
29
30#[derive(Debug, Clone, Copy)]
31pub enum TreeJsonTarget<'a> {
32    Workspace(&'a Path),
33    Script(&'a Path),
34}
35
36impl<'a> TreeJsonTarget<'a> {
37    fn root(self) -> &'a Path {
38        match self {
39            Self::Workspace(root) => root,
40            Self::Script(script) => script.parent().unwrap_or_else(|| Path::new("")),
41        }
42    }
43}
44
45#[derive(Debug)]
46pub struct TreeDisplay<'env> {
47    /// The constructed dependency graph.
48    graph: petgraph::graph::Graph<Node<'env>, Edge<'env>, petgraph::Directed>,
49    /// The packages considered as roots of the dependency tree.
50    roots: Vec<NodeIndex>,
51    /// The latest known version of each package.
52    latest: &'env PackageMap<Version>,
53    /// Maximum display depth of the dependency tree.
54    depth: usize,
55    /// Whether to de-duplicate the displayed dependencies.
56    no_dedupe: bool,
57    /// Whether the graph edges have been reversed (i.e., `--invert` mode).
58    invert: bool,
59    /// Whether production dependencies are included in the tree.
60    prod: bool,
61    /// The dependency groups included in the tree.
62    groups: DependencyGroupsWithDefaults,
63    /// Reference to the lock to look up additional metadata (e.g., wheel sizes).
64    lock: &'env Lock,
65    /// Whether to show sizes in the rendered output.
66    show_sizes: bool,
67    /// The marker constraints imposed by declared conflicting extras and groups.
68    conflict_marker: UniversalMarker,
69}
70
71impl<'env> TreeDisplay<'env> {
72    /// Create a new [`DisplayDependencyGraph`] for the set of installed packages.
73    pub fn new(
74        lock: &'env Lock,
75        markers: Option<&'env ResolverMarkerEnvironment>,
76        latest: &'env PackageMap<Version>,
77        depth: usize,
78        prune: &[PackageName],
79        packages: &[PackageName],
80        groups: &DependencyGroupsWithDefaults,
81        no_dedupe: bool,
82        invert: bool,
83        show_sizes: bool,
84    ) -> Self {
85        // Identify any workspace members.
86        //
87        // These include:
88        // - The members listed in the lockfile.
89        // - The root package, if it's not in the list of members. (The root package is omitted from
90        //   the list of workspace members for single-member workspaces with a `[project]` section,
91        //   to avoid cluttering the lockfile.
92        let members: BTreeSet<&PackageId> = if lock.members().is_empty() {
93            lock.root().into_iter().map(|package| &package.id).collect()
94        } else {
95            lock.packages
96                .iter()
97                .filter_map(|package| {
98                    if lock.members().contains(&package.id.name) {
99                        Some(&package.id)
100                    } else {
101                        None
102                    }
103                })
104                .collect()
105        };
106
107        // Conflict extras and groups are encoded as marker expressions. Include the declared
108        // mutual-exclusion constraints when checking whether a universal path is satisfiable.
109        let conflict_marker = UniversalMarker::new(
110            MarkerTree::TRUE,
111            ConflictMarker::from_conflicts(lock.conflicts()),
112        );
113
114        // Create a graph.
115        let size_guess = lock.packages.len();
116        let mut graph =
117            Graph::<Node, Edge, petgraph::Directed>::with_capacity(size_guess, size_guess);
118        let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
119        let mut queue: VecDeque<(&PackageId, Option<&ExtraName>)> = VecDeque::new();
120        let mut seen = FxHashSet::default();
121
122        let root = graph.add_node(Node::Root);
123
124        // Add the root packages to the graph.
125        for id in members.iter().copied() {
126            if prune.contains(&id.name) {
127                continue;
128            }
129
130            let dist = lock.find_by_id(id);
131
132            // Add the workspace package to the graph. Under `--only-group`, the workspace member
133            // may not be installed, but it's still relevant for the dependency tree, since we want
134            // to show the connection from the workspace package to the enabled dependency groups.
135            let index = *inverse
136                .entry(id)
137                .or_insert_with(|| graph.add_node(Node::Package(id)));
138
139            // Add an edge from the root.
140            graph.add_edge(root, index, Edge::Prod(None, UniversalMarker::TRUE));
141
142            if groups.prod() {
143                // Push its dependencies on the queue.
144                if seen.insert((id, None)) {
145                    queue.push_back((id, None));
146                }
147
148                // Push any extras on the queue.
149                for extra in dist.optional_dependencies.keys() {
150                    if seen.insert((id, Some(extra))) {
151                        queue.push_back((id, Some(extra)));
152                    }
153                }
154            }
155
156            // Add any development dependencies.
157            for (group, dep) in dist
158                .dependency_groups
159                .iter()
160                .filter_map(|(group, deps)| {
161                    if groups.contains(group) {
162                        Some(deps.iter().map(move |dep| (group, dep)))
163                    } else {
164                        None
165                    }
166                })
167                .flatten()
168            {
169                if prune.contains(&dep.package_id.name) {
170                    continue;
171                }
172
173                if markers
174                    .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
175                {
176                    continue;
177                }
178
179                // Add the dependency to the graph and get its index.
180                let dep_index = *inverse
181                    .entry(&dep.package_id)
182                    .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
183
184                // Add an edge from the workspace package.
185                graph.add_edge(
186                    index,
187                    dep_index,
188                    Edge::Dev(
189                        group,
190                        Some(RequestedExtras::Dependency(&dep.extra)),
191                        dep.complexified_marker,
192                    ),
193                );
194
195                // Push its dependencies on the queue.
196                if seen.insert((&dep.package_id, None)) {
197                    queue.push_back((&dep.package_id, None));
198                }
199                for extra in &dep.extra {
200                    if seen.insert((&dep.package_id, Some(extra))) {
201                        queue.push_back((&dep.package_id, Some(extra)));
202                    }
203                }
204            }
205        }
206
207        // Identify any packages that are connected directly to the synthetic root node, i.e.,
208        // requirements that are attached to the workspace itself.
209        //
210        // These include
211        // - `[dependency-groups]` dependencies for workspaces whose roots do not include a
212        //    `[project]` table, since those roots are not workspace members, but they _can_ define
213        //    dependencies.
214        // - `dependencies` in PEP 723 scripts.
215        {
216            // Index the lockfile by name.
217            let by_name: FxHashMap<_, Vec<_>> = {
218                lock.packages().iter().fold(
219                    FxHashMap::with_capacity_and_hasher(lock.len(), FxBuildHasher),
220                    |mut map, package| {
221                        map.entry(&package.id.name).or_default().push(package);
222                        map
223                    },
224                )
225            };
226
227            // Identify any requirements attached to the workspace itself.
228            for requirement in lock.requirements() {
229                for package in by_name.get(&requirement.name).into_iter().flatten() {
230                    // Determine whether this entry is "relevant" for the requirement, by intersecting
231                    // the markers.
232                    let marker = if package.fork_markers.is_empty() {
233                        requirement.marker
234                    } else {
235                        let mut combined = MarkerTree::FALSE;
236                        for fork_marker in &package.fork_markers {
237                            combined.or(fork_marker.pep508());
238                        }
239                        combined.and(requirement.marker);
240                        combined
241                    };
242                    if marker.is_false() {
243                        continue;
244                    }
245                    if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
246                        continue;
247                    }
248                    // Add the package to the graph.
249                    let index = inverse
250                        .entry(&package.id)
251                        .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
252
253                    // Add an edge from the root.
254                    graph.add_edge(
255                        root,
256                        *index,
257                        Edge::Prod(
258                            Some(RequestedExtras::Requirement(requirement.extras.as_ref())),
259                            UniversalMarker::from_combined(marker),
260                        ),
261                    );
262
263                    // Push its dependencies on the queue.
264                    if seen.insert((&package.id, None)) {
265                        queue.push_back((&package.id, None));
266                    }
267                    for extra in &*requirement.extras {
268                        if seen.insert((&package.id, Some(extra))) {
269                            queue.push_back((&package.id, Some(extra)));
270                        }
271                    }
272                }
273            }
274
275            // Identify any dependency groups attached to the workspace itself.
276            for (group, requirements) in lock.dependency_groups() {
277                if !groups.contains(group) {
278                    continue;
279                }
280                for requirement in requirements {
281                    for package in by_name.get(&requirement.name).into_iter().flatten() {
282                        // Determine whether this entry is "relevant" for the requirement, by intersecting
283                        // the markers.
284                        let marker = if package.fork_markers.is_empty() {
285                            requirement.marker
286                        } else {
287                            let mut combined = MarkerTree::FALSE;
288                            for fork_marker in &package.fork_markers {
289                                combined.or(fork_marker.pep508());
290                            }
291                            combined.and(requirement.marker);
292                            combined
293                        };
294                        if marker.is_false() {
295                            continue;
296                        }
297                        if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
298                            continue;
299                        }
300                        // Add the package to the graph.
301                        let index = inverse
302                            .entry(&package.id)
303                            .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
304
305                        // Add an edge from the root.
306                        graph.add_edge(
307                            root,
308                            *index,
309                            Edge::Dev(
310                                group,
311                                Some(RequestedExtras::Requirement(requirement.extras.as_ref())),
312                                UniversalMarker::from_combined(marker),
313                            ),
314                        );
315
316                        // Push its dependencies on the queue.
317                        if seen.insert((&package.id, None)) {
318                            queue.push_back((&package.id, None));
319                        }
320                        for extra in &*requirement.extras {
321                            if seen.insert((&package.id, Some(extra))) {
322                                queue.push_back((&package.id, Some(extra)));
323                            }
324                        }
325                    }
326                }
327            }
328        }
329
330        // Create all the relevant nodes.
331        while let Some((id, extra)) = queue.pop_front() {
332            let index = inverse[&id];
333            let package = lock.find_by_id(id);
334
335            let deps = if let Some(extra) = extra {
336                Either::Left(
337                    package
338                        .optional_dependencies
339                        .get(extra)
340                        .into_iter()
341                        .flatten(),
342                )
343            } else {
344                Either::Right(package.dependencies.iter())
345            };
346
347            for dep in deps {
348                if prune.contains(&dep.package_id.name) {
349                    continue;
350                }
351
352                if markers
353                    .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
354                {
355                    continue;
356                }
357
358                // Add the dependency to the graph.
359                let dep_index = *inverse
360                    .entry(&dep.package_id)
361                    .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
362
363                // Add an edge from the workspace package.
364                graph.add_edge(
365                    index,
366                    dep_index,
367                    if let Some(extra) = extra {
368                        Edge::Optional(
369                            extra,
370                            Some(RequestedExtras::Dependency(&dep.extra)),
371                            dep.complexified_marker,
372                        )
373                    } else {
374                        Edge::Prod(
375                            Some(RequestedExtras::Dependency(&dep.extra)),
376                            dep.complexified_marker,
377                        )
378                    },
379                );
380
381                // Push its dependencies on the queue.
382                if seen.insert((&dep.package_id, None)) {
383                    queue.push_back((&dep.package_id, None));
384                }
385                for extra in &dep.extra {
386                    if seen.insert((&dep.package_id, Some(extra))) {
387                        queue.push_back((&dep.package_id, Some(extra)));
388                    }
389                }
390            }
391        }
392
393        // Filter the graph to remove any unreachable nodes.
394        {
395            let mut reachable = graph
396                .node_indices()
397                .filter(|index| match graph[*index] {
398                    Node::Package(package_id) => members.contains(package_id),
399                    Node::Root => true,
400                })
401                .collect::<FxHashSet<_>>();
402            let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
403            while let Some(node) = stack.pop_front() {
404                for edge in graph.edges_directed(node, Direction::Outgoing) {
405                    if reachable.insert(edge.target()) {
406                        stack.push_back(edge.target());
407                    }
408                }
409            }
410
411            // Remove the unreachable nodes from the graph.
412            graph.retain_nodes(|_, index| reachable.contains(&index));
413        }
414
415        // Reverse the graph.
416        if invert {
417            graph.reverse();
418        }
419
420        // Filter the graph to those nodes reachable from the target packages.
421        if !packages.is_empty() {
422            let mut reachable = graph
423                .node_indices()
424                .filter(|index| {
425                    let Node::Package(package_id) = graph[*index] else {
426                        return false;
427                    };
428                    packages.contains(&package_id.name)
429                })
430                .collect::<FxHashSet<_>>();
431            let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
432            while let Some(node) = stack.pop_front() {
433                for edge in graph.edges_directed(node, Direction::Outgoing) {
434                    if reachable.insert(edge.target()) {
435                        stack.push_back(edge.target());
436                    }
437                }
438            }
439
440            // Remove the unreachable nodes from the graph.
441            graph.retain_nodes(|_, index| reachable.contains(&index));
442        }
443
444        // Compute the list of roots.
445        let roots = {
446            // If specific packages were requested, use them as roots.
447            if !packages.is_empty() {
448                let mut roots = graph
449                    .node_indices()
450                    .filter(|index| {
451                        let Node::Package(package_id) = graph[*index] else {
452                            return false;
453                        };
454                        packages.contains(&package_id.name)
455                    })
456                    .collect::<Vec<_>>();
457
458                // Sort the roots.
459                roots.sort_by_key(|index| &graph[*index]);
460
461                roots
462            } else {
463                let mut roots = if invert {
464                    // For inverted trees, find leaf packages (nodes with no incoming
465                    // edges).
466                    graph
467                        .node_indices()
468                        .filter(|index| {
469                            graph
470                                .edges_directed(*index, Direction::Incoming)
471                                .next()
472                                .is_none()
473                        })
474                        .collect::<Vec<_>>()
475                } else {
476                    // For non-inverted trees, use the root node directly.
477                    graph
478                        .node_indices()
479                        .filter(|index| matches!(graph[*index], Node::Root))
480                        .collect::<Vec<_>>()
481                };
482
483                roots.sort_by_key(|index| &graph[*index]);
484                roots
485            }
486        };
487
488        Self {
489            graph,
490            roots,
491            latest,
492            depth,
493            no_dedupe,
494            invert,
495            prod: groups.prod(),
496            groups: groups.clone(),
497            lock,
498            show_sizes,
499            conflict_marker,
500        }
501    }
502
503    /// Perform a depth-first traversal of the given package and its dependencies.
504    fn visit(
505        &'env self,
506        cursor: Cursor,
507        visited: &mut FxHashMap<VisitedNode<'env>, Vec<&'env PackageId>>,
508        path: &mut Vec<VisitedNode<'env>>,
509    ) -> Vec<String> {
510        // Short-circuit if the current path is longer than the provided depth.
511        if path.len() > self.depth {
512            return Vec::new();
513        }
514
515        let Node::Package(package_id) = self.graph[cursor.node()] else {
516            return Vec::new();
517        };
518        let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]);
519        let package = self.lock.find_by_id(package_id);
520
521        let expanded_extras = self.expanded_extras(package, edge);
522        let visited_node = VisitedNode {
523            package_id,
524            expanded_extras: expanded_extras.clone(),
525            marker: self.invert.then_some(cursor.marker()),
526        };
527
528        let line = {
529            let mut line = format!("{}", package_id.name);
530
531            if let Some(extras) = edge.and_then(Edge::extras) {
532                if !extras.is_empty() {
533                    line.push('[');
534                    line.push_str(extras.iter().join(", ").as_str());
535                    line.push(']');
536                }
537            }
538
539            if let Some(version) = package_id.version.as_ref() {
540                line.push(' ');
541                line.push('v');
542                let _ = write!(line, "{version}");
543            }
544
545            if let Some(edge) = edge {
546                match edge {
547                    Edge::Prod(..) => {}
548                    Edge::Optional(extra, ..) => {
549                        let _ = write!(line, " (extra: {extra})");
550                    }
551                    Edge::Dev(group, ..) => {
552                        let _ = write!(line, " (group: {group})");
553                    }
554                }
555            }
556
557            // Append compressed wheel size, if available in the lockfile.
558            // Keep it simple: use the first wheel entry that includes a size.
559            if self.show_sizes {
560                if let Some(size_bytes) = package.wheels.iter().find_map(|wheel| wheel.size) {
561                    let (bytes, unit) = human_readable_bytes(size_bytes);
562                    line.push(' ');
563                    line.push_str(format!("{}", format!("({bytes:.1}{unit})").dimmed()).as_str());
564                }
565            }
566
567            line
568        };
569
570        // Skip the traversal if:
571        // 1. The package is in the current traversal path (i.e., a dependency cycle).
572        // 2. The package has been visited and de-duplication is enabled (default).
573        if path.contains(&visited_node) {
574            return vec![format!("{line} (*)")];
575        }
576        if !self.no_dedupe
577            && let Some(requirements) = visited.get(&visited_node)
578        {
579            return if requirements.is_empty() {
580                vec![line]
581            } else {
582                vec![format!("{line} (*)")]
583            };
584        }
585
586        // Incorporate the latest version of the package, if known.
587        let line = if let Some(version) = self.latest.get(package_id) {
588            format!("{line} {}", format!("(latest: v{version})").bold().cyan())
589        } else {
590            line
591        };
592
593        let mut dependencies = if self.invert && edge.is_some_and(Edge::is_dev) {
594            // A member's dependency group is activated for the root member. It is not part of the
595            // member when that member is installed as another package's dependency.
596            Vec::new()
597        } else {
598            self.graph
599                .edges_directed(cursor.node(), Direction::Outgoing)
600                .filter_map(|edge| match self.graph[edge.target()] {
601                    Node::Root => None,
602                    Node::Package(_) => {
603                        let edge_kind = &self.graph[edge.id()];
604
605                        if self.invert {
606                            // If the path to the target requires an extra on this package, only
607                            // follow consumers that activate that extra.
608                            if !expanded_extras.is_empty()
609                                && edge_kind.extras().is_none_or(|extras| {
610                                    !expanded_extras.iter().all(|extra| extras.contains(extra))
611                                })
612                            {
613                                return None;
614                            }
615
616                            // A package node can appear in several universal marker branches. Do
617                            // not join incoming and outgoing edges that cannot coexist.
618                            let mut marker = cursor.marker();
619                            marker.and(edge_kind.marker());
620                            if marker.is_false() {
621                                return None;
622                            }
623                            Some(Cursor::new(edge.target(), edge.id(), marker))
624                        } else {
625                            // Only include extra-conditional dependencies if the activating extra
626                            // is enabled in the current context.
627                            if let Edge::Optional(required_extra, ..) = edge_kind
628                                && !expanded_extras.contains(required_extra)
629                            {
630                                return None;
631                            }
632                            Some(Cursor::new(edge.target(), edge.id(), UniversalMarker::TRUE))
633                        }
634                    }
635                })
636                .collect::<Vec<_>>()
637        };
638        dependencies.sort_by_key(|cursor| {
639            let node = &self.graph[cursor.node()];
640            let edge = cursor
641                .edge()
642                .map(|edge_id| &self.graph[edge_id])
643                .map(Edge::kind);
644            (edge, node)
645        });
646
647        let mut lines = vec![line];
648
649        // Keep track of the dependency path to avoid cycles.
650        // Only mark as visited if we're going to expand children (not at depth limit).
651        if path.len() < self.depth {
652            visited.insert(
653                visited_node.clone(),
654                dependencies
655                    .iter()
656                    .filter_map(|node| match self.graph[node.node()] {
657                        Node::Package(package_id) => Some(package_id),
658                        Node::Root => None,
659                    })
660                    .collect(),
661            );
662        }
663        path.push(visited_node);
664
665        for (index, dep) in dependencies.iter().enumerate() {
666            // For sub-visited packages, add the prefix to make the tree display user-friendly.
667            // The key observation here is you can group the tree as follows when you're at the
668            // root of the tree:
669            // root_package
670            // ├── level_1_0          // Group 1
671            // │   ├── level_2_0      ...
672            // │   │   ├── level_3_0  ...
673            // │   │   └── level_3_1  ...
674            // │   └── level_2_1      ...
675            // ├── level_1_1          // Group 2
676            // │   ├── level_2_2      ...
677            // │   └── level_2_3      ...
678            // └── level_1_2          // Group 3
679            //     └── level_2_4      ...
680            //
681            // The lines in Group 1 and 2 have `├── ` at the top and `|   ` at the rest while
682            // those in Group 3 have `└── ` at the top and `    ` at the rest.
683            // This observation is true recursively even when looking at the subtree rooted
684            // at `level_1_0`.
685            let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index {
686                ("└── ", "    ")
687            } else {
688                ("├── ", "│   ")
689            };
690            for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate()
691            {
692                let prefix = if visited_index == 0 {
693                    prefix_top
694                } else {
695                    prefix_rest
696                };
697                lines.push(format!("{prefix}{visited_line}"));
698            }
699        }
700
701        path.pop();
702
703        lines
704    }
705
706    /// Depth-first traverse the nodes to render the tree.
707    fn render(&self) -> Vec<String> {
708        let mut path = Vec::new();
709        let mut lines = Vec::with_capacity(self.graph.node_count());
710        let mut visited =
711            FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
712
713        for node in &self.roots {
714            match self.graph[*node] {
715                Node::Root => {
716                    for edge in self.graph.edges_directed(*node, Direction::Outgoing) {
717                        let node = edge.target();
718                        path.clear();
719                        lines.extend(self.visit(
720                            Cursor::new(node, edge.id(), self.conflict_marker),
721                            &mut visited,
722                            &mut path,
723                        ));
724                    }
725                }
726                Node::Package(_) => {
727                    path.clear();
728                    lines.extend(self.visit(
729                        Cursor::root(*node, self.conflict_marker),
730                        &mut visited,
731                        &mut path,
732                    ));
733                }
734            }
735        }
736
737        lines
738    }
739
740    /// Return the extras that can change this package's rendered child list.
741    fn expanded_extras(
742        &self,
743        package: &'env Package,
744        edge: Option<&Edge<'env>>,
745    ) -> BTreeSet<&'env ExtraName> {
746        if self.invert {
747            // In inverted mode, an optional edge records the extra that must have been activated
748            // on this package for the path to exist.
749            return edge.and_then(Edge::required_extra).into_iter().collect();
750        }
751
752        let Some(requested_extras) = edge.and_then(Edge::extras) else {
753            // Roots are rendered with all optional dependency groups expanded.
754            return package.optional_dependencies.keys().collect();
755        };
756
757        requested_extras
758            .iter()
759            .filter(|extra| package.optional_dependencies.contains_key(*extra))
760            .collect()
761    }
762
763    /// Serialize the displayed dependency graph as JSON.
764    pub fn to_json(&self, target: TreeJsonTarget<'_>) -> Result<String, serde_json::Error> {
765        serde_json::to_string_pretty(&JsonGraph::new(self, target))
766    }
767
768    /// Return the packages and edges reachable from the displayed roots within the requested
769    /// depth.
770    ///
771    /// Depth follows the text tree's package-graph semantics. The targets of edges from
772    /// [`Node::Root`] start at depth zero; the synthetic root itself does not consume a level.
773    /// JSON subsequently represents a script or workspace-owned dependency group as an explicit
774    /// node, so its direct requirements remain at depth zero despite appearing one edge away from
775    /// a root in the serialized graph. Structural extra-to-package relationships likewise do not
776    /// participate in depth traversal.
777    fn json_traversal(&self) -> JsonTraversal {
778        let mut distances = FxHashMap::default();
779        let mut queue = VecDeque::new();
780        let mut nodes = FxHashSet::default();
781        let mut edges = FxHashSet::default();
782
783        for root in &self.roots {
784            match self.graph[*root] {
785                Node::Root => {
786                    for edge in self.graph.edges_directed(*root, Direction::Outgoing) {
787                        let Node::Package(package_id) = self.graph[edge.target()] else {
788                            continue;
789                        };
790                        let state = JsonTraversalNode {
791                            index: edge.target(),
792                            expanded_extras: self.expanded_extras(
793                                self.lock.find_by_id(package_id),
794                                Some(edge.weight()),
795                            ),
796                            marker: UniversalMarker::TRUE,
797                            reached_via_dependency_group: false,
798                        };
799                        nodes.insert(state.index);
800                        if distances.insert(state.clone(), 0).is_none() {
801                            queue.push_back(state);
802                        }
803                    }
804                }
805                Node::Package(package_id) => {
806                    let state = JsonTraversalNode {
807                        index: *root,
808                        expanded_extras: self
809                            .expanded_extras(self.lock.find_by_id(package_id), None),
810                        marker: if self.invert {
811                            self.conflict_marker
812                        } else {
813                            UniversalMarker::TRUE
814                        },
815                        reached_via_dependency_group: false,
816                    };
817                    nodes.insert(state.index);
818                    if distances.insert(state.clone(), 0).is_none() {
819                        queue.push_back(state);
820                    }
821                }
822            }
823        }
824
825        while let Some(source) = queue.pop_front() {
826            let distance = distances[&source];
827            if distance >= self.depth || self.invert && source.reached_via_dependency_group {
828                continue;
829            }
830
831            for edge in self.graph.edges_directed(source.index, Direction::Outgoing) {
832                let edge_kind = edge.weight();
833                let marker = if self.invert {
834                    // If the path to the target requires an extra on this package, only follow
835                    // consumers that activate that extra.
836                    if !source.expanded_extras.is_empty()
837                        && edge_kind.extras().is_none_or(|extras| {
838                            !source
839                                .expanded_extras
840                                .iter()
841                                .all(|extra| extras.contains(extra))
842                        })
843                    {
844                        continue;
845                    }
846
847                    // Do not join incoming and outgoing edges that cannot coexist in the same
848                    // universal marker environment.
849                    let mut marker = source.marker;
850                    marker.and(edge_kind.marker());
851                    if marker.is_false() {
852                        continue;
853                    }
854                    marker
855                } else {
856                    // Only include extra-conditional dependencies if the activating extra is
857                    // enabled in the current context.
858                    if let Edge::Optional(required_extra, ..) = edge_kind
859                        && !source.expanded_extras.contains(required_extra)
860                    {
861                        continue;
862                    }
863                    UniversalMarker::TRUE
864                };
865
866                let target = edge.target();
867                if matches!(self.graph[target], Node::Root) {
868                    edges.insert(edge.id());
869                    continue;
870                }
871                let Node::Package(package_id) = self.graph[target] else {
872                    continue;
873                };
874                let state = JsonTraversalNode {
875                    index: target,
876                    expanded_extras: self
877                        .expanded_extras(self.lock.find_by_id(package_id), Some(edge.weight())),
878                    marker,
879                    reached_via_dependency_group: self.invert && edge_kind.is_dev(),
880                };
881                nodes.insert(state.index);
882                edges.insert(edge.id());
883                if !distances.contains_key(&state) {
884                    distances.insert(state.clone(), distance + 1);
885                    queue.push_back(state);
886                }
887            }
888        }
889
890        JsonTraversal { nodes, edges }
891    }
892}
893
894#[derive(Debug)]
895struct JsonTraversal {
896    nodes: FxHashSet<NodeIndex>,
897    edges: FxHashSet<EdgeIndex>,
898}
899
900#[derive(Debug, Clone, PartialEq, Eq, Hash)]
901struct JsonTraversalNode<'env> {
902    index: NodeIndex,
903    expanded_extras: BTreeSet<&'env ExtraName>,
904    marker: UniversalMarker,
905    reached_via_dependency_group: bool,
906}
907
908/// A JSON representation of the output of `uv tree`.
909///
910/// The core format is the one from `uv workspace metadata` ([`crate::lock::export::Metadata`]),
911/// because they're representing essentially the same data (the resolved dependency graph).
912///
913/// The two formats most notably diverge in what can or can't be roots, because `uv tree`
914/// supports filtering and inverting the graph. So while `metadata` has fixed "members",
915/// "workspace", and "script" entry points, `tree` needs to cope with filters
916/// and inversion making random nodes roots (that said, having workspace/member/script entries
917/// is still useful for quickly identifying those special kinds of node in the graph).
918/// (As with metadata it's discouraged for you to just iterate the `resolution`: you should
919/// start at a given entry point and traverse the graph from there.)
920///
921/// These more advanced operations raise interesting questions for the graph representation.
922/// The following discussion assumes you've read the documentation on
923/// [`crate::lock::export::MetadataNode`] and understand the notion of Node and Edge we use.
924///
925///
926/// # Roots
927///
928/// As noted in those docs, pedantically `roots` should include `mypackage`, `mypackage[extra]`,
929/// `mypackage:group` as separate roots. At first this seemed like a stance worth rejecting
930/// for ergonomics and clarity, but as I tried to rationalize the semantics of `uv tree` it
931/// felt increasingly necessary.
932///
933/// In particular, because `uv tree` has some limited support for changing what parts of the
934/// graph are "active" with flags like `--all-groups` or `--no-default-groups`, we "need" a
935/// way to refer to a package's groups without referring to the package itself. You can't
936/// actually toggle the extras on the workspace but I consider that a bug, and so our format
937/// should ideally support referring to *specifically* "a package with some extra(s) activated".
938///
939/// Thus with everything activated you *may* find all of `mypackage`, `mypackage[extra1]`,
940/// `mypackage[extra2]`, `mypackage:group1`, `mypackage:group2` in the `roots` list.
941///
942/// Conversely, we will *exclude* the workspace node from the `roots`, as it is a purely virtual
943/// concept that only exists to hang workspace-exclusive groups from (e.g. when you define
944/// `dependency-groups` in a `pyproject.toml` that does not contain a `[project]` table).
945///
946///
947/// # Depth
948///
949/// Node depth is an annoying concept. The baseline of the theory here is once again
950/// an appeal to `mypackage`, `mypackage[extra]`, and `mypackage:group` being all on an equal
951/// footing. However, `mypackage[extra]` and `mypackage:group` in the graph are "virtual"
952/// in the sense that they don't actually refer to a thing to install, but are instead a
953/// list of dependencies that should all be installed together.
954///
955/// Let's start with the nice examples with obvious answers to establish a baseline:
956/// a uv workspace with no extras or groups, just pure production dependencies.
957///
958/// * depth 0: lists off all workspace members
959/// * depth 1: lists off all workspace members and their direct dependencies
960/// * depth 2: lists off all workspace members and two levels of dependencies
961///
962/// So ok depth here refers to how many levels of edges we're willing to follow, great!
963/// Now let's add dependency groups and extras.
964///
965/// Do you list the *existence* of `mypackage:group` or `mypackage[extra]` at depth 0?
966/// Or do you only acknowledge their existence at depth 1 when they would be non-empty?
967///
968/// In the current textual display of `uv tree` we choose the second answer essentially
969/// garbage-collecting extras and groups that would be empty because all their edges
970/// have been deleted. For now the JSON output respects this behaviour, but we may
971/// change that decision if we decide we don't like it:
972/// <https://github.com/astral-sh/uv/issues/19973>
973///
974/// Now to be clear we do this "edge" analysis before lowering to the output graph,
975/// and this matters for several cases.
976///
977/// First, scripts and workspaces aren't considered nodes before the lowering, and
978/// so script dependencies and workspace-group dependencies appear at depth 0 (another case where
979/// the JSON output respects the behaviour of the textual output):
980/// <https://github.com/astral-sh/uv/issues/19976>
981///
982/// Second, the fact that `mypackage` is a dependency of `mypackage[extra]`.
983/// Specifically, in `metadata` if `foo` depends on `bar[extra1, extra2]`
984/// then we will only include edges to `bar[extra1]` and `bar[extra2]` and not to `bar` itself,
985/// because we know those two extra nodes will include the edge to `bar` anyway (nothing requires
986/// this, it just seemed tidier to simplify the graph in that way).
987///
988/// As long as we want to do that simplification, it is *not* correct for us to
989/// cut the edge from the extra to the package, and so we don't guarantee a simple
990/// statement like "the resulting graph will have at most depth N" when counting
991/// `dependencies` (and that's all muddy anyway since there can be cycles
992/// in the final graph).
993///
994///
995/// # Inversion
996///
997/// `--invert` should flip the edges of the graph, turning the leaves into roots
998/// (with operations like `--depth` being applied afterwards).
999///
1000/// Unfortunately this is ill-defined at the moment in the face of leaf-cycles:
1001/// <https://github.com/astral-sh/uv/issues/19972>
1002///
1003/// Ignoring the issue of cycles, the only thing to note here is that only the
1004/// `dependencies` lists of nodes should be inverted. The `optional_dependencies`
1005/// and `dependency_groups` listings remain unchanged, because those aren't edges
1006/// of the graph, they're metadata on those packages (or the workspace).
1007#[derive(Debug, Serialize)]
1008struct JsonGraph {
1009    schema: JsonSchema,
1010    workspace_root: PortablePathBuf,
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    script: Option<MetadataScript>,
1013    #[serde(skip_serializing_if = "Option::is_none")]
1014    workspace: Option<MetadataWorkspace>,
1015    roots: Vec<JsonRoot>,
1016    inverted: bool,
1017    /// Workspace members included in the projected resolution.
1018    #[serde(skip_serializing_if = "Vec::is_empty")]
1019    members: Vec<MetadataWorkspaceMember>,
1020    resolution: BTreeMap<String, MetadataNode>,
1021}
1022
1023impl JsonGraph {
1024    fn new(tree: &TreeDisplay<'_>, target: TreeJsonTarget<'_>) -> Self {
1025        let traversal = tree.json_traversal();
1026        let workspace_root = PortablePathBuf::from(target.root());
1027        let mut builder = JsonGraphBuilder::new(tree, workspace_root.clone());
1028
1029        for index in traversal.nodes.iter().copied() {
1030            let Node::Package(package_id) = tree.graph[index] else {
1031                continue;
1032            };
1033            builder.ensure_package(package_id, MetadataNodeKind::Package);
1034        }
1035
1036        for edge in tree
1037            .graph
1038            .edge_references()
1039            .filter(|edge| traversal.edges.contains(&edge.id()))
1040        {
1041            builder.add_package_edge(edge.source(), edge.target(), edge.weight());
1042        }
1043
1044        builder.add_target_edges(target, &traversal);
1045        let (script, workspace) = match target {
1046            TreeJsonTarget::Script(path) => {
1047                let path = PortablePathBuf::from(path);
1048                let id = builder.ensure_script(path.as_ref());
1049                (Some(MetadataScript::new(path, id)), None)
1050            }
1051            TreeJsonTarget::Workspace(path) => {
1052                let path = PortablePathBuf::from(path);
1053                let id = builder.ensure_workspace();
1054                (None, Some(MetadataWorkspace::new(path, id)))
1055            }
1056        };
1057        let roots = builder.roots(target);
1058        let members = builder.members(target);
1059        let resolution = builder.finish();
1060
1061        Self {
1062            schema: JsonSchema {
1063                version: JsonSchemaVersion::Preview,
1064            },
1065            workspace_root,
1066            script,
1067            workspace,
1068            roots,
1069            inverted: tree.invert,
1070            members,
1071            resolution,
1072        }
1073    }
1074}
1075
1076struct JsonGraphBuilder<'tree, 'env> {
1077    tree: &'tree TreeDisplay<'env>,
1078    workspace_root: PortablePathBuf,
1079    resolution: BTreeMap<String, MetadataNode>,
1080}
1081
1082impl<'tree, 'env> JsonGraphBuilder<'tree, 'env> {
1083    fn new(tree: &'tree TreeDisplay<'env>, workspace_root: PortablePathBuf) -> Self {
1084        Self {
1085            tree,
1086            workspace_root,
1087            resolution: BTreeMap::new(),
1088        }
1089    }
1090
1091    fn ensure_node(&mut self, identity: MetadataNodeId) -> String {
1092        let id = identity.to_flat();
1093        self.resolution
1094            .entry(id.clone())
1095            .or_insert_with(|| MetadataNode::new(identity));
1096        id
1097    }
1098
1099    fn ensure_package(&mut self, package_id: &'env PackageId, kind: MetadataNodeKind) -> String {
1100        let is_package = matches!(kind, MetadataNodeKind::Package);
1101        let id = MetadataNodeId::from_package_id(&self.workspace_root, package_id, kind.clone())
1102            .to_flat();
1103        self.resolution.entry(id.clone()).or_insert_with(|| {
1104            let package = self.tree.lock.find_by_id(package_id);
1105            let mut node = MetadataNode::from_package_id(&self.workspace_root, package_id, kind);
1106            if is_package {
1107                node.set_latest_version(self.tree.latest.get(package_id).cloned());
1108                node.set_wheels_from_package(&self.workspace_root, package);
1109            }
1110            node
1111        });
1112        id
1113    }
1114
1115    fn ensure_extra(&mut self, package_id: &'env PackageId, extra: &ExtraName) -> String {
1116        let package = self.ensure_package(package_id, MetadataNodeKind::Package);
1117        let extra_id = self.ensure_package(package_id, MetadataNodeKind::Extra(extra.clone()));
1118        self.add_link(
1119            package.clone(),
1120            extra_id.clone(),
1121            JsonLink::Optional(extra.clone()),
1122        );
1123        self.add_link(extra_id.clone(), package, JsonLink::Dependency(None));
1124        extra_id
1125    }
1126
1127    fn ensure_group(&mut self, package_id: &'env PackageId, group: &GroupName) -> String {
1128        let package = self.ensure_package(package_id, MetadataNodeKind::Package);
1129        let group_id = self.ensure_package(package_id, MetadataNodeKind::Group(group.clone()));
1130        self.add_link(package, group_id.clone(), JsonLink::Group(group.clone()));
1131        group_id
1132    }
1133
1134    fn ensure_workspace(&mut self) -> String {
1135        self.ensure_node(MetadataNodeId::from_workspace(self.workspace_root.clone()))
1136    }
1137
1138    fn ensure_workspace_group(&mut self, group: &GroupName) -> String {
1139        let workspace = self.ensure_workspace();
1140        let group_id = self.ensure_node(MetadataNodeId::from_workspace_group(
1141            self.workspace_root.clone(),
1142            group.clone(),
1143        ));
1144        self.add_link(workspace, group_id.clone(), JsonLink::Group(group.clone()));
1145        group_id
1146    }
1147
1148    fn ensure_script(&mut self, path: &Path) -> String {
1149        self.ensure_node(MetadataNodeId::from_script(PortablePathBuf::from(path)))
1150    }
1151
1152    fn dependency_targets(
1153        &mut self,
1154        package_id: &'env PackageId,
1155        extras: Option<RequestedExtras<'env>>,
1156    ) -> Vec<String> {
1157        let Some(extras) = extras.filter(|extras| !extras.is_empty()) else {
1158            return vec![self.ensure_package(package_id, MetadataNodeKind::Package)];
1159        };
1160        extras
1161            .iter()
1162            .map(|extra| self.ensure_extra(package_id, extra))
1163            .collect()
1164    }
1165
1166    fn add_package_edge(&mut self, source: NodeIndex, target: NodeIndex, edge: &Edge<'env>) {
1167        let (source, target) = if self.tree.invert {
1168            (target, source)
1169        } else {
1170            (source, target)
1171        };
1172        let (Node::Package(source), Node::Package(target)) =
1173            (&self.tree.graph[source], &self.tree.graph[target])
1174        else {
1175            return;
1176        };
1177        let (source, target) = (*source, *target);
1178
1179        let source = match edge {
1180            Edge::Prod(..) => self.ensure_package(source, MetadataNodeKind::Package),
1181            Edge::Optional(extra, ..) => self.ensure_extra(source, extra),
1182            Edge::Dev(group, ..) => self.ensure_group(source, group),
1183        };
1184        let marker = self.marker(edge);
1185        for target in self.dependency_targets(target, edge.extras()) {
1186            self.add_link(source.clone(), target, JsonLink::Dependency(marker.clone()));
1187        }
1188    }
1189
1190    fn add_target_edges(&mut self, target: TreeJsonTarget<'_>, traversal: &JsonTraversal) {
1191        // Forward edges from the synthetic root establish the target's depth-zero packages, so
1192        // they are retained even though they are not part of `traversal.edges`. Inverted target
1193        // edges must have been reached while traversing the reversed graph.
1194        let edges = self
1195            .tree
1196            .graph
1197            .edge_references()
1198            .filter(|edge| !self.tree.invert || traversal.edges.contains(&edge.id()))
1199            .filter_map(|edge| {
1200                let package = match (
1201                    &self.tree.graph[edge.source()],
1202                    &self.tree.graph[edge.target()],
1203                ) {
1204                    (Node::Root, Node::Package(package)) | (Node::Package(package), Node::Root) => {
1205                        *package
1206                    }
1207                    (Node::Root, Node::Root) | (Node::Package(_), Node::Package(_)) => return None,
1208                };
1209                Some((package, edge.weight()))
1210            })
1211            .collect::<Vec<_>>();
1212
1213        match target {
1214            TreeJsonTarget::Script(path) => {
1215                let script = self.ensure_script(path);
1216                for (package, edge) in edges {
1217                    let marker = self.marker(edge);
1218                    for package in self.dependency_targets(package, edge.extras()) {
1219                        self.add_link(
1220                            script.clone(),
1221                            package,
1222                            JsonLink::Dependency(marker.clone()),
1223                        );
1224                    }
1225                }
1226            }
1227            TreeJsonTarget::Workspace(_) => {
1228                self.ensure_workspace();
1229                for (package, edge) in edges {
1230                    let Edge::Dev(group, ..) = edge else {
1231                        continue;
1232                    };
1233                    let group = self.ensure_workspace_group(group);
1234                    let marker = self.marker(edge);
1235                    for package in self.dependency_targets(package, edge.extras()) {
1236                        self.add_link(group.clone(), package, JsonLink::Dependency(marker.clone()));
1237                    }
1238                }
1239            }
1240        }
1241    }
1242
1243    fn marker(&self, edge: &Edge<'_>) -> Option<String> {
1244        self.tree
1245            .lock
1246            .simplify_environment(edge.marker().pep508())
1247            .try_to_string()
1248    }
1249
1250    fn add_link(&mut self, source: String, target: String, link: JsonLink) {
1251        // `optional_dependencies` and `dependency_groups` advertise related nodes; they are not
1252        // dependency edges. Keep those relationships attached to their owner when inverting the
1253        // graph, and reverse only actual dependencies.
1254        let (source, target) = if self.tree.invert && matches!(&link, JsonLink::Dependency(_)) {
1255            (target, source)
1256        } else {
1257            (source, target)
1258        };
1259        let Some(node) = self.resolution.get_mut(&source) else {
1260            return;
1261        };
1262        match link {
1263            JsonLink::Dependency(marker) => {
1264                node.add_resolution_dependency(target, marker);
1265            }
1266            JsonLink::Optional(name) => {
1267                node.add_optional_dependency(name, target);
1268            }
1269            JsonLink::Group(name) => {
1270                node.add_dependency_group(name, target);
1271            }
1272        }
1273    }
1274
1275    fn add_package_roots(&mut self, roots: &mut Vec<JsonRoot>, package_id: &'env PackageId) {
1276        let package = self.tree.lock.find_by_id(package_id);
1277        let extras = package
1278            .optional_dependencies
1279            .keys()
1280            .cloned()
1281            .collect::<Vec<_>>();
1282        let groups = package
1283            .dependency_groups
1284            .keys()
1285            .filter(|group| self.tree.groups.contains(group))
1286            .cloned()
1287            .collect::<Vec<_>>();
1288
1289        if self.tree.prod {
1290            roots.push(JsonRoot {
1291                id: self.ensure_package(package_id, MetadataNodeKind::Package),
1292            });
1293            for extra in &extras {
1294                let id = MetadataNodeId::from_package_id(
1295                    &self.workspace_root,
1296                    package_id,
1297                    MetadataNodeKind::Extra(extra.clone()),
1298                )
1299                .to_flat();
1300                if self.resolution.contains_key(&id) {
1301                    roots.push(JsonRoot { id });
1302                }
1303            }
1304        }
1305
1306        for group in &groups {
1307            let id = MetadataNodeId::from_package_id(
1308                &self.workspace_root,
1309                package_id,
1310                MetadataNodeKind::Group(group.clone()),
1311            )
1312            .to_flat();
1313            if self.resolution.contains_key(&id) {
1314                roots.push(JsonRoot { id });
1315            }
1316        }
1317    }
1318
1319    fn roots(&mut self, target: TreeJsonTarget<'_>) -> Vec<JsonRoot> {
1320        let mut roots = Vec::new();
1321        for root in &self.tree.roots {
1322            match self.tree.graph[*root] {
1323                Node::Package(package_id) => {
1324                    if self.tree.invert {
1325                        roots.push(JsonRoot {
1326                            id: self.ensure_package(package_id, MetadataNodeKind::Package),
1327                        });
1328                    } else {
1329                        self.add_package_roots(&mut roots, package_id);
1330                    }
1331                }
1332                Node::Root => match target {
1333                    TreeJsonTarget::Script(path) => {
1334                        let script = self.ensure_script(path);
1335                        roots.push(JsonRoot { id: script });
1336                    }
1337                    TreeJsonTarget::Workspace(_) => {
1338                        let packages = self
1339                            .tree
1340                            .graph
1341                            .edges_directed(*root, Direction::Outgoing)
1342                            .filter(|edge| matches!(edge.weight(), Edge::Prod(..)))
1343                            .filter_map(|edge| {
1344                                let Node::Package(package_id) = self.tree.graph[edge.target()]
1345                                else {
1346                                    return None;
1347                                };
1348                                Some(package_id)
1349                            })
1350                            .collect::<Vec<_>>();
1351                        for package_id in packages {
1352                            self.add_package_roots(&mut roots, package_id);
1353                        }
1354                        let groups = self
1355                            .tree
1356                            .lock
1357                            .dependency_groups()
1358                            .keys()
1359                            .filter(|group| self.tree.groups.contains(group))
1360                            .cloned()
1361                            .collect::<Vec<_>>();
1362                        for group in groups {
1363                            let id = MetadataNodeId::from_workspace_group(
1364                                self.workspace_root.clone(),
1365                                group,
1366                            )
1367                            .to_flat();
1368                            if self.resolution.contains_key(&id) {
1369                                roots.push(JsonRoot { id });
1370                            }
1371                        }
1372                    }
1373                },
1374            }
1375        }
1376        roots.sort();
1377        roots.dedup();
1378        roots
1379    }
1380
1381    fn members(&self, target: TreeJsonTarget<'_>) -> Vec<MetadataWorkspaceMember> {
1382        if matches!(target, TreeJsonTarget::Script(_)) {
1383            return Vec::new();
1384        }
1385
1386        let packages = if self.tree.lock.members().is_empty() {
1387            self.tree.lock.root().into_iter().collect::<Vec<_>>()
1388        } else {
1389            self.tree
1390                .lock
1391                .packages()
1392                .iter()
1393                .filter(|package| self.tree.lock.members().contains(&package.id.name))
1394                .collect::<Vec<_>>()
1395        };
1396
1397        packages
1398            .into_iter()
1399            .filter(|package| {
1400                let id = MetadataNodeId::from_package_id(
1401                    &self.workspace_root,
1402                    &package.id,
1403                    MetadataNodeKind::Package,
1404                )
1405                .to_flat();
1406                self.resolution.contains_key(&id)
1407            })
1408            .filter_map(|package| {
1409                MetadataWorkspaceMember::from_locked_package(&self.workspace_root, &package.id)
1410            })
1411            .collect()
1412    }
1413
1414    fn finish(mut self) -> BTreeMap<String, MetadataNode> {
1415        for node in self.resolution.values_mut() {
1416            node.normalize_resolution();
1417        }
1418        self.resolution
1419    }
1420}
1421
1422enum JsonLink {
1423    Dependency(Option<String>),
1424    Optional(ExtraName),
1425    Group(GroupName),
1426}
1427
1428#[derive(Debug, Serialize)]
1429struct JsonSchema {
1430    version: JsonSchemaVersion,
1431}
1432
1433#[derive(Debug, Serialize)]
1434#[serde(rename_all = "snake_case")]
1435enum JsonSchemaVersion {
1436    Preview,
1437}
1438
1439#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1440struct JsonRoot {
1441    id: String,
1442}
1443
1444#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1445struct VisitedNode<'env> {
1446    package_id: &'env PackageId,
1447    expanded_extras: BTreeSet<&'env ExtraName>,
1448    marker: Option<UniversalMarker>,
1449}
1450
1451#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1452enum Node<'env> {
1453    /// The synthetic root node.
1454    Root,
1455    /// A package in the dependency graph.
1456    Package(&'env PackageId),
1457}
1458
1459#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1460enum Edge<'env> {
1461    Prod(Option<RequestedExtras<'env>>, UniversalMarker),
1462    Optional(
1463        &'env ExtraName,
1464        Option<RequestedExtras<'env>>,
1465        UniversalMarker,
1466    ),
1467    Dev(
1468        &'env GroupName,
1469        Option<RequestedExtras<'env>>,
1470        UniversalMarker,
1471    ),
1472}
1473
1474impl<'env> Edge<'env> {
1475    fn extras(&self) -> Option<RequestedExtras<'env>> {
1476        match self {
1477            Self::Prod(extras, _) => *extras,
1478            Self::Optional(_, extras, _) => *extras,
1479            Self::Dev(_, extras, _) => *extras,
1480        }
1481    }
1482
1483    fn required_extra(&self) -> Option<&'env ExtraName> {
1484        match self {
1485            Self::Optional(extra, ..) => Some(extra),
1486            Self::Prod(..) | Self::Dev(..) => None,
1487        }
1488    }
1489
1490    fn marker(&self) -> UniversalMarker {
1491        match self {
1492            Self::Prod(_, marker) | Self::Optional(_, _, marker) | Self::Dev(_, _, marker) => {
1493                *marker
1494            }
1495        }
1496    }
1497
1498    fn is_dev(&self) -> bool {
1499        matches!(self, Self::Dev(..))
1500    }
1501
1502    fn kind(&self) -> EdgeKind<'env> {
1503        match self {
1504            Self::Prod(..) => EdgeKind::Prod,
1505            Self::Optional(extra, ..) => EdgeKind::Optional(extra),
1506            Self::Dev(group, ..) => EdgeKind::Dev(group),
1507        }
1508    }
1509}
1510
1511#[derive(Debug, Copy, Clone)]
1512enum RequestedExtras<'env> {
1513    Dependency(&'env BTreeSet<ExtraName>),
1514    Requirement(&'env [ExtraName]),
1515}
1516
1517impl PartialEq for RequestedExtras<'_> {
1518    fn eq(&self, other: &Self) -> bool {
1519        self.iter().eq(other.iter())
1520    }
1521}
1522
1523impl Eq for RequestedExtras<'_> {}
1524
1525impl PartialOrd for RequestedExtras<'_> {
1526    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1527        Some(self.cmp(other))
1528    }
1529}
1530
1531impl Ord for RequestedExtras<'_> {
1532    fn cmp(&self, other: &Self) -> Ordering {
1533        self.iter().cmp(other.iter())
1534    }
1535}
1536
1537impl<'env> RequestedExtras<'env> {
1538    fn contains(self, extra: &ExtraName) -> bool {
1539        match self {
1540            Self::Dependency(extras) => extras.contains(extra),
1541            Self::Requirement(extras) => extras.contains(extra),
1542        }
1543    }
1544
1545    fn is_empty(self) -> bool {
1546        match self {
1547            Self::Dependency(extras) => extras.is_empty(),
1548            Self::Requirement(extras) => extras.is_empty(),
1549        }
1550    }
1551
1552    fn iter(self) -> impl Iterator<Item = &'env ExtraName> {
1553        match self {
1554            Self::Dependency(extras) => Either::Left(extras.iter()),
1555            Self::Requirement(extras) => Either::Right(extras.iter()),
1556        }
1557    }
1558}
1559
1560#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1561enum EdgeKind<'env> {
1562    Prod,
1563    Optional(&'env ExtraName),
1564    Dev(&'env GroupName),
1565}
1566
1567/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes.
1568#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
1569struct Cursor(NodeIndex, Option<EdgeIndex>, UniversalMarker);
1570
1571impl Cursor {
1572    /// Create a [`Cursor`] representing a node in the dependency tree.
1573    fn new(node: NodeIndex, edge: EdgeIndex, marker: UniversalMarker) -> Self {
1574        Self(node, Some(edge), marker)
1575    }
1576
1577    /// Create a [`Cursor`] representing a root node in the dependency tree.
1578    fn root(node: NodeIndex, marker: UniversalMarker) -> Self {
1579        Self(node, None, marker)
1580    }
1581
1582    /// Return the [`NodeIndex`] of the node.
1583    fn node(&self) -> NodeIndex {
1584        self.0
1585    }
1586
1587    /// Return the [`EdgeIndex`] of the edge that led to the node, if any.
1588    fn edge(&self) -> Option<EdgeIndex> {
1589        self.1
1590    }
1591
1592    /// Return the marker context accumulated along the path to this node.
1593    fn marker(&self) -> UniversalMarker {
1594        self.2
1595    }
1596}
1597
1598impl std::fmt::Display for TreeDisplay<'_> {
1599    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1600        use owo_colors::OwoColorize;
1601
1602        let mut deduped = false;
1603        for line in self.render() {
1604            deduped |= line.contains('*');
1605            writeln!(f, "{line}")?;
1606        }
1607
1608        if deduped {
1609            let message = if self.no_dedupe {
1610                "(*) Package tree is a cycle and cannot be shown".italic()
1611            } else {
1612                "(*) Package tree already displayed".italic()
1613            };
1614            writeln!(f, "{message}")?;
1615        }
1616
1617        Ok(())
1618    }
1619}