hir_expand/
attrs.rs

1//! A higher level attributes based on TokenTree, with also some shortcuts.
2use std::iter;
3use std::{borrow::Cow, fmt, ops};
4
5use base_db::Crate;
6use cfg::{CfgExpr, CfgOptions};
7use either::Either;
8use intern::{Interned, Symbol, sym};
9
10use mbe::{DelimiterKind, Punct};
11use smallvec::{SmallVec, smallvec};
12use span::{Span, SyntaxContext};
13use syntax::unescape;
14use syntax::{AstNode, AstToken, SyntaxNode, ast, match_ast};
15use syntax_bridge::{DocCommentDesugarMode, desugar_doc_comment_text, syntax_node_to_token_tree};
16use triomphe::ThinArc;
17
18use crate::{
19    db::ExpandDatabase,
20    mod_path::ModPath,
21    name::Name,
22    span_map::SpanMapRef,
23    tt::{self, TopSubtree, token_to_literal},
24};
25
26/// Syntactical attributes, without filtering of `cfg_attr`s.
27#[derive(Default, Debug, Clone, PartialEq, Eq)]
28pub struct RawAttrs {
29    // FIXME: This can become `Box<[Attr]>` if https://internals.rust-lang.org/t/layout-of-dst-box/21728?u=chrefr is accepted.
30    entries: Option<ThinArc<(), Attr>>,
31}
32
33impl ops::Deref for RawAttrs {
34    type Target = [Attr];
35
36    fn deref(&self) -> &[Attr] {
37        match &self.entries {
38            Some(it) => &it.slice,
39            None => &[],
40        }
41    }
42}
43
44impl RawAttrs {
45    pub const EMPTY: Self = Self { entries: None };
46
47    pub fn new(
48        db: &dyn ExpandDatabase,
49        owner: &dyn ast::HasAttrs,
50        span_map: SpanMapRef<'_>,
51    ) -> Self {
52        let entries: Vec<_> = Self::attrs_iter::<true>(db, owner, span_map).collect();
53
54        let entries = if entries.is_empty() {
55            None
56        } else {
57            Some(ThinArc::from_header_and_iter((), entries.into_iter()))
58        };
59
60        RawAttrs { entries }
61    }
62
63    /// A [`RawAttrs`] that has its `#[cfg_attr(...)]` attributes expanded.
64    pub fn new_expanded(
65        db: &dyn ExpandDatabase,
66        owner: &dyn ast::HasAttrs,
67        span_map: SpanMapRef<'_>,
68        cfg_options: &CfgOptions,
69    ) -> Self {
70        let entries: Vec<_> =
71            Self::attrs_iter_expanded::<true>(db, owner, span_map, cfg_options).collect();
72
73        let entries = if entries.is_empty() {
74            None
75        } else {
76            Some(ThinArc::from_header_and_iter((), entries.into_iter()))
77        };
78
79        RawAttrs { entries }
80    }
81
82    pub fn attrs_iter<const DESUGAR_COMMENTS: bool>(
83        db: &dyn ExpandDatabase,
84        owner: &dyn ast::HasAttrs,
85        span_map: SpanMapRef<'_>,
86    ) -> impl Iterator<Item = Attr> {
87        collect_attrs(owner).filter_map(move |(id, attr)| match attr {
88            Either::Left(attr) => {
89                attr.meta().and_then(|meta| Attr::from_src(db, meta, span_map, id))
90            }
91            Either::Right(comment) if DESUGAR_COMMENTS => comment.doc_comment().map(|doc| {
92                let span = span_map.span_for_range(comment.syntax().text_range());
93                let (text, kind) = desugar_doc_comment_text(doc, DocCommentDesugarMode::ProcMacro);
94                Attr {
95                    id,
96                    input: Some(Box::new(AttrInput::Literal(tt::Literal {
97                        symbol: text,
98                        span,
99                        kind,
100                        suffix: None,
101                    }))),
102                    path: Interned::new(ModPath::from(Name::new_symbol(sym::doc, span.ctx))),
103                    ctxt: span.ctx,
104                }
105            }),
106            Either::Right(_) => None,
107        })
108    }
109
110    pub fn attrs_iter_expanded<const DESUGAR_COMMENTS: bool>(
111        db: &dyn ExpandDatabase,
112        owner: &dyn ast::HasAttrs,
113        span_map: SpanMapRef<'_>,
114        cfg_options: &CfgOptions,
115    ) -> impl Iterator<Item = Attr> {
116        Self::attrs_iter::<DESUGAR_COMMENTS>(db, owner, span_map)
117            .flat_map(|attr| attr.expand_cfg_attr(db, cfg_options))
118    }
119
120    pub fn merge(&self, other: Self) -> Self {
121        match (&self.entries, other.entries) {
122            (None, None) => Self::EMPTY,
123            (None, entries @ Some(_)) => Self { entries },
124            (Some(entries), None) => Self { entries: Some(entries.clone()) },
125            (Some(a), Some(b)) => {
126                let last_ast_index = a.slice.last().map_or(0, |it| it.id.ast_index() + 1);
127                let items = a
128                    .slice
129                    .iter()
130                    .cloned()
131                    .chain(b.slice.iter().map(|it| {
132                        let mut it = it.clone();
133                        let id = it.id.ast_index() + last_ast_index;
134                        it.id = AttrId::new(id, it.id.is_inner_attr());
135                        it
136                    }))
137                    .collect::<Vec<_>>();
138                Self { entries: Some(ThinArc::from_header_and_iter((), items.into_iter())) }
139            }
140        }
141    }
142
143    /// Processes `cfg_attr`s
144    pub fn expand_cfg_attr(self, db: &dyn ExpandDatabase, krate: Crate) -> RawAttrs {
145        let has_cfg_attrs =
146            self.iter().any(|attr| attr.path.as_ident().is_some_and(|name| *name == sym::cfg_attr));
147        if !has_cfg_attrs {
148            return self;
149        }
150
151        let cfg_options = krate.cfg_options(db);
152        let new_attrs = self
153            .iter()
154            .cloned()
155            .flat_map(|attr| attr.expand_cfg_attr(db, cfg_options))
156            .collect::<Vec<_>>();
157        let entries = if new_attrs.is_empty() {
158            None
159        } else {
160            Some(ThinArc::from_header_and_iter((), new_attrs.into_iter()))
161        };
162        RawAttrs { entries }
163    }
164
165    pub fn is_empty(&self) -> bool {
166        self.entries.is_none()
167    }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub struct AttrId {
172    id: u32,
173}
174
175// FIXME: This only handles a single level of cfg_attr nesting
176// that is `#[cfg_attr(all(), cfg_attr(all(), cfg(any())))]` breaks again
177impl AttrId {
178    const INNER_ATTR_SET_BIT: u32 = 1 << 31;
179
180    pub fn new(id: usize, is_inner: bool) -> Self {
181        assert!(id <= !Self::INNER_ATTR_SET_BIT as usize);
182        let id = id as u32;
183        Self { id: if is_inner { id | Self::INNER_ATTR_SET_BIT } else { id } }
184    }
185
186    pub fn ast_index(&self) -> usize {
187        (self.id & !Self::INNER_ATTR_SET_BIT) as usize
188    }
189
190    pub fn is_inner_attr(&self) -> bool {
191        self.id & Self::INNER_ATTR_SET_BIT != 0
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Attr {
197    pub id: AttrId,
198    pub path: Interned<ModPath>,
199    pub input: Option<Box<AttrInput>>,
200    pub ctxt: SyntaxContext,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Hash)]
204pub enum AttrInput {
205    /// `#[attr = "string"]`
206    Literal(tt::Literal),
207    /// `#[attr(subtree)]`
208    TokenTree(tt::TopSubtree),
209}
210
211impl fmt::Display for AttrInput {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            AttrInput::Literal(lit) => write!(f, " = {lit}"),
215            AttrInput::TokenTree(tt) => tt.fmt(f),
216        }
217    }
218}
219
220impl Attr {
221    fn from_src(
222        db: &dyn ExpandDatabase,
223        ast: ast::Meta,
224        span_map: SpanMapRef<'_>,
225        id: AttrId,
226    ) -> Option<Attr> {
227        let path = ast.path()?;
228        let range = path.syntax().text_range();
229        let path = Interned::new(ModPath::from_src(db, path, &mut |range| {
230            span_map.span_for_range(range).ctx
231        })?);
232        let span = span_map.span_for_range(range);
233        let input = if let Some(ast::Expr::Literal(lit)) = ast.expr() {
234            let token = lit.token();
235            Some(Box::new(AttrInput::Literal(token_to_literal(token.text(), span))))
236        } else if let Some(tt) = ast.token_tree() {
237            let tree = syntax_node_to_token_tree(
238                tt.syntax(),
239                span_map,
240                span,
241                DocCommentDesugarMode::ProcMacro,
242            );
243            Some(Box::new(AttrInput::TokenTree(tree)))
244        } else {
245            None
246        };
247        Some(Attr { id, path, input, ctxt: span.ctx })
248    }
249
250    fn from_tt(
251        db: &dyn ExpandDatabase,
252        mut tt: tt::TokenTreesView<'_>,
253        id: AttrId,
254    ) -> Option<Attr> {
255        if matches!(tt.flat_tokens(),
256            [tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident { sym, .. })), ..]
257            if *sym == sym::unsafe_
258        ) {
259            match tt.iter().nth(1) {
260                Some(tt::TtElement::Subtree(_, iter)) => tt = iter.remaining(),
261                _ => return None,
262            }
263        }
264        let first = tt.flat_tokens().first()?;
265        let ctxt = first.first_span().ctx;
266        let (path, input) = {
267            let mut iter = tt.iter();
268            let start = iter.savepoint();
269            let mut input = tt::TokenTreesView::new(&[]);
270            let mut path = iter.from_savepoint(start);
271            let mut path_split_savepoint = iter.savepoint();
272            while let Some(tt) = iter.next() {
273                path = iter.from_savepoint(start);
274                if !matches!(
275                    tt,
276                    tt::TtElement::Leaf(
277                        tt::Leaf::Punct(tt::Punct { char: ':' | '$', .. }) | tt::Leaf::Ident(_),
278                    )
279                ) {
280                    input = path_split_savepoint.remaining();
281                    break;
282                }
283                path_split_savepoint = iter.savepoint();
284            }
285            (path, input)
286        };
287
288        let path = Interned::new(ModPath::from_tt(db, path)?);
289
290        let input = match (input.flat_tokens().first(), input.try_into_subtree()) {
291            (_, Some(tree)) => {
292                Some(Box::new(AttrInput::TokenTree(tt::TopSubtree::from_subtree(tree))))
293            }
294            (Some(tt::TokenTree::Leaf(tt::Leaf::Punct(tt::Punct { char: '=', .. }))), _) => {
295                match input.flat_tokens().get(1) {
296                    Some(tt::TokenTree::Leaf(tt::Leaf::Literal(lit))) => {
297                        Some(Box::new(AttrInput::Literal(lit.clone())))
298                    }
299                    _ => None,
300                }
301            }
302            _ => None,
303        };
304        Some(Attr { id, path, input, ctxt })
305    }
306
307    pub fn path(&self) -> &ModPath {
308        &self.path
309    }
310
311    pub fn expand_cfg_attr(
312        self,
313        db: &dyn ExpandDatabase,
314        cfg_options: &CfgOptions,
315    ) -> impl IntoIterator<Item = Self> {
316        let is_cfg_attr = self.path.as_ident().is_some_and(|name| *name == sym::cfg_attr);
317        if !is_cfg_attr {
318            return smallvec![self];
319        }
320
321        let subtree = match self.token_tree_value() {
322            Some(it) => it,
323            _ => return smallvec![self.clone()],
324        };
325
326        let (cfg, parts) = match parse_cfg_attr_input(subtree) {
327            Some(it) => it,
328            None => return smallvec![self.clone()],
329        };
330        let index = self.id;
331        let attrs = parts.filter_map(|attr| Attr::from_tt(db, attr, index));
332
333        let cfg = TopSubtree::from_token_trees(subtree.top_subtree().delimiter, cfg);
334        let cfg = CfgExpr::parse(&cfg);
335        if cfg_options.check(&cfg) == Some(false) {
336            smallvec![]
337        } else {
338            cov_mark::hit!(cfg_attr_active);
339
340            attrs.collect::<SmallVec<[_; 1]>>()
341        }
342    }
343}
344
345impl Attr {
346    /// #[path = "string"]
347    pub fn string_value(&self) -> Option<&Symbol> {
348        match self.input.as_deref()? {
349            AttrInput::Literal(tt::Literal {
350                symbol: text,
351                kind: tt::LitKind::Str | tt::LitKind::StrRaw(_),
352                ..
353            }) => Some(text),
354            _ => None,
355        }
356    }
357
358    /// #[path = "string"]
359    pub fn string_value_with_span(&self) -> Option<(&Symbol, span::Span)> {
360        match self.input.as_deref()? {
361            AttrInput::Literal(tt::Literal {
362                symbol: text,
363                kind: tt::LitKind::Str | tt::LitKind::StrRaw(_),
364                span,
365                suffix: _,
366            }) => Some((text, *span)),
367            _ => None,
368        }
369    }
370
371    pub fn string_value_unescape(&self) -> Option<Cow<'_, str>> {
372        match self.input.as_deref()? {
373            AttrInput::Literal(tt::Literal {
374                symbol: text, kind: tt::LitKind::StrRaw(_), ..
375            }) => Some(Cow::Borrowed(text.as_str())),
376            AttrInput::Literal(tt::Literal { symbol: text, kind: tt::LitKind::Str, .. }) => {
377                unescape(text.as_str())
378            }
379            _ => None,
380        }
381    }
382
383    /// #[path(ident)]
384    pub fn single_ident_value(&self) -> Option<&tt::Ident> {
385        match self.input.as_deref()? {
386            AttrInput::TokenTree(tt) => match tt.token_trees().flat_tokens() {
387                [tt::TokenTree::Leaf(tt::Leaf::Ident(ident))] => Some(ident),
388                _ => None,
389            },
390            _ => None,
391        }
392    }
393
394    /// #[path TokenTree]
395    pub fn token_tree_value(&self) -> Option<&TopSubtree> {
396        match self.input.as_deref()? {
397            AttrInput::TokenTree(tt) => Some(tt),
398            _ => None,
399        }
400    }
401
402    /// Parses this attribute as a token tree consisting of comma separated paths.
403    pub fn parse_path_comma_token_tree<'a>(
404        &'a self,
405        db: &'a dyn ExpandDatabase,
406    ) -> Option<impl Iterator<Item = (ModPath, Span)> + 'a> {
407        let args = self.token_tree_value()?;
408
409        if args.top_subtree().delimiter.kind != DelimiterKind::Parenthesis {
410            return None;
411        }
412        let paths = args
413            .token_trees()
414            .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. }))))
415            .filter_map(move |tts| {
416                let span = tts.flat_tokens().first()?.first_span();
417                Some((ModPath::from_tt(db, tts)?, span))
418            });
419
420        Some(paths)
421    }
422
423    pub fn cfg(&self) -> Option<CfgExpr> {
424        if *self.path.as_ident()? == sym::cfg {
425            self.token_tree_value().map(CfgExpr::parse)
426        } else {
427            None
428        }
429    }
430}
431
432fn unescape(s: &str) -> Option<Cow<'_, str>> {
433    let mut buf = String::new();
434    let mut prev_end = 0;
435    let mut has_error = false;
436    unescape::unescape_str(s, |char_range, unescaped_char| {
437        match (unescaped_char, buf.capacity() == 0) {
438            (Ok(c), false) => buf.push(c),
439            (Ok(_), true) if char_range.len() == 1 && char_range.start == prev_end => {
440                prev_end = char_range.end
441            }
442            (Ok(c), true) => {
443                buf.reserve_exact(s.len());
444                buf.push_str(&s[..prev_end]);
445                buf.push(c);
446            }
447            (Err(_), _) => has_error = true,
448        }
449    });
450
451    match (has_error, buf.capacity() == 0) {
452        (true, _) => None,
453        (false, false) => Some(Cow::Owned(buf)),
454        (false, true) => Some(Cow::Borrowed(s)),
455    }
456}
457
458pub fn collect_attrs(
459    owner: &dyn ast::HasAttrs,
460) -> impl Iterator<Item = (AttrId, Either<ast::Attr, ast::Comment>)> {
461    let inner_attrs =
462        inner_attributes(owner.syntax()).into_iter().flatten().zip(iter::repeat(true));
463    let outer_attrs = ast::AttrDocCommentIter::from_syntax_node(owner.syntax())
464        .filter(|el| match el {
465            Either::Left(attr) => attr.kind().is_outer(),
466            Either::Right(comment) => comment.is_outer(),
467        })
468        .zip(iter::repeat(false));
469    outer_attrs
470        .chain(inner_attrs)
471        .enumerate()
472        .map(|(id, (attr, is_inner))| (AttrId::new(id, is_inner), attr))
473}
474
475fn inner_attributes(
476    syntax: &SyntaxNode,
477) -> Option<impl Iterator<Item = Either<ast::Attr, ast::Comment>>> {
478    let node = match_ast! {
479        match syntax {
480            ast::SourceFile(_) => syntax.clone(),
481            ast::ExternBlock(it) => it.extern_item_list()?.syntax().clone(),
482            ast::Fn(it) => it.body()?.stmt_list()?.syntax().clone(),
483            ast::Impl(it) => it.assoc_item_list()?.syntax().clone(),
484            ast::Module(it) => it.item_list()?.syntax().clone(),
485            ast::BlockExpr(it) => {
486                if !it.may_carry_attributes() {
487                    return None
488                }
489                syntax.clone()
490            },
491            _ => return None,
492        }
493    };
494
495    let attrs = ast::AttrDocCommentIter::from_syntax_node(&node).filter(|el| match el {
496        Either::Left(attr) => attr.kind().is_inner(),
497        Either::Right(comment) => comment.is_inner(),
498    });
499    Some(attrs)
500}
501
502// Input subtree is: `(cfg, $(attr),+)`
503// Split it up into a `cfg` subtree and the `attr` subtrees.
504fn parse_cfg_attr_input(
505    subtree: &TopSubtree,
506) -> Option<(tt::TokenTreesView<'_>, impl Iterator<Item = tt::TokenTreesView<'_>>)> {
507    let mut parts = subtree
508        .token_trees()
509        .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. }))));
510    let cfg = parts.next()?;
511    Some((cfg, parts.filter(|it| !it.is_empty())))
512}