Written in collaboration with @detrumi.
This is a rough plan for adding lint configuration by config file to Cargo.
It’s a continuation of this Cargo issue from @rantanen. Relevant parts were copied and used as a starting ground for this text. @detrumi opened a Cargo PR when the issue was opened. However, it was decided at the time that the design should go through more discussion first. That’s what this proposal is meant to kick-off.
Summary
Rust currently only allows configuring lint levels on the command line or via
crate level attributes. This proposes an additional way to configure
lint levels and possibly lint-specific settings in Cargo.toml
.
Motivation
Configuring lint levels on the command line or via crate level attributes is fine when the project consists of a single crate, but gets more cumbersome when the project is a workspace with more than a few crates.
Being able to define the lints in an external file that will be used when building the crates would have several benefits:
- Sharing one configuration file for all the crates in a workspace, therefore ensuring consistent lint levels everywhere.
- A canonical place to check for lint configuration when contributing to a new project.
- An easy way to examine the version history for lint changes.
Some real world examples of where this could bring improvements:
- This rustc rollup which denies a single lint in various subcrates over multiple PRs.
- serde’s lib.rs and serde_derive’s lib.rs would also benefit from having a central lint configuration file. The same for diesel’s lib.rs and diesel_migrations’s lib.rs.
- The Amethyst game framework, with 15 different sub crates, has to
enable warnings for the
rust_2018_ideoms
andrust_2018_compatibilty
lint groups in every single one of the sub-crates (that’s 6 of them linked here). - Similarly, ripgrep denies the
missing_docs
lint in all of its 9 workspace members: GitHub search
Previously this topic came up a few times in the Clippy issue tracker as well:
- #574: Ability to conditionally set levels for lints when not using clippy through cargo features
- #1313: Allow/deny lints in clippy.toml
- #3164: It is impossible to understand from the readme file how to suppress lints using clippy.toml
Guide-level explanation
Lint configuration can also be done through a configuration file. The
configuration needs to be able to configure the allow/warn/deny/forbid
lint
levels for each lint. In order to be more teachable, using toml syntax is
probably the best way to configure the lints. The most obvious location is in
the Cargo.toml
file.
Reference-level explanation
In the most basic version, it would be possible to set the
allow/warn/deny/forbid
of lints that are registered with the lint registry.
These would be grouped in a new [lints]
section. Tool-specific lints (like
Clippy) are grouped together using the tool name.
That would give us a format like this:
[lints]
dead_code = "allow"
[lints.clippy]
non_snake_case = "warn"
This format would make git history easy to read and would allow you to add configuration options to lints later on. It also allows grouping of lints on more than just the lint level.
And if Cargo/rustc ever support lint configurations, this would be more future proof:
[lints.clippy]
cyclomatic_complexity = { state = "deny", threshold = 30 }
Another possible format would be:
[lints]
allow = ["dead_code", "non_snake_case"]
This has the benefit of not having to repeat the lint level for every single lint. However, this would probably make diffs more difficult to read.
cargo check
and tool lints interaction
If a user has configured a tool lint in Cargo.toml
and runs cargo check
, the
implementation needs to make sure to only pass the rustc lints to rustc
,
otherwise rustc would complain about unknown lints because it was invoked with
check
and not clippy
, for example.
When cargo clippy
is executed, it should include the rustc lints, as it does
today.
Lint precedence
The lints can be specified on the workspace level and for individual packages. Anything on the package level will override the workspace setup on a per lint basis. This means that lints configured in the workspace will act as a default for packages within that workspace, and can be overridden by individual packages.
Why Cargo.toml?
The biggest advantage of using Cargo.toml
is that it’s already used for
configuring project and workspace level settings. Most users would expect to
find lint configuration there and pretty much every Rust project uses it
already.
However, one downside is that there is currently no way to share a Cargo.toml
between separate projects. There’s no concrete solution here, but maybe
a new inherit_from
or inherit_lints_from
key could solve that problem. Prior
art includes at least Rubocop’s inherit_from
setting and
eslint’s extend
setting.
Additionally, one might argue that Cargo.toml
is part of Cargo, which should
just be concerned about package management and not lint configuration. However,
Cargo is currently the only tool available that interfaces with rustc
and
offers a way to configure interfacing with rustc.
For users it may also not be immediately clear that both cargo check
and
cargo clippy
work with the same underlying lint system and they may not expect
to be able to configure tool lints through a Cargo configuration file. This is
because other programming language ecosystems usually have completely separate
tools for their lints. See the Prior art section below for a small
discussion of this problem.
If we find a solution to Cargo.toml
not being shareable across projects, we
consider it to be the best approach mainly because the file is already present
in every Cargo project.
Drawbacks
- Adding lint configuration to
Cargo.toml
means that there will be more places where lints can be configured, making it more difficult to quickly see what lint levels are used. - Another downside of this is that it becomes less clear which lint level takes precedence, if the same lint is set multiple times in different locations. Even if the precedence rules are useful for the majority of use cases, developers still need to be aware of the precedence, or avoid specifying the lint multiple times.
Alternatives
Using package.metadata
tables
Another approach, that was proposed on IRLO recently, could
be using the metadata
configuration:
Cargo would look for a [package.metadata.lints]
section instead of a [lints]
section. Tools that are not hooking into the lint system, could use their own
[package.metadata.toolname]
section instead of using a custom file.
One problem with [package.metadata]
is that it does not support virtual
workspaces as it requires the package
table to be
present in the Cargo.toml
.
Different configuration file location
There are some other plausible locations to configure lints, such as
.cargo/config
or Lints.toml
.
.cargo/config
A more standard location, though less well-known, would be .cargo/config
(see Cargo reference).
As with Cargo.toml
, this file would need a new [lints]
section, too.
One big advantage would be that users could add their personal lint preferences
in their home directory (both $HOME/.cargo/config
and .cargo/config
are
supported) as .cargo/config
already
has a hierarchical configuration lookup.
A downside would be that Lint configuration via config file would supposedly be
pretty common, while custom Cargo configuration is rarely used. Almost every
project would want to make use of a lint configuration, which means that every
project would end up having to create the additional .cargo/config
file.
Lints.toml
Another option would be a completely separate file. Maybe called Lints.toml
.
This would be the most flexible solution because the implementation would not
have to care about existing code for Cargo.toml
or .cargo/config
. It is also
more similar to the existing Clippy.toml
.
However, it would also mean that, like with .cargo/config
, users have to add
an additional configuration file to the roots of their repositories.
Additionally, we may also want to handle rustfmt
configuration, and we would
need to find a more general name.
Prior art
In other programming language ecosystems, the concerns of dependency management
and things such as lint configuration are handled by completely separate tools.
This is usually because the language itself does not come with any lints like
Rust. For example, in Javascript, you have eslint and the
package.json
, which don’t really interact. In Ruby, you have
Rubocop for lints and bundler
/Gemfile
for dependencies.
Rust is different from these examples because it already comes with built-in lints and offers an interface for external tools to make use of the same lint system.
If another language exists that provides external tools with hooks into its lint system, it would be good to take a look. Specifically how the interaction between external and internals lints is handled.
Future possibilities
- It might be desirable to also be able to configure the settings of specifics lints, such as in the
cyclomatic_complexity
shown earlier. This could also replace theclippy.toml
file, which currently allows configuring lints. - It would make sense to include other settings related to lints, such as output verbosity or
--cap-lints
in the lint configuration. - Other tools could also add lints to the same lints section, not just Clippy.
- rustfmt configuration via
Cargo.toml
might be something that’s desired, too.
Diagnostics for lint level definition location reporting
With the current approach, we pass all lint level definitions from the config file to rustc via the command line arguments. Consequently, rustc will tell the user the lint level has been defined on the command-line. This is the opposite of helpful if the user wants to change the level of the lint:
error: any use of this value will cause an error
--> $DIR/const-err-multi.rs:13:1
|
LL | pub const A: i8 = -std::i8::MIN;
| ^^^^^^^^^^^^^^^^^^-------------^
| |
| attempt to negate with overflow
|
= note: requested on the command line with `-D const_err`
Ideally, the user would get a note
like this:
note: lint level defined here
--> $DIR/Cargo.toml:511:1
|
LL | const_err = { state = "deny" }
| ^^^^^^^^^
Adding a new variant to LintSource
is the easy part, but how is this
information passed from Cargo to rustc? Should we pass a json file with lint
level definition locations to rustc every time cargo invokes rustc?
Diagnostics for unknown lints reporting
There is currently no good way to detect whether a lint is known or not: all configured lints are passed to rustc directly, which throws an error if the lint isn’t known. Ideally, Cargo would issue a warning instead that includes the line number in the configuration file.