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, ...)]`). To bridge this gap, this module defines
8//! [`Meta`], which represents a desugared attribute. Various bits of r-a need different
9//! things from [`Meta`], therefore it contains many parts. The basic idea is:
10//!
11//!  - There are three kinds of attributes, `path = value`, `path`, and `path(token_tree)`.
12//!  - Most bits of rust-analyzer only need to deal with some paths. Therefore, we keep
13//!    the path only if it has up to 2 segments, or one segment for `path = value`.
14//!    We also only keep the value in `path = value` if it is a literal. However, we always
15//!    save the all relevant ranges of attributes (the path range, and the full attribute range)
16//!    for parts of r-a (e.g. name resolution) that need a faithful representation of the
17//!    attribute.
18//!
19//! [`expand_cfg_attr()`] expands `cfg_attr`s as it goes (as its name implies), to list
20//! all attributes.
21//!
22//! Another thing to note is that we need to be able to map an attribute back to a range
23//! (for diagnostic purposes etc.). This is only ever needed for attributes that participate
24//! in name resolution. An attribute is mapped back by its [`AttrId`], which is just an
25//! index into the item tree attributes list. To minimize the risk of bugs, we have one
26//! place (here) and one function ([`is_item_tree_filtered_attr()`]) that decides whether
27//! an attribute participate in name resolution.
28
29use std::{
30    borrow::Cow, cell::OnceCell, convert::Infallible, fmt, iter::Peekable, ops::ControlFlow,
31};
32
33use ::tt::{TextRange, TextSize};
34use arrayvec::ArrayVec;
35use base_db::Crate;
36use cfg::{CfgExpr, CfgOptions};
37use either::Either;
38use intern::{Interned, Symbol};
39use mbe::{DelimiterKind, Punct};
40use parser::T;
41use smallvec::SmallVec;
42use span::{RealSpanMap, Span, SyntaxContext};
43use syntax::{
44    AstNode, NodeOrToken, SyntaxNode, SyntaxToken,
45    ast::{self, TokenTreeChildren},
46    unescape,
47};
48use syntax_bridge::DocCommentDesugarMode;
49
50use crate::{
51    AstId,
52    db::ExpandDatabase,
53    mod_path::ModPath,
54    span_map::SpanMapRef,
55    tt::{self, TopSubtree},
56};
57
58#[derive(Debug)]
59pub struct AttrPath {
60    /// This can be empty if the path is not of 1 or 2 segments exactly.
61    pub segments: ArrayVec<SyntaxToken, 2>,
62    pub range: TextRange,
63    // FIXME: This shouldn't be textual, `#[test]` needs name resolution.
64    // And if textual, it shouldn't be here, it should be in hir-def/src/attrs.rs. But some macros
65    // fully qualify `test` as `core::prelude::vX::test`, and this is more than 2 segments, so hir-def
66    // attrs can't find it. But this will mean we have to push every up-to-4-segments path, which
67    // may impact perf. So it was easier to just hack it here.
68    pub is_test: bool,
69}
70
71impl AttrPath {
72    #[inline]
73    fn extract(path: &ast::Path) -> Self {
74        let mut is_test = false;
75        let segments = (|| {
76            let mut segments = ArrayVec::new();
77            let segment2 = path.segment()?.name_ref()?.syntax().first_token()?;
78            if segment2.text() == "test" {
79                // `#[test]` or `#[core::prelude::vX::test]`.
80                is_test = true;
81            }
82            let segment1 = path.qualifier();
83            if let Some(segment1) = segment1 {
84                if segment1.qualifier().is_some() {
85                    None
86                } else {
87                    let segment1 = segment1.segment()?.name_ref()?.syntax().first_token()?;
88                    segments.push(segment1);
89                    segments.push(segment2);
90                    Some(segments)
91                }
92            } else {
93                segments.push(segment2);
94                Some(segments)
95            }
96        })();
97        AttrPath {
98            segments: segments.unwrap_or(ArrayVec::new()),
99            range: path.syntax().text_range(),
100            is_test,
101        }
102    }
103
104    #[inline]
105    pub fn is1(&self, segment: &str) -> bool {
106        self.segments.len() == 1 && self.segments[0].text() == segment
107    }
108}
109
110#[derive(Debug)]
111pub enum Meta {
112    /// `name` is `None` if not a single token. `value` is a literal or `None`.
113    NamedKeyValue {
114        path_range: TextRange,
115        name: Option<SyntaxToken>,
116        value: Option<SyntaxToken>,
117    },
118    TokenTree {
119        path: AttrPath,
120        tt: ast::TokenTree,
121    },
122    Path {
123        path: AttrPath,
124    },
125}
126
127impl Meta {
128    #[inline]
129    pub fn path_range(&self) -> TextRange {
130        match self {
131            Meta::NamedKeyValue { path_range, .. } => *path_range,
132            Meta::TokenTree { path, .. } | Meta::Path { path } => path.range,
133        }
134    }
135
136    fn extract(iter: &mut Peekable<TokenTreeChildren>) -> Option<(Self, TextSize)> {
137        let mut start_offset = None;
138        if let Some(NodeOrToken::Token(colon1)) = iter.peek()
139            && colon1.kind() == T![:]
140        {
141            start_offset = Some(colon1.text_range().start());
142            iter.next();
143            iter.next_if(|it| it.as_token().is_some_and(|it| it.kind() == T![:]));
144        }
145        let first_segment = iter
146            .next_if(|it| it.as_token().is_some_and(|it| it.kind().is_any_identifier()))?
147            .into_token()?;
148        let mut is_test = first_segment.text() == "test";
149        let start_offset = start_offset.unwrap_or_else(|| first_segment.text_range().start());
150
151        let mut segments_len = 1;
152        let mut second_segment = None;
153        let mut path_range = first_segment.text_range();
154        while iter.peek().and_then(NodeOrToken::as_token).is_some_and(|it| it.kind() == T![:])
155            && let _ = iter.next()
156            && iter.peek().and_then(NodeOrToken::as_token).is_some_and(|it| it.kind() == T![:])
157            && let _ = iter.next()
158            && let Some(NodeOrToken::Token(segment)) = iter.peek()
159            && segment.kind().is_any_identifier()
160        {
161            segments_len += 1;
162            is_test = segment.text() == "test";
163            second_segment = Some(segment.clone());
164            path_range = TextRange::new(path_range.start(), segment.text_range().end());
165            iter.next();
166        }
167
168        let segments = |first, second| {
169            let mut segments = ArrayVec::new();
170            if segments_len <= 2 {
171                segments.push(first);
172                if let Some(second) = second {
173                    segments.push(second);
174                }
175            }
176            segments
177        };
178        let meta = match iter.peek() {
179            Some(NodeOrToken::Token(eq)) if eq.kind() == T![=] => {
180                iter.next();
181                let value = match iter.peek() {
182                    Some(NodeOrToken::Token(token)) if token.kind().is_literal() => {
183                        // No need to consume it, it will be consumed by `extract_and_eat_comma()`.
184                        Some(token.clone())
185                    }
186                    _ => None,
187                };
188                let name = if second_segment.is_none() { Some(first_segment) } else { None };
189                Meta::NamedKeyValue { path_range, name, value }
190            }
191            Some(NodeOrToken::Node(tt)) => Meta::TokenTree {
192                path: AttrPath {
193                    segments: segments(first_segment, second_segment),
194                    range: path_range,
195                    is_test,
196                },
197                tt: tt.clone(),
198            },
199            _ => Meta::Path {
200                path: AttrPath {
201                    segments: segments(first_segment, second_segment),
202                    range: path_range,
203                    is_test,
204                },
205            },
206        };
207        Some((meta, start_offset))
208    }
209
210    fn extract_possibly_unsafe(
211        iter: &mut Peekable<TokenTreeChildren>,
212        container: &ast::TokenTree,
213    ) -> Option<(Self, TextRange)> {
214        if iter.peek().is_some_and(|it| it.as_token().is_some_and(|it| it.kind() == T![unsafe])) {
215            iter.next();
216            let tt = iter.next()?.into_node()?;
217            let result = Self::extract(&mut TokenTreeChildren::new(&tt).peekable()).map(
218                |(meta, start_offset)| (meta, TextRange::new(start_offset, tt_end_offset(&tt))),
219            );
220            while iter.next().is_some_and(|it| it.as_token().is_none_or(|it| it.kind() != T![,])) {}
221            result
222        } else {
223            Self::extract(iter).map(|(meta, start_offset)| {
224                let end_offset = 'find_end_offset: {
225                    for it in iter {
226                        if let NodeOrToken::Token(it) = it
227                            && it.kind() == T![,]
228                        {
229                            break 'find_end_offset it.text_range().start();
230                        }
231                    }
232                    tt_end_offset(container)
233                };
234                (meta, TextRange::new(start_offset, end_offset))
235            })
236        }
237    }
238}
239
240fn tt_end_offset(tt: &ast::TokenTree) -> TextSize {
241    tt.syntax().last_token().unwrap().text_range().start()
242}
243
244/// The callback is passed a desugared form of the attribute ([`Meta`]), a [`SyntaxNode`] fully containing it
245/// (note: it may not be the direct parent), the range within the [`SyntaxNode`] bounding the attribute,
246/// and the outermost `ast::Attr`. Note that one node may map to multiple [`Meta`]s due to `cfg_attr`.
247#[inline]
248pub fn expand_cfg_attr<'a, BreakValue>(
249    attrs: impl Iterator<Item = ast::Attr>,
250    cfg_options: impl FnMut() -> &'a CfgOptions,
251    mut callback: impl FnMut(Meta, &SyntaxNode, TextRange, &ast::Attr) -> ControlFlow<BreakValue>,
252) -> Option<BreakValue> {
253    expand_cfg_attr_with_doc_comments::<Infallible, _>(
254        attrs.map(Either::Left),
255        cfg_options,
256        move |Either::Left((meta, container, range, top_attr))| {
257            callback(meta, container, range, top_attr)
258        },
259    )
260}
261
262#[inline]
263pub fn expand_cfg_attr_with_doc_comments<'a, DocComment, BreakValue>(
264    mut attrs: impl Iterator<Item = Either<ast::Attr, DocComment>>,
265    mut cfg_options: impl FnMut() -> &'a CfgOptions,
266    mut callback: impl FnMut(
267        Either<(Meta, &SyntaxNode, TextRange, &ast::Attr), DocComment>,
268    ) -> ControlFlow<BreakValue>,
269) -> Option<BreakValue> {
270    let mut stack = SmallVec::<[_; 1]>::new();
271    let result = attrs.try_for_each(|top_attr| {
272        let top_attr = match top_attr {
273            Either::Left(it) => it,
274            Either::Right(comment) => return callback(Either::Right(comment)),
275        };
276        if let Some((attr_name, tt)) = top_attr.as_simple_call()
277            && attr_name == "cfg_attr"
278        {
279            let mut tt_iter = TokenTreeChildren::new(&tt).peekable();
280            let cfg = cfg::CfgExpr::parse_from_ast(&mut tt_iter);
281            if cfg_options().check(&cfg) != Some(false) {
282                stack.push((tt_iter, tt));
283                while let Some((tt_iter, tt)) = stack.last_mut() {
284                    let Some((attr, range)) = Meta::extract_possibly_unsafe(tt_iter, tt) else {
285                        stack.pop();
286                        continue;
287                    };
288                    if let Meta::TokenTree { path, tt: nested_tt } = &attr
289                        && path.is1("cfg_attr")
290                    {
291                        let mut nested_tt_iter = TokenTreeChildren::new(nested_tt).peekable();
292                        let cfg = cfg::CfgExpr::parse_from_ast(&mut nested_tt_iter);
293                        if cfg_options().check(&cfg) != Some(false) {
294                            stack.push((nested_tt_iter, nested_tt.clone()));
295                        }
296                    } else {
297                        callback(Either::Left((attr, tt.syntax(), range, &top_attr)))?;
298                    }
299                }
300            }
301        } else if let Some(ast_meta) = top_attr.meta()
302            && let Some(path) = ast_meta.path()
303        {
304            let path = AttrPath::extract(&path);
305            let meta = if let Some(tt) = ast_meta.token_tree() {
306                Meta::TokenTree { path, tt }
307            } else if let Some(value) = ast_meta.expr() {
308                let value =
309                    if let ast::Expr::Literal(value) = value { Some(value.token()) } else { None };
310                let name =
311                    if path.segments.len() == 1 { Some(path.segments[0].clone()) } else { None };
312                Meta::NamedKeyValue { name, value, path_range: path.range }
313            } else {
314                Meta::Path { path }
315            };
316            callback(Either::Left((
317                meta,
318                ast_meta.syntax(),
319                ast_meta.syntax().text_range(),
320                &top_attr,
321            )))?;
322        }
323        ControlFlow::Continue(())
324    });
325    result.break_value()
326}
327
328#[inline]
329pub(crate) fn is_item_tree_filtered_attr(name: &str) -> bool {
330    matches!(
331        name,
332        "doc"
333            | "stable"
334            | "unstable"
335            | "target_feature"
336            | "allow"
337            | "expect"
338            | "warn"
339            | "deny"
340            | "forbid"
341            | "repr"
342            | "inline"
343            | "track_caller"
344            | "must_use"
345    )
346}
347
348/// This collects attributes exactly as the item tree needs them. This is used for the item tree,
349/// as well as for resolving [`AttrId`]s.
350pub fn collect_item_tree_attrs<'a, BreakValue>(
351    owner: &dyn ast::HasAttrs,
352    cfg_options: impl Fn() -> &'a CfgOptions,
353    mut on_attr: impl FnMut(Meta, &SyntaxNode, &ast::Attr, TextRange) -> ControlFlow<BreakValue>,
354) -> Option<Either<BreakValue, CfgExpr>> {
355    let attrs = ast::attrs_including_inner(owner);
356    expand_cfg_attr(
357        attrs,
358        || cfg_options(),
359        |attr, container, range, top_attr| {
360            // We filter builtin attributes that we don't need for nameres, because this saves memory.
361            // I only put the most common attributes, but if some attribute becomes common feel free to add it.
362            // Notice, however: for an attribute to be filtered out, it *must* not be shadowable with a macro!
363            let filter = match &attr {
364                Meta::NamedKeyValue { name: Some(name), .. } => {
365                    is_item_tree_filtered_attr(name.text())
366                }
367                Meta::TokenTree { path, tt } if path.segments.len() == 1 => {
368                    let name = path.segments[0].text();
369                    if name == "cfg" {
370                        let cfg =
371                            CfgExpr::parse_from_ast(&mut TokenTreeChildren::new(tt).peekable());
372                        if cfg_options().check(&cfg) == Some(false) {
373                            return ControlFlow::Break(Either::Right(cfg));
374                        }
375                        true
376                    } else {
377                        is_item_tree_filtered_attr(name)
378                    }
379                }
380                Meta::Path { path } => {
381                    path.segments.len() == 1 && is_item_tree_filtered_attr(path.segments[0].text())
382                }
383                _ => false,
384            };
385            if !filter && let ControlFlow::Break(v) = on_attr(attr, container, top_attr, range) {
386                return ControlFlow::Break(Either::Left(v));
387            }
388            ControlFlow::Continue(())
389        },
390    )
391}
392
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct Attr {
395    pub path: Interned<ModPath>,
396    pub input: Option<Box<AttrInput>>,
397    pub ctxt: SyntaxContext,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash)]
401pub enum AttrInput {
402    /// `#[attr = "string"]`
403    Literal(tt::Literal),
404    /// `#[attr(subtree)]`
405    TokenTree(tt::TopSubtree),
406}
407
408impl fmt::Display for AttrInput {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            AttrInput::Literal(lit) => write!(f, " = {lit}"),
412            AttrInput::TokenTree(tt) => tt.fmt(f),
413        }
414    }
415}
416
417impl Attr {
418    /// #[path = "string"]
419    pub fn string_value(&self) -> Option<&Symbol> {
420        match self.input.as_deref()? {
421            AttrInput::Literal(tt::Literal {
422                symbol: text,
423                kind: tt::LitKind::Str | tt::LitKind::StrRaw(_),
424                ..
425            }) => Some(text),
426            _ => None,
427        }
428    }
429
430    /// #[path = "string"]
431    pub fn string_value_with_span(&self) -> Option<(&Symbol, span::Span)> {
432        match self.input.as_deref()? {
433            AttrInput::Literal(tt::Literal {
434                symbol: text,
435                kind: tt::LitKind::Str | tt::LitKind::StrRaw(_),
436                span,
437                suffix: _,
438            }) => Some((text, *span)),
439            _ => None,
440        }
441    }
442
443    pub fn string_value_unescape(&self) -> Option<Cow<'_, str>> {
444        match self.input.as_deref()? {
445            AttrInput::Literal(tt::Literal {
446                symbol: text, kind: tt::LitKind::StrRaw(_), ..
447            }) => Some(Cow::Borrowed(text.as_str())),
448            AttrInput::Literal(tt::Literal { symbol: text, kind: tt::LitKind::Str, .. }) => {
449                unescape(text.as_str())
450            }
451            _ => None,
452        }
453    }
454
455    /// #[path(ident)]
456    pub fn single_ident_value(&self) -> Option<&tt::Ident> {
457        match self.input.as_deref()? {
458            AttrInput::TokenTree(tt) => match tt.token_trees().flat_tokens() {
459                [tt::TokenTree::Leaf(tt::Leaf::Ident(ident))] => Some(ident),
460                _ => None,
461            },
462            _ => None,
463        }
464    }
465
466    /// #[path TokenTree]
467    pub fn token_tree_value(&self) -> Option<&TopSubtree> {
468        match self.input.as_deref()? {
469            AttrInput::TokenTree(tt) => Some(tt),
470            _ => None,
471        }
472    }
473
474    /// Parses this attribute as a token tree consisting of comma separated paths.
475    pub fn parse_path_comma_token_tree<'a>(
476        &'a self,
477        db: &'a dyn ExpandDatabase,
478    ) -> Option<impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> + 'a> {
479        let args = self.token_tree_value()?;
480
481        if args.top_subtree().delimiter.kind != DelimiterKind::Parenthesis {
482            return None;
483        }
484        Some(parse_path_comma_token_tree(db, args))
485    }
486}
487
488fn parse_path_comma_token_tree<'a>(
489    db: &'a dyn ExpandDatabase,
490    args: &'a tt::TopSubtree,
491) -> impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> {
492    args.token_trees()
493        .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. }))))
494        .filter_map(move |tts| {
495            let span = tts.flat_tokens().first()?.first_span();
496            Some((ModPath::from_tt(db, tts)?, span, tts))
497        })
498}
499
500fn unescape(s: &str) -> Option<Cow<'_, str>> {
501    let mut buf = String::new();
502    let mut prev_end = 0;
503    let mut has_error = false;
504    unescape::unescape_str(s, |char_range, unescaped_char| {
505        match (unescaped_char, buf.capacity() == 0) {
506            (Ok(c), false) => buf.push(c),
507            (Ok(_), true) if char_range.len() == 1 && char_range.start == prev_end => {
508                prev_end = char_range.end
509            }
510            (Ok(c), true) => {
511                buf.reserve_exact(s.len());
512                buf.push_str(&s[..prev_end]);
513                buf.push(c);
514            }
515            (Err(_), _) => has_error = true,
516        }
517    });
518
519    match (has_error, buf.capacity() == 0) {
520        (true, _) => None,
521        (false, false) => Some(Cow::Owned(buf)),
522        (false, true) => Some(Cow::Borrowed(s)),
523    }
524}
525
526/// This is an index of an attribute *that always points to the item tree attributes*.
527///
528/// Outer attributes are counted first, then inner attributes. This does not support
529/// out-of-line modules, which may have attributes spread across 2 files!
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
531pub struct AttrId {
532    id: u32,
533}
534
535impl AttrId {
536    #[inline]
537    pub fn from_item_tree_index(id: u32) -> Self {
538        Self { id }
539    }
540
541    #[inline]
542    pub fn item_tree_index(self) -> u32 {
543        self.id
544    }
545
546    /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due
547    /// to `cfg_attr`), a `SyntaxNode` guaranteed to contain the attribute, the full range of the
548    /// attribute, and its desugared [`Meta`].
549    pub fn find_attr_range<N: ast::HasAttrs>(
550        self,
551        db: &dyn ExpandDatabase,
552        krate: Crate,
553        owner: AstId<N>,
554    ) -> (ast::Attr, SyntaxNode, TextRange, Meta) {
555        self.find_attr_range_with_source(db, krate, &owner.to_node(db))
556    }
557
558    /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due
559    /// to `cfg_attr`), a `SyntaxNode` guaranteed to contain the attribute, the full range of the
560    /// attribute, and its desugared [`Meta`].
561    pub fn find_attr_range_with_source(
562        self,
563        db: &dyn ExpandDatabase,
564        krate: Crate,
565        owner: &dyn ast::HasAttrs,
566    ) -> (ast::Attr, SyntaxNode, TextRange, Meta) {
567        let cfg_options = OnceCell::new();
568        let mut index = 0;
569        let result = collect_item_tree_attrs(
570            owner,
571            || cfg_options.get_or_init(|| krate.cfg_options(db)),
572            |meta, container, top_attr, range| {
573                if index == self.id {
574                    return ControlFlow::Break((top_attr.clone(), container.clone(), range, meta));
575                }
576                index += 1;
577                ControlFlow::Continue(())
578            },
579        );
580        match result {
581            Some(Either::Left(it)) => it,
582            _ => {
583                panic!("used an incorrect `AttrId`; crate={krate:?}, attr_id={self:?}");
584            }
585        }
586    }
587
588    pub fn find_derive_range(
589        self,
590        db: &dyn ExpandDatabase,
591        krate: Crate,
592        owner: AstId<ast::Adt>,
593        derive_index: u32,
594    ) -> TextRange {
595        let (_, _, derive_attr_range, derive_attr) = self.find_attr_range(db, krate, owner);
596        let Meta::TokenTree { tt, .. } = derive_attr else {
597            return derive_attr_range;
598        };
599        // Fake the span map, as we don't really need spans here, just the offsets of the node in the file.
600        let span_map = RealSpanMap::absolute(span::EditionedFileId::current_edition(
601            span::FileId::from_raw(0),
602        ));
603        let tt = syntax_bridge::syntax_node_to_token_tree(
604            tt.syntax(),
605            SpanMapRef::RealSpanMap(&span_map),
606            span_map.span_for_range(tt.syntax().text_range()),
607            DocCommentDesugarMode::ProcMacro,
608        );
609        let Some((_, _, derive_tts)) =
610            parse_path_comma_token_tree(db, &tt).nth(derive_index as usize)
611        else {
612            return derive_attr_range;
613        };
614        let (Some(first_tt), Some(last_tt)) =
615            (derive_tts.flat_tokens().first(), derive_tts.flat_tokens().last())
616        else {
617            return derive_attr_range;
618        };
619        let start = first_tt.first_span().range.start();
620        let end = match last_tt {
621            tt::TokenTree::Leaf(it) => it.span().range.end(),
622            tt::TokenTree::Subtree(it) => it.delimiter.close.range.end(),
623        };
624        TextRange::new(start, end)
625    }
626}