Skip to main content

clippy_utils/
attrs.rs

1//! Utility functions for attributes, including Clippy's built-in ones
2
3use crate::source::SpanExt as _;
4use crate::{sym, tokenize_with_text};
5use rustc_ast::attr::AttributeExt;
6use rustc_attr_ir::{AttrArgs, find_attr};
7use rustc_errors::Applicability;
8use rustc_hir::def_id::DefId;
9use rustc_lexer::TokenKind;
10use rustc_lint::LateContext;
11use rustc_middle::ty::{AdtDef, TyCtxt};
12use rustc_session::Session;
13use rustc_span::{Span, Symbol};
14use std::str::FromStr as _;
15
16/// Validates a single clippy attribute and emits errors for unknown or deprecated ones.
17pub fn check_clippy_attr<A: AttributeExt>(sess: &Session, attr: &A) {
18    if let [clippy, segment2] = &*attr.path()
19        && *clippy == sym::clippy
20    {
21        let path_span = attr
22            .path_span()
23            .expect("Clippy attributes are tool attributes, so they have a span");
24
25        match *segment2 {
26            sym::cyclomatic_complexity => {
27                sess.dcx()
28                    .struct_span_err(path_span, "usage of deprecated attribute")
29                    .with_span_suggestion(
30                        path_span,
31                        "consider using",
32                        "clippy::cognitive_complexity",
33                        Applicability::MachineApplicable,
34                    )
35                    .emit();
36            },
37            sym::author
38            | sym::version
39            | sym::cognitive_complexity
40            | sym::dump
41            | sym::msrv
42            | sym::has_significant_drop
43            | sym::format_args => {},
44            _ => {
45                sess.dcx().span_err(path_span, "usage of unknown attribute");
46            },
47        }
48    }
49}
50
51/// Retrieves the span of a Clippy attribute, such as `#[clippy::format_args]`.
52///
53/// Panics if used on an attribute that isn't a tool attribute, so always check
54/// for that first with e.g. `path` or `path_matches`.
55pub fn span_of_clippy_attr(attr: &(impl AttributeExt + 'static)) -> Span {
56    let a: &dyn core::any::Any = attr;
57    if let Some(a) = a.downcast_ref::<rustc_ast::ast::Attribute>() {
58        a.span
59    } else if let Some(rustc_attr_ir::Attribute::Tool(u)) = a.downcast_ref::<rustc_attr_ir::Attribute>() {
60        u.span
61    } else {
62        panic!("attribute {attr:?} is not a clippy attribute")
63    }
64}
65
66/// Returns the first occurrence of `#[clippy::<name>]`.
67///
68/// Additionally, raises an error for duplicates of this attribute.
69pub fn get_clippy_attr(tcx: TyCtxt<'_>, did: impl Into<DefId>, name: Symbol) -> Option<&rustc_attr_ir::AttrItem> {
70    let attr_name = [sym::clippy, name];
71    let mut attrs = tcx.tool_attrs(did, &attr_name);
72    let first = attrs.next()?;
73
74    //FIXME(mejrs): this should really be checked during validation
75    for attr in attrs {
76        tcx.dcx()
77            .struct_span_err(attr.span, format!("`{name}` is defined multiple times"))
78            .with_span_note(first.span, "first definition found here")
79            .emit();
80    }
81
82    Some(first)
83}
84
85/// Checks whether `attrs` contain any of `proc_macro`, `proc_macro_derive` or
86/// `proc_macro_attribute`
87pub fn is_proc_macro(attrs: &[impl AttributeExt]) -> bool {
88    attrs.iter().any(AttributeExt::is_proc_macro_attr)
89}
90
91/// Checks whether `attrs` contain `#[doc(hidden)]`
92pub fn is_doc_hidden(attrs: &[rustc_attr_ir::Attribute]) -> bool {
93    find_attr!(
94        attrs,
95        Doc(rustc_attr_ir::DocAttribute { hidden: Some(_), .. })
96    )
97}
98
99/// Checks whether the given ADT, or any of its fields/variants, are marked as `#[non_exhaustive]`
100pub fn has_non_exhaustive_attr(tcx: TyCtxt<'_>, adt: AdtDef<'_>) -> bool {
101    adt.is_variant_list_non_exhaustive()
102        || find_attr!(tcx, adt.did(), NonExhaustive(..))
103        || adt.variants().iter().any(|variant_def| {
104            variant_def.is_field_list_non_exhaustive() || find_attr!(tcx, variant_def.def_id, NonExhaustive(..))
105        })
106        || adt
107            .all_fields()
108            .any(|field_def| find_attr!(tcx, field_def.did, NonExhaustive(..)))
109}
110
111fn span_contains_attr_idents(cx: &LateContext<'_>, s: Span, idents: &[&str]) -> bool {
112    s.check_text(cx, |src| {
113        // PERF: Attributes need a literal `#`, so skip the lexer when the source has none.
114        if !src.contains('#') {
115            return false;
116        }
117
118        let mut iter = tokenize_with_text(src);
119
120        // Search for the token sequence [`#`, `[`, <ident>]
121        while iter.any(|(t, ..)| matches!(t, TokenKind::Pound)) {
122            let mut iter = iter.by_ref().skip_while(|(t, ..)| {
123                matches!(
124                    t,
125                    TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. }
126                )
127            });
128            if matches!(iter.next(), Some((TokenKind::OpenBracket, ..)))
129                && matches!(iter.next(), Some((TokenKind::Ident, name, _)) if idents.contains(&name))
130            {
131                return true;
132            }
133        }
134        false
135    })
136}
137
138/// Checks whether the given span contains a `#[cfg(..)]` attribute
139pub fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool {
140    span_contains_attr_idents(cx, s, &["cfg"])
141}
142
143/// Checks whether the given span contains a `#[cfg(..)]` or `#[cfg_attr(..)]` attribute
144pub fn span_contains_cfg_or_cfg_attr(cx: &LateContext<'_>, s: Span) -> bool {
145    span_contains_attr_idents(cx, s, &["cfg", "cfg_attr"])
146}
147
148/// Currently used to keep track of the current value of `#[clippy::cognitive_complexity(N)]`
149pub struct LimitStack {
150    default: u64,
151    stack: Vec<u64>,
152}
153
154impl Drop for LimitStack {
155    fn drop(&mut self) {
156        debug_assert_eq!(self.stack, Vec::<u64>::new()); // avoid `.is_empty()`, for a nicer error message
157    }
158}
159
160#[expect(missing_docs, reason = "they're all trivial...")]
161impl LimitStack {
162    #[must_use]
163    /// Initialize the stack starting with a default value, which usually comes from configuration
164    pub fn new(limit: u64) -> Self {
165        Self {
166            default: limit,
167            stack: vec![],
168        }
169    }
170    pub fn limit(&self) -> u64 {
171        self.stack.last().copied().unwrap_or(self.default)
172    }
173    pub fn push_attrs(&mut self, sess: &Session, attrs: &[rustc_attr_ir::Attribute], name: Symbol) {
174        let stack = &mut self.stack;
175        parse_attrs(sess, attrs, name, |val| stack.push(val));
176    }
177    pub fn pop_attrs(&mut self, sess: &Session, attrs: &[rustc_attr_ir::Attribute], name: Symbol) {
178        let stack = &mut self.stack;
179        parse_attrs(sess, attrs, name, |val| {
180            let popped = stack.pop();
181            debug_assert_eq!(popped, Some(val));
182        });
183    }
184}
185
186fn parse_attrs<F: FnMut(u64)>(sess: &Session, attrs: &[rustc_attr_ir::Attribute], name: Symbol, mut f: F) {
187    let attrs = attrs.iter().filter_map(move |attr| {
188        if let rustc_attr_ir::Attribute::Tool(t) = attr
189            && *t.path.segments == [sym::clippy, name]
190        {
191            Some(&**t)
192        } else {
193            None
194        }
195    });
196    for attr in attrs {
197        let AttrArgs::Eq { expr, .. } = attr.args else {
198            sess.dcx().span_err(attr.span, "bad clippy attribute");
199            continue;
200        };
201        let Some(value) = expr.value_as_str() else {
202            sess.dcx().span_err(attr.span, "not a number");
203            continue;
204        };
205        let Ok(value) = u64::from_str(value.as_str()) else {
206            sess.dcx().span_err(attr.span, "not a number");
207            continue;
208        };
209        f(value);
210    }
211}