Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/hir-expand/src/attrs.rs')
| -rw-r--r-- | crates/hir-expand/src/attrs.rs | 400 |
1 files changed, 112 insertions, 288 deletions
diff --git a/crates/hir-expand/src/attrs.rs b/crates/hir-expand/src/attrs.rs index e3f10b2129..49baecb90c 100644 --- a/crates/hir-expand/src/attrs.rs +++ b/crates/hir-expand/src/attrs.rs @@ -4,20 +4,8 @@ //! [`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. +//! (`#[cfg_attr(predicate, attr1, attr2, ...)]`). [`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 @@ -26,26 +14,18 @@ //! 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 std::{borrow::Cow, cell::OnceCell, convert::Infallible, fmt, ops::ControlFlow}; -use ::tt::{TextRange, TextSize}; -use arrayvec::ArrayVec; +use ::tt::TextRange; use base_db::Crate; use cfg::{CfgExpr, CfgOptions}; use either::Either; use intern::Interned; use itertools::Itertools; 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::{AstNode, SmolStr, ast, unescape}; use syntax_bridge::DocCommentDesugarMode; use crate::{ @@ -56,207 +36,75 @@ use crate::{ 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, +pub trait AstPathExt { + fn is1(&self, segment: &str) -> bool; + + fn as_one_segment(&self) -> Option<SmolStr>; + + fn as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)>; } -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, - } +impl AstPathExt for ast::Path { + fn is1(&self, segment: &str) -> bool { + self.as_one_segment().is_some_and(|it| it == segment) } - #[inline] - pub fn is1(&self, segment: &str) -> bool { - self.segments.len() == 1 && self.segments[0].text() == segment + fn as_one_segment(&self) -> Option<SmolStr> { + Some(self.as_single_name_ref()?.text().into()) } -} -#[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, - }, + fn as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)> { + let parent = self.qualifier().as_one_segment(); + let this = self.segment()?.name_ref()?.text().into(); + if let Some(parent) = parent { Some((parent, Some(this))) } else { Some((this, None)) } + } } -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, - } +impl AstPathExt for Option<ast::Path> { + fn is1(&self, segment: &str) -> bool { + self.as_ref().is_some_and(|it| it.is1(segment)) } - 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(); - } + fn as_one_segment(&self) -> Option<SmolStr> { + self.as_ref().and_then(|it| it.as_one_segment()) + } - 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 as_up_to_two_segment(&self) -> Option<(SmolStr, Option<SmolStr>)> { + self.as_ref().and_then(|it| it.as_up_to_two_segment()) } +} + +pub trait AstKeyValueMetaExt { + fn value_string(&self) -> Option<SmolStr>; +} - 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 +impl AstKeyValueMetaExt for ast::KeyValueMeta { + fn value_string(&self) -> Option<SmolStr> { + if let Some(ast::Expr::Literal(value)) = self.expr() + && let ast::LiteralKind::String(value) = value.kind() + && let Ok(value) = value.value() + { + Some((*value).into()) } 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)) - }) + None } } } -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`. +/// The callback is passed the attribute and the outermost `ast::Attr`. +/// Note that one node may map to multiple [`Meta`]s due to `cfg_attr`. +/// +/// `unsafe(attr)` are passed the inner attribute for now. #[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>, + mut callback: impl FnMut(ast::Meta, 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) - }, + move |Either::Left((meta, top_attr))| callback(meta, top_attr), ) } @@ -264,66 +112,47 @@ pub fn expand_cfg_attr<'a, BreakValue>( 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>, + mut callback: impl FnMut(Either<(ast::Meta, 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)))?; + loop { + let (mut meta, top_attr) = if let Some(it) = stack.pop() { + it + } else { + let attr = attrs.next()?; + match attr { + Either::Left(attr) => { + let Some(meta) = attr.meta() else { continue }; + stack.push((meta, attr)); + } + Either::Right(doc_comment) => { + if let ControlFlow::Break(break_value) = callback(Either::Right(doc_comment)) { + return Some(break_value); } } } - } 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, - )))?; + continue; + }; + + while let ast::Meta::UnsafeMeta(unsafe_meta) = &meta { + let Some(inner) = unsafe_meta.meta() else { continue }; + meta = inner; } - ControlFlow::Continue(()) - }); - result.break_value() + + if let ast::Meta::CfgAttrMeta(meta) = meta { + let Some(cfg_predicate) = meta.cfg_predicate() else { continue }; + let cfg_predicate = CfgExpr::parse_from_ast(cfg_predicate); + if cfg_options().check(&cfg_predicate) != Some(false) { + let prev_stack_len = stack.len(); + stack.extend(meta.metas().map(|meta| (meta, top_attr.clone()))); + stack[prev_stack_len..].reverse(); + } + } else { + if let ControlFlow::Break(break_value) = callback(Either::Left((meta, top_attr))) { + return Some(break_value); + } + } + } } #[inline] @@ -351,39 +180,33 @@ pub(crate) fn is_item_tree_filtered_attr(name: &str) -> bool { 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>, + mut on_attr: impl FnMut(ast::Meta, ast::Attr) -> ControlFlow<BreakValue>, ) -> Option<Either<BreakValue, CfgExpr>> { let attrs = ast::attrs_including_inner(owner); expand_cfg_attr( attrs, || cfg_options(), - |attr, container, range, top_attr| { + |attr, 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) + ast::Meta::CfgMeta(attr) => { + let Some(cfg_predicate) = attr.cfg_predicate() else { + return ControlFlow::Continue(()); + }; + let cfg = CfgExpr::parse_from_ast(cfg_predicate); + if cfg_options().check(&cfg) == Some(false) { + return ControlFlow::Break(Either::Right(cfg)); } + true } - Meta::Path { path } => { - path.segments.len() == 1 && is_item_tree_filtered_attr(path.segments[0].text()) - } - _ => false, + _ => attr + .path() + .and_then(|path| path.as_one_segment()) + .is_some_and(|segment| is_item_tree_filtered_attr(&segment)), }; - if !filter && let ControlFlow::Break(v) = on_attr(attr, container, top_attr, range) { + if !filter && let ControlFlow::Break(v) = on_attr(attr, top_attr) { return ControlFlow::Break(Either::Left(v)); } ControlFlow::Continue(()) @@ -540,34 +363,32 @@ impl AttrId { } /// 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`]. + /// to `cfg_attr`) and its [`ast::Meta`]. pub fn find_attr_range<N: ast::HasAttrs>( self, db: &dyn ExpandDatabase, krate: Crate, owner: AstId<N>, - ) -> (ast::Attr, SyntaxNode, TextRange, Meta) { + ) -> (ast::Attr, ast::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`]. + /// to `cfg_attr`) and its [`ast::Meta`]. pub fn find_attr_range_with_source( self, db: &dyn ExpandDatabase, krate: Crate, owner: &dyn ast::HasAttrs, - ) -> (ast::Attr, SyntaxNode, TextRange, Meta) { + ) -> (ast::Attr, ast::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| { + |meta, top_attr| { if index == self.id { - return ControlFlow::Break((top_attr.clone(), container.clone(), range, meta)); + return ControlFlow::Break((top_attr, meta)); } index += 1; ControlFlow::Continue(()) @@ -588,9 +409,12 @@ impl AttrId { 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; + let (_, derive_attr) = self.find_attr_range(db, krate, owner); + let ast::Meta::TokenTreeMeta(derive_attr) = derive_attr else { + return derive_attr.syntax().text_range(); + }; + let Some(tt) = derive_attr.token_tree() else { + return derive_attr.syntax().text_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( @@ -605,11 +429,11 @@ impl AttrId { let Some((_, _, derive_tts)) = parse_path_comma_token_tree(db, &tt).nth(derive_index as usize) else { - return derive_attr_range; + return derive_attr.syntax().text_range(); }; let (Some(first_span), Some(last_span)) = (derive_tts.first_span(), derive_tts.last_span()) else { - return derive_attr_range; + return derive_attr.syntax().text_range(); }; let start = first_span.range.start(); let end = last_span.range.end(); |