From 0949c7e0db52b636cb014755c429ebe6b4818e74 Mon Sep 17 00:00:00 2001
From: Alexandre Barone
Date: Fri, 7 Nov 2025 19:23:17 -0500
Subject: [PATCH 1/5] Add unit tests for cargo tree format parser
Signed-off-by: Alexandre Barone
---
src/cargo/ops/tree/format/parse.rs | 101 +++++++++++++++++++++++++++++
1 file changed, 101 insertions(+)
diff --git a/src/cargo/ops/tree/format/parse.rs b/src/cargo/ops/tree/format/parse.rs
index ee112fbee50..d68d96cc275 100644
--- a/src/cargo/ops/tree/format/parse.rs
+++ b/src/cargo/ops/tree/format/parse.rs
@@ -3,6 +3,7 @@
use std::iter;
use std::str;
+#[derive(Debug, PartialEq, Eq)]
pub enum RawChunk<'a> {
/// Raw text to include in the output.
Text(&'a str),
@@ -121,3 +122,103 @@ impl<'a> Iterator for Parser<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::{Parser, RawChunk};
+
+ #[test]
+ fn plain_text() {
+ let chunks: Vec<_> = Parser::new("Hello World").collect();
+ assert_eq!(chunks, vec![RawChunk::Text("Hello World")]);
+ }
+
+ #[test]
+ fn basic_argument() {
+ let chunks: Vec<_> = Parser::new("{pkg}").collect();
+ assert_eq!(chunks, vec![RawChunk::Argument("pkg")]);
+ }
+
+ #[test]
+ fn mixed_content() {
+ let chunks: Vec<_> = Parser::new("Package {p} version:{v}").collect();
+ assert_eq!(
+ chunks,
+ vec![
+ RawChunk::Text("Package "),
+ RawChunk::Argument("p"),
+ RawChunk::Text(" version:"),
+ RawChunk::Argument("v"),
+ ]
+ );
+ }
+
+ #[test]
+ fn escaped_braces() {
+ let chunks: Vec<_> = Parser::new("{{text}} in {{braces}}").collect();
+ assert_eq!(
+ chunks,
+ vec![
+ RawChunk::Text("{"),
+ RawChunk::Text("text"),
+ RawChunk::Text("}"),
+ RawChunk::Text(" in "),
+ RawChunk::Text("{"),
+ RawChunk::Text("braces"),
+ RawChunk::Text("}"),
+ ]
+ );
+ }
+
+ #[test]
+ fn unclosed_brace() {
+ let chunks: Vec<_> = Parser::new("{unclosed").collect();
+ assert_eq!(chunks, vec![RawChunk::Error("expected '}'")])
+ }
+
+ #[test]
+ fn unexpected_close_brace() {
+ let chunks: Vec<_> = Parser::new("unexpected}").collect();
+ assert_eq!(
+ chunks,
+ vec![
+ RawChunk::Text("unexpected"),
+ RawChunk::Error("unexpected '}'"),
+ ]
+ );
+ }
+
+ #[test]
+ fn empty_argument() {
+ let chunks: Vec<_> = Parser::new("{}").collect();
+ assert_eq!(chunks, vec![RawChunk::Argument("")]);
+ }
+
+ #[test]
+ fn invalid_argument_chars() {
+ let chunks: Vec<_> = Parser::new("{a-b} {123}").collect();
+ assert_eq!(chunks, vec![RawChunk::Error("expected '}'")]);
+ }
+
+ #[test]
+ fn complex_format() {
+ let format = "Pkg{{name}}: {p} [{v}] (License: {l})";
+ let chunks: Vec<_> = Parser::new(format).collect();
+ assert_eq!(
+ chunks,
+ vec![
+ RawChunk::Text("Pkg"),
+ RawChunk::Text("{"),
+ RawChunk::Text("name"),
+ RawChunk::Text("}"),
+ RawChunk::Text(": "),
+ RawChunk::Argument("p"),
+ RawChunk::Text(" ["),
+ RawChunk::Argument("v"),
+ RawChunk::Text("] (License: "),
+ RawChunk::Argument("l"),
+ RawChunk::Text(")"),
+ ]
+ );
+ }
+}
From 8e7b557e4d0146f4a39e46afc846177d974ef193 Mon Sep 17 00:00:00 2001
From: Alexandre Barone
Date: Fri, 7 Nov 2025 19:05:44 -0500
Subject: [PATCH 2/5] Support hyphens in cargo tree format parser
---
src/cargo/ops/tree/format/parse.rs | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/src/cargo/ops/tree/format/parse.rs b/src/cargo/ops/tree/format/parse.rs
index d68d96cc275..a157f748fe6 100644
--- a/src/cargo/ops/tree/format/parse.rs
+++ b/src/cargo/ops/tree/format/parse.rs
@@ -24,9 +24,9 @@ pub enum RawChunk<'a> {
/// (and optionally source), and the `{l}` will be the license from
/// `Cargo.toml`.
///
-/// Substitutions are alphabetic characters between curly braces, like `{p}`
-/// or `{foo}`. The actual interpretation of these are done in the `Pattern`
-/// struct.
+/// Substitutions are alphabetic characters or hyphens between curly braces,
+/// like `{p}`, {foo} or `{bar-baz}`. The actual interpretation of these is
+/// done in the `Pattern` struct.
///
/// Bare curly braces can be included in the output with double braces like
/// `{{` will include a single `{`, similar to Rust's format strings.
@@ -68,7 +68,7 @@ impl<'a> Parser<'a> {
loop {
match self.it.peek() {
- Some(&(_, ch)) if ch.is_alphanumeric() => {
+ Some(&(_, ch)) if ch.is_alphanumeric() || ch == '-' => {
self.it.next();
}
Some(&(end, _)) => return &self.s[start..end],
@@ -170,6 +170,12 @@ mod tests {
);
}
+ #[test]
+ fn hyphenated_argument() {
+ let chunks: Vec<_> = Parser::new("{foo-bar}").collect();
+ assert_eq!(chunks, vec![RawChunk::Argument("foo-bar")]);
+ }
+
#[test]
fn unclosed_brace() {
let chunks: Vec<_> = Parser::new("{unclosed").collect();
@@ -197,7 +203,14 @@ mod tests {
#[test]
fn invalid_argument_chars() {
let chunks: Vec<_> = Parser::new("{a-b} {123}").collect();
- assert_eq!(chunks, vec![RawChunk::Error("expected '}'")]);
+ assert_eq!(
+ chunks,
+ vec![
+ RawChunk::Argument("a-b"),
+ RawChunk::Text(" "),
+ RawChunk::Error("expected '}'"),
+ ]
+ );
}
#[test]
From 7a011066e2e317d1fb162c4deb942aac9e90f97a Mon Sep 17 00:00:00 2001
From: Alexandre Barone
Date: Sat, 1 Nov 2025 19:08:54 -0400
Subject: [PATCH 3/5] Update cargo tree format tests with version requirement
scenarios
Update cargo tree format tests and introduce different version requirements
and new placeholder tests to prepare for the next commits which will add
a new formatting option to display package version requirements.
Signed-off-by: Alexandre Barone
---
tests/testsuite/cargo_tree/deps.rs | 46 +++++++++++++++++++++++++++---
1 file changed, 42 insertions(+), 4 deletions(-)
diff --git a/tests/testsuite/cargo_tree/deps.rs b/tests/testsuite/cargo_tree/deps.rs
index f207ba705e2..1be50736142 100644
--- a/tests/testsuite/cargo_tree/deps.rs
+++ b/tests/testsuite/cargo_tree/deps.rs
@@ -1128,7 +1128,10 @@ foo v0.1.0 ([ROOT]/foo)
#[cargo_test]
fn format() {
Package::new("dep", "1.0.0").publish();
- Package::new("other-dep", "1.0.0").publish();
+ Package::new("dep", "2.0.0").publish();
+ Package::new("other-dep", "1.0.0")
+ .dep("dep", "^1.0")
+ .publish();
Package::new("dep_that_is_awesome", "1.0.0")
.file(
@@ -1140,6 +1143,10 @@ fn format() {
[lib]
name = "awesome_dep"
+
+ [dependencies]
+ dep1 = {package="dep", version="<2.0"}
+ dep2 = {package="dep", version="2.0"}
"#,
)
.file("src/lib.rs", "pub struct Straw;")
@@ -1156,9 +1163,9 @@ fn format() {
repository = "https://github.com/rust-lang/cargo"
[dependencies]
- dep = {version="1.0", optional=true}
+ dep = {version="=1.0"}
other-dep = {version="1.0", optional=true}
- dep_that_is_awesome = {version="1.0", optional=true}
+ dep_that_is_awesome = {version=">=1.0, <2", optional=true}
[features]
@@ -1173,6 +1180,7 @@ fn format() {
p.cargo("tree --format <<<{p}>>>")
.with_stdout_data(str![[r#"
<<>>
+└── <<>>
"#]])
.run();
@@ -1191,6 +1199,7 @@ Caused by:
p.cargo("tree --format {p}-{{hello}}")
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo)-{hello}
+└── dep v1.0.0-{hello}
"#]])
.run();
@@ -1199,6 +1208,7 @@ foo v0.1.0 ([ROOT]/foo)-{hello}
.arg("{p} {l} {r}")
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo) MIT https://github.com/rust-lang/cargo
+└── dep v1.0.0
"#]])
.run();
@@ -1207,6 +1217,7 @@ foo v0.1.0 ([ROOT]/foo) MIT https://github.com/rust-lang/cargo
.arg("{p} {f}")
.with_stdout_data(str![[r#"
foo v0.1.0 ([ROOT]/foo) bar,default,foo
+└── dep v1.0.0
"#]])
.run();
@@ -1214,10 +1225,11 @@ foo v0.1.0 ([ROOT]/foo) bar,default,foo
p.cargo("tree --all-features --format")
.arg("{p} [{f}]")
.with_stdout_data(str![[r#"
-foo v0.1.0 ([ROOT]/foo) [bar,default,dep,dep_that_is_awesome,foo,other-dep]
+foo v0.1.0 ([ROOT]/foo) [bar,default,dep_that_is_awesome,foo,other-dep]
├── dep v1.0.0 []
├── dep_that_is_awesome v1.0.0 []
└── other-dep v1.0.0 []
+ └── dep v1.0.0 []
"#]])
.run();
@@ -1227,8 +1239,34 @@ foo v0.1.0 ([ROOT]/foo) [bar,default,dep,dep_that_is_awesome,foo,other-dep]
.arg("--format={lib}")
.with_stdout_data(str![[r#"
+├── dep
├── awesome_dep
└── other_dep
+ └── dep
+
+"#]])
+ .run();
+
+ p.cargo("tree --all-features")
+ .arg("--format={p}")
+ .with_stdout_data(str![[r#"
+foo v0.1.0 ([ROOT]/foo)
+├── dep v1.0.0
+├── dep_that_is_awesome v1.0.0
+└── other-dep v1.0.0
+ └── dep v1.0.0
+
+"#]])
+ .run();
+
+ p.cargo("tree --all-features")
+ .arg("--format={p}")
+ .arg("--invert=dep")
+ .with_stdout_data(str![[r#"
+dep v1.0.0
+├── foo v0.1.0 ([ROOT]/foo)
+└── other-dep v1.0.0
+ └── foo v0.1.0 ([ROOT]/foo)
"#]])
.run();
From 995e6937a063e0846536fd53b1d7729bcdd055e4 Mon Sep 17 00:00:00 2001
From: Alexandre Barone
Date: Sat, 1 Nov 2025 19:13:09 -0400
Subject: [PATCH 4/5] Add version requirement format option to cargo tree
Also update unit tests with test cases for normal and inverted
output.
Signed-off-by: Alexandre Barone
---
src/cargo/ops/tree/format/mod.rs | 9 +++++++++
src/cargo/ops/tree/graph.rs | 17 ++++++++++++++++-
tests/testsuite/cargo_tree/deps.rs | 22 +++++++++++-----------
3 files changed, 36 insertions(+), 12 deletions(-)
diff --git a/src/cargo/ops/tree/format/mod.rs b/src/cargo/ops/tree/format/mod.rs
index ee09751a16d..c478049e9ac 100644
--- a/src/cargo/ops/tree/format/mod.rs
+++ b/src/cargo/ops/tree/format/mod.rs
@@ -14,6 +14,7 @@ enum Chunk {
Repository,
Features,
LibName,
+ VersionRequirement,
}
pub struct Pattern(Vec);
@@ -30,6 +31,7 @@ impl Pattern {
RawChunk::Argument("r") => Chunk::Repository,
RawChunk::Argument("f") => Chunk::Features,
RawChunk::Argument("lib") => Chunk::LibName,
+ RawChunk::Argument("ver-req") => Chunk::VersionRequirement,
RawChunk::Argument(a) => {
bail!("unsupported pattern `{}`", a);
}
@@ -111,6 +113,13 @@ impl<'a> fmt::Display for Display<'a> {
write!(fmt, "{}", target.crate_name())?;
}
}
+ Chunk::VersionRequirement => {
+ if let Some(version_req) =
+ self.graph.version_req_for_id(package.package_id())
+ {
+ write!(fmt, "{}", version_req)?;
+ }
+ }
}
}
}
diff --git a/src/cargo/ops/tree/graph.rs b/src/cargo/ops/tree/graph.rs
index 16951be3d7e..fed366df20e 100644
--- a/src/cargo/ops/tree/graph.rs
+++ b/src/cargo/ops/tree/graph.rs
@@ -6,8 +6,8 @@ use crate::core::dependency::DepKind;
use crate::core::resolver::Resolve;
use crate::core::resolver::features::{CliFeatures, FeaturesFor, ResolvedFeatures};
use crate::core::{FeatureMap, FeatureValue, Package, PackageId, PackageIdSpec, Workspace};
-use crate::util::CargoResult;
use crate::util::interning::{INTERNED_DEFAULT, InternedString};
+use crate::util::{CargoResult, OptVersionReq};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Copy, Clone)]
@@ -161,6 +161,8 @@ pub struct Graph<'a> {
/// Key is the index of a package node, value is a map of `dep_name` to a
/// set of `(pkg_node_index, is_optional)`.
dep_name_map: HashMap>>,
+ /// Map for looking up version requirements for dependency packages.
+ version_req_map: HashMap,
}
impl<'a> Graph<'a> {
@@ -172,6 +174,7 @@ impl<'a> Graph<'a> {
package_map,
cli_features: HashSet::new(),
dep_name_map: HashMap::new(),
+ version_req_map: HashMap::new(),
}
}
@@ -240,6 +243,12 @@ impl<'a> Graph<'a> {
}
}
+ /// Returns the version requirement for the given package ID. Returns `None`
+ /// if no version requirement is recorded (e.g., root packages).
+ pub fn version_req_for_id(&self, package_id: PackageId) -> Option<&OptVersionReq> {
+ self.version_req_map.get(&package_id)
+ }
+
/// Returns `true` if the given feature node index is a feature enabled
/// via the command-line.
pub fn is_cli_feature(&self, index: NodeId) -> bool {
@@ -523,6 +532,12 @@ fn add_pkg(
requested_kind,
opts,
);
+ // Store the version requirement for this dependency for
+ // later use in formatting.
+ graph.version_req_map.insert(
+ graph.package_id_for_index(dep_index),
+ dep.version_req().clone(),
+ );
let new_edge = Edge {
kind: EdgeKind::Dep(dep.kind()),
node: dep_index,
diff --git a/tests/testsuite/cargo_tree/deps.rs b/tests/testsuite/cargo_tree/deps.rs
index 1be50736142..49f34784afc 100644
--- a/tests/testsuite/cargo_tree/deps.rs
+++ b/tests/testsuite/cargo_tree/deps.rs
@@ -1248,25 +1248,25 @@ foo v0.1.0 ([ROOT]/foo) [bar,default,dep_that_is_awesome,foo,other-dep]
.run();
p.cargo("tree --all-features")
- .arg("--format={p}")
+ .arg("--format={p} satisfies {ver-req}")
.with_stdout_data(str![[r#"
-foo v0.1.0 ([ROOT]/foo)
-├── dep v1.0.0
-├── dep_that_is_awesome v1.0.0
-└── other-dep v1.0.0
- └── dep v1.0.0
+foo v0.1.0 ([ROOT]/foo) satisfies
+├── dep v1.0.0 satisfies ^1.0
+├── dep_that_is_awesome v1.0.0 satisfies >=1.0, <2
+└── other-dep v1.0.0 satisfies ^1.0
+ └── dep v1.0.0 satisfies ^1.0
"#]])
.run();
p.cargo("tree --all-features")
- .arg("--format={p}")
+ .arg("--format={p} satisfies {ver-req}")
.arg("--invert=dep")
.with_stdout_data(str![[r#"
-dep v1.0.0
-├── foo v0.1.0 ([ROOT]/foo)
-└── other-dep v1.0.0
- └── foo v0.1.0 ([ROOT]/foo)
+dep v1.0.0 satisfies ^1.0
+├── foo v0.1.0 ([ROOT]/foo) satisfies
+└── other-dep v1.0.0 satisfies ^1.0
+ └── foo v0.1.0 ([ROOT]/foo) satisfies
"#]])
.run();
From 93811239f5aed81590114b4f2b0621666443379c Mon Sep 17 00:00:00 2001
From: Alexandre Barone
Date: Sat, 1 Nov 2025 18:27:13 -0400
Subject: [PATCH 5/5] Update cargo tree command docs with new version
requirement option
Signed-off-by: Alexandre Barone
---
src/doc/man/cargo-tree.md | 1 +
src/doc/man/generated_txt/cargo-tree.txt | 2 ++
src/doc/src/commands/cargo-tree.md | 1 +
src/etc/man/cargo-tree.1 | 4 ++++
4 files changed, 8 insertions(+)
diff --git a/src/doc/man/cargo-tree.md b/src/doc/man/cargo-tree.md
index fc3521c9939..e617bc6a6c9 100644
--- a/src/doc/man/cargo-tree.md
+++ b/src/doc/man/cargo-tree.md
@@ -167,6 +167,7 @@ strings will be replaced with the corresponding value:
- `{r}` --- The package repository URL.
- `{f}` --- Comma-separated list of package features that are enabled.
- `{lib}` --- The name, as used in a `use` statement, of the package's library.
+- `{ver-req}` --- The version requirement resolved for the package.
{{/option}}
{{#option "`--prefix` _prefix_" }}
diff --git a/src/doc/man/generated_txt/cargo-tree.txt b/src/doc/man/generated_txt/cargo-tree.txt
index 0fc542f429f..926c58824a5 100644
--- a/src/doc/man/generated_txt/cargo-tree.txt
+++ b/src/doc/man/generated_txt/cargo-tree.txt
@@ -166,6 +166,8 @@ OPTIONS
o {lib} — The name, as used in a use statement, of the
package’s library.
+ o {ver-req} — The version requirement resolved for the package.
+
--prefix prefix
Sets how each line is displayed. The prefix value can be one of:
diff --git a/src/doc/src/commands/cargo-tree.md b/src/doc/src/commands/cargo-tree.md
index ee8c90fa7bc..c517fe07f20 100644
--- a/src/doc/src/commands/cargo-tree.md
+++ b/src/doc/src/commands/cargo-tree.md
@@ -170,6 +170,7 @@ strings will be replaced with the corresponding value:
{r} — The package repository URL.
{f} — Comma-separated list of package features that are enabled.
{lib} — The name, as used in a use statement, of the package’s library.
+{ver-req} — The version requirement resolved for the package.
diff --git a/src/etc/man/cargo-tree.1 b/src/etc/man/cargo-tree.1
index cd2739f9efa..b1fd0a4e90d 100644
--- a/src/etc/man/cargo-tree.1
+++ b/src/etc/man/cargo-tree.1
@@ -208,6 +208,10 @@ strings will be replaced with the corresponding value:
.RS 4
\h'-04'\(bu\h'+03'\fB{lib}\fR \[em] The name, as used in a \fBuse\fR statement, of the package\[cq]s library.
.RE
+.sp
+.RS 4
+\h'-04'\(bu\h'+03'\fB{ver\-req}\fR \[em] The version requirement resolved for the package.
+.RE
.RE
.sp
\fB\-\-prefix\fR \fIprefix\fR