From 0312cb923f57a75f0fa0a842d3ce5e9ab1ae1df5 Mon Sep 17 00:00:00 2001 From: Alex Macleod Date: Sat, 31 May 2025 16:20:26 +0000 Subject: [PATCH] Automatically set MSRV for code under `#[cfg(version)]` --- README.md | 30 ++----- book/src/configuration.md | 41 +++++++--- clippy_utils/src/msrvs.rs | 78 ++++++++++++++----- tests/ui/cfg_version_msrv.rs | 40 ++++++++++ tests/ui/cfg_version_msrv.stderr | 47 +++++++++++ tests/ui/min_rust_version_invalid_attr.rs | 2 + tests/ui/min_rust_version_invalid_attr.stderr | 26 ++++++- 7 files changed, 206 insertions(+), 58 deletions(-) create mode 100644 tests/ui/cfg_version_msrv.rs create mode 100644 tests/ui/cfg_version_msrv.stderr diff --git a/README.md b/README.md index 20a5e997e629..21915e5c6fe0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/book/src/configuration.md b/book/src/configuration.md index b13054431898..d47e9f282484 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -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) diff --git a/clippy_utils/src/msrvs.rs b/clippy_utils/src/msrvs.rs index a5e66ad463bb..64fb9656ae24 100644 --- a/clippy_utils/src/msrvs.rs +++ b/clippy_utils/src/msrvs.rs @@ -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; @@ -186,27 +186,67 @@ impl MsrvStack { } fn parse_attrs(sess: &Session, attrs: &[impl AttributeExt]) -> Option { - 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, 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); + } + } + }, + _ => {}, + } } diff --git a/tests/ui/cfg_version_msrv.rs b/tests/ui/cfg_version_msrv.rs new file mode 100644 index 000000000000..a9afca8cf190 --- /dev/null +++ b/tests/ui/cfg_version_msrv.rs @@ -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` +} diff --git a/tests/ui/cfg_version_msrv.stderr b/tests/ui/cfg_version_msrv.stderr new file mode 100644 index 000000000000..5610f7523799 --- /dev/null +++ b/tests/ui/cfg_version_msrv.stderr @@ -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 + diff --git a/tests/ui/min_rust_version_invalid_attr.rs b/tests/ui/min_rust_version_invalid_attr.rs index c8409d78ed77..1d232adb6216 100644 --- a/tests/ui/min_rust_version_invalid_attr.rs +++ b/tests/ui/min_rust_version_invalid_attr.rs @@ -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 diff --git a/tests/ui/min_rust_version_invalid_attr.stderr b/tests/ui/min_rust_version_invalid_attr.stderr index dbc276ed89df..dc05cc634f83 100644 --- a/tests/ui/min_rust_version_invalid_attr.stderr +++ b/tests/ui/min_rust_version_invalid_attr.stderr @@ -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"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -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