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