Skip to main content

clippy_utils/
msrvs.rs

1use crate::sym;
2use rustc_ast::Attribute;
3use rustc_ast::attr::AttributeExt;
4use rustc_attr_parsing::parse_version;
5use rustc_data_structures::smallvec::SmallVec;
6use rustc_hir::{HirId, RustcVersion};
7use rustc_lint::LateContext;
8use rustc_middle::ty::TyCtxt;
9use rustc_session::Session;
10use rustc_span::Symbol;
11use serde::Deserialize;
12use std::iter::once;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15macro_rules! msrv_aliases {
16    ($($major:literal,$minor:literal,$patch:literal {
17        $($name:ident),* $(,)?
18    })*) => {
19        $($(
20        pub const $name: RustcVersion = RustcVersion { major: $major, minor :$minor, patch: $patch };
21        )*)*
22    };
23}
24
25// names may refer to stabilized feature flags or library items
26msrv_aliases! {
27    1,97,0 { ISOLATE_LOWEST_ONE }
28    1,93,0 { VEC_DEQUE_POP_BACK_IF, VEC_DEQUE_POP_FRONT_IF }
29    1,91,0 { DURATION_FROM_MINUTES_HOURS }
30    1,88,0 { LET_CHAINS, AS_CHUNKS }
31    1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF, INTEGER_SIGN_CAST }
32    1,86,0 { VEC_POP_IF }
33    1,85,0 { UINT_FLOAT_MIDPOINT, CONST_SIZE_OF_VAL, WAKER_NOOP }
34    1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR }
35    1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
36    1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP, SPECIALIZED_TO_STRING_FOR_REFS }
37    1,81,0 { LINT_REASONS_STABILIZATION, ERROR_IN_CORE, EXPLICIT_SELF_TYPE_ELISION, DURATION_ABS_DIFF }
38    1,80,0 { BOX_INTO_ITER, LAZY_CELL }
39    1,79,0 { CONST_BLOCKS, CSTR_COUNT_BYTES }
40    1,77,0 { C_STR_LITERALS }
41    1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
42    1,75,0 { OPTION_AS_SLICE }
43    1,74,0 { REPR_RUST, IO_ERROR_OTHER }
44    1,73,0 { DIV_CEIL }
45    1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }
46    1,70,0 { OPTION_RESULT_IS_VARIANT_AND, BINARY_HEAP_RETAIN }
47    1,68,0 { PATH_MAIN_SEPARATOR_STR }
48    1,67,0 { ILOG2 }
49    1,65,0 { LET_ELSE, POINTER_CAST_CONSTNESS }
50    1,63,0 { CLONE_INTO, CONST_SLICE_FROM_REF }
51    1,62,0 { BOOL_THEN_SOME, DEFAULT_ENUM_ATTRIBUTE, CONST_EXTERN_C_FN }
52    1,61,0 { CONST_FN_TRAIT_BOUND }
53    1,60,0 { ABS_DIFF }
54    1,59,0 { THREAD_LOCAL_CONST_INIT }
55    1,58,0 { FORMAT_ARGS_CAPTURE, PATTERN_TRAIT_CHAR_ARRAY, CONST_RAW_PTR_DEREF }
56    1,57,0 { MAP_WHILE, CONST_PANIC }
57    1,56,0 { CONST_FN_UNION }
58    1,55,0 { SEEK_REWIND }
59    1,54,0 { INTO_KEYS }
60    1,53,0 { OR_PATTERNS, INTEGER_BITS, BTREE_MAP_RETAIN, BTREE_SET_RETAIN, ARRAY_INTO_ITERATOR }
61    1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST }
62    1,51,0 { BORROW_AS_PTR, SEEK_FROM_CURRENT, UNSIGNED_ABS }
63    1,50,0 { BOOL_THEN, CLAMP, SLICE_FILL }
64    1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST }
65    1,46,0 { CONST_IF_MATCH, OPTION_ZIP }
66    1,45,0 { STR_STRIP_PREFIX }
67    1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS }
68    1,42,0 { MATCHES_MACRO, SLICE_PATTERNS, PTR_SLICE_RAW_PARTS }
69    1,41,0 { RE_REBALANCING_COHERENCE, RESULT_MAP_OR, RESULT_MAP_OR_ELSE }
70    1,40,0 { MEM_TAKE, NON_EXHAUSTIVE, OPTION_AS_DEREF }
71    1,38,0 { POINTER_CAST, REM_EUCLID }
72    1,37,0 { TYPE_ALIAS_ENUM_VARIANTS }
73    1,36,0 { ITERATOR_COPIED }
74    1,35,0 { OPTION_COPIED, RANGE_CONTAINS }
75    1,34,0 { TRY_FROM }
76    1,33,0 { UNDERSCORE_IMPORTS }
77    1,32,0 { CONST_IS_POWER_OF_TWO, CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS }
78    1,31,0 { OPTION_REPLACE }
79    1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
80    1,29,0 { ITER_FLATTEN }
81    1,28,0 { FROM_BOOL, REPEAT_WITH, SLICE_FROM_REF }
82    1,27,0 { ITERATOR_TRY_FOLD, DOUBLE_ENDED_ITERATOR_RFIND, DURATION_FROM_NANOS_MICROS }
83    1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN, POINTER_ADD_SUB_METHODS }
84    1,24,0 { IS_ASCII_DIGIT, PTR_NULL }
85    1,18,0 { HASH_MAP_RETAIN, HASH_SET_RETAIN }
86    1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
87    1,16,0 { STR_REPEAT, RESULT_UNWRAP_OR_DEFAULT }
88    1,15,0 { MAYBE_BOUND_IN_WHERE }
89    1,13,0 { QUESTION_MARK_OPERATOR }
90    1,3,0 { DURATION_FROM_MILLIS_SECS }
91}
92
93/// `#[clippy::msrv]` attributes are rarely used outside of Clippy's test suite, as a basic
94/// optimization we can skip traversing the HIR in [`Msrv::meets`] if we never saw an MSRV attribute
95/// during the early lint passes
96static SEEN_MSRV_ATTR: AtomicBool = AtomicBool::new(false);
97
98/// Tracks the current MSRV from `clippy.toml`, `Cargo.toml` or set via `#[clippy::msrv]` in late
99/// lint passes, use [`MsrvStack`] for early passes
100#[derive(Copy, Clone, Debug, Default)]
101pub struct Msrv(Option<RustcVersion>);
102
103impl<'de> Deserialize<'de> for Msrv {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: serde::Deserializer<'de>,
107    {
108        let v = String::deserialize(deserializer)?;
109        parse_version(Symbol::intern(&v))
110            .map(|v| Self(Some(v)))
111            .ok_or_else(|| serde::de::Error::custom("not a valid Rust version"))
112    }
113}
114
115impl Msrv {
116    /// Returns the MSRV at the current node
117    ///
118    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
119    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
120    pub fn current(self, cx: &LateContext<'_>) -> Option<RustcVersion> {
121        if SEEN_MSRV_ATTR.load(Ordering::Relaxed) {
122            self.for_attrs(cx.tcx, cx.last_node_with_lint_attrs)
123        } else {
124            self.0
125        }
126    }
127
128    /// Returns the MSRV at the specified node
129    ///
130    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
131    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
132    pub fn at(self, tcx: TyCtxt<'_>, node: HirId) -> Option<RustcVersion> {
133        if SEEN_MSRV_ATTR.load(Ordering::Relaxed) {
134            self.for_attrs(tcx, node)
135        } else {
136            self.0
137        }
138    }
139
140    fn for_attrs(self, tcx: TyCtxt<'_>, node: HirId) -> Option<RustcVersion> {
141        once(node)
142            .chain(tcx.hir_parent_id_iter(node))
143            .find_map(|id| parse_attrs(tcx.sess, tcx.hir_attrs(id)))
144            .or(self.0)
145    }
146
147    /// Checks if a required version from [this module](self) is met at the current node
148    ///
149    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
150    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
151    pub fn meets(self, cx: &LateContext<'_>, required: RustcVersion) -> bool {
152        self.current(cx).is_none_or(|msrv| msrv >= required)
153    }
154
155    /// Checks if a required version from [this module](self) is met at the specified node
156    ///
157    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
158    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
159    pub fn meets_at(self, tcx: TyCtxt<'_>, node: HirId, required: RustcVersion) -> bool {
160        self.at(tcx, node).is_none_or(|msrv| msrv >= required)
161    }
162
163    pub fn read_cargo(&mut self, sess: &Session) {
164        let cargo_msrv = std::env::var("CARGO_PKG_RUST_VERSION")
165            .ok()
166            .and_then(|v| parse_version(Symbol::intern(&v)));
167
168        match (self.0, cargo_msrv) {
169            (None, Some(cargo_msrv)) => self.0 = Some(cargo_msrv),
170            (Some(clippy_msrv), Some(cargo_msrv)) if clippy_msrv != cargo_msrv => {
171                sess.dcx().warn(format!(
172                    "the MSRV in `clippy.toml` and `Cargo.toml` differ; using `{clippy_msrv}` from `clippy.toml`"
173                ));
174            },
175            _ => {},
176        }
177    }
178}
179
180/// Tracks the current MSRV from `clippy.toml`, `Cargo.toml` or set via `#[clippy::msrv]` in early
181/// lint passes, use [`Msrv`] for late passes
182#[derive(Debug, Clone)]
183pub struct MsrvStack {
184    stack: SmallVec<[RustcVersion; 2]>,
185}
186
187impl MsrvStack {
188    pub fn new(initial: Msrv) -> Self {
189        Self {
190            stack: SmallVec::from_iter(initial.0),
191        }
192    }
193
194    pub fn current(&self) -> Option<RustcVersion> {
195        self.stack.last().copied()
196    }
197
198    pub fn meets(&self, required: RustcVersion) -> bool {
199        self.current().is_none_or(|msrv| msrv >= required)
200    }
201
202    pub fn check_attributes(&mut self, sess: &Session, attrs: &[Attribute]) {
203        if let Some(version) = parse_attrs(sess, attrs) {
204            SEEN_MSRV_ATTR.store(true, Ordering::Relaxed);
205            self.stack.push(version);
206        }
207    }
208
209    pub fn check_attributes_post(&mut self, sess: &Session, attrs: &[Attribute]) {
210        if parse_attrs(sess, attrs).is_some() {
211            self.stack.pop();
212        }
213    }
214}
215
216fn parse_attrs(sess: &Session, attrs: &[impl AttributeExt]) -> Option<RustcVersion> {
217    let mut msrv_attrs = attrs.iter().filter(|attr| attr.path_matches(&[sym::clippy, sym::msrv]));
218
219    let msrv_attr = msrv_attrs.next()?;
220
221    if let Some(duplicate) = msrv_attrs.next_back() {
222        sess.dcx()
223            .struct_span_err(duplicate.span(), "`clippy::msrv` is defined multiple times")
224            .with_span_note(msrv_attr.span(), "first definition found here")
225            .emit();
226    }
227
228    let Some(msrv) = msrv_attr.value_str() else {
229        sess.dcx().span_err(msrv_attr.span(), "bad clippy attribute");
230        return None;
231    };
232
233    let Some(version) = parse_version(msrv) else {
234        sess.dcx()
235            .span_err(msrv_attr.span(), format!("`{msrv}` is not a valid Rust version"));
236        return None;
237    };
238
239    Some(version)
240}