Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "3"

[workspace.package]
version = "0.6.0"
version = "0.7.0"
edition = "2024"
license = "MIT"
rust-version = "1.93"
Expand Down
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Gizmos for working with CSVs
* [canspam](#canspam) -- generate random CAN traffic
* [canstruct](#canstruct) -- reconstruct NMEA 2000 Fast Packet / ISO 11783-3 Transport Protocol
sessions
* [depquery](#depquery) -- query properties of dependency graphs
* [bbclasses](#bbclasses) -- generate BitBake recipe inheritance diagrams

# Philosophy
Expand Down Expand Up @@ -262,19 +263,17 @@ recover lost metadata.
Filter or select subsets of dependency graphs. Works on the same formats as `depconv`, and is
designed to be chained with pipes.

* `depfilter select` keeps only nodes matching the given patterns
* `depfilter filter` removes nodes matching the given patterns
* `depfilter select` keeps nodes matching `--include` patterns and/or removes `--exclude` patterns
* `depfilter between` select nodes connecting multiple sets of query nodes
* `depfilter cycles` select any cycles in the graph

Both subcommands have extra options to tune their behavior.
Each subcommand has extra options to tune its behavior.

```sh
# From a cargo dependency tree, select the subtree rooted at "clap", then filter out
# all the proc-macro crates and their dependencies:
# From a cargo dependency tree, select the subtree rooted at "clap", excluding
# all the proc-macro crates:
$ cargo tree --depth 10 \
| depfilter select -p "clap*" --deps -I cargo-tree -O tgf \
| depfilter filter -p "*derive*" -p "*proc*" --deps -I tgf -O dot
| depfilter select -g "clap*" --deps -x "*derive*" -x "*proc*" -I cargo-tree -O dot
digraph {
clap [label="v4.5.57 clap"];
clap_builder [label="v4.5.57 clap_builder"];
Expand Down Expand Up @@ -315,6 +314,30 @@ $ cat data/depconv/bitbake.curl.task-depends.dot |
> The `deptransform` tool shares the same GPL-2.0 license caveat as `depconv` with respect to DOT
> parsing.

## depquery

Query properties of dependency graphs. Lists nodes, edges, and computes graph metrics. Supports the
same input formats as `depconv`, and is designed to be used in pipelines.

```sh
# Show the 5 crates with the most dependencies:
$ cargo metadata --format-version=1 |
depquery nodes --sort out-degree --limit 5
csvizmo-depgraph 20
csvizmo-stats 16
csvizmo-can 12
csvizmo-minpath 11
tracing-subscriber 10
```

The `depquery` tool supports outputting `nodes`, `edges`, and `metrics`. The output is intended to
be machine-readable, and is tab-separated.

> [!NOTE]
>
> The `depquery` tool shares the same GPL-2.0 license caveat as `depconv` with respect to DOT
> parsing.

## can2csv

Parse basic data from a CAN frame into a CSV record. Faster than `sed`, and also parses the canid.
Expand Down
80 changes: 61 additions & 19 deletions crates/csvizmo-depgraph/src/algorithm/between.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@ use crate::{DepGraph, FlatGraphView};
#[derive(Clone, Debug, Default, Parser)]
pub struct BetweenArgs {
/// Glob pattern selecting query endpoints (can be repeated, OR logic)
#[clap(short, long)]
pub pattern: Vec<String>,
#[clap(short = 'g', long)]
pub include: Vec<String>,

/// Glob pattern to exclude nodes from result (can be repeated, OR logic)
#[clap(short = 'x', long)]
pub exclude: Vec<String>,

/// Match patterns against 'id' or 'label'
#[clap(long, default_value_t = MatchKey::default())]
pub key: MatchKey,
}

impl BetweenArgs {
pub fn pattern(mut self, p: impl Into<String>) -> Self {
self.pattern.push(p.into());
pub fn include(mut self, p: impl Into<String>) -> Self {
self.include.push(p.into());
self
}

pub fn exclude(mut self, p: impl Into<String>) -> Self {
self.exclude.push(p.into());
self
}

Expand All @@ -36,7 +45,7 @@ impl BetweenArgs {
/// then for each ordered pair (qi, qj) collects nodes on directed paths from qi to qj
/// via `forward(qi) & backward(qj)`. The union of all pairwise results is the keep set.
pub fn between(graph: &DepGraph, args: &BetweenArgs) -> eyre::Result<DepGraph> {
let globset = build_globset(&args.pattern)?;
let globset = build_globset(&args.include)?;
let view = FlatGraphView::new(graph);

// Match query nodes by glob pattern (OR logic).
Expand Down Expand Up @@ -87,6 +96,22 @@ pub fn between(graph: &DepGraph, args: &BetweenArgs) -> eyre::Result<DepGraph> {
}
}

// Remove nodes matching --exclude patterns from keep set.
if !args.exclude.is_empty() {
let exclude_globset = build_globset(&args.exclude)?;
for (id, info) in graph.all_nodes() {
let text = match args.key {
MatchKey::Id => id.as_str(),
MatchKey::Label => info.label.as_str(),
};
if exclude_globset.is_match(text)
&& let Some(&idx) = view.id_to_idx.get(id.as_str())
{
keep.remove(&idx);
}
}
}

Ok(view.filter(&keep))
}

Expand Down Expand Up @@ -138,7 +163,7 @@ mod tests {
fn direct_path() {
// a -> b: between a and b yields both
let g = make_graph(&[("a", "a"), ("b", "b")], &[("a", "b")], vec![]);
let args = BetweenArgs::default().pattern("a").pattern("b");
let args = BetweenArgs::default().include("a").include("b");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b"]);
assert_eq!(sorted_edge_pairs(&result), vec![("a", "b")]);
Expand All @@ -152,7 +177,7 @@ mod tests {
&[("a", "b"), ("b", "c")],
vec![],
);
let args = BetweenArgs::default().pattern("a").pattern("c");
let args = BetweenArgs::default().include("a").include("c");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c"]);
assert_eq!(sorted_edge_pairs(&result), vec![("a", "b"), ("b", "c")]);
Expand All @@ -166,7 +191,7 @@ mod tests {
&[("a", "b"), ("c", "d")],
vec![],
);
let args = BetweenArgs::default().pattern("a").pattern("c");
let args = BetweenArgs::default().include("a").include("c");
let result = between(&g, &args).unwrap();
assert!(result.nodes.is_empty());
assert!(result.edges.is_empty());
Expand All @@ -180,7 +205,7 @@ mod tests {
&[("a", "b"), ("a", "c"), ("b", "d"), ("c", "d")],
vec![],
);
let args = BetweenArgs::default().pattern("a").pattern("d");
let args = BetweenArgs::default().include("a").include("d");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c", "d"]);
assert_eq!(
Expand All @@ -198,17 +223,17 @@ mod tests {
vec![],
);
let args = BetweenArgs::default()
.pattern("a")
.pattern("b")
.pattern("d");
.include("a")
.include("b")
.include("d");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c", "d"]);
}

#[test]
fn no_match_returns_empty() {
let g = make_graph(&[("a", "a"), ("b", "b")], &[("a", "b")], vec![]);
let args = BetweenArgs::default().pattern("nonexistent");
let args = BetweenArgs::default().include("nonexistent");
let result = between(&g, &args).unwrap();
assert!(result.nodes.is_empty());
assert!(result.edges.is_empty());
Expand All @@ -218,7 +243,7 @@ mod tests {
fn single_match_returns_empty() {
// Only one node matches -- need at least 2 for a path
let g = make_graph(&[("a", "a"), ("b", "b")], &[("a", "b")], vec![]);
let args = BetweenArgs::default().pattern("a");
let args = BetweenArgs::default().include("a");
let result = between(&g, &args).unwrap();
assert!(result.nodes.is_empty());
assert!(result.edges.is_empty());
Expand All @@ -232,7 +257,7 @@ mod tests {
&[("a", "b"), ("b", "c"), ("c", "a")],
vec![],
);
let args = BetweenArgs::default().pattern("a").pattern("c");
let args = BetweenArgs::default().include("a").include("c");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c"]);
}
Expand All @@ -241,8 +266,8 @@ mod tests {
fn match_by_id() {
let g = make_graph(&[("1", "libfoo"), ("2", "libbar")], &[("1", "2")], vec![]);
let args = BetweenArgs::default()
.pattern("1")
.pattern("2")
.include("1")
.include("2")
.key(MatchKey::Id);
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["1", "2"]);
Expand All @@ -257,7 +282,7 @@ mod tests {
&[("a", "b"), ("b", "c"), ("d", "e")],
vec![],
);
let args = BetweenArgs::default().pattern("a").pattern("c");
let args = BetweenArgs::default().include("a").include("c");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c"]);
assert_eq!(sorted_edge_pairs(&result), vec![("a", "b"), ("b", "c")]);
Expand All @@ -271,9 +296,26 @@ mod tests {
&[("a", "b"), ("b", "c")],
vec![],
);
let args = BetweenArgs::default().pattern("?");
let args = BetweenArgs::default().include("?");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "b", "c"]);
assert_eq!(sorted_edge_pairs(&result), vec![("a", "b"), ("b", "c")]);
}

#[test]
fn exclude_removes_from_result() {
// a -> b -> c: between a and c, exclude b
let g = make_graph(
&[("a", "a"), ("b", "b"), ("c", "c")],
&[("a", "b"), ("b", "c")],
vec![],
);
let args = BetweenArgs::default()
.include("a")
.include("c")
.exclude("b");
let result = between(&g, &args).unwrap();
assert_eq!(sorted_node_ids(&result), vec!["a", "c"]);
assert!(sorted_edge_pairs(&result).is_empty());
}
}
2 changes: 1 addition & 1 deletion crates/csvizmo-depgraph/src/algorithm/cycles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn cycles(graph: &DepGraph, _args: &CyclesArgs) -> eyre::Result<DepGraph> {
}

// Map each cycle node to its SCC index.
let mut node_to_scc: HashMap<NodeIndex, usize> = HashMap::new();
let mut node_to_scc = HashMap::new();
for (i, scc) in cycle_sccs.iter().enumerate() {
for &node in scc {
node_to_scc.insert(node, i);
Expand Down
Loading