From 14e15206ed1ce8b50c4d2038f10d2793faf87723 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 17 Jan 2024 15:56:53 +0100 Subject: [PATCH] Warn if an item coming from more recent version than MSRV is used --- CHANGELOG.md | 1 + clippy_config/src/msrvs.rs | 11 ++ clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/incompatible_msrv.rs | 133 ++++++++++++++++++ clippy_lints/src/lib.rs | 3 + .../min_rust_version/min_rust_version.fixed | 2 +- .../min_rust_version/min_rust_version.rs | 2 +- tests/ui/incompatible_msrv.rs | 23 +++ tests/ui/incompatible_msrv.stderr | 23 +++ 9 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 clippy_lints/src/incompatible_msrv.rs create mode 100644 tests/ui/incompatible_msrv.rs create mode 100644 tests/ui/incompatible_msrv.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa45ceeb39..7db5d4c45df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5198,6 +5198,7 @@ Released 2018-09-13 [`implied_bounds_in_impls`]: https://rust-lang.github.io/rust-clippy/master/index.html#implied_bounds_in_impls [`impossible_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#impossible_comparisons [`imprecise_flops`]: https://rust-lang.github.io/rust-clippy/master/index.html#imprecise_flops +[`incompatible_msrv`]: https://rust-lang.github.io/rust-clippy/master/index.html#incompatible_msrv [`inconsistent_digit_grouping`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_digit_grouping [`inconsistent_struct_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_struct_constructor [`incorrect_clone_impl_on_copy_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#incorrect_clone_impl_on_copy_type diff --git a/clippy_config/src/msrvs.rs b/clippy_config/src/msrvs.rs index 72d5b9aff28..856644b8568 100644 --- a/clippy_config/src/msrvs.rs +++ b/clippy_config/src/msrvs.rs @@ -3,6 +3,7 @@ use rustc_session::Session; use rustc_span::{sym, Symbol}; use serde::Deserialize; +use std::fmt; macro_rules! msrv_aliases { ($($major:literal,$minor:literal,$patch:literal { @@ -58,6 +59,16 @@ pub struct Msrv { stack: Vec, } +impl fmt::Display for Msrv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(msrv) = self.current() { + write!(f, "{msrv}") + } else { + f.write_str("1.0.0") + } + } +} + impl<'de> Deserialize<'de> for Msrv { fn deserialize(deserializer: D) -> Result where diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 639edd8da30..5342a6722db 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -212,6 +212,7 @@ crate::implicit_saturating_add::IMPLICIT_SATURATING_ADD_INFO, crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO, crate::implied_bounds_in_impls::IMPLIED_BOUNDS_IN_IMPLS_INFO, + crate::incompatible_msrv::INCOMPATIBLE_MSRV_INFO, crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO, crate::index_refutable_slice::INDEX_REFUTABLE_SLICE_INFO, crate::indexing_slicing::INDEXING_SLICING_INFO, diff --git a/clippy_lints/src/incompatible_msrv.rs b/clippy_lints/src/incompatible_msrv.rs new file mode 100644 index 00000000000..f2f0e7d4266 --- /dev/null +++ b/clippy_lints/src/incompatible_msrv.rs @@ -0,0 +1,133 @@ +use clippy_config::msrvs::Msrv; +use clippy_utils::diagnostics::span_lint; +use rustc_attr::{StabilityLevel, StableSince}; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::TyCtxt; +use rustc_semver::RustcVersion; +use rustc_session::impl_lint_pass; +use rustc_span::def_id::DefId; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// + /// This lint checks that no function newer than the defined MSRV (minimum + /// supported rust version) is used in the crate. + /// + /// ### Why is this bad? + /// + /// It would prevent the crate to be actually used with the specified MSRV. + /// + /// ### Example + /// ```no_run + /// // MSRV of 1.3.0 + /// use std::thread::sleep; + /// use std::time::Duration; + /// + /// // Sleep was defined in `1.4.0`. + /// sleep(Duration::new(1, 0)); + /// ``` + /// + /// To fix this problem, either increase your MSRV or use another item + /// available in your current MSRV. + #[clippy::version = "1.77.0"] + pub INCOMPATIBLE_MSRV, + suspicious, + "ensures that all items used in the crate are available for the current MSRV" +} + +pub struct IncompatibleMsrv { + msrv: Msrv, + is_above_msrv: FxHashMap, +} + +impl_lint_pass!(IncompatibleMsrv => [INCOMPATIBLE_MSRV]); + +impl IncompatibleMsrv { + pub fn new(msrv: Msrv) -> Self { + Self { + msrv, + is_above_msrv: FxHashMap::default(), + } + } + + #[allow(clippy::cast_lossless)] + 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; + } + let version = if let Some(version) = tcx + .lookup_stability(def_id) + .and_then(|stability| match stability.level { + StabilityLevel::Stable { + since: StableSince::Version(version), + .. + } => Some(RustcVersion::new( + version.major as _, + version.minor as _, + version.patch as _, + )), + _ => None, + }) { + version + } else if let Some(parent_def_id) = tcx.opt_parent(def_id) { + self.get_def_id_version(tcx, parent_def_id) + } else { + RustcVersion::new(1, 0, 0) + }; + self.is_above_msrv.insert(def_id, version); + version + } + + fn emit_lint_if_under_msrv(&mut self, cx: &LateContext<'_>, def_id: DefId, span: Span) { + if def_id.is_local() { + // We don't check local items since their MSRV is supposed to always be valid. + return; + } + let version = self.get_def_id_version(cx.tcx, def_id); + if self.msrv.meets(version) { + return; + } + self.emit_lint_for(cx, span, version); + } + + fn emit_lint_for(&self, cx: &LateContext<'_>, span: Span, version: RustcVersion) { + span_lint( + cx, + INCOMPATIBLE_MSRV, + span, + &format!( + "current MSRV (Minimum Supported Rust Version) is `{}` but this item is stable since `{version}`", + self.msrv + ), + ); + } +} + +impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv { + extract_msrv_attr!(LateContext); + + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if self.msrv.current().is_none() { + // If there is no MSRV, then no need to check anything... + return; + } + 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, span); + } + }, + ExprKind::Call(call, [_]) => { + if let ExprKind::Path(qpath) = 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, call.span); + } + }, + _ => {}, + } + } +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index feb4d188f39..907ff938293 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -26,6 +26,7 @@ extern crate rustc_arena; extern crate rustc_ast; extern crate rustc_ast_pretty; +extern crate rustc_attr; extern crate rustc_data_structures; extern crate rustc_driver; extern crate rustc_errors; @@ -153,6 +154,7 @@ mod implicit_saturating_add; mod implicit_saturating_sub; mod implied_bounds_in_impls; +mod incompatible_msrv; mod inconsistent_struct_constructor; mod index_refutable_slice; mod indexing_slicing; @@ -1094,6 +1096,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { store.register_late_pass(move |_| { Box::new(thread_local_initializer_can_be_made_const::ThreadLocalInitializerCanBeMadeConst::new(msrv())) }); + store.register_late_pass(move |_| Box::new(incompatible_msrv::IncompatibleMsrv::new(msrv()))); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/tests/ui-toml/min_rust_version/min_rust_version.fixed b/tests/ui-toml/min_rust_version/min_rust_version.fixed index 6c58e07d846..497f783087a 100644 --- a/tests/ui-toml/min_rust_version/min_rust_version.fixed +++ b/tests/ui-toml/min_rust_version/min_rust_version.fixed @@ -1,4 +1,4 @@ -#![allow(clippy::redundant_clone, clippy::unnecessary_operation)] +#![allow(clippy::redundant_clone, clippy::unnecessary_operation, clippy::incompatible_msrv)] #![warn(clippy::manual_non_exhaustive, clippy::borrow_as_ptr, clippy::manual_bits)] use std::mem::{size_of, size_of_val}; diff --git a/tests/ui-toml/min_rust_version/min_rust_version.rs b/tests/ui-toml/min_rust_version/min_rust_version.rs index e1dc3f4389c..6e7874108a3 100644 --- a/tests/ui-toml/min_rust_version/min_rust_version.rs +++ b/tests/ui-toml/min_rust_version/min_rust_version.rs @@ -1,4 +1,4 @@ -#![allow(clippy::redundant_clone, clippy::unnecessary_operation)] +#![allow(clippy::redundant_clone, clippy::unnecessary_operation, clippy::incompatible_msrv)] #![warn(clippy::manual_non_exhaustive, clippy::borrow_as_ptr, clippy::manual_bits)] use std::mem::{size_of, size_of_val}; diff --git a/tests/ui/incompatible_msrv.rs b/tests/ui/incompatible_msrv.rs new file mode 100644 index 00000000000..a92017fb0f6 --- /dev/null +++ b/tests/ui/incompatible_msrv.rs @@ -0,0 +1,23 @@ +#![warn(clippy::incompatible_msrv)] +#![feature(custom_inner_attributes)] +#![clippy::msrv = "1.3.0"] + +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::thread::sleep; +use std::time::Duration; + +fn foo() { + let mut map: HashMap<&str, u32> = HashMap::new(); + assert_eq!(map.entry("poneyland").key(), &"poneyland"); + //~^ ERROR: is `1.3.0` but this item is stable since `1.10.0` + if let Entry::Vacant(v) = map.entry("poneyland") { + v.into_key(); + //~^ ERROR: is `1.3.0` but this item is stable since `1.12.0` + } + // Should warn for `sleep` but not for `Duration` (which was added in `1.3.0`). + sleep(Duration::new(1, 0)); + //~^ ERROR: is `1.3.0` but this item is stable since `1.4.0` +} + +fn main() {} diff --git a/tests/ui/incompatible_msrv.stderr b/tests/ui/incompatible_msrv.stderr new file mode 100644 index 00000000000..bd5ecd6ed2f --- /dev/null +++ b/tests/ui/incompatible_msrv.stderr @@ -0,0 +1,23 @@ +error: current MSRV (Minimum Supported Rust Version) is `1.3.0` but this item is stable since `1.10.0` + --> $DIR/incompatible_msrv.rs:12:39 + | +LL | assert_eq!(map.entry("poneyland").key(), &"poneyland"); + | ^^^^^ + | + = 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.3.0` but this item is stable since `1.12.0` + --> $DIR/incompatible_msrv.rs:15: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` + --> $DIR/incompatible_msrv.rs:19:5 + | +LL | sleep(Duration::new(1, 0)); + | ^^^^^ + +error: aborting due to 3 previous errors +