diff options
| author | Jason Newcomb <jsnewcomb@pm.me> | 2025-07-16 16:24:01 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-16 16:24:01 +0000 |
| commit | c0dc3b616da6d4f2f53ee1da4139dc258cf9b27f (patch) | |
| tree | 3792afdceccead023663b52648aacbf88d00f122 | |
| parent | e113e66606834f0b8eb992c02798f1c737c6cac8 (diff) | |
| parent | 68ea46179b912e58ef9511559ba7b30ea569def0 (diff) | |
| download | rust-c0dc3b616da6d4f2f53ee1da4139dc258cf9b27f.tar.gz rust-c0dc3b616da6d4f2f53ee1da4139dc258cf9b27f.zip | |
Various improvements to the `incompatible_msrv` lint (#14433)
This PR supersedes #14328 following the [2025-03-18 Clippy meeting discussion](https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/Meeting.202025-03-18/with/506527762). It uses a simpler approach than what was proposed initially in #14328 and does not add new options. First, it documents how `cfg_attr` can be used to change the MSRV considered by Clippy to trigger the lint. This allows the MSRV to be feature gated, or to be raised in tests. Also, the lint stops warning about items which have been explicitly allowed through a rustc feature. This works even if the feature has been stabilized since. It allows using an older compiler with some features turned on, as is done in Rust for Linux. This fixes #14425. Then, if the lint triggers, and it looks like the code is located below a `cfg` or `cfg_attr` attribute, an additional note is issued, once, to indicate that the `clippy::msrv` attribute can be controlled by an attribute. Finally, the lint is extended to cover any path, not just method and function calls. For example, enumeration variants, or constants, were not MSRV checked. This required replacing two `u32::MAX` by `u32::max_value()` in MSRV-limited tests. An extra commit adds a TODO for checking the const stability also, as this is not done right now. @Centri3 I'll assign this to you because you were assigned #14328 and you were the one who nominated the issue for discussion (thanks!), but of course feel free to reroll! r? @Centri3 changelog: [`incompatible_msrv`]: better documentation, honor the `features` attribute, and lint non-function entities as well
| -rw-r--r-- | clippy_lints/src/incompatible_msrv.rs | 115 | ||||
| -rw-r--r-- | tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr | 2 | ||||
| -rw-r--r-- | tests/ui/checked_conversions.fixed | 2 | ||||
| -rw-r--r-- | tests/ui/checked_conversions.rs | 4 | ||||
| -rw-r--r-- | tests/ui/checked_conversions.stderr | 4 | ||||
| -rw-r--r-- | tests/ui/incompatible_msrv.rs | 53 | ||||
| -rw-r--r-- | tests/ui/incompatible_msrv.stderr | 66 |
7 files changed, 185 insertions, 61 deletions
diff --git a/clippy_lints/src/incompatible_msrv.rs b/clippy_lints/src/incompatible_msrv.rs index 5d0bd3e8ca3..a31168fc746 100644 --- a/clippy_lints/src/incompatible_msrv.rs +++ b/clippy_lints/src/incompatible_msrv.rs @@ -1,8 +1,8 @@ use clippy_config::Conf; -use clippy_utils::diagnostics::span_lint; +use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::is_in_test; use clippy_utils::msrvs::Msrv; -use rustc_attr_data_structures::{RustcVersion, StabilityLevel, StableSince}; +use rustc_attr_data_structures::{RustcVersion, Stability, StableSince}; use rustc_data_structures::fx::FxHashMap; use rustc_hir::{Expr, ExprKind, HirId, QPath}; use rustc_lint::{LateContext, LateLintPass}; @@ -33,15 +33,54 @@ declare_clippy_lint! { /// /// To fix this problem, either increase your MSRV or use another item /// available in your current MSRV. + /// + /// You can also locally change the MSRV that should be checked by Clippy, + /// for example if a feature in your crate (e.g., `modern_compiler`) should + /// allow you to use an item: + /// + /// ```no_run + /// //! This crate has a MSRV of 1.3.0, but we also have an optional feature + /// //! `sleep_well` which requires at least Rust 1.4.0. + /// + /// // When the `sleep_well` feature is set, do not warn for functions available + /// // in Rust 1.4.0 and below. + /// #![cfg_attr(feature = "sleep_well", clippy::msrv = "1.4.0")] + /// + /// use std::time::Duration; + /// + /// #[cfg(feature = "sleep_well")] + /// fn sleep_for_some_time() { + /// std::thread::sleep(Duration::new(1, 0)); // Will not trigger the lint + /// } + /// ``` + /// + /// You can also increase the MSRV in tests, by using: + /// + /// ```no_run + /// // Use a much higher MSRV for tests while keeping the main one low + /// #![cfg_attr(test, clippy::msrv = "1.85.0")] + /// + /// #[test] + /// fn my_test() { + /// // The tests can use items introduced in Rust 1.85.0 and lower + /// // without triggering the `incompatible_msrv` lint. + /// } + /// ``` #[clippy::version = "1.78.0"] pub INCOMPATIBLE_MSRV, suspicious, "ensures that all items used in the crate are available for the current MSRV" } +#[derive(Clone, Copy)] +enum Availability { + FeatureEnabled, + Since(RustcVersion), +} + pub struct IncompatibleMsrv { msrv: Msrv, - is_above_msrv: FxHashMap<DefId, RustcVersion>, + availability_cache: FxHashMap<DefId, Availability>, check_in_tests: bool, } @@ -51,35 +90,32 @@ impl IncompatibleMsrv { pub fn new(conf: &'static Conf) -> Self { Self { msrv: conf.msrv, - is_above_msrv: FxHashMap::default(), + availability_cache: FxHashMap::default(), check_in_tests: conf.check_incompatible_msrv_in_tests, } } - fn get_def_id_version(&mut self, tcx: TyCtxt<'_>, def_id: DefId) -> RustcVersion { - if let Some(version) = self.is_above_msrv.get(&def_id) { - return *version; + /// Returns the availability of `def_id`, whether it is enabled through a feature or + /// available since a given version (the default being Rust 1.0.0). + fn get_def_id_availability(&mut self, tcx: TyCtxt<'_>, def_id: DefId) -> Availability { + if let Some(availability) = self.availability_cache.get(&def_id) { + return *availability; } - let version = if let Some(version) = tcx - .lookup_stability(def_id) - .and_then(|stability| match stability.level { - StabilityLevel::Stable { - since: StableSince::Version(version), - .. - } => Some(version), - _ => None, - }) { - version + let stability = tcx.lookup_stability(def_id); + let version = if stability.is_some_and(|stability| tcx.features().enabled(stability.feature)) { + Availability::FeatureEnabled + } else if let Some(StableSince::Version(version)) = stability.as_ref().and_then(Stability::stable_since) { + Availability::Since(version) } else if let Some(parent_def_id) = tcx.opt_parent(def_id) { - self.get_def_id_version(tcx, parent_def_id) + self.get_def_id_availability(tcx, parent_def_id) } else { - RustcVersion { + Availability::Since(RustcVersion { major: 1, minor: 0, patch: 0, - } + }) }; - self.is_above_msrv.insert(def_id, version); + self.availability_cache.insert(def_id, version); version } @@ -110,16 +146,21 @@ impl IncompatibleMsrv { if (self.check_in_tests || !is_in_test(cx.tcx, node)) && let Some(current) = self.msrv.current(cx) - && let version = self.get_def_id_version(cx.tcx, def_id) + && let Availability::Since(version) = self.get_def_id_availability(cx.tcx, def_id) && version > current { - span_lint( + span_lint_and_then( cx, INCOMPATIBLE_MSRV, span, format!( "current MSRV (Minimum Supported Rust Version) is `{current}` but this item is stable since `{version}`" ), + |diag| { + if is_under_cfg_attribute(cx, node) { + diag.note_once("you may want to conditionally increase the MSRV considered by Clippy using the `clippy::msrv` attribute"); + } + }, ); } } @@ -127,23 +168,35 @@ impl IncompatibleMsrv { impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + // TODO: check for const stability when in const context match expr.kind { ExprKind::MethodCall(_, _, _, span) => { if let Some(method_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id) { self.emit_lint_if_under_msrv(cx, method_did, expr.hir_id, span); } }, - ExprKind::Call(call, _) => { - // Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should - // not be linted as they will not be generated in older compilers if the function is not available, - // and the compiler is allowed to call unstable functions. - if let ExprKind::Path(qpath @ (QPath::Resolved(..) | QPath::TypeRelative(..))) = call.kind - && let Some(path_def_id) = cx.qpath_res(&qpath, call.hir_id).opt_def_id() - { - self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, call.span); + // Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should + // not be linted as they will not be generated in older compilers if the function is not available, + // and the compiler is allowed to call unstable functions. + ExprKind::Path(qpath @ (QPath::Resolved(..) | QPath::TypeRelative(..))) => { + if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id() { + self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, expr.span); } }, _ => {}, } } } + +/// Heuristic checking if the node `hir_id` is under a `#[cfg()]` or `#[cfg_attr()]` +/// attribute. +fn is_under_cfg_attribute(cx: &LateContext<'_>, hir_id: HirId) -> bool { + cx.tcx.hir_parent_id_iter(hir_id).any(|id| { + cx.tcx.hir_attrs(id).iter().any(|attr| { + matches!( + attr.ident().map(|ident| ident.name), + Some(sym::cfg_trace | sym::cfg_attr_trace) + ) + }) + }) +} diff --git a/tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr b/tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr index 8a85d38fba3..608264beb10 100644 --- a/tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr +++ b/tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr @@ -18,6 +18,8 @@ error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is | LL | sleep(Duration::new(1, 0)); | ^^^^^ + | + = note: you may want to conditionally increase the MSRV considered by Clippy using the `clippy::msrv` attribute error: aborting due to 3 previous errors diff --git a/tests/ui/checked_conversions.fixed b/tests/ui/checked_conversions.fixed index 279a5b6e1ff..6175275ef04 100644 --- a/tests/ui/checked_conversions.fixed +++ b/tests/ui/checked_conversions.fixed @@ -95,7 +95,7 @@ pub const fn issue_8898(i: u32) -> bool { #[clippy::msrv = "1.33"] fn msrv_1_33() { let value: i64 = 33; - let _ = value <= (u32::MAX as i64) && value >= 0; + let _ = value <= (u32::max_value() as i64) && value >= 0; } #[clippy::msrv = "1.34"] diff --git a/tests/ui/checked_conversions.rs b/tests/ui/checked_conversions.rs index c339bc674bb..9ed0e8f660d 100644 --- a/tests/ui/checked_conversions.rs +++ b/tests/ui/checked_conversions.rs @@ -95,13 +95,13 @@ pub const fn issue_8898(i: u32) -> bool { #[clippy::msrv = "1.33"] fn msrv_1_33() { let value: i64 = 33; - let _ = value <= (u32::MAX as i64) && value >= 0; + let _ = value <= (u32::max_value() as i64) && value >= 0; } #[clippy::msrv = "1.34"] fn msrv_1_34() { let value: i64 = 34; - let _ = value <= (u32::MAX as i64) && value >= 0; + let _ = value <= (u32::max_value() as i64) && value >= 0; //~^ checked_conversions } diff --git a/tests/ui/checked_conversions.stderr b/tests/ui/checked_conversions.stderr index 3841b9d5a4d..624876dacb2 100644 --- a/tests/ui/checked_conversions.stderr +++ b/tests/ui/checked_conversions.stderr @@ -100,8 +100,8 @@ LL | let _ = value <= u16::MAX as u32 && value as i32 == 5; error: checked cast can be simplified --> tests/ui/checked_conversions.rs:104:13 | -LL | let _ = value <= (u32::MAX as i64) && value >= 0; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `u32::try_from(value).is_ok()` +LL | let _ = value <= (u32::max_value() as i64) && value >= 0; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `u32::try_from(value).is_ok()` error: aborting due to 17 previous errors diff --git a/tests/ui/incompatible_msrv.rs b/tests/ui/incompatible_msrv.rs index 99101b2bb8f..37e55e9e18a 100644 --- a/tests/ui/incompatible_msrv.rs +++ b/tests/ui/incompatible_msrv.rs @@ -1,6 +1,7 @@ #![warn(clippy::incompatible_msrv)] #![feature(custom_inner_attributes)] -#![feature(panic_internals)] +#![allow(stable_features)] +#![feature(strict_provenance)] // For use in test #![clippy::msrv = "1.3.0"] use std::collections::HashMap; @@ -13,6 +14,8 @@ fn foo() { let mut map: HashMap<&str, u32> = HashMap::new(); assert_eq!(map.entry("poneyland").key(), &"poneyland"); //~^ incompatible_msrv + //~| NOTE: `-D clippy::incompatible-msrv` implied by `-D warnings` + //~| HELP: to override `-D warnings` add `#[allow(clippy::incompatible_msrv)]` if let Entry::Vacant(v) = map.entry("poneyland") { v.into_key(); @@ -43,21 +46,22 @@ fn core_special_treatment(p: bool) { // But still lint code calling `core` functions directly if p { - core::panicking::panic("foo"); - //~^ ERROR: is `1.3.0` but this item is stable since `1.6.0` + let _ = core::iter::once_with(|| 0); + //~^ incompatible_msrv } // Lint code calling `core` from non-`core` macros macro_rules! my_panic { ($msg:expr) => { - core::panicking::panic($msg) - }; //~^ ERROR: is `1.3.0` but this item is stable since `1.6.0` + let _ = core::iter::once_with(|| $msg); + //~^ incompatible_msrv + }; } my_panic!("foo"); // Lint even when the macro comes from `core` and calls `core` functions - assert!(core::panicking::panic("out of luck")); - //~^ ERROR: is `1.3.0` but this item is stable since `1.6.0` + assert!(core::iter::once_with(|| 0).next().is_some()); + //~^ incompatible_msrv } #[clippy::msrv = "1.26.0"] @@ -70,7 +74,40 @@ fn lang_items() { #[clippy::msrv = "1.80.0"] fn issue14212() { let _ = std::iter::repeat_n((), 5); - //~^ ERROR: is `1.80.0` but this item is stable since `1.82.0` + //~^ incompatible_msrv +} + +fn local_msrv_change_suggestion() { + let _ = std::iter::repeat_n((), 5); + //~^ incompatible_msrv + + #[cfg(any(test, not(test)))] + { + let _ = std::iter::repeat_n((), 5); + //~^ incompatible_msrv + //~| NOTE: you may want to conditionally increase the MSRV + + // Emit the additional note only once + let _ = std::iter::repeat_n((), 5); + //~^ incompatible_msrv + } +} + +#[clippy::msrv = "1.78.0"] +fn feature_enable_14425(ptr: *const u8) -> usize { + // Do not warn, because it is enabled through a feature even though + // it is stabilized only since Rust 1.84.0. + let r = ptr.addr(); + + // Warn about this which has been introduced in the same Rust version + // but is not allowed through a feature. + r.isqrt() + //~^ incompatible_msrv +} + +fn non_fn_items() { + let _ = std::io::ErrorKind::CrossesDevices; + //~^ incompatible_msrv } fn main() {} diff --git a/tests/ui/incompatible_msrv.stderr b/tests/ui/incompatible_msrv.stderr index 5ea2bb9cc58..ed5ad5e1660 100644 --- a/tests/ui/incompatible_msrv.stderr +++ b/tests/ui/incompatible_msrv.stderr @@ -1,5 +1,5 @@ error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.10.0` - --> tests/ui/incompatible_msrv.rs:14:39 + --> tests/ui/incompatible_msrv.rs:15:39 | LL | assert_eq!(map.entry("poneyland").key(), &"poneyland"); | ^^^^^ @@ -8,45 +8,77 @@ LL | assert_eq!(map.entry("poneyland").key(), &"poneyland"); = help: to override `-D warnings` add `#[allow(clippy::incompatible_msrv)]` error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.12.0` - --> tests/ui/incompatible_msrv.rs:18:11 + --> tests/ui/incompatible_msrv.rs:21:11 | LL | v.into_key(); | ^^^^^^^^^^ error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.4.0` - --> tests/ui/incompatible_msrv.rs:22:5 + --> tests/ui/incompatible_msrv.rs:25:5 | LL | sleep(Duration::new(1, 0)); | ^^^^^ -error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.6.0` - --> tests/ui/incompatible_msrv.rs:46:9 +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.43.0` + --> tests/ui/incompatible_msrv.rs:49:17 | -LL | core::panicking::panic("foo"); - | ^^^^^^^^^^^^^^^^^^^^^^ +LL | let _ = core::iter::once_with(|| 0); + | ^^^^^^^^^^^^^^^^^^^^^ -error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.6.0` - --> tests/ui/incompatible_msrv.rs:53:13 +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.43.0` + --> tests/ui/incompatible_msrv.rs:56:21 | -LL | core::panicking::panic($msg) - | ^^^^^^^^^^^^^^^^^^^^^^ +LL | let _ = core::iter::once_with(|| $msg); + | ^^^^^^^^^^^^^^^^^^^^^ ... LL | my_panic!("foo"); | ---------------- in this macro invocation | = note: this error originates in the macro `my_panic` (in Nightly builds, run with -Z macro-backtrace for more info) -error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.6.0` - --> tests/ui/incompatible_msrv.rs:59:13 +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.43.0` + --> tests/ui/incompatible_msrv.rs:63:13 | -LL | assert!(core::panicking::panic("out of luck")); - | ^^^^^^^^^^^^^^^^^^^^^^ +LL | assert!(core::iter::once_with(|| 0).next().is_some()); + | ^^^^^^^^^^^^^^^^^^^^^ error: current MSRV (Minimum Supported Rust Version) is `1.80.0` but this item is stable since `1.82.0` - --> tests/ui/incompatible_msrv.rs:72:13 + --> tests/ui/incompatible_msrv.rs:76:13 | LL | let _ = std::iter::repeat_n((), 5); | ^^^^^^^^^^^^^^^^^^^ -error: aborting due to 7 previous errors +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.82.0` + --> tests/ui/incompatible_msrv.rs:81:13 + | +LL | let _ = std::iter::repeat_n((), 5); + | ^^^^^^^^^^^^^^^^^^^ + +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.82.0` + --> tests/ui/incompatible_msrv.rs:86:17 + | +LL | let _ = std::iter::repeat_n((), 5); + | ^^^^^^^^^^^^^^^^^^^ + | + = note: you may want to conditionally increase the MSRV considered by Clippy using the `clippy::msrv` attribute + +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.82.0` + --> tests/ui/incompatible_msrv.rs:91:17 + | +LL | let _ = std::iter::repeat_n((), 5); + | ^^^^^^^^^^^^^^^^^^^ + +error: current MSRV (Minimum Supported Rust Version) is `1.78.0` but this item is stable since `1.84.0` + --> tests/ui/incompatible_msrv.rs:104:7 + | +LL | r.isqrt() + | ^^^^^^^ + +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.85.0` + --> tests/ui/incompatible_msrv.rs:109:13 + | +LL | let _ = std::io::ErrorKind::CrossesDevices; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 12 previous errors |
