cargo-modules: Reading a Rust Crate's Module Tree Without Opening Every File
Visualize/analyze a Rust crate's internal structure
At a glance
- What is it?
- cargo-modules is a cargo plugin that prints a crate's module hierarchy, internal dependency graph and unlinked source files. It is useful for auditing visibility and structure in large crates, and it inherits whatever the rustc/rust-analyzer pipeline can resolve.
- Who is it for?
- Adopt cargo-modules if you maintain a crate whose module tree has outgrown a single mental model, or if you are preparing a visibility cleanup and want the pub / pub(crate) / pub(in path) split rendered before you edit anything. Skip it if you need cross-crate or workspace-wide dependency analysis: the README describes the dependencies command as printing a crate's internal dependencies, so the boundary is the crate, not the workspace.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The mental-model gap cargo-modules fills
Rust's module system lets you split a crate into arbitrarily small sub-modules. The README states the trade-off plainly: this avoids monolithic chunks of code, but it also makes it hard to stay on top of the overall high-level structure. Nothing in rustc or cargo prints that structure back to you. You can read src/lib.rs, follow each mod declaration, and reconstruct the tree by hand, and that works until the crate has a few hundred items spread across nested modules with mixed visibility.
cargo-modules addresses exactly that gap. It is a cargo plugin, installed with cargo install cargo-modules, and it exposes three subcommands: structure, dependencies and orphans. The audience is the person who owns a crate rather than the person consuming it. If you are deciding whether to depend on a library, this tool tells you nothing. If you are the one who has to decide whether pub(in crate::amet) is the right visibility for a union buried three modules deep, it is aimed at you.
What structure actually prints, and how visibility is encoded
The structure subcommand prints the crate as a text tree. The README's example, run from tests/projects/readme_tree_example, produces a root line reading crate readme_tree_example followed by indented entries such as trait Lorem: pub, mod amet: pub(crate), and a union Elit: pub(in crate::amet) nested under three levels of module.
Each line follows the shape <visibility> <keyword> <name> [<test-attributes>]. The visibility is colour-coded when the terminal supports it and NO_COLOR is not set: green for pub, yellow for pub(crate), orange for pub(in path), red for pub(self) or the implied private case. That colour mapping is the most useful part of the design, because visibility is the thing that is hardest to hold in your head when reading source. A module that looks public at a glance may be pub(crate), and a type may be reachable only from one ancestor module. The tree makes that legible in one screen.
Items guarded by #[cfg(test)] and functions marked #[test] carry their attributes in gray and cyan. The --cfg-test flag makes the analysis run as if built via cargo test, which is why the README example includes a mod tests: pub(crate) #[cfg(test)] branch containing fn it_works: pub(self) #[test]. Without that flag, the test module would not appear at all. That distinction matters when you are auditing what ships in a release build versus what only exists under test.
Filters, sorting and the focus-on expression
The structure command carries a wide option set, and the filters are where it becomes practical on a large crate. --no-fns, --no-traits and --no-types each remove a category from the tree, so you can look at module layout alone without function noise. --sort-by accepts name, visibility or kind, with --sort-reversed to flip the order. Sorting by visibility is the natural companion to the colour scheme: it groups the public surface at one end and the private internals at the other.
The two options that do the most work are --focus-on and --max-depth. --focus-on takes a path or a use-tree's environment, with the README giving the form "foo::bar::{self, baz, blee::*}". --max-depth limits the generated tree to a number of levels relative to the crate root, or relative to the nodes selected by --focus-on. Combined, they let you answer a narrow question, such as what is reachable under one module and how deep it goes, without printing the whole crate.
The command also accepts the standard cargo selection flags: --lib, --bin <BIN>, -p/--package, --manifest-path, --target, and the feature flags --no-default-features, --all-features and --features. The help text notes that --features is ignored when --cargo-all-features is provided, which is worth reading twice if you script the tool, since a feature flag you pass may silently have no effect.
dependencies and orphans: two narrower jobs
The dependencies subcommand prints a crate's internal dependencies as a graph. The help output retrieved here is truncated partway through the --no-fns option, so the full flag list for this subcommand is not something I can state. What is visible is --no-externs, which filters extern items from extern crates out of the graph. That flag tells you the default behaviour includes extern items, and that the graph is about relationships inside the crate rather than a full picture of your dependency tree. If you want to know which crates you depend on, this is not that tool.
The orphans subcommand detects unlinked source files within a crate's directory. This is the smallest of the three commands and, in practice, often the most immediately actionable: a file sitting in src/ that no mod declaration reaches is either dead code or a module someone forgot to wire up. It is a cheap check to run and it produces a binary answer. The caveat is that "unlinked" and "unused" are not the same claim, and the README does not enumerate the resolution rules the tool applies, so treat a flagged file as a question to investigate rather than a verdict.
Where the tool stops being the right choice
The scope boundary is the crate. The README describes the tool as analysing a crate's internal structure and internal dependencies. Nothing in the material suggests it aggregates across a workspace or traces dependencies between your crates. On a multi-crate workspace you will be running it once per package, and the cross-crate edges will not appear in the graph.
There is a second boundary that is easy to miss. The subcommands take --target and feature flags, which means the output is configuration-dependent. A tree produced with --all-features will differ from one produced with --no-default-features, and both will differ from a --cfg-test run. There is no single canonical structure for a crate whose module tree is gated by cfg. If you paste a structure dump into a design document without recording the flags that produced it, the next reader may not be able to reproduce it.
Finally, the tool depends on resolving the crate the way the compiler does. Anything that requires a build script, a proc macro, or an include! to produce source will be invisible to a static module walk unless the underlying resolution handles it. The README does not document the resolution pipeline or its failure modes, so I cannot say how it behaves on such crates. That is a gap in the documentation, not a claim about a defect.
Compared with writing the tree by hand or reaching for rustdoc
The obvious alternative for inspecting a Rust crate's structure is rustdoc, which already ships with the toolchain and renders a module tree with item listings. The difference in approach is the audience. rustdoc produces documentation for people consuming the crate, and it is oriented toward the public surface; private items are hidden by default and the output is HTML meant for browsing. cargo-modules prints to the terminal, defaults to including private and pub(self) items, and encodes visibility as the primary signal. It answers "what can see what" rather than "what does this API look like".
cargo tree is the other comparison that comes to mind, and it is the wrong one: cargo tree walks the dependency graph of external crates, while cargo-modules walks the module tree inside one crate. They overlap in name only.
The honest alternative for a small crate is reading src/lib.rs. For a crate with a handful of modules, a text editor and two minutes of scrolling beats installing a plugin, and the mental model you build by reading the source is more durable than a dump you skim once. cargo-modules earns its place when the tree is too large to hold in working memory, or when you need the visibility distribution rather than the names.
Installation, licence and the upkeep you are signing up for
Installation is one command: cargo install cargo-modules. There is no configuration file, no daemon, and no state to manage. The tool is invoked as a cargo subcommand, so cargo modules structure and cargo modules dependencies work from any directory containing a Cargo.toml, with --manifest-path available when you are elsewhere.
The licence is MPL-2.0, the Mozilla Public License 2.0. That is a file-level copyleft licence: if you modify files that are part of cargo-modules and distribute those modified files, the MPL's terms attach to those files. Installing the binary with cargo install and using it on your own crate does not put your crate under the MPL. I am describing the licence's general shape, not giving legal advice; if you intend to vendor or fork the source, read the licence text and, where the stakes justify it, talk to someone qualified.
Upkeep is the more practical cost. The repository shows no releases retrieved, so there is no changelog to scan before an upgrade, and the last push recorded is 2026-09-10. Because the tool resolves Rust source, it tracks the language and the compiler: a new syntax form or an edition change is the kind of thing that can require a matching update. Pinning a version in a CI image and bumping it deliberately is the low-drama approach. The repository is not archived, which is the minimum signal that maintenance is ongoing.
Editorial conclusion
Adopt cargo-modules if you maintain a crate whose module tree has outgrown a single mental model, or if you are preparing a visibility cleanup and want the pub / pub(crate) / pub(in path) split rendered before you edit anything. Skip it if you need cross-crate or workspace-wide dependency analysis: the README describes the dependencies command as printing a crate's internal dependencies, so the boundary is the crate, not the workspace. Before relying on it in CI, run cargo modules orphans on your own tree and check whether every file it flags is genuinely unreachable, because the tool reports files that are not linked into the module tree, and a build-script-generated or include!-ed file can land in that list without being dead code.
Community notes