about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-01-26 13:15:29 +0000
committerbors <bors@rust-lang.org>2024-01-26 13:15:29 +0000
commit8de9d8ce996072f728fbba6fb6a10f01df6600c6 (patch)
tree3a2649d7dfa95feda0e74185f1c04c625b6deb90
parented74c22f33da446eb91d2069b3fdc8e3d93bff16 (diff)
parent14e15206ed1ce8b50c4d2038f10d2793faf87723 (diff)
downloadrust-8de9d8ce996072f728fbba6fb6a10f01df6600c6.tar.gz
rust-8de9d8ce996072f728fbba6fb6a10f01df6600c6.zip
Auto merge of #12160 - GuillaumeGomez:incompatible-msrv, r=blyxyas
Warn if an item coming from more recent version than MSRV is used

Part of https://github.com/rust-lang/rust-clippy/issues/6324.

~~Currently, the lint is not working for the simple reason that the `stable` attribute is not kept in dependencies. I'll send a PR to rustc to see if they'd be okay with keeping it.~~

EDIT: There was actually a `lookup_stability` function providing this information, so all good now!

cc `@epage`

changelog: create new [`incompatible_msrv`] lint
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_config/src/msrvs.rs11
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/incompatible_msrv.rs133
-rw-r--r--clippy_lints/src/lib.rs3
-rw-r--r--tests/ui-toml/min_rust_version/min_rust_version.fixed2
-rw-r--r--tests/ui-toml/min_rust_version/min_rust_version.rs2
-rw-r--r--tests/ui/incompatible_msrv.rs23
-rw-r--r--tests/ui/incompatible_msrv.stderr23
9 files changed, 197 insertions, 2 deletions
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_semver::RustcVersion;
 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<RustcVersion>,
 }
 
+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<D>(deserializer: D) -> Result<Self, D::Error>
     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 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     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<DefId, RustcVersion>,
+}
+
+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_abi;
 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_return;
 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
+