Skip to content

About

Dylint lint that rejects cyclic dependencies between top-level modules

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dylint-module-cycles

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)

Usage

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.3

Add 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 --all

The 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.

Configuration

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 = true

Test code is always excluded in one more way: the lint bails out on a test-mode compilation, so #[cfg(test)] blocks never create dependencies.

Known limitations

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.

Development

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 test

To try it against a real crate without publishing, point the consuming workspace at a local checkout:

[workspace.metadata.dylint]
libraries = [{ path = "../dylint-module-cycles" }]

License

Licensed under either of

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.

About

Dylint lint that rejects cyclic dependencies between top-level modules

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages