Skip to content

Commit 5d811a4

Browse files
Warn if an item coming from more recent version than MSRV is used
1 parent 37947ff commit 5d811a4

File tree

5 files changed

+163
-0
lines changed

5 files changed

+163
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5198,6 +5198,7 @@ Released 2018-09-13
51985198
[`implied_bounds_in_impls`]: https://rust-lang.github.io/rust-clippy/master/index.html#implied_bounds_in_impls
51995199
[`impossible_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#impossible_comparisons
52005200
[`imprecise_flops`]: https://rust-lang.github.io/rust-clippy/master/index.html#imprecise_flops
5201+
[`incompatible_msrv`]: https://rust-lang.github.io/rust-clippy/master/index.html#incompatible_msrv
52015202
[`inconsistent_digit_grouping`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_digit_grouping
52025203
[`inconsistent_struct_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_struct_constructor
52035204
[`incorrect_clone_impl_on_copy_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#incorrect_clone_impl_on_copy_type

clippy_config/src/msrvs.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use rustc_semver::RustcVersion;
33
use rustc_session::Session;
44
use rustc_span::{sym, Symbol};
55
use serde::Deserialize;
6+
use std::fmt;
67

78
macro_rules! msrv_aliases {
89
($($major:literal,$minor:literal,$patch:literal {
@@ -58,6 +59,16 @@ pub struct Msrv {
5859
stack: Vec<RustcVersion>,
5960
}
6061

62+
impl fmt::Display for Msrv {
63+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64+
if let Some(msrv) = self.current() {
65+
write!(f, "{msrv}")
66+
} else {
67+
f.write_str("1.0.0")
68+
}
69+
}
70+
}
71+
6172
impl<'de> Deserialize<'de> for Msrv {
6273
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6374
where

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
212212
crate::implicit_saturating_add::IMPLICIT_SATURATING_ADD_INFO,
213213
crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO,
214214
crate::implied_bounds_in_impls::IMPLIED_BOUNDS_IN_IMPLS_INFO,
215+
crate::incompatible_msrv::INCOMPATIBLE_MSRV_INFO,
215216
crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO,
216217
crate::index_refutable_slice::INDEX_REFUTABLE_SLICE_INFO,
217218
crate::indexing_slicing::INDEXING_SLICING_INFO,

clippy_lints/src/incompatible_msrv.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use clippy_config::msrvs::Msrv;
2+
use clippy_utils::diagnostics::span_lint;
3+
use rustc_ast::Attribute;
4+
use rustc_data_structures::fx::FxHashMap;
5+
use rustc_hir::{Expr, ExprKind};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::ty::TyCtxt;
8+
use rustc_semver::RustcVersion;
9+
use rustc_session::impl_lint_pass;
10+
use rustc_span::def_id::DefId;
11+
use rustc_span::{sym, Span, Symbol};
12+
13+
use std::collections::hash_map::Entry;
14+
15+
declare_clippy_lint! {
16+
/// ### What it does
17+
///
18+
/// ### Why is this bad?
19+
///
20+
/// ### Example
21+
/// ```no_run
22+
/// // example code where clippy issues a warning
23+
/// ```
24+
/// Use instead:
25+
/// ```no_run
26+
/// // example code which does not raise clippy warning
27+
/// ```
28+
#[clippy::version = "1.77.0"]
29+
pub INCOMPATIBLE_MSRV,
30+
suspicious,
31+
"default lint description"
32+
}
33+
34+
pub struct IncompatibleMsrv {
35+
msrv: Msrv,
36+
is_above_msrv: FxHashMap<DefId, RustcVersion>,
37+
}
38+
39+
impl_lint_pass!(IncompatibleMsrv => [INCOMPATIBLE_MSRV]);
40+
41+
// This is the source code of `rustc_attr::builtin::parse_version`.
42+
fn parse_version(s: Symbol) -> Option<RustcVersion> {
43+
let mut components = s.as_str().split('-');
44+
let d = components.next()?;
45+
if components.next().is_some() {
46+
return None;
47+
}
48+
let mut digits = d.splitn(3, '.');
49+
let major = digits.next()?.parse().ok()?;
50+
let minor = digits.next()?.parse().ok()?;
51+
let patch = digits.next().unwrap_or("0").parse().ok()?;
52+
Some(RustcVersion::new(major, minor, patch))
53+
}
54+
55+
impl IncompatibleMsrv {
56+
pub fn new(msrv: Msrv) -> Self {
57+
eprintln!("---> {msrv:?}",);
58+
Self {
59+
msrv,
60+
is_above_msrv: FxHashMap::default(),
61+
}
62+
}
63+
64+
fn get_def_id_version(&mut self, tcx: TyCtxt<'_>, def_id: DefId) -> RustcVersion {
65+
match self.is_above_msrv.entry(def_id) {
66+
Entry::Occupied(o) => *o.get(),
67+
Entry::Vacant(v) => {
68+
let version = tcx
69+
.get_attrs(def_id, sym::stable)
70+
.map(|attr| attr.meta_item_list())
71+
.flatten()
72+
.flatten()
73+
.find_map(|attr| {
74+
if attr.name_or_empty() == sym::since
75+
&& let Some(version) = attr.value_str()
76+
{
77+
parse_version(version)
78+
} else {
79+
None
80+
}
81+
})
82+
// No `stable` attribute found so it's `1.0.0`.
83+
.unwrap_or(RustcVersion::new(1, 0, 0));
84+
*v.insert(version)
85+
},
86+
}
87+
}
88+
89+
fn emit_lint_if_under_msrv(&mut self, cx: &LateContext<'_>, mut def_id: DefId, span: Span) {
90+
loop {
91+
if def_id.is_local() {
92+
// We don't check local items since their MSRV is supposed to always be valid.
93+
return;
94+
}
95+
let version = self.get_def_id_version(cx.tcx, def_id);
96+
if !self.msrv.meets(version) {
97+
self.emit_lint_for(cx, span, version);
98+
} else {
99+
return;
100+
}
101+
match cx.tcx.opt_parent(def_id) {
102+
Some(parent_def_id) => def_id = parent_def_id,
103+
None => return,
104+
}
105+
}
106+
}
107+
108+
fn emit_lint_for(&self, cx: &LateContext<'_>, span: Span, version: RustcVersion) {
109+
span_lint(
110+
cx,
111+
INCOMPATIBLE_MSRV,
112+
span,
113+
&format!("MSRV is `{}` but this item is stable since `{version}`", self.msrv),
114+
);
115+
}
116+
}
117+
118+
impl<'tcx> LateLintPass<'tcx> for IncompatibleMsrv {
119+
fn enter_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {
120+
self.msrv.enter_lint_attrs(cx.tcx.sess, attrs);
121+
}
122+
123+
fn exit_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {
124+
self.msrv.exit_lint_attrs(cx.tcx.sess, attrs);
125+
}
126+
127+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
128+
if self.msrv.current().is_none() {
129+
// If there is no MSRV, then no need to check anything...
130+
return;
131+
}
132+
match expr.kind {
133+
ExprKind::MethodCall(_, _, _, span) => {
134+
if let Some(method_did) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
135+
self.emit_lint_if_under_msrv(cx, method_did, span);
136+
}
137+
},
138+
ExprKind::Call(call, [_]) => {
139+
if let ExprKind::Path(qpath) = call.kind
140+
&& let Some(path_def_id) = cx.qpath_res(&qpath, call.hir_id).opt_def_id()
141+
{
142+
self.emit_lint_if_under_msrv(cx, path_def_id, call.span);
143+
}
144+
},
145+
_ => {},
146+
}
147+
}
148+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ mod implicit_return;
153153
mod implicit_saturating_add;
154154
mod implicit_saturating_sub;
155155
mod implied_bounds_in_impls;
156+
mod incompatible_msrv;
156157
mod inconsistent_struct_constructor;
157158
mod index_refutable_slice;
158159
mod indexing_slicing;
@@ -1092,6 +1093,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
10921093
store.register_late_pass(move |_| {
10931094
Box::new(thread_local_initializer_can_be_made_const::ThreadLocalInitializerCanBeMadeConst::new(msrv()))
10941095
});
1096+
store.register_late_pass(move |_| Box::new(incompatible_msrv::IncompatibleMsrv::new(msrv())));
10951097
// add lints here, do not remove this comment, it's used in `new_lint`
10961098
}
10971099

0 commit comments

Comments
 (0)