HardMax71/module-cycles

A Dylint lint for sibling modules that depend on each other in a cycle

Rust

0

1 commits

updated Sep 24, 2026

See the code

See what people are saying

SourceMessageScoreDate

module-cycles: a Dylint lint for modules that depend on each other in a cycle (r/rust)

Clippy has had an open issue for this since 2020 ([\#5782](https://github.com/rust-lang/rust-clippy/issues/5782)), so I wrote it as a Dylint lint: [https://github.com/HardMax71/module-cycles](https://github.com/HardMax71/module-cycles) For now, it reports sibling modules that use each other, like…

0

Sep 24, 2026

README

module-cycles

A Dylint lint that reports sibling modules depending on each other in a cycle.

Two modules are siblings when they share a parent, and each one counts together with everything nested inside it. So crate::report and crate::model are compared as whole subtrees: if anything in report uses model and anything in model uses report, that's a cycle. A child using its parent's items, or a parent using its children's, isn't reported, because that's one module split into files and nearly every crate does it.

A dependency is any reference the compiler resolved: paths (including use and re-exports), method calls and Type::assoc calls. A method call depends on the module holding the impl block that defines the method, which isn't always the module of the type. References written by another crate's macros are skipped. References written by the crate's own macros count and are shown at the macro call.

Each cycle is reported once, on one reference, with a note for every other step of the shortest loop through it:

warning: modules `model` and `report` depend on each other
  --> src/lib.rs:9:22
   |
9  |     pub fn kind() -> crate::report::Row {
   |                      ^^^^^^^^^^^^^^^^^^ `model` depends on `report` here
   |
note: `report` depends on `model` here
  --> src/lib.rs:16:22
   |
16 |     pub fn rows() -> crate::model::Id {
   |                      ^^^^^^^^^^^^^^^^
   = help: move the items they share into one of them, or into a module both depend on

Running it

Add the library to the workspace's Cargo.toml, pinned to a commit:

[workspace.metadata.dylint]
libraries = [{ git = "https://github.com/HardMax71/module-cycles", rev = "<commit>" }]

Then install Dylint and run it:

cargo install cargo-dylint dylint-link
cargo dylint --all -- --workspace --all-targets

It warns by default. To fail the run on any cycle, deny it:

DYLINT_RUSTFLAGS="-D module_cycles" cargo dylint --all -- --workspace --all-targets

To allow one cycle, put the attribute on the reported item or on its module. The cfg_attr keeps a plain cargo build from warning about a lint it doesn't know:

#[cfg_attr(dylint_lib = "module_cycles", allow(module_cycles))]

The crate then declares the cfg, so rustc doesn't flag it as unexpected:

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(dylint_lib, values(any()))"] }

The lint builds on the nightly pinned in rust-toolchain.toml. Dylint installs that toolchain itself the first time it builds the library.

architecture
clippy
dependency-cycles
dylint
lint
linter
modules
rust
rustc
static-analysis

Contributors

HardMax71

1 commits

HardMax71/module-cycles

A Dylint lint for sibling modules that depend on each other in a cycle

Rust

0

1 commits

updated Sep 24, 2026

See the code

See what people are saying

SourceMessageScoreDate

module-cycles: a Dylint lint for modules that depend on each other in a cycle (r/rust)

Clippy has had an open issue for this since 2020 ([\#5782](https://github.com/rust-lang/rust-clippy/issues/5782)), so I wrote it as a Dylint lint: [https://github.com/HardMax71/module-cycles](https://github.com/HardMax71/module-cycles) For now, it reports sibling modules that use each other, like…

0

Sep 24, 2026

README

module-cycles

A Dylint lint that reports sibling modules depending on each other in a cycle.

Two modules are siblings when they share a parent, and each one counts together with everything nested inside it. So crate::report and crate::model are compared as whole subtrees: if anything in report uses model and anything in model uses report, that's a cycle. A child using its parent's items, or a parent using its children's, isn't reported, because that's one module split into files and nearly every crate does it.

A dependency is any reference the compiler resolved: paths (including use and re-exports), method calls and Type::assoc calls. A method call depends on the module holding the impl block that defines the method, which isn't always the module of the type. References written by another crate's macros are skipped. References written by the crate's own macros count and are shown at the macro call.

Each cycle is reported once, on one reference, with a note for every other step of the shortest loop through it:

warning: modules `model` and `report` depend on each other
  --> src/lib.rs:9:22
   |
9  |     pub fn kind() -> crate::report::Row {
   |                      ^^^^^^^^^^^^^^^^^^ `model` depends on `report` here
   |
note: `report` depends on `model` here
  --> src/lib.rs:16:22
   |
16 |     pub fn rows() -> crate::model::Id {
   |                      ^^^^^^^^^^^^^^^^
   = help: move the items they share into one of them, or into a module both depend on

Running it

Add the library to the workspace's Cargo.toml, pinned to a commit:

[workspace.metadata.dylint]
libraries = [{ git = "https://github.com/HardMax71/module-cycles", rev = "<commit>" }]

Then install Dylint and run it:

cargo install cargo-dylint dylint-link
cargo dylint --all -- --workspace --all-targets

It warns by default. To fail the run on any cycle, deny it:

DYLINT_RUSTFLAGS="-D module_cycles" cargo dylint --all -- --workspace --all-targets

To allow one cycle, put the attribute on the reported item or on its module. The cfg_attr keeps a plain cargo build from warning about a lint it doesn't know:

#[cfg_attr(dylint_lib = "module_cycles", allow(module_cycles))]

The crate then declares the cfg, so rustc doesn't flag it as unexpected:

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(dylint_lib, values(any()))"] }

The lint builds on the nightly pinned in rust-toolchain.toml. Dylint installs that toolchain itself the first time it builds the library.

architecture
clippy
dependency-cycles
dylint
lint
linter
modules
rust
rustc
static-analysis

Contributors

HardMax71

1 commits

Languages

Rust

100.0%