| //! Defines the basics of attributes lowering. |
| //! |
| //! The heart and soul of this module is [`expand_cfg_attr()`], alongside its sibling |
| //! [`expand_cfg_attr_with_doc_comments()`]. It is used to implement all attribute lowering |
| //! in r-a. Its basic job is to list attributes; however, attributes do not necessarily map |
| //! into [`ast::Attr`], because `cfg_attr` can map to zero, one, or more attributes |
| //! (`#[cfg_attr(predicate, attr1, attr2, ...)]`). To bridge this gap, this module defines |
| //! [`Meta`], which represents a desugared attribute. Various bits of r-a need different |
| //! things from [`Meta`], therefore it contains many parts. The basic idea is: |
| //! |
| //! - There are three kinds of attributes, `path = value`, `path`, and `path(token_tree)`. |
| //! - Most bits of rust-analyzer only need to deal with some paths. Therefore, we keep |
| //! the path only if it has up to 2 segments, or one segment for `path = value`. |
| //! We also only keep the value in `path = value` if it is a literal. However, we always |
| //! save the all relevant ranges of attributes (the path range, and the full attribute range) |
| //! for parts of r-a (e.g. name resolution) that need a faithful representation of the |
| //! attribute. |
| //! |
| //! [`expand_cfg_attr()`] expands `cfg_attr`s as it goes (as its name implies), to list |
| //! all attributes. |
| //! |
| //! Another thing to note is that we need to be able to map an attribute back to a range |
| //! (for diagnostic purposes etc.). This is only ever needed for attributes that participate |
| //! in name resolution. An attribute is mapped back by its [`AttrId`], which is just an |
| //! index into the item tree attributes list. To minimize the risk of bugs, we have one |
| //! place (here) and one function ([`is_item_tree_filtered_attr()`]) that decides whether |
| //! an attribute participate in name resolution. |
| |
| use std::{ |
| borrow::Cow, cell::OnceCell, convert::Infallible, fmt, iter::Peekable, ops::ControlFlow, |
| }; |
| |
| use ::tt::{TextRange, TextSize}; |
| use arrayvec::ArrayVec; |
| use base_db::Crate; |
| use cfg::{CfgExpr, CfgOptions}; |
| use either::Either; |
| use intern::{Interned, Symbol}; |
| use mbe::{DelimiterKind, Punct}; |
| use parser::T; |
| use smallvec::SmallVec; |
| use span::{RealSpanMap, Span, SyntaxContext}; |
| use syntax::{ |
| AstNode, NodeOrToken, SyntaxNode, SyntaxToken, |
| ast::{self, TokenTreeChildren}, |
| unescape, |
| }; |
| use syntax_bridge::DocCommentDesugarMode; |
| |
| use crate::{ |
| AstId, |
| db::ExpandDatabase, |
| mod_path::ModPath, |
| span_map::SpanMapRef, |
| tt::{self, TopSubtree}, |
| }; |
| |
| #[derive(Debug)] |
| pub struct AttrPath { |
| /// This can be empty if the path is not of 1 or 2 segments exactly. |
| pub segments: ArrayVec<SyntaxToken, 2>, |
| pub range: TextRange, |
| // FIXME: This shouldn't be textual, `#[test]` needs name resolution. |
| // And if textual, it shouldn't be here, it should be in hir-def/src/attrs.rs. But some macros |
| // fully qualify `test` as `core::prelude::vX::test`, and this is more than 2 segments, so hir-def |
| // attrs can't find it. But this will mean we have to push every up-to-4-segments path, which |
| // may impact perf. So it was easier to just hack it here. |
| pub is_test: bool, |
| } |
| |
| impl AttrPath { |
| #[inline] |
| fn extract(path: &ast::Path) -> Self { |
| let mut is_test = false; |
| let segments = (|| { |
| let mut segments = ArrayVec::new(); |
| let segment2 = path.segment()?.name_ref()?.syntax().first_token()?; |
| if segment2.text() == "test" { |
| // `#[test]` or `#[core::prelude::vX::test]`. |
| is_test = true; |
| } |
| let segment1 = path.qualifier(); |
| if let Some(segment1) = segment1 { |
| if segment1.qualifier().is_some() { |
| None |
| } else { |
| let segment1 = segment1.segment()?.name_ref()?.syntax().first_token()?; |
| segments.push(segment1); |
| segments.push(segment2); |
| Some(segments) |
| } |
| } else { |
| segments.push(segment2); |
| Some(segments) |
| } |
| })(); |
| AttrPath { |
| segments: segments.unwrap_or(ArrayVec::new()), |
| range: path.syntax().text_range(), |
| is_test, |
| } |
| } |
| |
| #[inline] |
| pub fn is1(&self, segment: &str) -> bool { |
| self.segments.len() == 1 && self.segments[0].text() == segment |
| } |
| } |
| |
| #[derive(Debug)] |
| pub enum Meta { |
| /// `name` is `None` if not a single token. `value` is a literal or `None`. |
| NamedKeyValue { |
| path_range: TextRange, |
| name: Option<SyntaxToken>, |
| value: Option<SyntaxToken>, |
| }, |
| TokenTree { |
| path: AttrPath, |
| tt: ast::TokenTree, |
| }, |
| Path { |
| path: AttrPath, |
| }, |
| } |
| |
| impl Meta { |
| #[inline] |
| pub fn path_range(&self) -> TextRange { |
| match self { |
| Meta::NamedKeyValue { path_range, .. } => *path_range, |
| Meta::TokenTree { path, .. } | Meta::Path { path } => path.range, |
| } |
| } |
| |
| fn extract(iter: &mut Peekable<TokenTreeChildren>) -> Option<(Self, TextSize)> { |
| let mut start_offset = None; |
| if let Some(NodeOrToken::Token(colon1)) = iter.peek() |
| && colon1.kind() == T![:] |
| { |
| start_offset = Some(colon1.text_range().start()); |
| iter.next(); |
| iter.next_if(|it| it.as_token().is_some_and(|it| it.kind() == T![:])); |
| } |
| let first_segment = iter |
| .next_if(|it| it.as_token().is_some_and(|it| it.kind().is_any_identifier()))? |
| .into_token()?; |
| let mut is_test = first_segment.text() == "test"; |
| let start_offset = start_offset.unwrap_or_else(|| first_segment.text_range().start()); |
| |
| let mut segments_len = 1; |
| let mut second_segment = None; |
| let mut path_range = first_segment.text_range(); |
| while iter.peek().and_then(NodeOrToken::as_token).is_some_and(|it| it.kind() == T![:]) |
| && let _ = iter.next() |
| && iter.peek().and_then(NodeOrToken::as_token).is_some_and(|it| it.kind() == T![:]) |
| && let _ = iter.next() |
| && let Some(NodeOrToken::Token(segment)) = iter.peek() |
| && segment.kind().is_any_identifier() |
| { |
| segments_len += 1; |
| is_test = segment.text() == "test"; |
| second_segment = Some(segment.clone()); |
| path_range = TextRange::new(path_range.start(), segment.text_range().end()); |
| iter.next(); |
| } |
| |
| let segments = |first, second| { |
| let mut segments = ArrayVec::new(); |
| if segments_len <= 2 { |
| segments.push(first); |
| if let Some(second) = second { |
| segments.push(second); |
| } |
| } |
| segments |
| }; |
| let meta = match iter.peek() { |
| Some(NodeOrToken::Token(eq)) if eq.kind() == T![=] => { |
| iter.next(); |
| let value = match iter.peek() { |
| Some(NodeOrToken::Token(token)) if token.kind().is_literal() => { |
| // No need to consume it, it will be consumed by `extract_and_eat_comma()`. |
| Some(token.clone()) |
| } |
| _ => None, |
| }; |
| let name = if second_segment.is_none() { Some(first_segment) } else { None }; |
| Meta::NamedKeyValue { path_range, name, value } |
| } |
| Some(NodeOrToken::Node(tt)) => Meta::TokenTree { |
| path: AttrPath { |
| segments: segments(first_segment, second_segment), |
| range: path_range, |
| is_test, |
| }, |
| tt: tt.clone(), |
| }, |
| _ => Meta::Path { |
| path: AttrPath { |
| segments: segments(first_segment, second_segment), |
| range: path_range, |
| is_test, |
| }, |
| }, |
| }; |
| Some((meta, start_offset)) |
| } |
| |
| fn extract_possibly_unsafe( |
| iter: &mut Peekable<TokenTreeChildren>, |
| container: &ast::TokenTree, |
| ) -> Option<(Self, TextRange)> { |
| if iter.peek().is_some_and(|it| it.as_token().is_some_and(|it| it.kind() == T![unsafe])) { |
| iter.next(); |
| let tt = iter.next()?.into_node()?; |
| let result = Self::extract(&mut TokenTreeChildren::new(&tt).peekable()).map( |
| |(meta, start_offset)| (meta, TextRange::new(start_offset, tt_end_offset(&tt))), |
| ); |
| while iter.next().is_some_and(|it| it.as_token().is_none_or(|it| it.kind() != T![,])) {} |
| result |
| } else { |
| Self::extract(iter).map(|(meta, start_offset)| { |
| let end_offset = 'find_end_offset: { |
| for it in iter { |
| if let NodeOrToken::Token(it) = it |
| && it.kind() == T![,] |
| { |
| break 'find_end_offset it.text_range().start(); |
| } |
| } |
| tt_end_offset(container) |
| }; |
| (meta, TextRange::new(start_offset, end_offset)) |
| }) |
| } |
| } |
| } |
| |
| fn tt_end_offset(tt: &ast::TokenTree) -> TextSize { |
| tt.syntax().last_token().unwrap().text_range().start() |
| } |
| |
| /// The callback is passed a desugared form of the attribute ([`Meta`]), a [`SyntaxNode`] fully containing it |
| /// (note: it may not be the direct parent), the range within the [`SyntaxNode`] bounding the attribute, |
| /// and the outermost `ast::Attr`. Note that one node may map to multiple [`Meta`]s due to `cfg_attr`. |
| #[inline] |
| pub fn expand_cfg_attr<'a, BreakValue>( |
| attrs: impl Iterator<Item = ast::Attr>, |
| cfg_options: impl FnMut() -> &'a CfgOptions, |
| mut callback: impl FnMut(Meta, &SyntaxNode, TextRange, &ast::Attr) -> ControlFlow<BreakValue>, |
| ) -> Option<BreakValue> { |
| expand_cfg_attr_with_doc_comments::<Infallible, _>( |
| attrs.map(Either::Left), |
| cfg_options, |
| move |Either::Left((meta, container, range, top_attr))| { |
| callback(meta, container, range, top_attr) |
| }, |
| ) |
| } |
| |
| #[inline] |
| pub fn expand_cfg_attr_with_doc_comments<'a, DocComment, BreakValue>( |
| mut attrs: impl Iterator<Item = Either<ast::Attr, DocComment>>, |
| mut cfg_options: impl FnMut() -> &'a CfgOptions, |
| mut callback: impl FnMut( |
| Either<(Meta, &SyntaxNode, TextRange, &ast::Attr), DocComment>, |
| ) -> ControlFlow<BreakValue>, |
| ) -> Option<BreakValue> { |
| let mut stack = SmallVec::<[_; 1]>::new(); |
| let result = attrs.try_for_each(|top_attr| { |
| let top_attr = match top_attr { |
| Either::Left(it) => it, |
| Either::Right(comment) => return callback(Either::Right(comment)), |
| }; |
| if let Some((attr_name, tt)) = top_attr.as_simple_call() |
| && attr_name == "cfg_attr" |
| { |
| let mut tt_iter = TokenTreeChildren::new(&tt).peekable(); |
| let cfg = cfg::CfgExpr::parse_from_ast(&mut tt_iter); |
| if cfg_options().check(&cfg) != Some(false) { |
| stack.push((tt_iter, tt)); |
| while let Some((tt_iter, tt)) = stack.last_mut() { |
| let Some((attr, range)) = Meta::extract_possibly_unsafe(tt_iter, tt) else { |
| stack.pop(); |
| continue; |
| }; |
| if let Meta::TokenTree { path, tt: nested_tt } = &attr |
| && path.is1("cfg_attr") |
| { |
| let mut nested_tt_iter = TokenTreeChildren::new(nested_tt).peekable(); |
| let cfg = cfg::CfgExpr::parse_from_ast(&mut nested_tt_iter); |
| if cfg_options().check(&cfg) != Some(false) { |
| stack.push((nested_tt_iter, nested_tt.clone())); |
| } |
| } else { |
| callback(Either::Left((attr, tt.syntax(), range, &top_attr)))?; |
| } |
| } |
| } |
| } else if let Some(ast_meta) = top_attr.meta() |
| && let Some(path) = ast_meta.path() |
| { |
| let path = AttrPath::extract(&path); |
| let meta = if let Some(tt) = ast_meta.token_tree() { |
| Meta::TokenTree { path, tt } |
| } else if let Some(value) = ast_meta.expr() { |
| let value = |
| if let ast::Expr::Literal(value) = value { Some(value.token()) } else { None }; |
| let name = |
| if path.segments.len() == 1 { Some(path.segments[0].clone()) } else { None }; |
| Meta::NamedKeyValue { name, value, path_range: path.range } |
| } else { |
| Meta::Path { path } |
| }; |
| callback(Either::Left(( |
| meta, |
| ast_meta.syntax(), |
| ast_meta.syntax().text_range(), |
| &top_attr, |
| )))?; |
| } |
| ControlFlow::Continue(()) |
| }); |
| result.break_value() |
| } |
| |
| #[inline] |
| pub(crate) fn is_item_tree_filtered_attr(name: &str) -> bool { |
| matches!( |
| name, |
| "doc" |
| | "stable" |
| | "unstable" |
| | "target_feature" |
| | "allow" |
| | "expect" |
| | "warn" |
| | "deny" |
| | "forbid" |
| | "repr" |
| | "inline" |
| | "track_caller" |
| | "must_use" |
| ) |
| } |
| |
| /// This collects attributes exactly as the item tree needs them. This is used for the item tree, |
| /// as well as for resolving [`AttrId`]s. |
| pub fn collect_item_tree_attrs<'a, BreakValue>( |
| owner: &dyn ast::HasAttrs, |
| cfg_options: impl Fn() -> &'a CfgOptions, |
| mut on_attr: impl FnMut(Meta, &SyntaxNode, &ast::Attr, TextRange) -> ControlFlow<BreakValue>, |
| ) -> Option<Either<BreakValue, CfgExpr>> { |
| let attrs = ast::attrs_including_inner(owner); |
| expand_cfg_attr( |
| attrs, |
| || cfg_options(), |
| |attr, container, range, top_attr| { |
| // We filter builtin attributes that we don't need for nameres, because this saves memory. |
| // I only put the most common attributes, but if some attribute becomes common feel free to add it. |
| // Notice, however: for an attribute to be filtered out, it *must* not be shadowable with a macro! |
| let filter = match &attr { |
| Meta::NamedKeyValue { name: Some(name), .. } => { |
| is_item_tree_filtered_attr(name.text()) |
| } |
| Meta::TokenTree { path, tt } if path.segments.len() == 1 => { |
| let name = path.segments[0].text(); |
| if name == "cfg" { |
| let cfg = |
| CfgExpr::parse_from_ast(&mut TokenTreeChildren::new(tt).peekable()); |
| if cfg_options().check(&cfg) == Some(false) { |
| return ControlFlow::Break(Either::Right(cfg)); |
| } |
| true |
| } else { |
| is_item_tree_filtered_attr(name) |
| } |
| } |
| Meta::Path { path } => { |
| path.segments.len() == 1 && is_item_tree_filtered_attr(path.segments[0].text()) |
| } |
| _ => false, |
| }; |
| if !filter && let ControlFlow::Break(v) = on_attr(attr, container, top_attr, range) { |
| return ControlFlow::Break(Either::Left(v)); |
| } |
| ControlFlow::Continue(()) |
| }, |
| ) |
| } |
| |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct Attr { |
| pub path: Interned<ModPath>, |
| pub input: Option<Box<AttrInput>>, |
| pub ctxt: SyntaxContext, |
| } |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| pub enum AttrInput { |
| /// `#[attr = "string"]` |
| Literal(tt::Literal), |
| /// `#[attr(subtree)]` |
| TokenTree(tt::TopSubtree), |
| } |
| |
| impl fmt::Display for AttrInput { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| AttrInput::Literal(lit) => write!(f, " = {lit}"), |
| AttrInput::TokenTree(tt) => tt.fmt(f), |
| } |
| } |
| } |
| |
| impl Attr { |
| /// #[path = "string"] |
| pub fn string_value(&self) -> Option<&Symbol> { |
| match self.input.as_deref()? { |
| AttrInput::Literal(tt::Literal { |
| symbol: text, |
| kind: tt::LitKind::Str | tt::LitKind::StrRaw(_), |
| .. |
| }) => Some(text), |
| _ => None, |
| } |
| } |
| |
| /// #[path = "string"] |
| pub fn string_value_with_span(&self) -> Option<(&Symbol, span::Span)> { |
| match self.input.as_deref()? { |
| AttrInput::Literal(tt::Literal { |
| symbol: text, |
| kind: tt::LitKind::Str | tt::LitKind::StrRaw(_), |
| span, |
| suffix: _, |
| }) => Some((text, *span)), |
| _ => None, |
| } |
| } |
| |
| pub fn string_value_unescape(&self) -> Option<Cow<'_, str>> { |
| match self.input.as_deref()? { |
| AttrInput::Literal(tt::Literal { |
| symbol: text, kind: tt::LitKind::StrRaw(_), .. |
| }) => Some(Cow::Borrowed(text.as_str())), |
| AttrInput::Literal(tt::Literal { symbol: text, kind: tt::LitKind::Str, .. }) => { |
| unescape(text.as_str()) |
| } |
| _ => None, |
| } |
| } |
| |
| /// #[path(ident)] |
| pub fn single_ident_value(&self) -> Option<&tt::Ident> { |
| match self.input.as_deref()? { |
| AttrInput::TokenTree(tt) => match tt.token_trees().flat_tokens() { |
| [tt::TokenTree::Leaf(tt::Leaf::Ident(ident))] => Some(ident), |
| _ => None, |
| }, |
| _ => None, |
| } |
| } |
| |
| /// #[path TokenTree] |
| pub fn token_tree_value(&self) -> Option<&TopSubtree> { |
| match self.input.as_deref()? { |
| AttrInput::TokenTree(tt) => Some(tt), |
| _ => None, |
| } |
| } |
| |
| /// Parses this attribute as a token tree consisting of comma separated paths. |
| pub fn parse_path_comma_token_tree<'a>( |
| &'a self, |
| db: &'a dyn ExpandDatabase, |
| ) -> Option<impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> + 'a> { |
| let args = self.token_tree_value()?; |
| |
| if args.top_subtree().delimiter.kind != DelimiterKind::Parenthesis { |
| return None; |
| } |
| Some(parse_path_comma_token_tree(db, args)) |
| } |
| } |
| |
| fn parse_path_comma_token_tree<'a>( |
| db: &'a dyn ExpandDatabase, |
| args: &'a tt::TopSubtree, |
| ) -> impl Iterator<Item = (ModPath, Span, tt::TokenTreesView<'a>)> { |
| args.token_trees() |
| .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. })))) |
| .filter_map(move |tts| { |
| let span = tts.flat_tokens().first()?.first_span(); |
| Some((ModPath::from_tt(db, tts)?, span, tts)) |
| }) |
| } |
| |
| fn unescape(s: &str) -> Option<Cow<'_, str>> { |
| let mut buf = String::new(); |
| let mut prev_end = 0; |
| let mut has_error = false; |
| unescape::unescape_str(s, |char_range, unescaped_char| { |
| match (unescaped_char, buf.capacity() == 0) { |
| (Ok(c), false) => buf.push(c), |
| (Ok(_), true) if char_range.len() == 1 && char_range.start == prev_end => { |
| prev_end = char_range.end |
| } |
| (Ok(c), true) => { |
| buf.reserve_exact(s.len()); |
| buf.push_str(&s[..prev_end]); |
| buf.push(c); |
| } |
| (Err(_), _) => has_error = true, |
| } |
| }); |
| |
| match (has_error, buf.capacity() == 0) { |
| (true, _) => None, |
| (false, false) => Some(Cow::Owned(buf)), |
| (false, true) => Some(Cow::Borrowed(s)), |
| } |
| } |
| |
| /// This is an index of an attribute *that always points to the item tree attributes*. |
| /// |
| /// Outer attributes are counted first, then inner attributes. This does not support |
| /// out-of-line modules, which may have attributes spread across 2 files! |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct AttrId { |
| id: u32, |
| } |
| |
| impl AttrId { |
| #[inline] |
| pub fn from_item_tree_index(id: u32) -> Self { |
| Self { id } |
| } |
| |
| #[inline] |
| pub fn item_tree_index(self) -> u32 { |
| self.id |
| } |
| |
| /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due |
| /// to `cfg_attr`), a `SyntaxNode` guaranteed to contain the attribute, the full range of the |
| /// attribute, and its desugared [`Meta`]. |
| pub fn find_attr_range<N: ast::HasAttrs>( |
| self, |
| db: &dyn ExpandDatabase, |
| krate: Crate, |
| owner: AstId<N>, |
| ) -> (ast::Attr, SyntaxNode, TextRange, Meta) { |
| self.find_attr_range_with_source(db, krate, &owner.to_node(db)) |
| } |
| |
| /// Returns the containing `ast::Attr` (note that it may contain other attributes as well due |
| /// to `cfg_attr`), a `SyntaxNode` guaranteed to contain the attribute, the full range of the |
| /// attribute, and its desugared [`Meta`]. |
| pub fn find_attr_range_with_source( |
| self, |
| db: &dyn ExpandDatabase, |
| krate: Crate, |
| owner: &dyn ast::HasAttrs, |
| ) -> (ast::Attr, SyntaxNode, TextRange, Meta) { |
| let cfg_options = OnceCell::new(); |
| let mut index = 0; |
| let result = collect_item_tree_attrs( |
| owner, |
| || cfg_options.get_or_init(|| krate.cfg_options(db)), |
| |meta, container, top_attr, range| { |
| if index == self.id { |
| return ControlFlow::Break((top_attr.clone(), container.clone(), range, meta)); |
| } |
| index += 1; |
| ControlFlow::Continue(()) |
| }, |
| ); |
| match result { |
| Some(Either::Left(it)) => it, |
| _ => { |
| panic!("used an incorrect `AttrId`; crate={krate:?}, attr_id={self:?}"); |
| } |
| } |
| } |
| |
| pub fn find_derive_range( |
| self, |
| db: &dyn ExpandDatabase, |
| krate: Crate, |
| owner: AstId<ast::Adt>, |
| derive_index: u32, |
| ) -> TextRange { |
| let (_, _, derive_attr_range, derive_attr) = self.find_attr_range(db, krate, owner); |
| let Meta::TokenTree { tt, .. } = derive_attr else { |
| return derive_attr_range; |
| }; |
| // Fake the span map, as we don't really need spans here, just the offsets of the node in the file. |
| let span_map = RealSpanMap::absolute(span::EditionedFileId::current_edition( |
| span::FileId::from_raw(0), |
| )); |
| let tt = syntax_bridge::syntax_node_to_token_tree( |
| tt.syntax(), |
| SpanMapRef::RealSpanMap(&span_map), |
| span_map.span_for_range(tt.syntax().text_range()), |
| DocCommentDesugarMode::ProcMacro, |
| ); |
| let Some((_, _, derive_tts)) = |
| parse_path_comma_token_tree(db, &tt).nth(derive_index as usize) |
| else { |
| return derive_attr_range; |
| }; |
| let (Some(first_tt), Some(last_tt)) = |
| (derive_tts.flat_tokens().first(), derive_tts.flat_tokens().last()) |
| else { |
| return derive_attr_range; |
| }; |
| let start = first_tt.first_span().range.start(); |
| let end = match last_tt { |
| tt::TokenTree::Leaf(it) => it.span().range.end(), |
| tt::TokenTree::Subtree(it) => it.delimiter.close.range.end(), |
| }; |
| TextRange::new(start, end) |
| } |
| } |