Skip to main content

hir_expand/
attrs.rs

1//! Defines the basics of attributes lowering.
2//!
3//! The heart and soul of this module is [`expand_cfg_attr()`], alongside its sibling
4//! [`expand_cfg_attr_with_doc_comments()`]. It is used to implement all attribute lowering
5//! in r-a. Its basic job is to list attributes; however, attributes do not necessarily map
6//! into [`ast::Attr`], because `cfg_attr` can map to zero, one, or more attributes
7//! (`#[cfg_attr(predicate, attr1, attr2, ...)]`). [`expand_cfg_attr()`] expands `cfg_attr`s
8//! as it goes (as its name implies), to list all attributes.
9//!
10//! Another thing to note is that we need to be able to map an attribute back to a range
11//! (for diagnostic purposes etc.). This is only ever needed for attributes that participate
12//! in name resolution. An attribute is mapped back by its [`AttrId`], which is just an
13//! index into the item tree attributes list. To minimize the risk of bugs, we have one
14//! place (here) and one function ([`is_item_tree_filtered_attr()`]) that decides whether
15//! an attribute participate in name resolution.
16
17use std::{borrow::Cow, cell::OnceCell, convert::Infallible, fmt, ops::ControlFlow};
18
19use ::tt::TextRange;
20use base_db::{Crate, SourceDatabase};
21use cfg::{CfgExpr, CfgOptions};
22use either::Either;
23use intern::Interned;
24use itertools::Itertools;
25use mbe::{DelimiterKind, Punct};
26use smallvec::SmallVec;
27use span::{RealSpanMap, Span, SyntaxContext};
28use syntax::{AstNode, SmolStr, ast, unescape};
29use syntax_bridge::DocCommentDesugarMode;
30
31use crate::{
32    AstId,
33    mod_path::ModPath,
34    span_map::SpanMap,
35    tt::{self, TopSubtree},
36};
37
38pub trait AstPathExt {
39    fn is1(&self, segment: &str) -> bool;
40
41    fn as_one_segment(&self) -> Option<SmolStr>;
42
43    fn as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)>;
44}
45
46impl AstPathExt for ast::Path {
47    fn is1(&self, segment: &str) -> bool {
48        self.as_one_segment().is_some_and(|it| it == segment)
49    }
50
51    fn as_one_segment(&self) -> Option<SmolStr> {
52        Some(self.as_single_name_ref()?.text().into())
53    }
54
55    fn as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)> {
56        let parent = self.qualifier().as_one_segment();
57        let this = self.segment()?.name_ref()?.text().into();
58        if let Some(parent) = parent { Some((parent, Some(this))) } else { Some((this, None)) }
59    }
60}
61
62impl AstPathExt for Option<ast::Path> {
63    fn is1(&self, segment: &str) -> bool {
64        self.as_ref().is_some_and(|it| it.is1(segment))
65    }
66
67    fn as_one_segment(&self) -> Option<SmolStr> {
68        self.as_ref().and_then(|it| it.as_one_segment())
69    }
70
71    fn as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)> {
72        self.as_ref().and_then(|it| it.as_up_to_two_segment())
73    }
74}
75
76pub trait AstKeyValueMetaExt {
77    fn value_string(&self) -> Option<SmolStr>;
78}
79
80impl AstKeyValueMetaExt for ast::KeyValueMeta {
81    fn value_string(&self) -> Option<SmolStr> {
82        if let Some(ast::Expr::Literal(value)) = self.expr()
83            && let ast::LiteralKind::String(value) = value.kind()
84            && let Ok(value) = value.value()
85        {
86            Some((*value).into())
87        } else {
88            None
89        }
90    }
91}
92
93/// The callback is passed the attribute and the outermost `ast::Attr`.
94/// Note that one node may map to multiple [`ast::Meta`]s due to `cfg_attr`.
95///
96/// `unsafe(attr)` are passed the inner attribute for now.
97#[inline]
98pub fn expand_cfg_attr<'a, BreakValue>(
99    attrs: impl Iterator<Item = ast::Attr>,
100    cfg_options: impl FnMut() -> &'a CfgOptions,
101    mut callback: impl FnMut(ast::Meta, ast::Attr) -> ControlFlow<BreakValue>,
102) -> Option<BreakValue> {
103    expand_cfg_attr_with_doc_comments::<Infallible, _>(
104        attrs.map(Either::Left),
105        cfg_options,
106        move |Either::Left((meta, top_attr))| callback(meta, top_attr),
107    )
108}
109
110#[inline]
111pub fn expand_cfg_attr_with_doc_comments<'a, DocComment, BreakValue>(
112    mut attrs: impl Iterator<Item = Either<ast::Attr, DocComment>>,
113    mut cfg_options: impl FnMut() -> &'a CfgOptions,
114    mut callback: impl FnMut(Either<(ast::Meta, ast::Attr), DocComment>) -> ControlFlow<BreakValue>,
115) -> Option<BreakValue> {
116    let mut stack = SmallVec::<[_; 1]>::new();
117    loop {
118        let (mut meta, top_attr) = if let Some(it) = stack.pop() {
119            it
120        } else {
121            let attr = attrs.next()?;
122            match attr {
123                Either::Left(attr) => {
124                    let Some(meta) = attr.meta() else { continue };
125                    stack.push((meta, attr));
126                }
127                Either::Right(doc_comment) => {
128                    if let ControlFlow::Break(break_value) = callback(Either::Right(doc_comment)) {
129                        return Some(break_value);
130                    }
131                }
132            }
133            continue;
134        };
135
136        while let ast::Meta::UnsafeMeta(unsafe_meta) = &meta {
137            let Some(inner) = unsafe_meta.meta() else { continue };
138            meta = inner;
139        }
140
141        if let ast::Meta::CfgAttrMeta(meta) = meta {
142            let Some(cfg_predicate) = meta.cfg_predicate() else { continue };
143            let cfg_predicate = CfgExpr::parse_from_ast(cfg_predicate);
144            if cfg_options().check(&cfg_predicate) != Some(false) {
145                let prev_stack_len = stack.len();
146                stack.extend(meta.metas().map(|meta| (meta, top_attr.clone())));
147                stack[prev_stack_len..].reverse();
148            }
149        } else {
150            if let ControlFlow::Break(break_value) = callback(Either::Left((meta, top_attr))) {
151                return Some(break_value);
152            }
153        }
154    }
155}
156
157#[inline]
158pub(crate) fn is_item_tree_filtered_attr(name: &str) -> bool {
159    matches!(
160        name,
161        "doc"
162            | "stable"
163            | "unstable"
164            | "target_feature"
165            | "allow"
166            | "expect"
167            | "warn"
168            | "deny"
169            | "forbid"
170            | "repr"
171            | "inline"
172            | "track_caller"
173            | "must_use"
174    )
175}
176
177/// This collects attributes exactly as the item tree needs them. This is used for the item tree,
178/// as well as for resolving [`AttrId`]s.
179pub fn collect_item_tree_attrs<'a, BreakValue>(
180    owner: &dyn ast::HasAttrs,
181    cfg_options: impl Fn() -> &'a CfgOptions,
182    mut on_attr: impl FnMut(ast::Meta, ast::Attr) -> ControlFlow<BreakValue>,
183) -> Option<Either<BreakValue, CfgExpr>> {
184    let attrs = ast::attrs_including_inner(owner);
185    expand_cfg_attr(
186        attrs,
187        || cfg_options(),
188        |attr, top_attr| {
189            // We filter builtin attributes that we don't need for nameres, because this saves memory.
190            // I only put the most common attributes, but if some attribute becomes common feel free to add it.
191            // Notice, however: for an attribute to be filtered out, it *must* not be shadowable with a macro!
192            let filter = match &attr {
193                ast::Meta::CfgMeta(attr) => {
194                    let Some(cfg_predicate) = attr.cfg_predicate() else {
195                        return ControlFlow::Continue(());
196                    };
197                    let cfg = CfgExpr::parse_from_ast(cfg_predicate);
198                    if cfg_options().check(&cfg) == Some(false) {
199                        return ControlFlow::Break(Either::Right(cfg));
200                    }
201                    true
202                }
203                _ => attr
204                    .path()
205                    .and_then(|path| path.as_one_segment())
206                    .is_some_and(|segment| is_item_tree_filtered_attr(&segment)),
207            };
208            if !filter && let ControlFlow::Break(v) = on_attr(attr, top_attr) {
209                return ControlFlow::Break(Either::Left(v));
210            }
211            ControlFlow::Continue(())
212        },
213    )
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct Attr {
218    pub path: Interned<ModPath>,
219    pub input: Option<Box<AttrInput>>,
220    pub ctxt: SyntaxContext,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
224pub enum AttrInput {
225    /// `#[attr = "string"]`
226    Literal(tt::Literal),
227    /// `#[attr(subtree)]`
228    TokenTree(tt::TopSubtree),
229}
230
231impl fmt::Display for AttrInput {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        match self {
234            AttrInput::Literal(lit) => write!(f, " = {lit}"),
235            AttrInput::TokenTree(tt) => tt.fmt(f),
236        }
237    }
238}
239
240impl Attr {
241    /// #[path = "string"]
242    pub fn string_value(&self) -> Option<&str> {
243        match self.input.as_deref()? {
244            AttrInput::Literal(
245                lit @ tt::Literal { kind: tt::LitKind::Str | tt::LitKind::StrRaw(_), .. },
246            ) => Some(lit.text()),
247            _ => None,
248        }
249    }
250
251    /// #[path = "string"]
252    pub fn string_value_with_span(&self) -> Option<(&str, span::Span)> {
253        match self.input.as_deref()? {
254            AttrInput::Literal(
255                lit @ tt::Literal { kind: tt::LitKind::Str | tt::LitKind::StrRaw(_), span, .. },
256            ) => Some((lit.text(), *span)),
257            _ => None,
258        }
259    }
260
261    pub fn string_value_unescape(&self) -> Option<Cow<'_, str>> {
262        match self.input.as_deref()? {
263            AttrInput::Literal(lit @ tt::Literal { kind: tt::LitKind::StrRaw(_), .. }) => {
264                Some(Cow::Borrowed(lit.text()))
265            }
266            AttrInput::Literal(lit @ tt::Literal { kind: tt::LitKind::Str, .. }) => {
267                unescape(lit.text())
268            }
269            _ => None,
270        }
271    }
272
273    /// #[path(ident)]
274    pub fn single_ident_value(&self) -> Option<tt::Ident> {
275        match self.input.as_deref()? {
276            AttrInput::TokenTree(tt) => match tt.token_trees().iter().collect_array() {
277                Some([tt::TtElement::Leaf(tt::Leaf::Ident(ident))]) => Some(ident),
278                _ => None,
279            },
280            _ => None,
281        }
282    }
283
284    /// #[path TokenTree]
285    pub fn token_tree_value(&self) -> Option<&TopSubtree> {
286        match self.input.as_deref()? {
287            AttrInput::TokenTree(tt) => Some(tt),
288            _ => None,
289        }
290    }
291
292    /// Parses this attribute as a token tree consisting of comma separated paths.
293    pub fn parse_path_comma_token_tree<'a>(
294        &'a self,
295        db: &'a dyn SourceDatabase,
296    ) -> Option<impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> + 'a> {
297        let args = self.token_tree_value()?;
298
299        if args.top_subtree().delimiter.kind != DelimiterKind::Parenthesis {
300            return None;
301        }
302        Some(parse_path_comma_token_tree(db, args))
303    }
304}
305
306fn parse_path_comma_token_tree<'a>(
307    db: &'a dyn SourceDatabase,
308    args: &'a tt::TopSubtree,
309) -> impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> {
310    args.token_trees()
311        .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. }))))
312        .filter_map(move |tts| {
313            let span = tts.first_span()?;
314            Some((ModPath::from_tt(db, tts)?, span, tts))
315        })
316}
317
318fn unescape(s: &str) -> Option<Cow<'_, str>> {
319    let mut buf = String::new();
320    let mut prev_end = 0;
321    let mut has_error = false;
322    unescape::unescape_str(s, |char_range, unescaped_char| {
323        match (unescaped_char, buf.capacity() == 0) {
324            (Ok(c), false) => buf.push(c),
325            (Ok(_), true) if char_range.len() == 1 && char_range.start == prev_end => {
326                prev_end = char_range.end
327            }
328            (Ok(c), true) => {
329                buf.reserve_exact(s.len());
330                buf.push_str(&s[..prev_end]);
331                buf.push(c);
332            }
333            (Err(_), _) => has_error = true,
334        }
335    });
336
337    match (has_error, buf.capacity() == 0) {
338        (true, _) => None,
339        (false, false) => Some(Cow::Owned(buf)),
340        (false, true) => Some(Cow::Borrowed(s)),
341    }
342}
343
344/// This is an index of an attribute *that always points to the item tree attributes*.
345///
346/// Outer attributes are counted first, then inner attributes. This does not support
347/// out-of-line modules, which may have attributes spread across 2 files!
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
349pub struct AttrId {
350    id: u32,
351}
352
353impl AttrId {
354    #[inline]
355    pub fn from_item_tree_index(id: u32) -> Self {
356        Self { id }
357    }
358
359    #[inline]
360    pub fn item_tree_index(self) -> u32 {
361        self.id
362    }
363
364    /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due
365    /// to `cfg_attr`) and its [`ast::Meta`].
366    pub fn find_attr_range<N: ast::HasAttrs>(
367        self,
368        db: &dyn SourceDatabase,
369        krate: Crate,
370        owner: AstId<N>,
371    ) -> (ast::Attr, ast::Meta) {
372        self.find_attr_range_with_source(db, krate, &owner.to_node(db))
373    }
374
375    /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due
376    /// to `cfg_attr`) and its [`ast::Meta`].
377    ///
378    /// Assumes that the attribute syntax node was present in the
379    /// original file (not speculatively expanded macro output).
380    pub fn find_attr_range_with_source(
381        self,
382        db: &dyn SourceDatabase,
383        krate: Crate,
384        owner: &dyn ast::HasAttrs,
385    ) -> (ast::Attr, ast::Meta) {
386        self.find_attr_range_with_source_opt(db, krate, owner).unwrap_or_else(|| {
387            panic!("used an incorrect `AttrId`; crate={krate:?}, attr_id={self:?}");
388        })
389    }
390
391    /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due
392    /// to `cfg_attr`) and its [`ast::Meta`].
393    pub(crate) fn find_attr_range_with_source_opt(
394        self,
395        db: &dyn SourceDatabase,
396        krate: Crate,
397        owner: &dyn ast::HasAttrs,
398    ) -> Option<(ast::Attr, ast::Meta)> {
399        let cfg_options = OnceCell::new();
400        let mut index = 0;
401        let result = collect_item_tree_attrs(
402            owner,
403            || cfg_options.get_or_init(|| krate.cfg_options(db)),
404            |meta, top_attr| {
405                if index == self.id {
406                    return ControlFlow::Break((top_attr, meta));
407                }
408                index += 1;
409                ControlFlow::Continue(())
410            },
411        );
412        match result {
413            Some(Either::Left(it)) => Some(it),
414            _ => None,
415        }
416    }
417
418    pub fn find_derive_range(
419        self,
420        db: &dyn SourceDatabase,
421        krate: Crate,
422        owner: AstId<ast::Adt>,
423        derive_index: u32,
424    ) -> TextRange {
425        let (_, derive_attr) = self.find_attr_range(db, krate, owner);
426        let ast::Meta::TokenTreeMeta(derive_attr) = derive_attr else {
427            return derive_attr.syntax().text_range();
428        };
429        let Some(tt) = derive_attr.token_tree() else {
430            return derive_attr.syntax().text_range();
431        };
432        // Fake the span map, as we don't really need spans here, just the offsets of the node in the file.
433        let span_map = RealSpanMap::absolute(span::EditionedFileId::current_edition(
434            span::FileId::from_raw(0),
435        ));
436        let tt = syntax_bridge::syntax_node_to_token_tree(
437            tt.syntax(),
438            SpanMap::RealSpanMap(&span_map),
439            span_map.span_for_range(tt.syntax().text_range()),
440            DocCommentDesugarMode::ProcMacro,
441        );
442        let Some((_, _, derive_tts)) =
443            parse_path_comma_token_tree(db, &tt).nth(derive_index as usize)
444        else {
445            return derive_attr.syntax().text_range();
446        };
447        let (Some(first_span), Some(last_span)) = (derive_tts.first_span(), derive_tts.last_span())
448        else {
449            return derive_attr.syntax().text_range();
450        };
451        let start = first_span.range.start();
452        let end = last_span.range.end();
453        TextRange::new(start, end)
454    }
455}