Skip to content

Automatically set MSRV for code under #[cfg(version)] #14940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 7 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,37 +237,21 @@ define the `CLIPPY_DISABLE_DOCS_LINKS` environment variable.
### Specifying the minimum supported Rust version

Projects that intend to support old versions of Rust can disable lints pertaining to newer features by
specifying the minimum supported Rust version (MSRV) in the Clippy configuration file.

```toml
msrv = "1.30.0"
```

Alternatively, the [`rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field)
in the `Cargo.toml` can be used.
specifying the minimum supported Rust version (MSRV) in the [`rust-version` field](https://doc.rust-lang.org/cargo/reference/rust-version.html)
of `Cargo.toml`.

```toml
# Cargo.toml
rust-version = "1.30"
```

The MSRV can also be specified as an attribute, like below.

```rust,ignore
#![feature(custom_inner_attributes)]
#![clippy::msrv = "1.30.0"]

fn main() {
...
}
```

You can also omit the patch version when specifying the MSRV, so `msrv = 1.30`
is equivalent to `msrv = 1.30.0`.
Alternatively the [`msrv` field](https://doc.rust-lang.org/clippy/lint_configuration.html#msrv) can be specified in the
Clippy configuration file.

Note: `custom_inner_attributes` is an unstable feature, so it has to be enabled explicitly.
Clippy will automatically adjust the MSRV for sections of code that uses `#[cfg(version)]`, alternatively the
`#[clippy::msrv]` attribute can be used to specifiy the MSRV without any other effect.

Lints that recognize this configuration option can be found [here](https://rust-lang.github.io/rust-clippy/master/index.html#msrv)
For more information see [Specifying the minimum supported Rust version](https://doc.rust-lang.org/clippy/configuration.html#specifying-the-minimum-supported-rust-version).

## Contributing

Expand Down
41 changes: 29 additions & 12 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,45 @@ For more details and options, refer to the Cargo documentation.

### Specifying the minimum supported Rust version

Projects that intend to support old versions of Rust can disable lints pertaining to newer features by specifying the
minimum supported Rust version (MSRV) in the Clippy configuration file.
Projects that intend to support old versions of Rust can disable lints pertaining to newer features by
specifying the minimum supported Rust version (MSRV) in the [`rust-version` field](https://doc.rust-lang.org/cargo/reference/rust-version.html)
of `Cargo.toml`.

```toml
msrv = "1.30.0"
# Cargo.toml
rust-version = "1.30"
```

The MSRV can also be specified as an attribute, like below.
Alternatively the [`msrv` field](https://doc.rust-lang.org/clippy/lint_configuration.html#msrv) can be specified in the
Clippy configuration file.

```rust,ignore
#![feature(custom_inner_attributes)]
#![clippy::msrv = "1.30.0"]
```toml
# clippy.toml
msrv = "1.30"
```

Clippy will automatically adjust the MSRV for sections of code that use `#[cfg(version)]`:

fn main() {
...
```rust
#[cfg(version("1.90"))]
fn f() {
// The MSRV here is set to 1.90
}
```

You can also omit the patch version when specifying the MSRV, so `msrv = 1.30`
is equivalent to `msrv = 1.30.0`.
> **Note:** `cfg(version)` is not yet [available on stable](https://github.com/rust-lang/rust/pull/141766)

The `#[clippy::msrv]` can also be used to set the MSRV for a section of code with no other effect:

```rust
#[clippy::msrv = "1.30"]
fn f() {
// The MSRV here is set to 1.30
}
```

Note: `custom_inner_attributes` is an unstable feature, so it has to be enabled explicitly.
If both `#[cfg(version)]` and `#[clippy::msrv]` attributes are applied to the same node then `#[clippy::msrv]` takes
precedence.

Lints that recognize this configuration option can be
found [here](https://rust-lang.github.io/rust-clippy/master/index.html#msrv)
Expand Down
78 changes: 59 additions & 19 deletions clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::sym;
use rustc_ast::Attribute;
use rustc_ast::attr::AttributeExt;
use rustc_ast::{Attribute, LitKind, MetaItem, MetaItemInner};
use rustc_attr_data_structures::RustcVersion;
use rustc_attr_parsing::parse_version;
use rustc_lint::LateContext;
Expand Down Expand Up @@ -186,27 +186,67 @@ impl MsrvStack {
}

fn parse_attrs(sess: &Session, attrs: &[impl AttributeExt]) -> Option<RustcVersion> {
let mut msrv_attrs = attrs.iter().filter(|attr| attr.path_matches(&[sym::clippy, sym::msrv]));

if let Some(msrv_attr) = msrv_attrs.next() {
if let Some(duplicate) = msrv_attrs.next_back() {
sess.dcx()
.struct_span_err(duplicate.span(), "`clippy::msrv` is defined multiple times")
.with_span_note(msrv_attr.span(), "first definition found here")
.emit();
}

if let Some(msrv) = msrv_attr.value_str() {
if let Some(version) = parse_version(msrv) {
return Some(version);
let mut first_clippy_attr = None;
let mut clippy_msrv = None;
let mut cfg_version = None;
for attr in attrs {
if attr.path_matches(&[sym::clippy, sym::msrv]) {
match first_clippy_attr {
None => first_clippy_attr = Some(attr),
Some(first) => {
sess.dcx()
.struct_span_err(attr.span(), "`clippy::msrv` is defined multiple times")
.with_span_note(first.span(), "first definition found here")
.emit();
},
}

sess.dcx()
.span_err(msrv_attr.span(), format!("`{msrv}` is not a valid Rust version"));
} else {
sess.dcx().span_err(msrv_attr.span(), "bad clippy attribute");
if let Some(msrv) = attr.value_str() {
if let Some(version) = parse_version(msrv) {
clippy_msrv = Some(version);
} else {
sess.dcx()
.span_err(attr.span(), format!("`{msrv}` is not a valid Rust version"));
}
} else {
sess.dcx().span_err(attr.span(), "bad clippy attribute");
}
} else if matches!(attr.name(), Some(sym::cfg | sym::cfg_trace)) // cfg in early passes, cfg_trace in late
&& let Some(list) = attr.meta_item_list()
&& let [MetaItemInner::MetaItem(meta_item)] = list.as_slice()
{
parse_cfg_version(&mut cfg_version, meta_item, false);
}
}

None
clippy_msrv.or(cfg_version)
}

fn parse_cfg_version(current: &mut Option<RustcVersion>, meta_item: &MetaItem, mut negated: bool) {
let Some(name) = meta_item.name() else { return };
match name {
sym::version => {
if !negated
&& let Some([MetaItemInner::Lit(lit)]) = meta_item.meta_item_list()
&& let LitKind::Str(s, _) = lit.kind
&& let Some(version) = parse_version(s)
{
match current {
Some(current) => *current = version.min(*current),
None => *current = Some(version),
}
}
},
sym::any | sym::all | sym::not => {
if name == sym::not {
negated = !negated;
}
for inner in meta_item.meta_item_list().into_iter().flatten() {
if let Some(inner_meta_item) = inner.meta_item() {
parse_cfg_version(current, inner_meta_item, negated);
}
}
},
_ => {},
}
}
40 changes: 40 additions & 0 deletions tests/ui/cfg_version_msrv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![feature(cfg_version)]

fn f(i: i32) {
#[cfg(version("1.50"))]
let _ = i.isqrt();
//~^ ERROR: is `1.50.0`

// When `any/all` are used pick the smallest version seen
#[cfg(any(version("1.49"), version("1.50")))]
let _ = i.isqrt();
//~^ ERROR: is `1.49.0`
#[cfg(all(version("1.60"), version("1.59")))]
let _ = i.isqrt();
//~^ ERROR: is `1.59.0`

// Ignore negated version requirements
#[cfg(not(version("1.50")))]
let _ = i.isqrt();
#[cfg(not(not(version("1.50"))))]
let _ = i.isqrt();
//~^ ERROR: is `1.50.0`
#[cfg(not(all(version("1.40"), not(version("1.50")))))]
let _ = i.isqrt();
//~^ ERROR: is `1.50.0`
}

/// If both are specified on the same node then `clippy::msrv` takes precedence
#[clippy::msrv = "1.50"]
#[cfg(version("1.40"))]
fn both_attributes_cfg_lower(i: i32) {
let _ = i.isqrt();
//~^ ERROR: is `1.50.0`
}

#[clippy::msrv = "1.40"]
#[cfg(version("1.50"))]
fn both_attributes_cfg_higher(i: i32) {
let _ = i.isqrt();
//~^ ERROR: is `1.40.0`
}
47 changes: 47 additions & 0 deletions tests/ui/cfg_version_msrv.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
error: current MSRV (Minimum Supported Rust Version) is `1.50.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:5:15
|
LL | let _ = i.isqrt();
| ^^^^^^^
|
= note: `-D clippy::incompatible-msrv` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::incompatible_msrv)]`

error: current MSRV (Minimum Supported Rust Version) is `1.49.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:10:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: current MSRV (Minimum Supported Rust Version) is `1.59.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:13:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: current MSRV (Minimum Supported Rust Version) is `1.50.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:20:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: current MSRV (Minimum Supported Rust Version) is `1.50.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:23:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: current MSRV (Minimum Supported Rust Version) is `1.50.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:31:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: current MSRV (Minimum Supported Rust Version) is `1.40.0` but this item is stable since `1.84.0`
--> tests/ui/cfg_version_msrv.rs:38:15
|
LL | let _ = i.isqrt();
| ^^^^^^^

error: aborting due to 7 previous errors

2 changes: 2 additions & 0 deletions tests/ui/min_rust_version_invalid_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ fn outer_attr() {}
mod multiple {
#![clippy::msrv = "1.40"]
#![clippy::msrv = "=1.35.0"]
//~^ ERROR: `clippy::msrv` is defined multiple times
//~| ERROR: `=1.35.0` is not a valid Rust version
#![clippy::msrv = "1.10.1"]
//~^ ERROR: `clippy::msrv` is defined multiple times

Expand Down
26 changes: 22 additions & 4 deletions tests/ui/min_rust_version_invalid_attr.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,25 @@ LL | #[clippy::msrv = "invalid.version"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: `clippy::msrv` is defined multiple times
--> tests/ui/min_rust_version_invalid_attr.rs:16:5
--> tests/ui/min_rust_version_invalid_attr.rs:15:5
|
LL | #![clippy::msrv = "=1.35.0"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: first definition found here
--> tests/ui/min_rust_version_invalid_attr.rs:14:5
|
LL | #![clippy::msrv = "1.40"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: `=1.35.0` is not a valid Rust version
--> tests/ui/min_rust_version_invalid_attr.rs:15:5
|
LL | #![clippy::msrv = "=1.35.0"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: `clippy::msrv` is defined multiple times
--> tests/ui/min_rust_version_invalid_attr.rs:18:5
|
LL | #![clippy::msrv = "1.10.1"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -23,16 +41,16 @@ LL | #![clippy::msrv = "1.40"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: `clippy::msrv` is defined multiple times
--> tests/ui/min_rust_version_invalid_attr.rs:21:9
--> tests/ui/min_rust_version_invalid_attr.rs:23:9
|
LL | #![clippy::msrv = "1.0.0"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: first definition found here
--> tests/ui/min_rust_version_invalid_attr.rs:20:9
--> tests/ui/min_rust_version_invalid_attr.rs:22:9
|
LL | #![clippy::msrv = "1.0"]
| ^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 4 previous errors
error: aborting due to 6 previous errors