Add JSON formatter.

This commit is contained in:
Marc-André Bélanger 2025-10-21 15:54:18 -04:00
parent a4ad184ad9
commit 060ee6d654
7 changed files with 206 additions and 1 deletions

1
Cargo.lock generated
View File

@ -6528,6 +6528,7 @@ dependencies = [
"same-file",
"schemars",
"serde",
"serde_json",
"smallvec",
"textwrap",
"thiserror 2.0.17",

View File

@ -6644,6 +6644,10 @@ pub struct DisplayTreeArgs {
/// Show compressed wheel sizes for packages in the tree.
#[arg(long)]
pub show_sizes: bool,
/// Output the dependency tree as JSON.
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug)]

View File

@ -62,6 +62,7 @@ rustc-hash = { workspace = true }
same-file = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
smallvec = { workspace = true }
textwrap = { workspace = true }
thiserror = { workspace = true }

View File

@ -3,6 +3,7 @@ use std::collections::{BTreeSet, VecDeque};
use either::Either;
use itertools::Itertools;
use owo_colors::OwoColorize;
use serde_json::json;
use petgraph::graph::{EdgeIndex, NodeIndex};
use petgraph::prelude::EdgeRef;
use petgraph::{Direction, Graph};
@ -287,6 +288,137 @@ impl TreeFormatter for TextFormatter {
}
}
/// A JSON tree formatter that produces structured JSON output.
///
/// This formatter produces output like:
/// ```json
/// {
/// "name": "package-name",
/// "version": "1.0.0",
/// "dependencies": [
/// {
/// "name": "dependency-1",
/// "version": "2.0.0",
/// "dependencies": []
/// }
/// ]
/// }
/// ```
#[derive(Debug)]
struct JsonFormatter {
/// Stack of JSON objects being built.
/// The top of the stack is the current node being processed.
stack: Vec<serde_json::Value>,
/// The root nodes (top-level packages).
roots: Vec<serde_json::Value>,
}
impl JsonFormatter {
/// Create a new JSON formatter.
fn new() -> Self {
Self {
stack: Vec::new(),
roots: Vec::new(),
}
}
}
impl TreeFormatter for JsonFormatter {
type Output = serde_json::Value;
fn begin_tree(&mut self) {
// Nothing to do for JSON output
}
fn end_tree(&mut self) -> Self::Output {
// Return all roots as a JSON array
json!(self.roots)
}
fn begin_node(&mut self, info: &NodeInfo, _position: NodePosition) {
// Create a JSON object for this node
let mut node = json!({
"name": info.package_id.name.to_string(),
});
// Add optional fields
if let Some(version) = info.version {
node["version"] = json!(version.to_string());
}
if let Some(extras) = info.extras {
if !extras.is_empty() {
node["extras"] = json!(extras.iter().map(|e| e.to_string()).collect::<Vec<_>>());
}
}
if let Some(edge_type) = info.edge_type {
match edge_type {
EdgeType::Optional(extra) => {
node["extra"] = json!(extra.to_string());
}
EdgeType::Dev(group) => {
node["group"] = json!(group.to_string());
}
EdgeType::Prod => {}
}
}
if let Some(size) = info.size {
node["size"] = json!(size);
}
if let Some(latest) = info.latest_version {
node["latest"] = json!(latest.to_string());
}
// Initialize empty dependencies array
node["dependencies"] = json!([]);
// Push onto stack
self.stack.push(node);
}
fn end_node(&mut self) {
// Pop the current node from the stack
let node = self.stack.pop().expect("Stack should not be empty");
if self.stack.is_empty() {
// This is a root node - add to roots
self.roots.push(node);
} else {
// This is a child node - add to parent's dependencies
let parent = self.stack.last_mut().expect("Parent should exist");
parent["dependencies"]
.as_array_mut()
.expect("Dependencies should be an array")
.push(node);
}
}
fn mark_visited(&mut self) {
// Mark the current node as deduplicated
if let Some(node) = self.stack.last_mut() {
node["deduplicated"] = json!(true);
}
}
fn mark_cycle(&mut self) {
// Mark the current node as a cycle
if let Some(node) = self.stack.last_mut() {
node["cycle"] = json!(true);
}
}
fn begin_children(&mut self, _count: usize) {
// Nothing to do for JSON output
}
fn end_children(&mut self) {
// Nothing to do for JSON output
}
}
#[derive(Debug)]
pub struct TreeDisplay<'env> {
/// The constructed dependency graph.
@ -899,6 +1031,62 @@ impl<'env> TreeDisplay<'env> {
formatter.end_tree()
}
/// Depth-first traverse the nodes to render the tree as JSON.
pub fn render_json(&self) -> serde_json::Value {
let mut path = Vec::new();
let mut visited =
FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
let mut formatter = JsonFormatter::new();
formatter.begin_tree();
for node in &self.roots {
match self.graph[*node] {
Node::Root => {
let edges: Vec<_> = self.graph.edges_directed(*node, Direction::Outgoing).collect();
let total_siblings = edges.len();
for (index, edge) in edges.into_iter().enumerate() {
let node = edge.target();
path.clear();
let position = NodePosition {
depth: 0,
is_first_child: index == 0,
is_last_child: index == total_siblings - 1,
child_index: index,
total_siblings,
};
self.visit_with_formatter(
Cursor::new(node, edge.id()),
&mut formatter,
&mut visited,
&mut path,
position,
);
}
}
Node::Package(_) => {
path.clear();
let position = NodePosition {
depth: 0,
is_first_child: true,
is_last_child: true,
child_index: 0,
total_siblings: 1,
};
self.visit_with_formatter(
Cursor::root(*node),
&mut formatter,
&mut visited,
&mut path,
position,
);
}
}
}
formatter.end_tree()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]

View File

@ -47,6 +47,7 @@ pub(crate) async fn tree(
invert: bool,
outdated: bool,
show_sizes: bool,
json: bool,
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
python: Option<String>,
@ -283,7 +284,14 @@ pub(crate) async fn tree(
show_sizes,
);
if json {
// Output JSON
let json_output = tree.render_json();
println!("{}", serde_json::to_string_pretty(&json_output)?);
} else {
// Output text
print!("{tree}");
}
Ok(ExitStatus::Success)
}

View File

@ -2205,6 +2205,7 @@ async fn run_project(
args.invert,
args.outdated,
args.show_sizes,
args.json,
args.python_version,
args.python_platform,
args.python,

View File

@ -1874,6 +1874,7 @@ pub(crate) struct TreeSettings {
pub(crate) invert: bool,
pub(crate) outdated: bool,
pub(crate) show_sizes: bool,
pub(crate) json: bool,
#[allow(dead_code)]
pub(crate) script: Option<PathBuf>,
pub(crate) python_version: Option<PythonVersion>,
@ -1941,6 +1942,7 @@ impl TreeSettings {
invert: tree.invert,
outdated: tree.outdated,
show_sizes: tree.show_sizes,
json: tree.json,
script,
python_version,
python_platform,