A Dylint lint library that rejects cyclic dependencies between the top-level modules of a crate.
A component is a top-level directory under src/; files sitting directly in
src/ form the root component. Cycles are of any length: A -> B -> A and
A -> B -> C -> A are both rejected. One error is emitted per group of
components that all reach each other, naming the cheapest edge in the group and
listing the dependencies that make it up. Breaking that one edge is the cheapest
way into the cycle; a group that needs more than one edge removed reports again
on the next run.
Every dependency between components is also reported as a note, cyclic or not, so a run shows the shape of the graph even when it rejects nothing:
note: component dependencies in `my_crate`:
root -> src/core (3 dependencies)
src/api -> src/core (12 dependencies, cyclic)
src/core -> src/api (1 dependency, cyclic)
Install the Dylint tooling. The version must match the dylint_linting
dependency of this library (see Cargo.toml):
cargo install --locked cargo-dylint dylint-link --version 6.0.3Add the library to [workspace.metadata.dylint] in your Cargo.toml or
dylint.toml:
[workspace.metadata.dylint]
libraries = [{ git = "https://github.com/tweedegolf/dylint-module-cycles", tag = "v0.1.0" }]Then run, from your workspace root:
cargo dylint --allThe command exits non-zero when a cycle is found, since the lint level is
Deny. The first run clones this repository, builds the lint library with the
nightly toolchain pinned in rust-toolchain.toml, and
type-checks your crate with it, which takes a few minutes. Builds land in
target/dylint, a separate directory from the normal target, so this does
not invalidate anything cargo check or cargo clippy has cached.
The lint reads a [module_dependency_cycles] table from dylint.toml in your
workspace root:
[module_dependency_cycles]
# Directories under src/ whose files belong to no component and are not
# analysed. Default: ["bin"].
excluded_directories = ["bin", "fixtures"]
# Skip files with `test` in the filename. Default: true.
exclude_test_files = trueTest code is always excluded in one more way: the lint bails out on a test-mode
compilation, so #[cfg(test)] blocks never create dependencies.
Dependencies come from resolved paths, so a call reached only through the type
of a receiver (value.method()) does not count, and references produced by
macro expansion are attributed to neither side.
The crate needs the pinned nightly, rustc_private, and a cdylib crate type,
with dylint-link installed as the linker. Then it builds and tests like any
other crate:
cargo build
cargo testTo try it against a real crate without publishing, point the consuming workspace at a local checkout:
[workspace.metadata.dylint]
libraries = [{ path = "../dylint-module-cycles" }]Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.