feat(tree): Add --depth public behind -Zunstable-options (#15243)

### What does this PR try to resolve?

I was investigating some issues around public dependency lints and
wanted to see the structure of the public dependencies and had the idea
to add this with us having added `--depth workspace`.

See
https://github.com/rust-lang/rust/issues/119428#issuecomment-2686384070
for some example output (comparing `cargo tree` with `cargo tree --depth
public`)

### How should we test and review this PR?

### Additional information
This commit is contained in:
Weihang Lo 2025-03-06 21:52:54 +00:00 committed by GitHub
commit 73b3092fd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 343 additions and 13 deletions

View File

@ -78,6 +78,7 @@ impl Node {
pub struct Edge {
kind: EdgeKind,
node: NodeId,
public: bool,
}
impl Edge {
@ -88,6 +89,10 @@ impl Edge {
pub fn node(&self) -> NodeId {
self.node
}
pub fn public(&self) -> bool {
self.public
}
}
/// The kind of edge, for separating dependencies into different sections.
@ -105,7 +110,7 @@ pub enum EdgeKind {
///
/// The value is a `Vec` because each edge kind can have multiple outgoing
/// edges. For example, package "foo" can have multiple normal dependencies.
#[derive(Clone)]
#[derive(Clone, Debug)]
struct Edges(HashMap<EdgeKind, Vec<Edge>>);
impl Edges {
@ -135,6 +140,7 @@ impl Edges {
}
/// A graph of dependencies.
#[derive(Debug)]
pub struct Graph<'a> {
nodes: Vec<Node>,
/// The indexes of `edges` correspond to the `nodes`. That is, `edges[0]`
@ -268,6 +274,7 @@ impl<'a> Graph<'a> {
let new_edge = Edge {
kind: edge.kind(),
node: new_to_index,
public: edge.public(),
};
new_graph.edges_mut(new_from).add_edge(new_edge);
}
@ -290,6 +297,7 @@ impl<'a> Graph<'a> {
let new_edge = Edge {
kind: edge.kind(),
node: NodeId::new(from_idx, self.nodes[from_idx].name()),
public: edge.public(),
};
new_edges[edge.node().index].add_edge(new_edge);
}
@ -514,6 +522,7 @@ fn add_pkg(
let new_edge = Edge {
kind: EdgeKind::Dep(dep.kind()),
node: dep_index,
public: dep.is_public(),
};
if opts.graph_features {
// Add the dependency node with feature nodes in-between.
@ -577,12 +586,14 @@ fn add_feature(
let from_edge = Edge {
kind: to.kind(),
node: node_index,
public: to.public(),
};
graph.edges_mut(from).add_edge(from_edge);
}
let to_edge = Edge {
kind: EdgeKind::Feature,
node: to.node(),
public: true,
};
graph.edges_mut(node_index).add_edge(to_edge);
(missing, node_index)
@ -620,6 +631,7 @@ fn add_cli_features(
let feature_edge = Edge {
kind: EdgeKind::Feature,
node: package_index,
public: true,
};
let index = add_feature(graph, feature, None, feature_edge).1;
graph.cli_features.insert(index);
@ -654,6 +666,7 @@ fn add_cli_features(
let feature_edge = Edge {
kind: EdgeKind::Feature,
node: package_index,
public: true,
};
let index = add_feature(graph, dep_name, None, feature_edge).1;
graph.cli_features.insert(index);
@ -661,6 +674,7 @@ fn add_cli_features(
let dep_edge = Edge {
kind: EdgeKind::Feature,
node: dep_index,
public: true,
};
let index = add_feature(graph, dep_feature, None, dep_edge).1;
graph.cli_features.insert(index);
@ -721,6 +735,7 @@ fn add_feature_rec(
let feature_edge = Edge {
kind: EdgeKind::Feature,
node: package_index,
public: true,
};
let (missing, feat_index) = add_feature(graph, *dep_name, Some(from), feature_edge);
// Don't recursive if the edge already exists to deal with cycles.
@ -771,12 +786,14 @@ fn add_feature_rec(
let feature_edge = Edge {
kind: EdgeKind::Feature,
node: package_index,
public: true,
};
add_feature(graph, *dep_name, Some(from), feature_edge);
}
let dep_edge = Edge {
kind: EdgeKind::Feature,
node: dep_index,
public: true,
};
let (missing, feat_index) =
add_feature(graph, *dep_feature, Some(from), dep_edge);

View File

@ -91,6 +91,7 @@ impl FromStr for Prefix {
#[derive(Clone, Copy)]
pub enum DisplayDepth {
MaxDisplayDepth(u32),
Public,
Workspace,
}
@ -100,6 +101,7 @@ impl FromStr for DisplayDepth {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"workspace" => Ok(Self::Workspace),
"public" => Ok(Self::Public),
s => s.parse().map(Self::MaxDisplayDepth).map_err(|_| {
clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
@ -282,7 +284,7 @@ fn print(
&mut visited_deps,
&mut levels_continue,
&mut print_stack,
);
)?;
}
Ok(())
@ -302,7 +304,7 @@ fn print_node<'a>(
visited_deps: &mut HashSet<NodeId>,
levels_continue: &mut Vec<(anstyle::Style, bool)>,
print_stack: &mut Vec<NodeId>,
) {
) -> CargoResult<()> {
let new = no_dedupe || visited_deps.insert(node_index);
match prefix {
@ -343,7 +345,7 @@ fn print_node<'a>(
drop_println!(ws.gctx(), "{}{}", format.display(graph, node_index), star);
if !new || in_cycle {
return;
return Ok(());
}
print_stack.push(node_index);
@ -367,9 +369,11 @@ fn print_node<'a>(
levels_continue,
print_stack,
kind,
);
)?;
}
print_stack.pop();
Ok(())
}
/// Prints all the dependencies of a package for the given dependency kind.
@ -387,10 +391,10 @@ fn print_dependencies<'a>(
levels_continue: &mut Vec<(anstyle::Style, bool)>,
print_stack: &mut Vec<NodeId>,
kind: &EdgeKind,
) {
) -> CargoResult<()> {
let deps = graph.edges_of_kind(node_index, kind);
if deps.is_empty() {
return;
return Ok(());
}
let name = match kind {
@ -415,14 +419,20 @@ fn print_dependencies<'a>(
}
}
let (max_display_depth, filter_non_workspace_member) = match display_depth {
DisplayDepth::MaxDisplayDepth(max) => (max, false),
DisplayDepth::Workspace => (u32::MAX, true),
let (max_display_depth, filter_non_workspace_member, filter_private) = match display_depth {
DisplayDepth::MaxDisplayDepth(max) => (max, false, false),
DisplayDepth::Workspace => (u32::MAX, true, false),
DisplayDepth::Public => {
if !ws.gctx().cli_unstable().unstable_options {
anyhow::bail!("`--depth public` requires `-Zunstable-options`")
}
(u32::MAX, false, true)
}
};
// Current level exceeds maximum display depth. Skip.
if levels_continue.len() + 1 > max_display_depth as usize {
return;
return Ok(());
}
let mut it = deps
@ -434,9 +444,17 @@ fn print_dependencies<'a>(
if filter_non_workspace_member && !ws.is_member_id(*package_id) {
return false;
}
if filter_private && !dep.public() {
return false;
}
!pkgs_to_prune.iter().any(|spec| spec.matches(*package_id))
}
_ => true,
Node::Feature { .. } => {
if filter_private && !dep.public() {
return false;
}
true
}
}
})
.peekable();
@ -457,9 +475,11 @@ fn print_dependencies<'a>(
visited_deps,
levels_continue,
print_stack,
);
)?;
levels_continue.pop();
}
Ok(())
}
fn edge_line_color(kind: EdgeKind) -> anstyle::Style {

View File

@ -1901,6 +1901,138 @@ c v0.1.0 ([ROOT]/foo/c) (*)
.run();
}
#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")]
fn depth_public() {
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["diamond", "left-pub", "right-priv", "dep"]
"#,
)
.file(
"diamond/Cargo.toml",
r#"
cargo-features = ["public-dependency"]
[package]
name = "diamond"
version = "0.1.0"
[dependencies]
left-pub = { path = "../left-pub", public = true }
right-priv = { path = "../right-priv", public = true }
"#,
)
.file("diamond/src/lib.rs", "")
.file(
"left-pub/Cargo.toml",
r#"
cargo-features = ["public-dependency"]
[package]
name = "left-pub"
version = "0.1.0"
[dependencies]
dep = { path = "../dep", public = true }
"#,
)
.file("left-pub/src/lib.rs", "")
.file(
"right-priv/Cargo.toml",
r#"
[package]
name = "right-priv"
version = "0.1.0"
[dependencies]
dep = { path = "../dep" }
"#,
)
.file("right-priv/src/lib.rs", "")
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "0.1.0"
"#,
)
.file("dep/src/lib.rs", "")
.build();
p.cargo("tree --depth public")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] `--depth public` requires `-Zunstable-options`
"#]])
.run();
p.cargo("tree --depth public -p left-pub")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
left-pub v0.1.0 ([ROOT]/foo/left-pub)
dep v0.1.0 ([ROOT]/foo/dep)
"#]])
.run();
p.cargo("tree --depth public -p right-priv")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
right-priv v0.1.0 ([ROOT]/foo/right-priv)
"#]])
.run();
p.cargo("tree --depth public -p diamond")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
diamond v0.1.0 ([ROOT]/foo/diamond)
left-pub v0.1.0 ([ROOT]/foo/left-pub)
dep v0.1.0 ([ROOT]/foo/dep)
right-priv v0.1.0 ([ROOT]/foo/right-priv)
"#]])
.run();
p.cargo("tree --depth public")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
dep v0.1.0 ([ROOT]/foo/dep)
diamond v0.1.0 ([ROOT]/foo/diamond)
left-pub v0.1.0 ([ROOT]/foo/left-pub)
dep v0.1.0 ([ROOT]/foo/dep)
right-priv v0.1.0 ([ROOT]/foo/right-priv)
left-pub v0.1.0 ([ROOT]/foo/left-pub) (*)
right-priv v0.1.0 ([ROOT]/foo/right-priv) (*)
"#]])
.run();
p.cargo("tree --depth public --invert dep")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
dep v0.1.0 ([ROOT]/foo/dep)
left-pub v0.1.0 ([ROOT]/foo/left-pub)
diamond v0.1.0 ([ROOT]/foo/diamond)
"#]])
.run();
}
#[cargo_test]
fn prune() {
let p = make_simple_proj();

View File

@ -350,3 +350,164 @@ foo v0.1.0 ([ROOT]/foo)
"#]])
.run();
}
#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")]
fn depth_public_no_features() {
Package::new("pub-defaultdep", "1.0.0").publish();
Package::new("priv-defaultdep", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
cargo-features = ["public-dependency"]
[package]
name = "foo"
version = "0.1.0"
[dependencies]
pub-defaultdep = { version = "1.0.0", public = true }
priv-defaultdep = "1.0.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("tree -e features --depth public")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
pub-defaultdep feature "default"
pub-defaultdep v1.0.0
"#]])
.run();
}
#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")]
fn depth_public_transitive_features() {
Package::new("pub-defaultdep", "1.0.0")
.feature("default", &["f1"])
.feature("f1", &["f2"])
.feature("f2", &["optdep"])
.add_dep(Dependency::new("optdep", "1.0").optional(true).public(true))
.publish();
Package::new("priv-defaultdep", "1.0.0")
.feature("default", &["f1"])
.feature("f1", &["f2"])
.feature("f2", &["optdep"])
.add_dep(Dependency::new("optdep", "1.0").optional(true))
.publish();
Package::new("optdep", "1.0.0")
.feature("default", &["f"])
.feature("f", &[])
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
cargo-features = ["public-dependency"]
[package]
name = "foo"
version = "0.1.0"
[dependencies]
pub-defaultdep = { version = "1.0.0", public = true }
priv-defaultdep = { version = "1.0.0", public = true }
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("tree -e features --depth public")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
priv-defaultdep feature "default"
priv-defaultdep v1.0.0
priv-defaultdep feature "f1"
priv-defaultdep v1.0.0 (*)
priv-defaultdep feature "f2"
priv-defaultdep v1.0.0 (*)
priv-defaultdep feature "optdep"
priv-defaultdep v1.0.0 (*)
pub-defaultdep feature "default"
pub-defaultdep v1.0.0
optdep feature "default"
optdep v1.0.0
optdep feature "f"
optdep v1.0.0
pub-defaultdep feature "f1"
pub-defaultdep v1.0.0 (*)
pub-defaultdep feature "f2"
pub-defaultdep v1.0.0 (*)
pub-defaultdep feature "optdep"
pub-defaultdep v1.0.0 (*)
"#]])
.run();
}
#[cargo_test(nightly, reason = "exported_private_dependencies lint is unstable")]
fn depth_public_cli() {
Package::new("priv", "1.0.0").feature("f", &[]).publish();
Package::new("pub", "1.0.0").feature("f", &[]).publish();
let p = project()
.file(
"Cargo.toml",
r#"
cargo-features = ["public-dependency"]
[package]
name = "foo"
version = "0.1.0"
[features]
priv-indirect = ["priv"]
priv = ["dep:priv", "priv?/f"]
pub-indirect = ["pub"]
pub = ["dep:pub", "priv?/f"]
[dependencies]
priv = { version = "1.0.0", optional = true }
pub = { version = "1.0.0", optional = true, public = true }
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("tree -e features --depth public")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
"#]])
.run();
p.cargo("tree -e features --depth public --features pub-indirect")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
pub feature "default"
pub v1.0.0
"#]])
.run();
p.cargo("tree -e features --depth public --features priv-indirect")
.arg("-Zunstable-options")
.masquerade_as_nightly_cargo(&["public-dependency", "depth-public"])
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)
"#]])
.run();
}