Skip to main content

clippy_utils/
disallowed_profiles.rs

1use crate::sym;
2use rustc_ast::ast::{LitKind, MetaItemInner};
3use rustc_data_structures::fx::FxHashMap;
4use rustc_data_structures::smallvec::SmallVec;
5use rustc_hir::{Attribute, HirId};
6use rustc_lint::LateContext;
7use rustc_span::{Span, Symbol};
8
9/// One profile name referenced by a `#[clippy::disallowed_profile(...)]` or
10/// `#[clippy::disallowed_profiles(...)]` attribute on an item.
11///
12/// A single attribute produces one `ProfileEntry` per string argument. The entry records which
13/// attribute variant introduced it, the profile name, and the span of that string literal so
14/// diagnostics (e.g. "unknown profile") can point at the exact argument.
15#[derive(Copy, Clone)]
16pub struct ProfileEntry {
17    pub attr_name: Symbol,
18    pub name: Symbol,
19    pub span: Span,
20}
21
22/// The set of profiles active at some `HirId`, obtained by walking up the HIR from that id and
23/// collecting the first ancestor that carries a `#[clippy::disallowed_profile(s)]` attribute.
24///
25/// An empty selection is represented by `None` at the call site; a `ProfileSelection` is always
26/// non-empty.
27#[derive(Clone)]
28pub struct ProfileSelection {
29    entries: SmallVec<[ProfileEntry; 2]>,
30}
31
32impl ProfileSelection {
33    pub fn new(entries: SmallVec<[ProfileEntry; 2]>) -> Self {
34        Self { entries }
35    }
36
37    pub fn is_empty(&self) -> bool {
38        self.entries.is_empty()
39    }
40
41    pub fn iter(&self) -> impl Iterator<Item = &ProfileEntry> {
42        self.entries.iter()
43    }
44}
45
46#[derive(Default)]
47pub struct ProfileResolver {
48    cache: FxHashMap<HirId, Option<ProfileSelection>>,
49}
50
51impl ProfileResolver {
52    pub fn active_profiles(&mut self, cx: &LateContext<'_>, hir_id: HirId) -> Option<&ProfileSelection> {
53        // NOTE: The `contains_key`+`get` dance is intentional: using only `get` here triggers borrowck
54        // errors because we need to mutate `self.cache` on cache misses.
55        if self.cache.contains_key(&hir_id) {
56            return self.cache.get(&hir_id).and_then(|selection| selection.as_ref());
57        }
58
59        let (resolved, visited) = self.resolve(cx, hir_id);
60
61        for id in visited {
62            self.cache.entry(id).or_insert_with(|| resolved.clone());
63        }
64        self.cache.insert(hir_id, resolved);
65
66        self.cache.get(&hir_id).and_then(|selection| selection.as_ref())
67    }
68
69    fn resolve(&self, cx: &LateContext<'_>, start: HirId) -> (Option<ProfileSelection>, SmallVec<[HirId; 8]>) {
70        let mut visited = SmallVec::<[HirId; 8]>::new();
71        let mut current = Some(start);
72
73        while let Some(id) = current {
74            if let Some(cached) = self.cache.get(&id) {
75                return (cached.clone(), visited);
76            }
77
78            visited.push(id);
79
80            if let Some(selection) = profiles_from_attrs(cx, cx.tcx.hir_attrs(id)) {
81                return (Some(selection), visited);
82            }
83
84            if id == rustc_hir::CRATE_HIR_ID {
85                current = None;
86            } else {
87                current = Some(cx.tcx.parent_hir_id(id));
88            }
89        }
90
91        (None, visited)
92    }
93}
94
95fn profiles_from_attrs(cx: &LateContext<'_>, attrs: &[Attribute]) -> Option<ProfileSelection> {
96    let mut entries = SmallVec::<[ProfileEntry; 2]>::new();
97
98    for attr in attrs {
99        let path = attr.path();
100        if path.len() != 2 || path[0] != sym::clippy {
101            continue;
102        }
103
104        let name = path[1];
105        if name != sym::disallowed_profile && name != sym::disallowed_profiles {
106            continue;
107        }
108
109        let attr_label = if name == sym::disallowed_profiles {
110            "`clippy::disallowed_profiles`"
111        } else {
112            "`clippy::disallowed_profile`"
113        };
114
115        let Some(items) = attr.meta_item_list() else {
116            cx.tcx
117                .sess
118                .dcx()
119                .struct_span_err(attr.span(), format!("{attr_label} expects string arguments"))
120                .emit();
121            continue;
122        };
123
124        if items.is_empty() {
125            cx.tcx
126                .sess
127                .dcx()
128                .struct_span_err(attr.span(), format!("{attr_label} expects at least one profile name"))
129                .emit();
130            continue;
131        }
132
133        if name == sym::disallowed_profile && items.len() != 1 {
134            cx.tcx
135                .sess
136                .dcx()
137                .struct_span_err(attr.span(), "use `clippy::disallowed_profiles` for multiple profiles")
138                .emit();
139        }
140
141        for item in items {
142            match literal_symbol(&item) {
143                Some((symbol, span)) => entries.push(ProfileEntry {
144                    attr_name: name,
145                    name: symbol,
146                    span,
147                }),
148                None => emit_string_error(cx, &item),
149            }
150        }
151    }
152
153    if entries.is_empty() {
154        None
155    } else {
156        Some(ProfileSelection::new(entries))
157    }
158}
159
160fn literal_symbol(item: &MetaItemInner) -> Option<(Symbol, Span)> {
161    match item {
162        MetaItemInner::Lit(lit) => {
163            let LitKind::Str(symbol, _) = lit.kind else { return None };
164            Some((symbol, lit.span))
165        },
166        MetaItemInner::MetaItem(_) => None,
167    }
168}
169
170fn emit_string_error(cx: &LateContext<'_>, item: &MetaItemInner) {
171    let span = match item {
172        MetaItemInner::Lit(lit) => lit.span,
173        MetaItemInner::MetaItem(meta) => meta.span,
174    };
175    cx.tcx
176        .sess
177        .dcx()
178        .struct_span_err(span, "expected string literal profile name")
179        .emit();
180}