From 6cf3896558cafca24a868789333a23422a221d10 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Sun, 8 Mar 2026 11:26:56 -0700 Subject: [PATCH 1/9] Custom lint profiles --- text/3926-custom-lint-profiles.md | 423 ++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 text/3926-custom-lint-profiles.md diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md new file mode 100644 index 00000000000..12f57619808 --- /dev/null +++ b/text/3926-custom-lint-profiles.md @@ -0,0 +1,423 @@ +- Feature Name: `custom_lint_profiles` +- Start Date: 2026-03-08 +- RFC PR: [rust-lang/rfcs#3926](https://github.com/rust-lang/rfcs/pull/3926) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +## Summary +[summary]: #summary + + +This proposes the ability to add "lint profiles" to Cargo.toml to allow for wholesale toggling of lint levels in predefined ways. + +In essence, it is a much more powerful version of the coarse lint modality currently offered by `-Dwarnings`. + +# Motivation + + + +## The many ways of toggling lints + +There are multiple ways that lint levels can be toggled in modern Rust. For the purpose of this design we assume usage of Cargo; though some of these work outside of the Cargo world too. + + - In code, by means of `#[allow]` and friends. + - This **does** support use with `cfg` + - This **does** allow fine grained control over code sections + - This **does** allow fine grained control over individual lints + - This **cannot** be easily tweaked at runtime without having to edit code + - This **cannot** be easily shared between crates + - In code, by means of `[lints]` in Cargo.toml. + - This **does not** support use with `cfg` + - This **does** allow fine grained control over code sections + - This **does** allow fine grained control over individual lints + - This **cannot** be easily tweaked at runtime without having to edit code + - This **can** be easily shared between crates (via workspaces) + - In code, by means of `[profiles.foo.rustflags]` and `-Afoobar` + - This **does not** support use with `cfg` + - This **does not** allow fine grained control over code sections + - This **does** allow fine grained control over individual lints + - This **cannot** be easily tweaked at runtime + - This **can** be easily shared between crates (via workspaces) + - In the CLI, by means of `RUSTFLAGS=-Afoobar` and friends. + - This **does not** support use with `cfg` + - This **does not** allow fine grained control over code sections + - This **does** allow fine grained control over individual lints + - This **can** be easily tweaked at runtime + - This **is always** shared between crates + - In the CLI, by means of `RUSTFLAGS=-Dwarnings` or `CARGO_BUILD_WARNINGS=deny` + - This **does not** support use with `cfg` + - This **does not** allow fine grained control over code sections + - This **does not** allow fine grained control over individual lints + - This **can** be easily tweaked at runtime + - This **is always** shared between crates + - In the CLI, by choosing to call `cargo clippy` + - (This is technically a modality too) + +At first glance, it appears that fine grained control is available at both "code editing time" and at runtime, however `-Afoobar` is not pleasant to use at all when you are configuring hundreds of lints. What is missing is a way to toggle groups of lints on and off at runtime, where these groups can be controlled by the developer at a fine grained level in source code somewhere. + +Furthermore, `-Afoobar`, either via `[profiles]` or via `RUSTFLAGS` works poorly with Cargo: most solutions for doing this at runtime can trigger recompilation of the entire crate. `[lints]` was developed in part as a way to avoid this problem. + +By and large, people currently use a mix of `-Dwarnings` and separately calling `cargo clippy` as a way to run different sets of lints on the same codebase. This proposal aims to expand this ability. + + +## When is a lint noticed by the user? + +One of the subtleties here is that different lint levels have different bars of noticeability depending on *where* they are run. A `warn` rustc lint will be noticed locally, and perhaps noticed locally when running `clippy` (depending on workflow!), but will not be noticed if run just in CI. A `deny` lint will be noticed in CI but may be bothersomely in the way when running locally. + +"PR-integrated CI linters" like [clippy-action] and [clippy-sarif] which leave notes on PRs change the dynamic here a little bit, such that "warn" lints run by CI may still be noticed by the user (but potentially only on code they are changing). + +Tools like rust-analyzer can also change the dynamic here, such that "warn" lints do not inundate the user but rather are noticed as a soft note/highlight somewhere. + +In essence, lints can have different effects in different contexts. + +## Lint modalities and their use cases + +Overall, it's quite common in codebases to want to have different "modes" for lints for the different contexts a linter might be run in. +### Deny in CI + +The most common use case is wanting to have the codebase be lint-free but not hinder development while hacking on something, but have the lints gate landing on `main`. Workflows around this typically involve running CI with `-Dwarnings` (or the new `CARGO_BUILD_WARNINGS=deny`), with contributors often running `cargo check` / `cargo clippy` locally and ensuring they are warnings-clean before opening a PR. + +### Noisier PR-integrated linters or IDEs + +If using a PR-integrated CI linter, your bar for non-blocking noisy informative lints can be lower since the linter will only flag things in code touched by the current PR. One may wish to enable far more pedantic lints in such CI. + +_Ideally_ one can do this in the same CI task as "deny in CI". + +Something similar can happen for IDEs which have a relatively muted lint display (often a small :warning: icon near the offending line of code): it's somewhat fine to inundate the programmer with lints because they'll only see the ones affecting the files they are editing, not every file. This expands the scope of lints a user is exposed to without necessarily showing them more than a manageable number of lints. + + +### Check at release time + +In some cases, a lint would be too noisy to deny in CI, but people expect to have the release be lint-free and use that as an opportunity to clean things up. Such a workflow typically involves calling `cargo clippy -D{lints} --fix ` and then `cargo clippy -D{lints}` to catch any stragglers. I've mostly seen this around lints that are _automatable_: It's just not worth it to ask contributors to fix this each PR, but it is worth it to run a pass right before release. + +This isn't a lint, but similar workflows can be found around `rustfmt`'s "format code in doc comments" mode: not worth it to require everyone to do all the time, but worth running before releases. + +Currently, this requires a manual specification of all the relevant lints. + + +### Only on non-test-code + +Some lints protect production code from things like panics and bad API choices, things which aren't as much of a big deal (or even, counterproductive to prevent) for test code. It's common to do something like `#[cfg_attr(test, allow(...))]`, however this can't be combined with the Cargo `[lints]` table, making it less useful as a feature. + +Typically you want something that applies to `cargo test`, `cargo bench`, and test/bench targets during `cargo check --all-targets`. + +Clippy has a patchwork of config options that disable lints in tests, like `allow-unwrap-in-tests`, however not all lints have this, and [they don't work consistently in all test code](https://github.com/rust-lang/rust-clippy/issues/13981). So far most codebases I have worked on end up with a lot of allows in test code for lints that would be easier to global allow. + + +### "Teaching" lints + +A proposal that comes up semi-regularly is for there to be lints that teach you more about your code. These may not even point out _mistakes_, but rather teach you things about code you are writing. While lint profiles doesn't solve this problem entirely, being able to toggle sets of lints for the purposes of "I am learning Rust and want some more helpful nudges" helps solve part of the problem for designs here. + + + + +# Design + +Currently, in Cargo.toml, it is possible to control lints with the `[lints]` section, like this: + +```toml +[lints.rust] +unsafe_code = "forbid" +[lints.clippy] +pedantic = "warn" +enum_glob_use = { level = "deny", priority = 0 } +``` + +In the context of this proposal, this set of lint settings will be called the "default" profile. + + +This proposal adds the ability to define custom lint profiles: + +```toml +[lints.profiles.ci] +inherits = "default" +[lints.profiles.ci.clippy] +pedantic = "allow" +correctness = "deny" +``` + +Lint profiles can control lint groups and lints as usual. They can also "inherit" from an existing profile, which means that they copy over the settings from that profile, applying further lint levels on top. + + +Lint profiles can be controlled from the CLI: + +``` +$ cargo build --lints ci +``` + +Open question: what should the flag be called? Should it also be available as an environment variable? `--lints` is short but perhaps too short, `--lint-profiles` also seems nice. + +Open question: Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. + +## Interaction with workspaces + +Lint profiles set via CLI flag are only relevant for the current workspace. Dependencies outside of the workspace cannot be expected to use the same naming scheme for profiles, and it is unlikely that users will wish to run lints on third-party dependencies. `--lints ci` will set lints to the levels defined by the `ci` profile for all crates in the workspace, for crates with such a profile (falling back to `default` otherwise). + +Not having such a profile is not a hard error in this mode since it's acceptable to not have the profile defined on every workspace crate. + +Open question: When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? + +Open question: Would it hurt to *by default* inherit lint profiles from the workspace? A straightforward implementation would break current behavior of `[lints]` (which does not autoinherit, though perhaps it ought to?), but we could make this behavior kick in only when you specify `--lint someprofile` and `someprofile` is defined on the workspace but not the individual crate. + + +### Workspace inheritance + +The following patterns are all legal: + +```toml +# Inherit all lints and lint profiles from workspace +[lints] +workspace = true + +# Inherit the ci profile from the workspace +[lint.profiles.ci] +workspace = true + +# Inherit just the default profile (`[lints.lintname]` / `[lints.clippy.lintname]`) from the workspace +[lints.profiles.default] +workspace = true + +# Inherit the ci profile from the workspace but override some things +[lint.profiles.ci] +workspace = true +[lint.profiles.ci.clippy] +ptr_eq = "deny" # this is a lint +correctness = "warn" # this is a lint group +``` + +Use of `workspace = true` does not prevent addition of new profiles or tweaking of existing ones. + +Note that currently, `[lints] workspace = true` cannot be combined with explicitly specified lints. + + +## Applying `-Dwarnings` to a build + +This straight up applies `-Dwarnings`. The only allowed values here are `"deny"`, `"warn"`, and `"allow"`. + + +```toml +[lints.profiles.ci] +warn = "deny" +``` + +Open question: Someone should pick how this prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). I don't see any particular choice as having more merit, but a choice must be made. + +Open question: `warn` or `warnings`? + +## Changing lint levels in bulk based on existing profile + +This provides a more flexible way to do `-Dwarnings`-style transforms when inheriting. + +```toml +[lints.profiles.ci] +inherits = { profile = "default", warn = "deny" } +[lints.profiles.ci.rust] +deprecated = "warn" +[lints.profiles.ci.clippy] +some-noisy-lint = "warn" +some-other-noisy-lint = "warn" +``` + +This creates a profile that inherits from the default profile, but with all warnings replaced with hard errors, and further tweaks to some other lints. + +This can be used to turn `warn` or `deny` lint level into any other lint level _when inheriting_. This has no impact on lint levels specified in the source code, or warnings that come from places other than the warning system (in other words, this does not apply `-Dwarnings`) + +This could work well with PR-integrated CI linters, where you want to simultaneously: + + - Ensure the code is lint-free with the regular (default) profile + - Ensure additional nitpicky lints _show up_ on PRs so that people try to fix them (but don't block landing) + + +## Test modalities + +There are two ways of making test modalities work here. One is less powerful but simpler, the other will complicate the implementation. + +Open question: Pick one + +### Option 1: A magic `test` profile + +By default, all crates have the implicit equivalent of this: + +```toml +[lints.profiles.test] +inherits = "default" +``` + +This profile is what is chosen when running `cargo test`/`cargo bench` or building `test` (including integration, unit, and doc tests) and `bench` targets during `cargo check --all-targets`. + +Explicity filling in the `test` profile allows for selecting a different set of lints to include there. For example, one may choose to: + +```toml +[lints.profiles.test.clippy] +indexing-slicing = "allow" +unwrap-used = "allow" +expect-used = "allow" +``` + +Open question: A potential tweak would be to introduce separate `bench`/`doc` lint profiles as well. I don't quite see this as necessary, but it's a simple enough extension. + +This profile selection can still be overridden with `--lints somethingelse` + +This is pretty straightforward, but a lot of the other configurability in this feature is lost on tests. With a special `test` profile, one can't, for example, have a distinction between test and non-test lints work for PR integrated CI linters. + +### Option 2: Test-only contexts + +Instead, a different way to do this would be to have lint inheritance work contextually; so you can define a test profile but only inherit from it in a test context. + +```toml +[lints] +inherits = { profile = "testprofile", context = "test" } + +[lints.profiles.testprofile.clippy] +indexing-slicing = "allow" +unwrap-used = "allow" +expect-used = "test" +``` + +(The name "testprofile" is used here so it's unambiguous when "test" is used as a keyword) + + +Open question: Does configuring the default profile happen on `[lints]`, `[lints.profiles.default]`, or both? + + +`context` can be `normal`, `all` (default), or `test`. Other contexts can be added if desired. + +In this case the `testprofile` profile is automatically inherited from when constructing the lint set for `cargo test`, `cargo bench`, or test targets in `cargo test --all-targets`, but ignored otherwise. + + +This is more powerful but it complicates lint profile inheritance and lint profiles need to keep a list of their lint sets for test and non-test contexts. The benefit of this is that it allows full configurability for test profiles, for example, one can do this: + +```toml +[lints] +inherits = { profile = "testprofile", context = "test" } + +[lints.profiles.testprofile.clippy] +indexing-slicing = "allow" +unwrap-used = "allow" +expect-used = "allow" + +[lints.profiles.ci] +inherits = { profile = "testprofile", context = "test" } +[lints.profiles.ci.rust] +deprecated = "warn" +``` + +Now the CI profile is also able to ignore certain lints in test mode. + + +This does open up the question of multiple inheritance: if we want to inherit both a normal and a test context profile, we may need some form of multiple inheritance. There are three ways to do this that fit well with this design: + + - `inherits` accepts an array: `inherits = ["default", {profile = "testprofile", context = "test"}]` + - `inherits` instead takes a profile name: `inherits.default = true`, `inherits.testprofile = {context = "test"}` + - We add a separate `inherits-test` key: `inherits-test = "testprofile"` + +The former two allow multiple inheritance in general, though we could disallow that. The first and third fits well with the existing syntax of `inherits`, which is modeled after _regular_ profile inheritance. The second feels a bit cleaner. + +Open question: should this only apply to inheritance, or if it should be possible to directly specify lints as being contextual, like so: + +```toml + +[lints.clippy] +indexing-slicing = [{profile = "normal", level = "warn", profile = "test", level = "allow"}] +``` + +It seems like the underlying data model will likely need the ability to store multiple contextual levels per lint, so it might be worth allowing the user to do that too. + +Open question: For `#[test]` in `--lib` test targets, should these lint levels apply _only_ to the tests, or all of the code? The latter is the easy option, the former needs extensions to rustc. I don't really see a strong motivation for this, but I'm calling it out as something to think about. + + [clippy-action]: https://github.com/giraffate/clippy-action + [clippy-sarif]: https://crates.io/crates/clippy-sarif + +## Interaction with rustc + +This design lives entirely in Cargo: In essence it uses the existing `[lints]` infrastructure (which just feeds `rustc` a bunch of flags). + +`#[allow()]`s (etc) in Rust cannot reference lint profiles, they interact with the `[lints]` table much the same as they do today. + +It would be an interesting extension to allow rustc to be provided with custom named lint groups that can be toggled wholesale, see "future work" below. + +## Interaction with regular (build) profiles + +Cargo already allows one to configure `[profile.dev]` `[profile.release]` and custom named profiles for choosing compilation flags. + +This iteration of this proposal does not have any overlap between lint profiles and regular profiles, you can select them in free variation. Lint profile names do not need to match regular profile names. + +The way I see it is that the regular compilation profile is about what kind of artifact you want, whereas the lint profile is more about the actual compilation experience wrt warnings. Lints don't affect the final artifact. + +Regular profiles already have inheritance (etc), so it _is_ tempting to merge the two, and integrates nicely. But this will tie lint profiles to regular profiles which may lead to people needing to define profiles like `release-default` and `release-ci` to get different sets of lints. + +Overall I see the use case of toggling lints to be different from that of toggling other compilation flags. + +Open question: Maybe we want to merge it with profiles anyway? Or perhaps provide a way to set a profile's default lint profile? I haven't seen a strong motivation for this yet. + + +# Drawbacks + +I think the main drawback here is that this is Cargo.toml-focused, so it requires people to buy in to doing lints via `[lints]` and not any other method if they wish to have the benefits. It also + +# Rationale and alternatives + +Overall I think there are a lot of reasons to have "modalities" for lints, some already in use, some attained by a patchwork of features, and some that people would likely use if they were more convenient. Having profiles directly addresses them by giving the user a way to define and name a modality, so they or their CI/IDE/tooling/whatever can pick the right one based on the context. + + +One major alternative is allowing `[lints]` to exist in `[profiles]` (having them inherit the default profile if so). This could work, but it doesn't allow some of the things this RFC does, like disabling lints in test mode, or inheriting with a warn -> deny transform. + +[RFC 3730: Add a semantically non-blocking lint level][RFC 3730] attempts to address the problem of wanting lints to be blocking in some contexts and non blocking in others, essentially by extending `-Dwarnings` to be a bit more flexible, and adding an IDE-useful "nit" lint level that only applies to new code. I think this attempts to solve a bunch of the same problems as this, but it solves them more narrowly: it doesn't help non-IDE users as much, and doesn't solve things like the test mode problem. + +Both of these solutions are likely to be simpler, though. + +# Prior art + +The [custom named Cargo profiles](https://rust-lang.github.io/rfcs/2678-named-custom-cargo-profiles.html) RFC is probably the main prior art here. I think this feature works well, including profile inheritance. + + + +# Unresolved questions + +I've marked most unresolved questions as "open question" in the text of this RFC. I'd rather not duplicate them here, but I can do so if people like. + +Picking a way to handle tests is one major question. + +There's also a bunch of tricky design around how inheritance works around the "default" profile. + + +# Future work + + +## Custom lint groups + + +A thing this feature does *not* let one do is toggle multiple lints at once in code sections, a feature that is useful to have. A *potential* extension of this feature would be to allow profiles to be defined as lint groups so that one can write `#[allo w(customprofile)]`. There's a lot of subtletlies of such a design, including: + + - How does one provide this specification to rustc? + - Profiles contain `allow`s and `warns` and other things, they are not _just_ a grouping of lints. What does it mean to `#[warn(group)]` for a group that contains some `allow`s and some `warn`s? Does it turn on every lint explicitly mentioned in the profile? Does it turn on every `allow` lint from the profile? There's not an easy answer here. + + +It's not yet clear to me how feasible this is, or if we should have such a feature, but it's worth listing. + + +## Teaching lints + +In the past people wished for tooling that produces lints that potentially tell new users about subtleties in their code, subtleties that are not really *problems* to be fixed, but interesting things to be noted. These would be opted in to by individual users and as they get used to a concept, disabled globally one by one. Lint profiles allow one to better handle toggles like this, but it is not in an of itself a major step in this direction. + + +## Further lint levels + +Currently there are only two choices for lint UX: either the lint is a hard error or it is not, and it shows up the same way either way in the CLI. IDEs may choose to offer some customizeability but it's typically global. + +It would be nice to have more choices for the UX of lints, and, ideally, those choices could be targeted to specific noisy-and-less-important lints. A "nit" lint level, similar to that proposed in [RFC 3730], could serve this purpose. + +An example of improved lint UX (in CLI) for a noisy-and-less-important lint would be that instead of showing each instance of the lint, the error message could be something like: + +``` +warn: found 5 instances of the `needless_borrow` lint: + + - src/foo.rs:32 + - src/bar.rs:45 + - ... + +``` + +This works well with lint groups since you may then upgrade nits to warnings when you decide to try and fix these. PR-integrated linters can also choose to have different behavior with these if desired. + + + [RFC 3730](https://github.com/rust-lang/rfcs/pull/3730) \ No newline at end of file From 0e84a245d73dfc17559e95f1796d6c01344cf3c8 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 12 Mar 2026 17:47:25 -0700 Subject: [PATCH 2/9] guide and reference level --- text/3926-custom-lint-profiles.md | 248 +++++++++++++++++++----------- 1 file changed, 158 insertions(+), 90 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index 12f57619808..ce602f08d3c 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -69,6 +69,11 @@ Tools like rust-analyzer can also change the dynamic here, such that "warn" lint In essence, lints can have different effects in different contexts. + + + [clippy-action]: https://github.com/giraffate/clippy-action + [clippy-sarif]: https://crates.io/crates/clippy-sarif + ## Lint modalities and their use cases Overall, it's quite common in codebases to want to have different "modes" for lints for the different contexts a linter might be run in. @@ -107,11 +112,9 @@ Clippy has a patchwork of config options that disable lints in tests, like `allo A proposal that comes up semi-regularly is for there to be lints that teach you more about your code. These may not even point out _mistakes_, but rather teach you things about code you are writing. While lint profiles doesn't solve this problem entirely, being able to toggle sets of lints for the purposes of "I am learning Rust and want some more helpful nudges" helps solve part of the problem for designs here. +# Guide level explanation - -# Design - Currently, in Cargo.toml, it is possible to control lints with the `[lints]` section, like this: ```toml @@ -144,31 +147,29 @@ Lint profiles can be controlled from the CLI: $ cargo build --lints ci ``` -Open question: what should the flag be called? Should it also be available as an environment variable? `--lints` is short but perhaps too short, `--lint-profiles` also seems nice. +## Inheritance and workspaces -Open question: Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. - -## Interaction with workspaces - -Lint profiles set via CLI flag are only relevant for the current workspace. Dependencies outside of the workspace cannot be expected to use the same naming scheme for profiles, and it is unlikely that users will wish to run lints on third-party dependencies. `--lints ci` will set lints to the levels defined by the `ci` profile for all crates in the workspace, for crates with such a profile (falling back to `default` otherwise). - -Not having such a profile is not a hard error in this mode since it's acceptable to not have the profile defined on every workspace crate. +Lint profiles can be inherited from the same package and from the workspace -Open question: When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? -Open question: Would it hurt to *by default* inherit lint profiles from the workspace? A straightforward implementation would break current behavior of `[lints]` (which does not autoinherit, though perhaps it ought to?), but we could make this behavior kick in only when you specify `--lint someprofile` and `someprofile` is defined on the workspace but not the individual crate. +```toml +# Simple inheritance -### Workspace inheritance +[lints] +unused = "allow" +missing-debug-implementations = "warn" +[lint.profiles.ci] +inherits = "default" # inherits from the default profile +unused = "deny" # overrides `unused` preference -The following patterns are all legal: +# Workspace examples -```toml # Inherit all lints and lint profiles from workspace [lints] workspace = true -# Inherit the ci profile from the workspace +# Inherit just the ci profile from the workspace [lint.profiles.ci] workspace = true @@ -180,18 +181,19 @@ workspace = true [lint.profiles.ci] workspace = true [lint.profiles.ci.clippy] -ptr_eq = "deny" # this is a lint +ptr-eq = "deny" # this is a lint correctness = "warn" # this is a lint group ``` -Use of `workspace = true` does not prevent addition of new profiles or tweaking of existing ones. -Note that currently, `[lints] workspace = true` cannot be combined with explicitly specified lints. +Note that if you wish to inherit from a profile defined in the worksp + +## Changing warn levels wholesale -## Applying `-Dwarnings` to a build +It is possible to use lint profiles to map warnings to another level. -This straight up applies `-Dwarnings`. The only allowed values here are `"deny"`, `"warn"`, and `"allow"`. +For example, the following is equivalent to using `-Dwarnings` in CI: ```toml @@ -199,134 +201,181 @@ This straight up applies `-Dwarnings`. The only allowed values here are `"deny"` warn = "deny" ``` -Open question: Someone should pick how this prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). I don't see any particular choice as having more merit, but a choice must be made. - -Open question: `warn` or `warnings`? +The only allowed values here `"deny"`, `"warn"`, and `"allow"`. -## Changing lint levels in bulk based on existing profile +This can be used when inheriting profiles to map `warn` to `deny` for the lint groups that are inherited: -This provides a more flexible way to do `-Dwarnings`-style transforms when inheriting. ```toml +[lints] +some-lint = "warn" +some-other-lint = "warn" [lints.profiles.ci] inherits = { profile = "default", warn = "deny" } -[lints.profiles.ci.rust] +[lints.profiles.ci] deprecated = "warn" -[lints.profiles.ci.clippy] -some-noisy-lint = "warn" -some-other-noisy-lint = "warn" +clippy.some-noisy-lint = "warn" +clippy.some-other-noisy-lint = "warn" ``` This creates a profile that inherits from the default profile, but with all warnings replaced with hard errors, and further tweaks to some other lints. -This can be used to turn `warn` or `deny` lint level into any other lint level _when inheriting_. This has no impact on lint levels specified in the source code, or warnings that come from places other than the warning system (in other words, this does not apply `-Dwarnings`) +This has no impact on lint levels specified in the source code, or warnings that come from places other than the warning system (in other words, this does not apply `-Dwarnings`) + -This could work well with PR-integrated CI linters, where you want to simultaneously: +This is useful with PR-integrated CI linters, where you want to simultaneously: - Ensure the code is lint-free with the regular (default) profile - Ensure additional nitpicky lints _show up_ on PRs so that people try to fix them (but don't block landing) -## Test modalities -There are two ways of making test modalities work here. One is less powerful but simpler, the other will complicate the implementation. +### Workspace inheritance -Open question: Pick one +The following patterns are all legal: + +```toml +# Inherit all lints and lint profiles from workspace +[lints] +workspace = true + +# Inherit the ci profile from the workspace +[lint.profiles.ci] +workspace = true + +# Inherit just the default profile (`[lints.lintname]` / `[lints.clippy.lintname]`) from the workspace +[lints.profiles.default] +workspace = true + +# Inherit the ci profile from the workspace but override some things +[lint.profiles.ci] +workspace = true +[lint.profiles.ci.clippy] +ptr_eq = "deny" # this is a lint +correctness = "warn" # this is a lint group +``` + +Use of `workspace = true` does not prevent addition of new profiles or tweaking of existing ones. + +Note that currently, `[lints] workspace = true` cannot be combined with explicitly specified lints. + + + + +## Test-only lint levels + +There are two ways of making test modalities work here. One is less powerful but simpler, the other will complicate the implementation. This RFC has not yet selected one, but it prefers Option 1 + + +### Option 1: A `test` subprofile + +Each lint profile contains a "test" sub-profile, which behaves like a normal profile, but is applied when you build `cfg(test)` binaries with that profile in test mode. These are integration tests, unit tests, doctests, and benchmarks. -### Option 1: A magic `test` profile -By default, all crates have the implicit equivalent of this: ```toml -[lints.profiles.test] -inherits = "default" +[lints.profiles.ci.test] +some-lint = "allow" +``` + +This profile can inherit from other profiles normally: + +```toml +[lints.profiles.testprofile] +some-lint = "allow" + +[lints.profiles.ci.test] +some-other-lint = "allow" +inherits = "testprofile" ``` -This profile is what is chosen when running `cargo test`/`cargo bench` or building `test` (including integration, unit, and doc tests) and `bench` targets during `cargo check --all-targets`. +When building with `--lints ci`, all the lints specified in the `ci` profile will be used, plus any overrides from the testing subprofile. + +When inheriting a profile into a regular profile, its `test` sub-profile is also inherited. + +(It's unclear if this really needs to support inheritance.) -Explicity filling in the `test` profile allows for selecting a different set of lints to include there. For example, one may choose to: + +### Option 2: Cfg-gated lints + +This works similarly to how you can specify cfg-specific dependencies ```toml -[lints.profiles.test.clippy] +[lints.profiles.ci.'cfg(test)'] indexing-slicing = "allow" -unwrap-used = "allow" -expect-used = "allow" ``` -Open question: A potential tweak would be to introduce separate `bench`/`doc` lint profiles as well. I don't quite see this as necessary, but it's a simple enough extension. +This is a lot more flexible, but it might be too flexible: is there actually a use case for target-specific lints? There is some use case for different lints based on `cfg(test)`, `cfg(doc)`, `cfg(bench)`, etc, but less so for `cfg(windows)`. -This profile selection can still be overridden with `--lints somethingelse` +On the other hand, this does not allow for inheritance, which is simpler. Similar to dependencies, this just lets you conditionally specify additional members. -This is pretty straightforward, but a lot of the other configurability in this feature is lost on tests. With a special `test` profile, one can't, for example, have a distinction between test and non-test lints work for PR integrated CI linters. +# Reference-level explanation -### Option 2: Test-only contexts +## General feature -Instead, a different way to do this would be to have lint inheritance work contextually; so you can define a test profile but only inherit from it in a test context. +A lint profile can be specified as ```toml -[lints] -inherits = { profile = "testprofile", context = "test" } +[lints.profiles.nameofprofile] +workspace = true +lintname = "allow" # allow, warn, deny, forbid +clippy.lintname = "allow" # same +warn = "deny" # allow, warn, deny +inherits = "default" # a name of a profile +# or +inherits = {profile = "default", warn = "deny"} # name of a profile, and an allow/warn/deny value +# Option 1 +test = {} # can contain all the same fields as above, except for `test` itself +# Option 2 +'cfg(test)'.lintname = "allow" -[lints.profiles.testprofile.clippy] -indexing-slicing = "allow" -unwrap-used = "allow" -expect-used = "test" ``` -(The name "testprofile" is used here so it's unambiguous when "test" is used as a keyword) +The "default" lint profile specified as `[lints]` can also have the same fields. + +Note that currently [workspace overriding is not supported][ws-override]. The inheritance mechanism in this RFC provides a straightforward way to support workspace overriding, but it's not necessary to fix to support this RFC. -Open question: Does configuring the default profile happen on `[lints]`, `[lints.profiles.default]`, or both? + [ws-override]: https://github.com/rust-lang/cargo/issues/13157 +## Inheritance -`context` can be `normal`, `all` (default), or `test`. Other contexts can be added if desired. +Internally, each lint profile is *resolved* to a list of lints and lint levels, plus an optional `warn = "somelevel"` setting. In case we go with Option 1 for tests, each profile contains an additional "test" profile. If we go with option 2, it also contains a list of lints and lint levels with `cfg` predicates. This lint is sorted in definition order. -In this case the `testprofile` profile is automatically inherited from when constructing the lint set for `cargo test`, `cargo bench`, or test targets in `cargo test --all-targets`, but ignored otherwise. +When profiles are inherited via `inherits` or `workspace = true`, it is the resolved profile that is inherited. Further overrides are applied on top of that resolved profile, producing a new resolved profile. We do not have multiple inheritance: a single resolved profile is inherited and further overrides can be applied on top of it. +`lints.workspace = true` inherits all lint profiles (including the default one) from the workspace. -This is more powerful but it complicates lint profile inheritance and lint profiles need to keep a list of their lint sets for test and non-test contexts. The benefit of this is that it allows full configurability for test profiles, for example, one can do this: +If we choose to fix the [workspace override][ws-override] issue in this RFC, then the following will work: ```toml [lints] -inherits = { profile = "testprofile", context = "test" } +workspace = true +some-lint = "allow" -[lints.profiles.testprofile.clippy] -indexing-slicing = "allow" -unwrap-used = "allow" -expect-used = "allow" -[lints.profiles.ci] -inherits = { profile = "testprofile", context = "test" } -[lints.profiles.ci.rust] -deprecated = "warn" +[lints.profiles.ci] # assume the workspace has a `ci` profile +some-other-lint = "allow" ``` -Now the CI profile is also able to ignore certain lints in test mode. +Here, the crate will have a default and `ci` profile copied from the workspace, with overrides applied on top. -This does open up the question of multiple inheritance: if we want to inherit both a normal and a test context profile, we may need some form of multiple inheritance. There are three ways to do this that fit well with this design: +When using `{inherits = "someprofile", warn = "deny"}`, the resolved profile has all `warn` entries replaced with `deny` entries before being inherited. - - `inherits` accepts an array: `inherits = ["default", {profile = "testprofile", context = "test"}]` - - `inherits` instead takes a profile name: `inherits.default = true`, `inherits.testprofile = {context = "test"}` - - We add a separate `inherits-test` key: `inherits-test = "testprofile"` -The former two allow multiple inheritance in general, though we could disallow that. The first and third fits well with the existing syntax of `inherits`, which is modeled after _regular_ profile inheritance. The second feels a bit cleaner. +When running with a resolved profile, it is simply a matter of applying the `-A`/`-W`/etc flags specified by the lint list in the resolved profile. Inheritance is already "handled" once you compute a resolved profile. -Open question: should this only apply to inheritance, or if it should be possible to directly specify lints as being contextual, like so: +The interaction of testing solutions with resolved profiles will be covered below. -```toml -[lints.clippy] -indexing-slicing = [{profile = "normal", level = "warn", profile = "test", level = "allow"}] -``` +## Interaction with workspaces + +Lint profiles set via CLI flag are only relevant for the current workspace. Dependencies outside of the workspace cannot be expected to use the same naming scheme for profiles, and it is unlikely that users will wish to run lints on third-party dependencies. `--lints ci` will set lints to the levels defined by the `ci` profile for all crates in the workspace, for crates with such a profile (falling back to `default` otherwise). -It seems like the underlying data model will likely need the ability to store multiple contextual levels per lint, so it might be worth allowing the user to do that too. +Not having such a profile is not a hard error in this mode since it's acceptable to not have the profile defined on every workspace crate. -Open question: For `#[test]` in `--lib` test targets, should these lint levels apply _only_ to the tests, or all of the code? The latter is the easy option, the former needs extensions to rustc. I don't really see a strong motivation for this, but I'm calling it out as something to think about. - [clippy-action]: https://github.com/giraffate/clippy-action - [clippy-sarif]: https://crates.io/crates/clippy-sarif - ## Interaction with rustc This design lives entirely in Cargo: In essence it uses the existing `[lints]` infrastructure (which just feeds `rustc` a bunch of flags). @@ -347,7 +396,18 @@ Regular profiles already have inheritance (etc), so it _is_ tempting to merge th Overall I see the use case of toggling lints to be different from that of toggling other compilation flags. -Open question: Maybe we want to merge it with profiles anyway? Or perhaps provide a way to set a profile's default lint profile? I haven't seen a strong motivation for this yet. +## Testing: Testing subprofiles (Option 1) + +Each profile's `test` subprofile is resolved by taking the "parent" profile and then overlaying any overrides from the test subprofile. It is applied whenever something is being built with `--cfg test` while using that profile. The `test` subprofile is itself resolved + +When inheriting a profile, the resolved `test` subprofile is also inherited, and further overrides can be applied on top. + +It's not clear if `test` profiles need to support inheritance, but if we decide to support that, then an `inherits = ` key in a test subprofile will *switch* the base profile used for inheritance to being the specified profile, rather than the parent profile. + +## Testing: CFG'd lints (option 2) + + +As noted before, these inherit with their predicates. When computing the set of lint levels, the resolved profile is taken, and all `cfg-`predicated lint levels that apply to the current build setting are applied in definition order (inherited cfgs apply first). # Drawbacks @@ -373,11 +433,19 @@ The [custom named Cargo profiles](https://rust-lang.github.io/rfcs/2678-named-cu # Unresolved questions -I've marked most unresolved questions as "open question" in the text of this RFC. I'd rather not duplicate them here, but I can do so if people like. -Picking a way to handle tests is one major question. + - What should the `--lints` flag be called? `--lints`? `--lint-profiles`? + - Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. Either way, we should reserve the profile name `default`. + - Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). I don't see any particular choice as having more merit, but a choice must be made. + - Should it be `warn = "deny"` or `warnings = "deny"`? + - Should we go with Option 1 or 2 for tests? + - In Option 1, should we introduce `bench`/`doc` lint profiles as well? I don't quite see this as necessary, but it's a simple enough extension. + - In Option 1, should we support inheritance with test profiles? + - I've made an argument against merging lint profiles and regular profiles, but maybe we want to merge it with profiles anyway? Or perhaps provide a way to set a profile's default lint profile? I haven't seen a strong motivation for this yet. + - When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? + - Would it hurt to *by default* inherit lint profiles from the workspace? A straightforward implementation would break current behavior of `[lints]` (which does not autoinherit, though perhaps it ought to?), but we could make this behavior kick in only when you specify `--lint someprofile` and `someprofile` is defined on the workspace but not the individual crate. + -There's also a bunch of tricky design around how inheritance works around the "default" profile. # Future work From d010a3486234ec12a6bc802c927b79b834f4d626 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 12 Mar 2026 18:04:03 -0700 Subject: [PATCH 3/9] upgrade workflows --- text/3926-custom-lint-profiles.md | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index ce602f08d3c..1c082845745 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -90,6 +90,14 @@ _Ideally_ one can do this in the same CI task as "deny in CI". Something similar can happen for IDEs which have a relatively muted lint display (often a small :warning: icon near the offending line of code): it's somewhat fine to inundate the programmer with lints because they'll only see the ones affecting the files they are editing, not every file. This expands the scope of lints a user is exposed to without necessarily showing them more than a manageable number of lints. +### Upgrade workflows + +Large projects, as is recommended, often pin a Rust version for their clippy CI, so that their developers are not hit by piles of failures just because the compiler updated. + +Usually, once a new Rust compiler is released, these projects will spend some time updating everything and make a new PR. + +It is sometimes nice to be able to do this at a crate-by-crate level. Especially large projects would like to be able to update their CI toolchain without needing to fix lints everywhere. Having more flexible control over lint levels would allow them to e.g. disable new lints by default, but allow individual crates to opt in to the newer lints, giving a smoother migration path that can be handled at an appropriate pace for individual subcomponents. + ### Check at release time In some cases, a lint would be too noisy to deny in CI, but people expect to have the release be lint-free and use that as an opportunity to clean things up. Such a workflow typically involves calling `cargo clippy -D{lints} --fix ` and then `cargo clippy -D{lints}` to catch any stragglers. I've mostly seen this around lints that are _automatable_: It's just not worth it to ask contributors to fix this each PR, but it is worth it to run a pass right before release. @@ -333,10 +341,10 @@ test = {} # can contain all the same fields as above, except for `test` itself The "default" lint profile specified as `[lints]` can also have the same fields. -Note that currently [workspace overriding is not supported][ws-override]. The inheritance mechanism in this RFC provides a straightforward way to support workspace overriding, but it's not necessary to fix to support this RFC. +The profile can be selected via a `--lints` flag available in all commands that produce a build: `build`, `test`, `run`, `check`, `bench`. - [ws-override]: https://github.com/rust-lang/cargo/issues/13157 +Note that currently [workspace overriding is not supported][ws-override]. More on that below ## Inheritance @@ -346,6 +354,16 @@ When profiles are inherited via `inherits` or `workspace = true`, it is the reso `lints.workspace = true` inherits all lint profiles (including the default one) from the workspace. + +When using `{inherits = "someprofile", warn = "deny"}`, the resolved profile has all `warn` entries replaced with `deny` entries before being inherited. + + +When running with a resolved profile, it is simply a matter of applying the `-A`/`-W`/etc flags specified by the lint list in the resolved profile. Inheritance is already "handled" once you compute a resolved profile. + +The interaction of testing solutions with resolved profiles will be covered below. + +### The workspace override issue + If we choose to fix the [workspace override][ws-override] issue in this RFC, then the following will work: ```toml @@ -358,15 +376,8 @@ some-lint = "allow" some-other-lint = "allow" ``` -Here, the crate will have a default and `ci` profile copied from the workspace, with overrides applied on top. - - -When using `{inherits = "someprofile", warn = "deny"}`, the resolved profile has all `warn` entries replaced with `deny` entries before being inherited. - +Here, the crate will have a default and `ci` resolved profile copied from the workspace, with overrides applied on top. -When running with a resolved profile, it is simply a matter of applying the `-A`/`-W`/etc flags specified by the lint list in the resolved profile. Inheritance is already "handled" once you compute a resolved profile. - -The interaction of testing solutions with resolved profiles will be covered below. ## Interaction with workspaces @@ -412,7 +423,9 @@ As noted before, these inherit with their predicates. When computing the set of # Drawbacks -I think the main drawback here is that this is Cargo.toml-focused, so it requires people to buy in to doing lints via `[lints]` and not any other method if they wish to have the benefits. It also +I think the main drawback here is that this is Cargo.toml-focused, so it requires people to buy in to doing lints via `[lints]` and not any other method if they wish to have the benefits. + +This is also not a simple system: is the complexity worth it? # Rationale and alternatives From 365b84ee780d626dd9556ccdad25547373287480 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 12 Mar 2026 18:10:24 -0700 Subject: [PATCH 4/9] address some comments --- text/3926-custom-lint-profiles.md | 50 +++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index 1c082845745..09c261163cd 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -155,9 +155,15 @@ Lint profiles can be controlled from the CLI: $ cargo build --lints ci ``` + +Open question: What should the `--lints` flag be called? `--lints`? `--lint-profiles`? + + ## Inheritance and workspaces -Lint profiles can be inherited from the same package and from the workspace +Lint profiles can be inherited from the same package and from the workspace. The profile specified in `[lints]` is called `default`. + +Open question: Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. Either way, we should reserve the profile name `default`. @@ -193,8 +199,7 @@ ptr-eq = "deny" # this is a lint correctness = "warn" # this is a lint group ``` - -Note that if you wish to inherit from a profile defined in the worksp +Note that if you wish to inherit from a profile defined in the workspace, you must first inherit the profile via `lints.profiles.profilename.workspace = true`, and then you can inherit from `profilename`. ## Changing warn levels wholesale @@ -211,6 +216,10 @@ warn = "deny" The only allowed values here `"deny"`, `"warn"`, and `"allow"`. +Open question: Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). I don't see any particular choice as having more merit, but a choice must be made. + +Open question: Should it be `warn = "deny"` or `warnings = "deny"`? + This can be used when inheriting profiles to map `warn` to `deny` for the lint groups that are inherited: @@ -272,7 +281,9 @@ Note that currently, `[lints] workspace = true` cannot be combined with explicit ## Test-only lint levels -There are two ways of making test modalities work here. One is less powerful but simpler, the other will complicate the implementation. This RFC has not yet selected one, but it prefers Option 1 +There are two ways of making test modalities work here. One is less powerful but simpler, the other will complicate the implementation. This RFC has not yet selected one, but I prefer Option 1. + +Open question: Should we go with Option 1 or 2 for tests? ### Option 1: A `test` subprofile @@ -303,6 +314,9 @@ When inheriting a profile into a regular profile, its `test` sub-profile is also (It's unclear if this really needs to support inheritance.) +Open question: Should we introduce `bench`/`doc` lint profiles as well? I don't quite see this as necessary, but it's a simple enough extension. + +Open question: Should we support inheritance with test profiles? ### Option 2: Cfg-gated lints @@ -344,7 +358,11 @@ The "default" lint profile specified as `[lints]` can also have the same fields. The profile can be selected via a `--lints` flag available in all commands that produce a build: `build`, `test`, `run`, `check`, `bench`. -Note that currently [workspace overriding is not supported][ws-override]. More on that below +Custom profiles cannot be named `default`. The name `default` is reserved for referencing the default profile. + +Rustc cannot add lints named `workspace`, `inherits`, `warn`, or `test`. + +Note that currently [workspace overriding is not supported][ws-override]. More on that below. ## Inheritance @@ -352,8 +370,9 @@ Internally, each lint profile is *resolved* to a list of lints and lint levels, When profiles are inherited via `inherits` or `workspace = true`, it is the resolved profile that is inherited. Further overrides are applied on top of that resolved profile, producing a new resolved profile. We do not have multiple inheritance: a single resolved profile is inherited and further overrides can be applied on top of it. -`lints.workspace = true` inherits all lint profiles (including the default one) from the workspace. +Loops are not allowed during inheritance. +`lints.workspace = true` copies all resolved lint profiles (including the default one) from the workspace. When using `{inherits = "someprofile", warn = "deny"}`, the resolved profile has all `warn` entries replaced with `deny` entries before being inherited. @@ -386,6 +405,9 @@ Lint profiles set via CLI flag are only relevant for the current workspace. Depe Not having such a profile is not a hard error in this mode since it's acceptable to not have the profile defined on every workspace crate. +Open question: When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? + +Open question: Would it hurt to *by default* inherit lint profiles from the workspace? A straightforward implementation would break current behavior of `[lints]` (which does not autoinherit, though perhaps it ought to?), but we could make this behavior kick in only when you specify `--lint someprofile` and `someprofile` is defined on the workspace but not the individual crate. ## Interaction with rustc @@ -407,6 +429,8 @@ Regular profiles already have inheritance (etc), so it _is_ tempting to merge th Overall I see the use case of toggling lints to be different from that of toggling other compilation flags. +Open question: Maybe we want to merge lint profiles with profiles anyway? Or perhaps provide a way to set a profile's default lint profile? I haven't seen a strong motivation for this yet. + ## Testing: Testing subprofiles (Option 1) Each profile's `test` subprofile is resolved by taking the "parent" profile and then overlaying any overrides from the test subprofile. It is applied whenever something is being built with `--cfg test` while using that profile. The `test` subprofile is itself resolved @@ -443,21 +467,23 @@ Both of these solutions are likely to be simpler, though. The [custom named Cargo profiles](https://rust-lang.github.io/rfcs/2678-named-custom-cargo-profiles.html) RFC is probably the main prior art here. I think this feature works well, including profile inheritance. +These designs are superficially similar, but not super similar in the details. Cargo profiles are workspace-level, whereas this is package-level. The inheritance works similarly, where each profile is a list of key-value pairs, and when inherited that list of pairs is inherited with overrides being applied on top. # Unresolved questions +These are all mentioned inline in the RFC under "open question", but duplicated here for referencing. I prefer having these discussions inline in the RFC. + - What should the `--lints` flag be called? `--lints`? `--lint-profiles`? - - Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. Either way, we should reserve the profile name `default`. - - Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). I don't see any particular choice as having more merit, but a choice must be made. + - Should it be possible to also access the default profile via `[lints.profiles.default]`? + - Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). - Should it be `warn = "deny"` or `warnings = "deny"`? - Should we go with Option 1 or 2 for tests? - - In Option 1, should we introduce `bench`/`doc` lint profiles as well? I don't quite see this as necessary, but it's a simple enough extension. + - In Option 1, should we introduce `bench`/`doc` lint profiles as well? - In Option 1, should we support inheritance with test profiles? - - I've made an argument against merging lint profiles and regular profiles, but maybe we want to merge it with profiles anyway? Or perhaps provide a way to set a profile's default lint profile? I haven't seen a strong motivation for this yet. + - Maybe we want to merge lint profiles with profiles anyway? - When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? - - Would it hurt to *by default* inherit lint profiles from the workspace? A straightforward implementation would break current behavior of `[lints]` (which does not autoinherit, though perhaps it ought to?), but we could make this behavior kick in only when you specify `--lint someprofile` and `someprofile` is defined on the workspace but not the individual crate. - + - Would it hurt to *by default* inherit lint profiles from the workspace? From 48ee63f5b76d686e4a8068ac48375fa939e622a8 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 12 Mar 2026 18:19:47 -0700 Subject: [PATCH 5/9] profile not profiles --- text/3926-custom-lint-profiles.md | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index 09c261163cd..629afaf0ff0 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -139,9 +139,9 @@ In the context of this proposal, this set of lint settings will be called the "d This proposal adds the ability to define custom lint profiles: ```toml -[lints.profiles.ci] +[lints.profile.ci] inherits = "default" -[lints.profiles.ci.clippy] +[lints.profile.ci.clippy] pedantic = "allow" correctness = "deny" ``` @@ -163,7 +163,7 @@ Open question: What should the `--lints` flag be called? `--lints`? `--lint-prof Lint profiles can be inherited from the same package and from the workspace. The profile specified in `[lints]` is called `default`. -Open question: Should it be possible to also access the default profile via `[lints.profiles.default]`? What's the behavior of specifying both? Probably doesn't matter too much. Either way, we should reserve the profile name `default`. +Open question: Should it be possible to also access the default profile via `[lints.profile.default]`? What's the behavior of specifying both? Probably doesn't matter too much. Either way, we should reserve the profile name `default`. @@ -173,7 +173,7 @@ Open question: Should it be possible to also access the default profile via `[li [lints] unused = "allow" missing-debug-implementations = "warn" -[lint.profiles.ci] +[lint.profile.ci] inherits = "default" # inherits from the default profile unused = "deny" # overrides `unused` preference @@ -184,22 +184,22 @@ unused = "deny" # overrides `unused` preference workspace = true # Inherit just the ci profile from the workspace -[lint.profiles.ci] +[lint.profile.ci] workspace = true # Inherit just the default profile (`[lints.lintname]` / `[lints.clippy.lintname]`) from the workspace -[lints.profiles.default] +[lints.profile.default] workspace = true # Inherit the ci profile from the workspace but override some things -[lint.profiles.ci] +[lint.profile.ci] workspace = true -[lint.profiles.ci.clippy] +[lint.profile.ci.clippy] ptr-eq = "deny" # this is a lint correctness = "warn" # this is a lint group ``` -Note that if you wish to inherit from a profile defined in the workspace, you must first inherit the profile via `lints.profiles.profilename.workspace = true`, and then you can inherit from `profilename`. +Note that if you wish to inherit from a profile defined in the workspace, you must first inherit the profile via `lints.profile.profilename.workspace = true`, and then you can inherit from `profilename`. ## Changing warn levels wholesale @@ -210,7 +210,7 @@ For example, the following is equivalent to using `-Dwarnings` in CI: ```toml -[lints.profiles.ci] +[lints.profile.ci] warn = "deny" ``` @@ -227,9 +227,9 @@ This can be used when inheriting profiles to map `warn` to `deny` for the lint g [lints] some-lint = "warn" some-other-lint = "warn" -[lints.profiles.ci] +[lints.profile.ci] inherits = { profile = "default", warn = "deny" } -[lints.profiles.ci] +[lints.profile.ci] deprecated = "warn" clippy.some-noisy-lint = "warn" clippy.some-other-noisy-lint = "warn" @@ -257,17 +257,17 @@ The following patterns are all legal: workspace = true # Inherit the ci profile from the workspace -[lint.profiles.ci] +[lint.profile.ci] workspace = true # Inherit just the default profile (`[lints.lintname]` / `[lints.clippy.lintname]`) from the workspace -[lints.profiles.default] +[lints.profile.default] workspace = true # Inherit the ci profile from the workspace but override some things -[lint.profiles.ci] +[lint.profile.ci] workspace = true -[lint.profiles.ci.clippy] +[lint.profile.ci.clippy] ptr_eq = "deny" # this is a lint correctness = "warn" # this is a lint group ``` @@ -293,17 +293,17 @@ Each lint profile contains a "test" sub-profile, which behaves like a normal pro ```toml -[lints.profiles.ci.test] +[lints.profile.ci.test] some-lint = "allow" ``` This profile can inherit from other profiles normally: ```toml -[lints.profiles.testprofile] +[lints.profile.testprofile] some-lint = "allow" -[lints.profiles.ci.test] +[lints.profile.ci.test] some-other-lint = "allow" inherits = "testprofile" ``` @@ -323,7 +323,7 @@ Open question: Should we support inheritance with test profiles? This works similarly to how you can specify cfg-specific dependencies ```toml -[lints.profiles.ci.'cfg(test)'] +[lints.profile.ci.'cfg(test)'] indexing-slicing = "allow" ``` @@ -338,7 +338,7 @@ On the other hand, this does not allow for inheritance, which is simpler. Simila A lint profile can be specified as ```toml -[lints.profiles.nameofprofile] +[lints.profile.nameofprofile] workspace = true lintname = "allow" # allow, warn, deny, forbid clippy.lintname = "allow" # same @@ -391,7 +391,7 @@ workspace = true some-lint = "allow" -[lints.profiles.ci] # assume the workspace has a `ci` profile +[lints.profile.ci] # assume the workspace has a `ci` profile some-other-lint = "allow" ``` @@ -475,7 +475,7 @@ These designs are superficially similar, but not super similar in the details. C These are all mentioned inline in the RFC under "open question", but duplicated here for referencing. I prefer having these discussions inline in the RFC. - What should the `--lints` flag be called? `--lints`? `--lint-profiles`? - - Should it be possible to also access the default profile via `[lints.profiles.default]`? + - Should it be possible to also access the default profile via `[lints.profile.default]`? - Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). - Should it be `warn = "deny"` or `warnings = "deny"`? - Should we go with Option 1 or 2 for tests? From cd06d0192deef2bb4022e9450457a115439f4e9b Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 12 Mar 2026 18:25:17 -0700 Subject: [PATCH 6/9] more comments --- text/3926-custom-lint-profiles.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index 629afaf0ff0..e2b912f4158 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -25,36 +25,42 @@ There are multiple ways that lint levels can be toggled in modern Rust. For the - This **does** allow fine grained control over individual lints - This **cannot** be easily tweaked at runtime without having to edit code - This **cannot** be easily shared between crates + - Changing this invalidates the build cache for the edited file/crate. - In code, by means of `[lints]` in Cargo.toml. - This **does not** support use with `cfg` - - This **does** allow fine grained control over code sections + - This **does not** allow fine grained control over code sections - This **does** allow fine grained control over individual lints - This **cannot** be easily tweaked at runtime without having to edit code - This **can** be easily shared between crates (via workspaces) - - In code, by means of `[profiles.foo.rustflags]` and `-Afoobar` + - Changing this invalidates the build cache for the edited crate (or the entire workspace if this was in the workspace) + - In code, by means of `[profiles.foo.rustflags]` (unstable) and `-Afoobar` - This **does not** support use with `cfg` - This **does not** allow fine grained control over code sections - This **does** allow fine grained control over individual lints - This **cannot** be easily tweaked at runtime - This **can** be easily shared between crates (via workspaces) + - Changing this invalidates the entire build - In the CLI, by means of `RUSTFLAGS=-Afoobar` and friends. - This **does not** support use with `cfg` - This **does not** allow fine grained control over code sections - This **does** allow fine grained control over individual lints - This **can** be easily tweaked at runtime - This **is always** shared between crates + - Changing this invalidates the entire build - In the CLI, by means of `RUSTFLAGS=-Dwarnings` or `CARGO_BUILD_WARNINGS=deny` - This **does not** support use with `cfg` - This **does not** allow fine grained control over code sections - This **does not** allow fine grained control over individual lints - This **can** be easily tweaked at runtime - This **is always** shared between crates + - Changing this invalidates the entire build - In the CLI, by choosing to call `cargo clippy` - (This is technically a modality too) + - This usually rebuilds the workspace. At first glance, it appears that fine grained control is available at both "code editing time" and at runtime, however `-Afoobar` is not pleasant to use at all when you are configuring hundreds of lints. What is missing is a way to toggle groups of lints on and off at runtime, where these groups can be controlled by the developer at a fine grained level in source code somewhere. -Furthermore, `-Afoobar`, either via `[profiles]` or via `RUSTFLAGS` works poorly with Cargo: most solutions for doing this at runtime can trigger recompilation of the entire crate. `[lints]` was developed in part as a way to avoid this problem. +Furthermore, `-Afoobar`, either via `[profiles]` or via `RUSTFLAGS` works poorly with Cargo: most solutions for doing this at runtime can trigger recompilation of the entire crate graph. `[lints]` was developed in part as a way to avoid this problem. By and large, people currently use a mix of `-Dwarnings` and separately calling `cargo clippy` as a way to run different sets of lints on the same codebase. This proposal aims to expand this ability. @@ -107,7 +113,7 @@ This isn't a lint, but similar workflows can be found around `rustfmt`'s "format Currently, this requires a manual specification of all the relevant lints. -### Only on non-test-code +### Only on certain types of targets Some lints protect production code from things like panics and bad API choices, things which aren't as much of a big deal (or even, counterproductive to prevent) for test code. It's common to do something like `#[cfg_attr(test, allow(...))]`, however this can't be combined with the Cargo `[lints]` table, making it less useful as a feature. @@ -115,6 +121,8 @@ Typically you want something that applies to `cargo test`, `cargo bench`, and te Clippy has a patchwork of config options that disable lints in tests, like `allow-unwrap-in-tests`, however not all lints have this, and [they don't work consistently in all test code](https://github.com/rust-lang/rust-clippy/issues/13981). So far most codebases I have worked on end up with a lot of allows in test code for lints that would be easier to global allow. +Similarly, someone may wish to only enable certain lints on bin targets. + ### "Teaching" lints @@ -158,6 +166,7 @@ $ cargo build --lints ci Open question: What should the `--lints` flag be called? `--lints`? `--lint-profiles`? +Open question: `profile` is ambiguous with build profiles. Should we pick a different name? ## Inheritance and workspaces @@ -451,6 +460,8 @@ I think the main drawback here is that this is Cargo.toml-focused, so it require This is also not a simple system: is the complexity worth it? +Similarly, the Cargo team is worried about adding too many CLI flags, since each flag affects the discoverability of other flags. + # Rationale and alternatives Overall I think there are a lot of reasons to have "modalities" for lints, some already in use, some attained by a patchwork of features, and some that people would likely use if they were more convenient. Having profiles directly addresses them by giving the user a way to define and name a modality, so they or their CI/IDE/tooling/whatever can pick the right one based on the context. @@ -475,6 +486,7 @@ These designs are superficially similar, but not super similar in the details. C These are all mentioned inline in the RFC under "open question", but duplicated here for referencing. I prefer having these discussions inline in the RFC. - What should the `--lints` flag be called? `--lints`? `--lint-profiles`? + - `profile` is ambiguous with build profiles. Should we pick a different name? - Should it be possible to also access the default profile via `[lints.profile.default]`? - Someone should pick how `warn = "deny"` prioritizes with [`build.warnings`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#warnings). - Should it be `warn = "deny"` or `warnings = "deny"`? @@ -485,6 +497,9 @@ These are all mentioned inline in the RFC under "open question", but duplicated - When should it be a hard error to specify `--lints foo` for a nonexistant profile `foo`? - Would it hurt to *by default* inherit lint profiles from the workspace? +Other unresolved questions: + +Should using this feature require an MSRV bump? Technically crates consuming your crate do not need to care about the `[lints]` section, but older versions of Cargo are likely to misinterpret `lints.profile`. Needs investigation. # Future work From 154c6975a7de189b10fa730b8b26798e8f0cf548 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Fri, 13 Mar 2026 13:35:18 -0700 Subject: [PATCH 7/9] oops --- text/3926-custom-lint-profiles.md | 38 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index e2b912f4158..d067998a5a4 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -53,7 +53,7 @@ There are multiple ways that lint levels can be toggled in modern Rust. For the - This **does not** allow fine grained control over individual lints - This **can** be easily tweaked at runtime - This **is always** shared between crates - - Changing this invalidates the entire build + - Changing RUSTFLAGS invalidates the entire build, but `CARGO_BUILD_WARNINGS` does not invalidate anything. - In the CLI, by choosing to call `cargo clippy` - (This is technically a modality too) - This usually rebuilds the workspace. @@ -180,11 +180,11 @@ Open question: Should it be possible to also access the default profile via `[li # Simple inheritance [lints] -unused = "allow" -missing-debug-implementations = "warn" +rust.unused = "allow" +rust.missing-debug-implementations = "warn" [lint.profile.ci] inherits = "default" # inherits from the default profile -unused = "deny" # overrides `unused` preference +rust.unused = "deny" # overrides `unused` preference # Workspace examples @@ -234,12 +234,12 @@ This can be used when inheriting profiles to map `warn` to `deny` for the lint g ```toml [lints] -some-lint = "warn" -some-other-lint = "warn" +rust.some-lint = "warn" +rust.some-other-lint = "warn" [lints.profile.ci] inherits = { profile = "default", warn = "deny" } [lints.profile.ci] -deprecated = "warn" +rust.deprecated = "warn" clippy.some-noisy-lint = "warn" clippy.some-other-noisy-lint = "warn" ``` @@ -277,8 +277,8 @@ workspace = true [lint.profile.ci] workspace = true [lint.profile.ci.clippy] -ptr_eq = "deny" # this is a lint -correctness = "warn" # this is a lint group +rust.ptr_eq = "deny" # this is a lint +rust.correctness = "warn" # this is a lint group ``` Use of `workspace = true` does not prevent addition of new profiles or tweaking of existing ones. @@ -310,10 +310,10 @@ This profile can inherit from other profiles normally: ```toml [lints.profile.testprofile] -some-lint = "allow" +rust.some-lint = "allow" [lints.profile.ci.test] -some-other-lint = "allow" +rust.some-other-lint = "allow" inherits = "testprofile" ``` @@ -333,7 +333,7 @@ This works similarly to how you can specify cfg-specific dependencies ```toml [lints.profile.ci.'cfg(test)'] -indexing-slicing = "allow" +clippy.indexing-slicing = "allow" ``` This is a lot more flexible, but it might be too flexible: is there actually a use case for target-specific lints? There is some use case for different lints based on `cfg(test)`, `cfg(doc)`, `cfg(bench)`, etc, but less so for `cfg(windows)`. @@ -349,7 +349,7 @@ A lint profile can be specified as ```toml [lints.profile.nameofprofile] workspace = true -lintname = "allow" # allow, warn, deny, forbid +rust.lintname = "allow" # allow, warn, deny, forbid clippy.lintname = "allow" # same warn = "deny" # allow, warn, deny inherits = "default" # a name of a profile @@ -358,7 +358,7 @@ inherits = {profile = "default", warn = "deny"} # name of a profile, and an all # Option 1 test = {} # can contain all the same fields as above, except for `test` itself # Option 2 -'cfg(test)'.lintname = "allow" +'cfg(test)'.rust.lintname = "allow" ``` @@ -369,10 +369,12 @@ The profile can be selected via a `--lints` flag available in all commands that Custom profiles cannot be named `default`. The name `default` is reserved for referencing the default profile. -Rustc cannot add lints named `workspace`, `inherits`, `warn`, or `test`. +Linter types named `workspace`, `inherits`, `warn`, or `test` are forbidden. Note that currently [workspace overriding is not supported][ws-override]. More on that below. + [ws-override]: https://github.com/rust-lang/cargo/issues/13157 + ## Inheritance Internally, each lint profile is *resolved* to a list of lints and lint levels, plus an optional `warn = "somelevel"` setting. In case we go with Option 1 for tests, each profile contains an additional "test" profile. If we go with option 2, it also contains a list of lints and lint levels with `cfg` predicates. This lint is sorted in definition order. @@ -397,11 +399,11 @@ If we choose to fix the [workspace override][ws-override] issue in this RFC, the ```toml [lints] workspace = true -some-lint = "allow" +rust.some-lint = "allow" [lints.profile.ci] # assume the workspace has a `ci` profile -some-other-lint = "allow" +rust.some-other-lint = "allow" ``` Here, the crate will have a default and `ci` resolved profile copied from the workspace, with overrides applied on top. @@ -508,7 +510,7 @@ Should using this feature require an MSRV bump? Technically crates consuming you ## Custom lint groups -A thing this feature does *not* let one do is toggle multiple lints at once in code sections, a feature that is useful to have. A *potential* extension of this feature would be to allow profiles to be defined as lint groups so that one can write `#[allo w(customprofile)]`. There's a lot of subtletlies of such a design, including: +A thing this feature does *not* let one do is toggle multiple lints at once in code sections, a feature that is useful to have. A *potential* extension of this feature would be to allow profiles to be defined as lint groups so that one can write `#[allow(customprofile)]`. There's a lot of subtletlies of such a design, including: - How does one provide this specification to rustc? - Profiles contain `allow`s and `warns` and other things, they are not _just_ a grouping of lints. What does it mean to `#[warn(group)]` for a group that contains some `allow`s and some `warn`s? Does it turn on every lint explicitly mentioned in the profile? Does it turn on every `allow` lint from the profile? There's not an easy answer here. From 032d9ae1d1b818b70fa94eca88e7536628cf4b38 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 26 Mar 2026 16:01:59 -0700 Subject: [PATCH 8/9] some fixes --- text/3926-custom-lint-profiles.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index d067998a5a4..f1eb5d14d9c 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -47,13 +47,13 @@ There are multiple ways that lint levels can be toggled in modern Rust. For the - This **can** be easily tweaked at runtime - This **is always** shared between crates - Changing this invalidates the entire build - - In the CLI, by means of `RUSTFLAGS=-Dwarnings` or `CARGO_BUILD_WARNINGS=deny` + - In the CLI, by means of `RUSTFLAGS=-Dwarnings`, `build.warnings`, or `CARGO_BUILD_WARNINGS=deny` - This **does not** support use with `cfg` - This **does not** allow fine grained control over code sections - This **does not** allow fine grained control over individual lints - This **can** be easily tweaked at runtime - This **is always** shared between crates - - Changing RUSTFLAGS invalidates the entire build, but `CARGO_BUILD_WARNINGS` does not invalidate anything. + - Changing RUSTFLAGS invalidates the entire build, but `CARGO_BUILD_WARNINGS` and `build.warnings` do not invalidate anything. - In the CLI, by choosing to call `cargo clippy` - (This is technically a modality too) - This usually rebuilds the workspace. @@ -83,10 +83,13 @@ In essence, lints can have different effects in different contexts. ## Lint modalities and their use cases Overall, it's quite common in codebases to want to have different "modes" for lints for the different contexts a linter might be run in. + ### Deny in CI The most common use case is wanting to have the codebase be lint-free but not hinder development while hacking on something, but have the lints gate landing on `main`. Workflows around this typically involve running CI with `-Dwarnings` (or the new `CARGO_BUILD_WARNINGS=deny`), with contributors often running `cargo check` / `cargo clippy` locally and ensuring they are warnings-clean before opening a PR. +Note that while this is very common, a downside of this approach is that it's not always clear what the "severity" of a check is: warnings showing errors may make a newer contributor think they *have* to do the steps mentioned in the warning, rather than potentially silencing the lint or choosing another approach. + ### Noisier PR-integrated linters or IDEs If using a PR-integrated CI linter, your bar for non-blocking noisy informative lints can be lower since the linter will only flag things in code touched by the current PR. One may wish to enable far more pedantic lints in such CI. @@ -246,6 +249,8 @@ clippy.some-other-noisy-lint = "warn" This creates a profile that inherits from the default profile, but with all warnings replaced with hard errors, and further tweaks to some other lints. +Note that profiles cannot inherit from profiles defined in the workspace without first inheriting the to-be-inherited profile from the workspace with `lints.profile.foo.workspace = true`. + This has no impact on lint levels specified in the source code, or warnings that come from places other than the warning system (in other words, this does not apply `-Dwarnings`) @@ -507,6 +512,7 @@ Should using this feature require an MSRV bump? Technically crates consuming you # Future work + ## Custom lint groups @@ -519,6 +525,12 @@ A thing this feature does *not* let one do is toggle multiple lints at once in c It's not yet clear to me how feasible this is, or if we should have such a feature, but it's worth listing. +## Better diagnostics for warn-to-deny lints + +The inheritance model proposed here, when inheriting a profile as warn-to-deny, does not retain memory of the lint originally being "warn". + +In theory, rustc could choose to display `deny` and `warn`-but-`-Dwarnings` lints differently. If it wishes to do that, `inherits = {profile = "default", warn = "deny"}` may also need a way to retain memory of the original lint level. This is an interesting avenue to investigate but somewhat out of scope for this RFC. + ## Teaching lints In the past people wished for tooling that produces lints that potentially tell new users about subtleties in their code, subtleties that are not really *problems* to be fixed, but interesting things to be noted. These would be opted in to by individual users and as they get used to a concept, disabled globally one by one. Lint profiles allow one to better handle toggles like this, but it is not in an of itself a major step in this direction. @@ -544,4 +556,4 @@ warn: found 5 instances of the `needless_borrow` lint: This works well with lint groups since you may then upgrade nits to warnings when you decide to try and fix these. PR-integrated linters can also choose to have different behavior with these if desired. - [RFC 3730](https://github.com/rust-lang/rfcs/pull/3730) \ No newline at end of file +[RFC 3730]: https://github.com/rust-lang/rfcs/pull/3730 \ No newline at end of file From b55b35f7267da55230c819d17cf316e8debf8972 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Fri, 27 Mar 2026 07:55:36 -0700 Subject: [PATCH 9/9] clarify inheritance --- text/3926-custom-lint-profiles.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/3926-custom-lint-profiles.md b/text/3926-custom-lint-profiles.md index f1eb5d14d9c..a5467b98497 100644 --- a/text/3926-custom-lint-profiles.md +++ b/text/3926-custom-lint-profiles.md @@ -302,7 +302,7 @@ Open question: Should we go with Option 1 or 2 for tests? ### Option 1: A `test` subprofile -Each lint profile contains a "test" sub-profile, which behaves like a normal profile, but is applied when you build `cfg(test)` binaries with that profile in test mode. These are integration tests, unit tests, doctests, and benchmarks. +Each lint profile contains a "test" sub-profile, which is specified similar to a normal profile, but is applied as an override to the normal profile when you build `cfg(test)` binaries with that profile in test mode. These are integration tests, unit tests, doctests, and benchmarks. @@ -453,7 +453,7 @@ Each profile's `test` subprofile is resolved by taking the "parent" profile and When inheriting a profile, the resolved `test` subprofile is also inherited, and further overrides can be applied on top. -It's not clear if `test` profiles need to support inheritance, but if we decide to support that, then an `inherits = ` key in a test subprofile will *switch* the base profile used for inheritance to being the specified profile, rather than the parent profile. +It's not clear if `test` profiles need to support explicit inheritance from profiles other than their parent, but if we decide to support that, then an `inherits = ` key in a test subprofile will *switch* the base profile used for inheritance to being the specified profile, rather than the parent profile. ## Testing: CFG'd lints (option 2)