Skip to main content

clippy_utils/
msrvs.rs

1use crate::{is_in_const_context, sym};
2use rustc_ast::Attribute;
3use rustc_ast::attr::AttributeExt;
4use rustc_attr_ir::{RustcVersion, StabilityLevel, StableSince};
5use rustc_attr_parsing::parse_version;
6use rustc_data_structures::smallvec::SmallVec;
7use rustc_hir::def::DefKind;
8use rustc_hir::def_id::DefId;
9use rustc_hir::{Constness, HirId};
10use rustc_lint::LateContext;
11use rustc_middle::ty::TyCtxt;
12use rustc_session::Session;
13use std::iter::once;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16macro_rules! msrv_aliases {
17    ($($major:literal,$minor:literal,$patch:literal {
18        $($name:ident),* $(,)?
19    })*) => {
20        $($(
21        pub const $name: RustcVersion = RustcVersion { major: $major, minor :$minor, patch: $patch };
22        )*)*
23    };
24}
25
26// names may refer to stabilized feature flags or library items
27msrv_aliases! {
28    1,99,0 { BOX_INTO_NON_NULL }
29    1,98,0 { MAP_OR_DEFAULT }
30    1,97,0 { ISOLATE_LOWEST_ONE, BIT_WIDTH }
31    1,94,0 { EULER_GAMMA, GOLDEN_RATIO, MUL_ADD_CONST }
32    1,93,0 { VEC_DEQUE_POP_BACK_IF, VEC_DEQUE_POP_FRONT_IF }
33    1,91,0 { DURATION_FROM_MINUTES_HOURS }
34    1,89,0 { NONNULL_FROM_MUT }
35    1,88,0 { LET_CHAINS, AS_CHUNKS, RAW_PTR_DEFAULT }
36    1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF, INTEGER_SIGN_CAST }
37    1,86,0 { VEC_POP_IF }
38    1,85,0 {
39        UINT_FLOAT_MIDPOINT, CONST_SIZE_OF_VAL, WAKER_NOOP, ABS_CONST, RADIANS_CONST, CONST_MEM_SWAP, CONST_PTR_SWAP,
40    }
41    1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR }
42    1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
43    1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP, SPECIALIZED_TO_STRING_FOR_REFS }
44    1,81,0 { LINT_REASONS_STABILIZATION, ERROR_IN_CORE, EXPLICIT_SELF_TYPE_ELISION, DURATION_ABS_DIFF }
45    1,80,0 { BOX_INTO_ITER, LAZY_CELL }
46    1,79,0 { CONST_BLOCKS, CSTR_COUNT_BYTES }
47    1,77,0 { C_STR_LITERALS }
48    1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
49    1,75,0 { OPTION_AS_SLICE }
50    1,74,0 { REPR_RUST, IO_ERROR_OTHER }
51    1,73,0 { DIV_CEIL }
52    1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }
53    1,70,0 { OPTION_RESULT_IS_VARIANT_AND, BINARY_HEAP_RETAIN }
54    1,68,0 { PATH_MAIN_SEPARATOR_STR }
55    1,67,0 { ILOG2 }
56    1,65,0 { LET_ELSE, POINTER_CAST_CONSTNESS }
57    1,63,0 { CLONE_INTO, CONST_SLICE_FROM_REF }
58    1,62,0 { BOOL_THEN_SOME, DEFAULT_ENUM_ATTRIBUTE, CONST_EXTERN_C_FN }
59    1,61,0 { CONST_FN_TRAIT_BOUND }
60    1,60,0 { ABS_DIFF }
61    1,59,0 { THREAD_LOCAL_CONST_INIT }
62    1,58,0 { FORMAT_ARGS_CAPTURE, PATTERN_TRAIT_CHAR_ARRAY, CONST_RAW_PTR_DEREF }
63    1,57,0 { MAP_WHILE, CONST_PANIC }
64    1,56,0 { CONST_FN_UNION }
65    1,55,0 { SEEK_REWIND }
66    1,54,0 { INTO_KEYS }
67    1,53,0 { OR_PATTERNS, INTEGER_BITS, BTREE_MAP_RETAIN, BTREE_SET_RETAIN, ARRAY_INTO_ITERATOR }
68    1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST }
69    1,51,0 { BORROW_AS_PTR, SEEK_FROM_CURRENT, UNSIGNED_ABS }
70    1,50,0 { BOOL_THEN, CLAMP, SLICE_FILL }
71    1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST }
72    1,46,0 { CONST_IF_MATCH, OPTION_ZIP }
73    1,45,0 { STR_STRIP_PREFIX }
74    1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS }
75    1,42,0 { MATCHES_MACRO, SLICE_PATTERNS, PTR_SLICE_RAW_PARTS }
76    1,41,0 { RE_REBALANCING_COHERENCE, RESULT_MAP_OR, RESULT_MAP_OR_ELSE }
77    1,40,0 { MEM_TAKE, NON_EXHAUSTIVE, OPTION_AS_DEREF }
78    1,38,0 { POINTER_CAST, REM_EUCLID }
79    1,37,0 { TYPE_ALIAS_ENUM_VARIANTS }
80    1,36,0 { ITERATOR_COPIED }
81    1,35,0 { OPTION_COPIED, RANGE_CONTAINS }
82    1,34,0 { TRY_FROM }
83    1,33,0 { UNDERSCORE_IMPORTS }
84    1,32,0 { CONST_IS_POWER_OF_TWO, CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS }
85    1,31,0 { OPTION_REPLACE }
86    1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
87    1,29,0 { ITER_FLATTEN }
88    1,28,0 { FROM_BOOL, REPEAT_WITH, SLICE_FROM_REF }
89    1,27,0 { ITERATOR_TRY_FOLD, DOUBLE_ENDED_ITERATOR_RFIND, DURATION_FROM_NANOS_MICROS }
90    1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN, POINTER_ADD_SUB_METHODS, BOX_LEAK }
91    1,24,0 { IS_ASCII_DIGIT, PTR_NULL }
92    1,18,0 { HASH_MAP_RETAIN, HASH_SET_RETAIN }
93    1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
94    1,16,0 { STR_REPEAT, RESULT_UNWRAP_OR_DEFAULT }
95    1,15,0 { MAYBE_BOUND_IN_WHERE }
96    1,13,0 { QUESTION_MARK_OPERATOR }
97    1,6,0 { CORE_PTR_SWAP }
98    1,3,0 { DURATION_FROM_MILLIS_SECS }
99}
100
101/// `#[clippy::msrv]` attributes are rarely used outside of Clippy's test suite, as a basic
102/// optimization we can skip traversing the HIR in [`Msrv::meets`] if we never saw an MSRV attribute
103/// during the early lint passes
104static SEEN_MSRV_ATTR: AtomicBool = AtomicBool::new(false);
105
106/// Tracks the current MSRV from `clippy.toml`, `Cargo.toml` or set via `#[clippy::msrv]` in late
107/// lint passes, use [`MsrvStack`] for early passes
108#[derive(Copy, Clone, Debug, Default)]
109pub struct Msrv(Option<RustcVersion>);
110
111impl From<Option<RustcVersion>> for Msrv {
112    #[inline]
113    fn from(value: Option<RustcVersion>) -> Self {
114        Self(value)
115    }
116}
117
118impl Msrv {
119    /// Returns the MSRV at the current node
120    ///
121    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
122    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
123    pub fn current(self, cx: &LateContext<'_>) -> Option<RustcVersion> {
124        if SEEN_MSRV_ATTR.load(Ordering::Relaxed) {
125            self.for_attrs(cx.tcx, cx.last_node_with_lint_attrs)
126        } else {
127            self.0
128        }
129    }
130
131    /// Returns the MSRV at the specified node
132    ///
133    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
134    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
135    pub fn at(self, tcx: TyCtxt<'_>, node: HirId) -> Option<RustcVersion> {
136        if SEEN_MSRV_ATTR.load(Ordering::Relaxed) {
137            self.for_attrs(tcx, node)
138        } else {
139            self.0
140        }
141    }
142
143    fn for_attrs(self, tcx: TyCtxt<'_>, node: HirId) -> Option<RustcVersion> {
144        once(node)
145            .chain(tcx.hir_parent_id_iter(node))
146            .find_map(|id| parse_attrs(tcx.hir_attrs(id)))
147            .or(self.0)
148    }
149
150    /// Checks if a required version from [this module](self) is met at the current node
151    ///
152    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
153    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
154    pub fn meets(self, cx: &LateContext<'_>, required: RustcVersion) -> bool {
155        self.current(cx).is_none_or(|msrv| msrv >= required)
156    }
157    /// Checks if a required version from [this module](self) is met at the specified node
158    ///
159    /// If the crate being linted uses an `#[clippy::msrv]` attribute this will search the parent
160    /// nodes for that attribute, prefer to run this check after cheaper pattern matching operations
161    pub fn meets_at(self, tcx: TyCtxt<'_>, node: HirId, required: RustcVersion) -> bool {
162        self.at(tcx, node).is_none_or(|msrv| msrv >= required)
163    }
164
165    pub fn is_stable(self, cx: &LateContext<'_>, def_id: DefId) -> bool {
166        self.stability_met(cx, cx.tcx.lookup_stability(def_id).map(|stability| stability.level))
167    }
168
169    /// Checks whether `def_id` is `const` and const-stable since a version met by the MSRV.
170    ///
171    /// `def_id` must identify a function-like definition or an impl.
172    ///
173    /// Nothing in the crate being linted carries a const-stability attribute, so `const` fns and
174    /// impls defined there are treated as meeting any MSRV, mirroring
175    /// [`is_stable`](Self::is_stable).
176    pub fn is_const_stable(self, cx: &LateContext<'_>, def_id: DefId) -> bool {
177        let constness = match cx.tcx.def_kind(def_id) {
178            // The constness of a trait impl is not encoded in crate metadata, where `constness`
179            // would decode as its default of `Const`. It is only available from the impl header.
180            DefKind::Impl { of_trait: true } => cx.tcx.impl_trait_header(def_id).constness,
181            _ => cx.tcx.constness(def_id),
182        };
183
184        matches!(constness, Constness::Const { .. })
185            && self.stability_met(
186                cx,
187                cx.tcx.lookup_const_stability(def_id).map(|stability| stability.level),
188            )
189    }
190
191    /// Checks the stability relevant to where we are: const-stability inside a `const` context,
192    /// regular stability everywhere else.
193    ///
194    /// Like [`is_in_const_context`], this requires the `LateContext` to have an enclosing body.
195    pub fn is_stable_or_const_stable(self, cx: &LateContext<'_>, def_id: DefId) -> bool {
196        if is_in_const_context(cx) {
197            self.is_const_stable(cx, def_id)
198        } else {
199            self.is_stable(cx, def_id)
200        }
201    }
202
203    fn stability_met(self, cx: &LateContext<'_>, level: Option<StabilityLevel>) -> bool {
204        level.is_none_or(|level| {
205            if let StabilityLevel::Stable { since, .. } = level {
206                let version = match since {
207                    StableSince::Version(version) => version,
208                    StableSince::Current => RustcVersion::CURRENT,
209                    StableSince::Err(_) => return false,
210                };
211
212                self.meets(cx, version)
213            } else {
214                // Unstable fn.
215                // FIXME: can we check that the feature is enabled?
216                // Please see https://github.com/rust-lang/rust-clippy/pull/17309#discussion_r3486693263 for false-positive concerns.
217                false
218            }
219        })
220    }
221}
222
223/// Tracks the current MSRV from `clippy.toml`, `Cargo.toml` or set via `#[clippy::msrv]` in early
224/// lint passes, use [`Msrv`] for late passes
225#[derive(Debug, Clone)]
226pub struct MsrvStack {
227    stack: SmallVec<[RustcVersion; 2]>,
228}
229
230impl From<Option<RustcVersion>> for MsrvStack {
231    #[inline]
232    fn from(value: Option<RustcVersion>) -> Self {
233        Self {
234            stack: SmallVec::from_iter(value),
235        }
236    }
237}
238
239impl MsrvStack {
240    pub fn current(&self) -> Option<RustcVersion> {
241        self.stack.last().copied()
242    }
243
244    pub fn meets(&self, required: RustcVersion) -> bool {
245        self.current().is_none_or(|msrv| msrv >= required)
246    }
247
248    pub fn check_attributes(&mut self, attrs: &[Attribute]) {
249        if let Some(version) = parse_attrs(attrs) {
250            SEEN_MSRV_ATTR.store(true, Ordering::Relaxed);
251            self.stack.push(version);
252        }
253    }
254
255    pub fn check_attributes_post(&mut self, attrs: &[Attribute]) {
256        if parse_attrs(attrs).is_some() {
257            self.stack.pop();
258        }
259    }
260}
261
262fn parse_attrs(attrs: &[impl AttributeExt]) -> Option<RustcVersion> {
263    let msrv_attr = attrs.iter().find(|attr| attr.path_matches(&[sym::clippy, sym::msrv]))?;
264
265    let msrv = msrv_attr.value_str()?;
266
267    parse_version(msrv)
268}
269
270pub fn check_attrs(sess: &Session, attrs: &[impl AttributeExt]) {
271    let mut msrv_attrs = attrs.iter().filter(|attr| attr.path_matches(&[sym::clippy, sym::msrv]));
272
273    let Some(msrv_attr) = msrv_attrs.next() else {
274        return;
275    };
276
277    if let Some(duplicate) = msrv_attrs.next_back() {
278        sess.dcx()
279            .struct_span_err(duplicate.span(), "`clippy::msrv` is defined multiple times")
280            .with_span_note(msrv_attr.span(), "first definition found here")
281            .emit();
282    }
283
284    let Some(msrv) = msrv_attr.value_str() else {
285        sess.dcx().span_err(msrv_attr.span(), "bad clippy attribute");
286        return;
287    };
288
289    if parse_version(msrv).is_none() {
290        sess.dcx()
291            .span_err(msrv_attr.span(), format!("`{msrv}` is not a valid Rust version"));
292    }
293}