Run Rust lints from dynamic libraries (EuroRust 2024 slides)
cargo install cargo-dylint dylint-link
Dylint is a Rust linting tool, similar to Clippy. But whereas Clippy runs a predetermined, static set of lints, Dylint runs lints from user-specified, dynamic libraries. Thus, Dylint allows developers to maintain their own personal lint collections.
Contents
Documentation is also available on how Dylint works.
The next two steps install Dylint and run all of this repository's general-purpose, example lints on a workspace:
-
Install
cargo-dylint
anddylint-link
:cargo install cargo-dylint dylint-link
-
Run
cargo-dylint
:cargo dylint --git https://github.com/trailofbits/dylint --pattern examples/general
In the above example, the libraries are found via the command line. If you plan to run Dylint regularly, then consider using workspace metadata. For additional ways of finding libraries, see How Dylint works.
You can start writing your own Dylint library by running cargo dylint new new_lint_name
. Doing so will produce a loadable library right out of the box. You can verify this as follows:
cargo dylint new new_lint_name
cd new_lint_name
cargo build
cargo dylint list --path .
All you have to do is implement the LateLintPass
trait and accommodate the symbols asking to be filled in.
Helpful resources for writing lints appear below.
A workspace can name the libraries it should be linted with in its Cargo.toml
or dylint.toml
file. Specifically, either file can contain a TOML array under workspace.metadata.dylint.libraries
. Each array entry must have the form of a Cargo git
or path
dependency, with the following differences:
- There is no leading package name, i.e., no
package =
. path
entries can contain glob patterns, e.g.,*
.- Any entry can contain a
pattern
field whose value is a glob pattern. Thepattern
field indicates the subdirectories that contain Dylint libraries.
Dylint downloads and builds each entry, similar to how Cargo downloads and builds a dependency. The resulting target/release
directories are searched for files with names of the form that Dylint recognizes (see Library requirements under How Dylint works).
As an example, if you include the following in your workspace's Cargo.toml
or dylint.toml
file and run cargo dylint --all
, Dylint will run all of this repository's example general-purpose lints, as well as the example restriction lint try_io_result
.
[workspace.metadata.dylint]
libraries = [
{ git = "https://github.com/trailofbits/dylint", pattern = "examples/general" },
{ git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/try_io_result" },
]
For convenience, the pattern
field can contain an array, in which case the pattern is considered to be the union of the array elements. Thus, the just given workspace.metadata.dylint.libraries
example could alternatively be written as:
[workspace.metadata.dylint]
libraries = [
{ git = "https://github.com/trailofbits/dylint", pattern = [
"examples/general",
"examples/restriction/try_io_result",
] },
]
Libraries can be configured by including a dylint.toml
file in a linted workspace's root directory. The file should encode a toml table whose keys are library names. A library determines how its value in the table (if any) is interpreted.
As an example, a dylint.toml
file with the following contents sets the non_local_effect_before_error_return
library's work_limit
configuration to 1_000_000
:
[non_local_effect_before_error_return]
work_limit = 1_000_000
For instructions on creating a configurable library, see the dylint_linting
documentation.
For each library that Dylint uses to check a crate, Dylint passes the following to the Rust compiler:
--cfg=dylint_lib="LIBRARY_NAME"
You can use this feature to allow a lint when Dylint is used, but also avoid an "unknown lint" warning when Dylint is not used. Specifically, you can do the following:
#[cfg_attr(dylint_lib = "LIBRARY_NAME", allow(LINT_NAME))]
Note that LIBRARY_NAME
and LINT_NAME
may be the same. For an example involving non_thread_safe_call_in_test
, see dylint/src/lib.rs in this repository.
Also note that the just described approach does not work for pre-expansion lints. The only known workaround for pre-expansion lints is allow the compiler's built-in unknown_lints
lint. Specifically, you can do the following:
#[allow(unknown_lints)]
#[allow(PRE_EXPANSION_LINT_NAME)]
For an example involving abs_home_path
, see internal/src/examples.rs in this repository.
As of nightly-2024-05-05, the names and values of every reachable #[cfg]
are checked. This causes the compiler to produce warnings for cfg_attr
attributes as described above.
To suppress such warnings, add the following to your packages' Cargo.toml files:
[lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(dylint_lib, values(any()))"]
Or, if you're using a Cargo workspace, add the following the workspace's Cargo.toml file:
[workspace.lints.rust.unexpected_cfgs]
level = "warn"
check-cfg = ["cfg(dylint_lib, values(any()))"]
Then, add the following to the Cargo.toml file of each package in the workspace:
[lints]
workspace = true
For an example, see commit c8fabc5
in this repository.
Dylint results can be viewed in VS Code using rust-analyzer. To do so, add the following to your VS Code settings.json
file:
"rust-analyzer.checkOnSave.overrideCommand": [
"cargo",
"dylint",
"--all",
"--",
"--all-targets",
"--message-format=json"
]
If you want to use rust-analyzer inside a lint library, you need to add the following to your VS Code settings.json
file:
"rust-analyzer.rustc.source": "discover",
And add the following to the library's Cargo.toml
file:
[package.metadata.rust-analyzer]
rustc_private = true
The following utilities can be helpful for writing Dylint libraries:
dylint-link
is a wrapper around Rust's default linker (cc
) that creates a copy of your library with a filename that Dylint recognizes.dylint_library!
is a macro that automatically defines thedylint_version
function and adds theextern crate rustc_driver
declaration.ui_test
is a function that can be used to test Dylint libraries. It provides convenient access to thecompiletest_rs
package.clippy_utils
is a collection of utilities to make writing lints easier. It is generously made public by the Rust Clippy Developers. Note that, likerustc
,clippy_utils
provides no stability guarantees for its APIs.
Helpful resources for writing lints include the following:
- Adding a new lint (targeted at Clippy but still useful)
- Author lint
- Common tools for writing lints
- Guide to Rustc Development
- Crate
rustc_hir
- Crate
rustc_middle
- Struct
rustc_lint::LateContext
A bump of the Dylint library's MSRV will be accompanied by a bump of at least Dylint's minor version.
Put another way, we strive to preserve Dylint's MSRV when releasing bug fixes, and to change it only when releasing new features.