Skip to content

Commit c0dc3b6

Browse files
authored
Various improvements to the incompatible_msrv lint (rust-lang#14433)
This PR supersedes rust-lang#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 rust-lang#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 rust-lang#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 rust-lang#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
2 parents e113e66 + 68ea461 commit c0dc3b6

File tree

7 files changed

+185
-61
lines changed

7 files changed

+185
-61
lines changed

clippy_lints/src/incompatible_msrv.rs

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use clippy_config::Conf;
2-
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::diagnostics::span_lint_and_then;
33
use clippy_utils::is_in_test;
44
use clippy_utils::msrvs::Msrv;
5-
use rustc_attr_data_structures::{RustcVersion, StabilityLevel, StableSince};
5+
use rustc_attr_data_structures::{RustcVersion, Stability, StableSince};
66
use rustc_data_structures::fx::FxHashMap;
77
use rustc_hir::{Expr, ExprKind, HirId, QPath};
88
use rustc_lint::{LateContext, LateLintPass};
@@ -33,15 +33,54 @@ declare_clippy_lint! {
3333
///
3434
/// To fix this problem, either increase your MSRV or use another item
3535
/// available in your current MSRV.
36+
///
37+
/// You can also locally change the MSRV that should be checked by Clippy,
38+
/// for example if a feature in your crate (e.g., `modern_compiler`) should
39+
/// allow you to use an item:
40+
///
41+
/// ```no_run
42+
/// //! This crate has a MSRV of 1.3.0, but we also have an optional feature
43+
/// //! `sleep_well` which requires at least Rust 1.4.0.
44+
///
45+
/// // When the `sleep_well` feature is set, do not warn for functions available
46+
/// // in Rust 1.4.0 and below.
47+
/// #![cfg_attr(feature = "sleep_well", clippy::msrv = "1.4.0")]
48+
///
49+
/// use std::time::Duration;
50+
///
51+
/// #[cfg(feature = "sleep_well")]
52+
/// fn sleep_for_some_time() {
53+
/// std::thread::sleep(Duration::new(1, 0)); // Will not trigger the lint
54+
/// }
55+
/// ```
56+
///
57+
/// You can also increase the MSRV in tests, by using:
58+
///
59+
/// ```no_run
60+
/// // Use a much higher MSRV for tests while keeping the main one low
61+
/// #![cfg_attr(test, clippy::msrv = "1.85.0")]
62+
///
63+
/// #[test]
64+
/// fn my_test() {
65+
/// // The tests can use items introduced in Rust 1.85.0 and lower
66+
/// // without triggering the `incompatible_msrv` lint.
67+
/// }
68+
/// ```
3669
#[clippy::version = "1.78.0"]
3770
pub INCOMPATIBLE_MSRV,
3871
suspicious,
3972
"ensures that all items used in the crate are available for the current MSRV"
4073
}
4174

75+
#[derive(Clone, Copy)]
76+
enum Availability {
77+
FeatureEnabled,
78+
Since(RustcVersion),
79+
}
80+
4281
pub struct IncompatibleMsrv {
4382
msrv: Msrv,
44-
is_above_msrv: FxHashMap<DefId, RustcVersion>,
83+
availability_cache: FxHashMap<DefId, Availability>,
4584
check_in_tests: bool,
4685
}
4786

@@ -51,35 +90,32 @@ impl IncompatibleMsrv {
5190
pub fn new(conf: &'static Conf) -> Self {
5291
Self {
5392
msrv: conf.msrv,
54-
is_above_msrv: FxHashMap::default(),
93+
availability_cache: FxHashMap::default(),
5594
check_in_tests: conf.check_incompatible_msrv_in_tests,
5695
}
5796
}
5897

59-
fn get_def_id_version(&mut self, tcx: TyCtxt<'_>, def_id: DefId) -> RustcVersion {
60-
if let Some(version) = self.is_above_msrv.get(&def_id) {
61-
return *version;
98+
/// Returns the availability of `def_id`, whether it is enabled through a feature or
99+
/// available since a given version (the default being Rust 1.0.0).
100+
fn get_def_id_availability(&mut self, tcx: TyCtxt<'_>, def_id: DefId) -> Availability {
101+
if let Some(availability) = self.availability_cache.get(&def_id) {
102+
return *availability;
62103
}
63-
let version = if let Some(version) = tcx
64-
.lookup_stability(def_id)
65-
.and_then(|stability| match stability.level {
66-
StabilityLevel::Stable {
67-
since: StableSince::Version(version),
68-
..
69-
} => Some(version),
70-
_ => None,
71-
}) {
72-
version
104+
let stability = tcx.lookup_stability(def_id);
105+
let version = if stability.is_some_and(|stability| tcx.features().enabled(stability.feature)) {
106+
Availability::FeatureEnabled
107+
} else if let Some(StableSince::Version(version)) = stability.as_ref().and_then(Stability::stable_since) {
108+
Availability::Since(version)
73109
} else if let Some(parent_def_id) = tcx.opt_parent(def_id) {
74-
self.get_def_id_version(tcx, parent_def_id)
110+
self.get_def_id_availability(tcx, parent_def_id)
75111
} else {
76-
RustcVersion {
112+
Availability::Since(RustcVersion {
77113
major: 1,
78114
minor: 0,
79115
patch: 0,
80-
}
116+
})
81117
};
82-
self.is_above_msrv.insert(def_id, version);
118+
self.availability_cache.insert(def_id, version);
83119
version
84120
}
85121

@@ -110,40 +146,57 @@ impl IncompatibleMsrv {
110146

111147
if (self.check_in_tests || !is_in_test(cx.tcx, node))
112148
&& let Some(current) = self.msrv.current(cx)
113-
&& let version = self.get_def_id_version(cx.tcx, def_id)
149+
&& let Availability::Since(version) = self.get_def_id_availability(cx.tcx, def_id)
114150
&& version > current
115151
{
116-
span_lint(
152+
span_lint_and_then(
117153
cx,
118154
INCOMPATIBLE_MSRV,
119155
span,
120156
format!(
121157
"current MSRV (Minimum Supported Rust Version) is `{current}` but this item is stable since `{version}`"
122158
),
159+
|diag| {
160+
if is_under_cfg_attribute(cx, node) {
161+
diag.note_once("you may want to conditionally increase the MSRV considered by Clippy using the `clippy::msrv` attribute");
162+
}
163+
},
123164
);
124165
}
125166
}
126167
}
127168

128169
impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
129170
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
171+
// TODO: check for const stability when in const context
130172
match expr.kind {
131173
ExprKind::MethodCall(_, _, _, span) => {
132174
if let Some(method_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
133175
self.emit_lint_if_under_msrv(cx, method_did, expr.hir_id, span);
134176
}
135177
},
136-
ExprKind::Call(call, _) => {
137-
// Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should
138-
// not be linted as they will not be generated in older compilers if the function is not available,
139-
// and the compiler is allowed to call unstable functions.
140-
if let ExprKind::Path(qpath @ (QPath::Resolved(..) | QPath::TypeRelative(..))) = call.kind
141-
&& let Some(path_def_id) = cx.qpath_res(&qpath, call.hir_id).opt_def_id()
142-
{
143-
self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, call.span);
178+
// Desugaring into function calls by the compiler will use `QPath::LangItem` variants. Those should
179+
// not be linted as they will not be generated in older compilers if the function is not available,
180+
// and the compiler is allowed to call unstable functions.
181+
ExprKind::Path(qpath @ (QPath::Resolved(..) | QPath::TypeRelative(..))) => {
182+
if let Some(path_def_id) = cx.qpath_res(&qpath, expr.hir_id).opt_def_id() {
183+
self.emit_lint_if_under_msrv(cx, path_def_id, expr.hir_id, expr.span);
144184
}
145185
},
146186
_ => {},
147187
}
148188
}
149189
}
190+
191+
/// Heuristic checking if the node `hir_id` is under a `#[cfg()]` or `#[cfg_attr()]`
192+
/// attribute.
193+
fn is_under_cfg_attribute(cx: &LateContext<'_>, hir_id: HirId) -> bool {
194+
cx.tcx.hir_parent_id_iter(hir_id).any(|id| {
195+
cx.tcx.hir_attrs(id).iter().any(|attr| {
196+
matches!(
197+
attr.ident().map(|ident| ident.name),
198+
Some(sym::cfg_trace | sym::cfg_attr_trace)
199+
)
200+
})
201+
})
202+
}

tests/ui-toml/check_incompatible_msrv_in_tests/check_incompatible_msrv_in_tests.enabled.stderr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is
1818
|
1919
LL | sleep(Duration::new(1, 0));
2020
| ^^^^^
21+
|
22+
= note: you may want to conditionally increase the MSRV considered by Clippy using the `clippy::msrv` attribute
2123

2224
error: aborting due to 3 previous errors
2325

tests/ui/checked_conversions.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ pub const fn issue_8898(i: u32) -> bool {
9595
#[clippy::msrv = "1.33"]
9696
fn msrv_1_33() {
9797
let value: i64 = 33;
98-
let _ = value <= (u32::MAX as i64) && value >= 0;
98+
let _ = value <= (u32::max_value() as i64) && value >= 0;
9999
}
100100

101101
#[clippy::msrv = "1.34"]

tests/ui/checked_conversions.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ pub const fn issue_8898(i: u32) -> bool {
9595
#[clippy::msrv = "1.33"]
9696
fn msrv_1_33() {
9797
let value: i64 = 33;
98-
let _ = value <= (u32::MAX as i64) && value >= 0;
98+
let _ = value <= (u32::max_value() as i64) && value >= 0;
9999
}
100100

101101
#[clippy::msrv = "1.34"]
102102
fn msrv_1_34() {
103103
let value: i64 = 34;
104-
let _ = value <= (u32::MAX as i64) && value >= 0;
104+
let _ = value <= (u32::max_value() as i64) && value >= 0;
105105
//~^ checked_conversions
106106
}
107107

tests/ui/checked_conversions.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ LL | let _ = value <= u16::MAX as u32 && value as i32 == 5;
100100
error: checked cast can be simplified
101101
--> tests/ui/checked_conversions.rs:104:13
102102
|
103-
LL | let _ = value <= (u32::MAX as i64) && value >= 0;
104-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `u32::try_from(value).is_ok()`
103+
LL | let _ = value <= (u32::max_value() as i64) && value >= 0;
104+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `u32::try_from(value).is_ok()`
105105

106106
error: aborting due to 17 previous errors
107107

tests/ui/incompatible_msrv.rs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![warn(clippy::incompatible_msrv)]
22
#![feature(custom_inner_attributes)]
3-
#![feature(panic_internals)]
3+
#![allow(stable_features)]
4+
#![feature(strict_provenance)] // For use in test
45
#![clippy::msrv = "1.3.0"]
56

67
use std::collections::HashMap;
@@ -13,6 +14,8 @@ fn foo() {
1314
let mut map: HashMap<&str, u32> = HashMap::new();
1415
assert_eq!(map.entry("poneyland").key(), &"poneyland");
1516
//~^ incompatible_msrv
17+
//~| NOTE: `-D clippy::incompatible-msrv` implied by `-D warnings`
18+
//~| HELP: to override `-D warnings` add `#[allow(clippy::incompatible_msrv)]`
1619

1720
if let Entry::Vacant(v) = map.entry("poneyland") {
1821
v.into_key();
@@ -43,21 +46,22 @@ fn core_special_treatment(p: bool) {
4346

4447
// But still lint code calling `core` functions directly
4548
if p {
46-
core::panicking::panic("foo");
47-
//~^ ERROR: is `1.3.0` but this item is stable since `1.6.0`
49+
let _ = core::iter::once_with(|| 0);
50+
//~^ incompatible_msrv
4851
}
4952

5053
// Lint code calling `core` from non-`core` macros
5154
macro_rules! my_panic {
5255
($msg:expr) => {
53-
core::panicking::panic($msg)
54-
}; //~^ ERROR: is `1.3.0` but this item is stable since `1.6.0`
56+
let _ = core::iter::once_with(|| $msg);
57+
//~^ incompatible_msrv
58+
};
5559
}
5660
my_panic!("foo");
5761

5862
// Lint even when the macro comes from `core` and calls `core` functions
59-
assert!(core::panicking::panic("out of luck"));
60-
//~^ ERROR: is `1.3.0` but this item is stable since `1.6.0`
63+
assert!(core::iter::once_with(|| 0).next().is_some());
64+
//~^ incompatible_msrv
6165
}
6266

6367
#[clippy::msrv = "1.26.0"]
@@ -70,7 +74,40 @@ fn lang_items() {
7074
#[clippy::msrv = "1.80.0"]
7175
fn issue14212() {
7276
let _ = std::iter::repeat_n((), 5);
73-
//~^ ERROR: is `1.80.0` but this item is stable since `1.82.0`
77+
//~^ incompatible_msrv
78+
}
79+
80+
fn local_msrv_change_suggestion() {
81+
let _ = std::iter::repeat_n((), 5);
82+
//~^ incompatible_msrv
83+
84+
#[cfg(any(test, not(test)))]
85+
{
86+
let _ = std::iter::repeat_n((), 5);
87+
//~^ incompatible_msrv
88+
//~| NOTE: you may want to conditionally increase the MSRV
89+
90+
// Emit the additional note only once
91+
let _ = std::iter::repeat_n((), 5);
92+
//~^ incompatible_msrv
93+
}
94+
}
95+
96+
#[clippy::msrv = "1.78.0"]
97+
fn feature_enable_14425(ptr: *const u8) -> usize {
98+
// Do not warn, because it is enabled through a feature even though
99+
// it is stabilized only since Rust 1.84.0.
100+
let r = ptr.addr();
101+
102+
// Warn about this which has been introduced in the same Rust version
103+
// but is not allowed through a feature.
104+
r.isqrt()
105+
//~^ incompatible_msrv
106+
}
107+
108+
fn non_fn_items() {
109+
let _ = std::io::ErrorKind::CrossesDevices;
110+
//~^ incompatible_msrv
74111
}
75112

76113
fn main() {}

0 commit comments

Comments
 (0)