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 | 808 |
1 files changed, 461 insertions, 347 deletions
diff --git a/crates/hir-expand/src/attrs.rs b/crates/hir-expand/src/attrs.rs index 986f8764f5..e1807cd2e1 100644 --- a/crates/hir-expand/src/attrs.rs +++ b/crates/hir-expand/src/attrs.rs @@ -1,200 +1,397 @@ -//! A higher level attributes based on TokenTree, with also some shortcuts. -use std::iter; -use std::{borrow::Cow, fmt, ops}; +//! 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, sym}; - +use intern::{Interned, Symbol}; use mbe::{DelimiterKind, Punct}; -use smallvec::{SmallVec, smallvec}; -use span::{Span, SyntaxContext}; -use syntax::unescape; -use syntax::{AstNode, AstToken, SyntaxNode, ast, match_ast}; -use syntax_bridge::{DocCommentDesugarMode, desugar_doc_comment_text, syntax_node_to_token_tree}; -use triomphe::ThinArc; +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, - name::Name, span_map::SpanMapRef, - tt::{self, TopSubtree, token_to_literal}, + tt::{self, TopSubtree}, }; -/// Syntactical attributes, without filtering of `cfg_attr`s. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RawAttrs { - // FIXME: This can become `Box<[Attr]>` if https://internals.rust-lang.org/t/layout-of-dst-box/21728?u=chrefr is accepted. - entries: Option<ThinArc<(), Attr>>, -} - -impl ops::Deref for RawAttrs { - type Target = [Attr]; - - fn deref(&self) -> &[Attr] { - match &self.entries { - Some(it) => &it.slice, - None => &[], - } - } +#[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 RawAttrs { - pub const EMPTY: Self = Self { entries: None }; - - pub fn new( - db: &dyn ExpandDatabase, - owner: &dyn ast::HasAttrs, - span_map: SpanMapRef<'_>, - ) -> Self { - let entries: Vec<_> = Self::attrs_iter::<true>(db, owner, span_map).collect(); - - let entries = if entries.is_empty() { - None - } else { - Some(ThinArc::from_header_and_iter((), entries.into_iter())) - }; - - RawAttrs { entries } - } - - /// A [`RawAttrs`] that has its `#[cfg_attr(...)]` attributes expanded. - pub fn new_expanded( - db: &dyn ExpandDatabase, - owner: &dyn ast::HasAttrs, - span_map: SpanMapRef<'_>, - cfg_options: &CfgOptions, - ) -> Self { - let entries: Vec<_> = - Self::attrs_iter_expanded::<true>(db, owner, span_map, cfg_options).collect(); - - let entries = if entries.is_empty() { - None - } else { - Some(ThinArc::from_header_and_iter((), entries.into_iter())) - }; - - RawAttrs { entries } - } - - pub fn attrs_iter<const DESUGAR_COMMENTS: bool>( - db: &dyn ExpandDatabase, - owner: &dyn ast::HasAttrs, - span_map: SpanMapRef<'_>, - ) -> impl Iterator<Item = Attr> { - collect_attrs(owner).filter_map(move |(id, attr)| match attr { - Either::Left(attr) => { - attr.meta().and_then(|meta| Attr::from_src(db, meta, span_map, id)) +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; } - Either::Right(comment) if DESUGAR_COMMENTS => comment.doc_comment().map(|doc| { - let span = span_map.span_for_range(comment.syntax().text_range()); - let (text, kind) = desugar_doc_comment_text(doc, DocCommentDesugarMode::ProcMacro); - Attr { - id, - input: Some(Box::new(AttrInput::Literal(tt::Literal { - symbol: text, - span, - kind, - suffix: None, - }))), - path: Interned::new(ModPath::from(Name::new_symbol(sym::doc, span.ctx))), - ctxt: span.ctx, + 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) } - }), - Either::Right(_) => None, - }) + } else { + segments.push(segment2); + Some(segments) + } + })(); + AttrPath { + segments: segments.unwrap_or(ArrayVec::new()), + range: path.syntax().text_range(), + is_test, + } } - pub fn attrs_iter_expanded<const DESUGAR_COMMENTS: bool>( - db: &dyn ExpandDatabase, - owner: &dyn ast::HasAttrs, - span_map: SpanMapRef<'_>, - cfg_options: &CfgOptions, - ) -> impl Iterator<Item = Attr> { - Self::attrs_iter::<DESUGAR_COMMENTS>(db, owner, span_map) - .flat_map(|attr| attr.expand_cfg_attr(db, cfg_options)) + #[inline] + pub fn is1(&self, segment: &str) -> bool { + self.segments.len() == 1 && self.segments[0].text() == segment } +} - pub fn merge(&self, other: Self) -> Self { - match (&self.entries, other.entries) { - (None, None) => Self::EMPTY, - (None, entries @ Some(_)) => Self { entries }, - (Some(entries), None) => Self { entries: Some(entries.clone()) }, - (Some(a), Some(b)) => { - let last_ast_index = a.slice.last().map_or(0, |it| it.id.ast_index() + 1); - let items = a - .slice - .iter() - .cloned() - .chain(b.slice.iter().map(|it| { - let mut it = it.clone(); - let id = it.id.ast_index() + last_ast_index; - it.id = AttrId::new(id, it.id.is_inner_attr()); - it - })) - .collect::<Vec<_>>(); - Self { entries: Some(ThinArc::from_header_and_iter((), items.into_iter())) } - } +#[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, } } - /// Processes `cfg_attr`s - pub fn expand_cfg_attr(self, db: &dyn ExpandDatabase, krate: Crate) -> RawAttrs { - let has_cfg_attrs = - self.iter().any(|attr| attr.path.as_ident().is_some_and(|name| *name == sym::cfg_attr)); - if !has_cfg_attrs { - return self; + 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 cfg_options = krate.cfg_options(db); - let new_attrs = self - .iter() - .cloned() - .flat_map(|attr| attr.expand_cfg_attr(db, cfg_options)) - .collect::<Vec<_>>(); - let entries = if new_attrs.is_empty() { - None - } else { - Some(ThinArc::from_header_and_iter((), new_attrs.into_iter())) + 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, + }, + }, }; - RawAttrs { entries } + Some((meta, start_offset)) } - pub fn is_empty(&self) -> bool { - self.entries.is_none() + 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)) + }) + } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AttrId { - id: u32, +fn tt_end_offset(tt: &ast::TokenTree) -> TextSize { + tt.syntax().last_token().unwrap().text_range().start() } -// FIXME: This only handles a single level of cfg_attr nesting -// that is `#[cfg_attr(all(), cfg_attr(all(), cfg(any())))]` breaks again -impl AttrId { - const INNER_ATTR_SET_BIT: u32 = 1 << 31; +/// 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) + }, + ) +} - pub fn new(id: usize, is_inner: bool) -> Self { - assert!(id <= !Self::INNER_ATTR_SET_BIT as usize); - let id = id as u32; - Self { id: if is_inner { id | Self::INNER_ATTR_SET_BIT } else { id } } - } +#[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() +} - pub fn ast_index(&self) -> usize { - (self.id & !Self::INNER_ATTR_SET_BIT) as usize - } +#[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" + ) +} - pub fn is_inner_attr(&self) -> bool { - self.id & Self::INNER_ATTR_SET_BIT != 0 - } +/// 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 id: AttrId, pub path: Interned<ModPath>, pub input: Option<Box<AttrInput>>, pub ctxt: SyntaxContext, @@ -218,131 +415,6 @@ impl fmt::Display for AttrInput { } impl Attr { - fn from_src( - db: &dyn ExpandDatabase, - ast: ast::Meta, - span_map: SpanMapRef<'_>, - id: AttrId, - ) -> Option<Attr> { - let path = ast.path()?; - let range = path.syntax().text_range(); - let path = Interned::new(ModPath::from_src(db, path, &mut |range| { - span_map.span_for_range(range).ctx - })?); - let span = span_map.span_for_range(range); - let input = if let Some(ast::Expr::Literal(lit)) = ast.expr() { - let token = lit.token(); - Some(Box::new(AttrInput::Literal(token_to_literal(token.text(), span)))) - } else if let Some(tt) = ast.token_tree() { - let tree = syntax_node_to_token_tree( - tt.syntax(), - span_map, - span, - DocCommentDesugarMode::ProcMacro, - ); - Some(Box::new(AttrInput::TokenTree(tree))) - } else { - None - }; - Some(Attr { id, path, input, ctxt: span.ctx }) - } - - fn from_tt( - db: &dyn ExpandDatabase, - mut tt: tt::TokenTreesView<'_>, - id: AttrId, - ) -> Option<Attr> { - if matches!(tt.flat_tokens(), - [tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident { sym, .. })), ..] - if *sym == sym::unsafe_ - ) { - match tt.iter().nth(1) { - Some(tt::TtElement::Subtree(_, iter)) => tt = iter.remaining(), - _ => return None, - } - } - let first = tt.flat_tokens().first()?; - let ctxt = first.first_span().ctx; - let (path, input) = { - let mut iter = tt.iter(); - let start = iter.savepoint(); - let mut input = tt::TokenTreesView::new(&[]); - let mut path = iter.from_savepoint(start); - let mut path_split_savepoint = iter.savepoint(); - while let Some(tt) = iter.next() { - path = iter.from_savepoint(start); - if !matches!( - tt, - tt::TtElement::Leaf( - tt::Leaf::Punct(tt::Punct { char: ':' | '$', .. }) | tt::Leaf::Ident(_), - ) - ) { - input = path_split_savepoint.remaining(); - break; - } - path_split_savepoint = iter.savepoint(); - } - (path, input) - }; - - let path = Interned::new(ModPath::from_tt(db, path)?); - - let input = match (input.flat_tokens().first(), input.try_into_subtree()) { - (_, Some(tree)) => { - Some(Box::new(AttrInput::TokenTree(tt::TopSubtree::from_subtree(tree)))) - } - (Some(tt::TokenTree::Leaf(tt::Leaf::Punct(tt::Punct { char: '=', .. }))), _) => { - match input.flat_tokens().get(1) { - Some(tt::TokenTree::Leaf(tt::Leaf::Literal(lit))) => { - Some(Box::new(AttrInput::Literal(lit.clone()))) - } - _ => None, - } - } - _ => None, - }; - Some(Attr { id, path, input, ctxt }) - } - - pub fn path(&self) -> &ModPath { - &self.path - } - - pub fn expand_cfg_attr( - self, - db: &dyn ExpandDatabase, - cfg_options: &CfgOptions, - ) -> impl IntoIterator<Item = Self> { - let is_cfg_attr = self.path.as_ident().is_some_and(|name| *name == sym::cfg_attr); - if !is_cfg_attr { - return smallvec![self]; - } - - let subtree = match self.token_tree_value() { - Some(it) => it, - _ => return smallvec![self.clone()], - }; - - let (cfg, parts) = match parse_cfg_attr_input(subtree) { - Some(it) => it, - None => return smallvec![self.clone()], - }; - let index = self.id; - let attrs = parts.filter_map(|attr| Attr::from_tt(db, attr, index)); - - let cfg = TopSubtree::from_token_trees(subtree.top_subtree().delimiter, cfg); - let cfg = CfgExpr::parse(&cfg); - if cfg_options.check(&cfg) == Some(false) { - smallvec![] - } else { - cov_mark::hit!(cfg_attr_active); - - attrs.collect::<SmallVec<[_; 1]>>() - } - } -} - -impl Attr { /// #[path = "string"] pub fn string_value(&self) -> Option<&Symbol> { match self.input.as_deref()? { @@ -403,30 +475,26 @@ impl Attr { pub fn parse_path_comma_token_tree<'a>( &'a self, db: &'a dyn ExpandDatabase, - ) -> Option<impl Iterator<Item = (ModPath, Span)> + 'a> { + ) -> 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; } - let paths = 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)) - }); - - Some(paths) + Some(parse_path_comma_token_tree(db, args)) } +} - pub fn cfg(&self) -> Option<CfgExpr> { - if *self.path.as_ident()? == sym::cfg { - self.token_tree_value().map(CfgExpr::parse) - } else { - None - } - } +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>> { @@ -455,58 +523,104 @@ fn unescape(s: &str) -> Option<Cow<'_, str>> { } } -pub fn collect_attrs( - owner: &dyn ast::HasAttrs, -) -> impl Iterator<Item = (AttrId, Either<ast::Attr, ast::Comment>)> { - let inner_attrs = - inner_attributes(owner.syntax()).into_iter().flatten().zip(iter::repeat(true)); - let outer_attrs = ast::AttrDocCommentIter::from_syntax_node(owner.syntax()) - .filter(|el| match el { - Either::Left(attr) => attr.kind().is_outer(), - Either::Right(comment) => comment.is_outer(), - }) - .zip(iter::repeat(false)); - outer_attrs - .chain(inner_attrs) - .enumerate() - .map(|(id, (attr, is_inner))| (AttrId::new(id, is_inner), attr)) +/// 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, } -fn inner_attributes( - syntax: &SyntaxNode, -) -> Option<impl Iterator<Item = Either<ast::Attr, ast::Comment>>> { - let node = match_ast! { - match syntax { - ast::SourceFile(_) => syntax.clone(), - ast::ExternBlock(it) => it.extern_item_list()?.syntax().clone(), - ast::Fn(it) => it.body()?.stmt_list()?.syntax().clone(), - ast::Impl(it) => it.assoc_item_list()?.syntax().clone(), - ast::Module(it) => it.item_list()?.syntax().clone(), - ast::BlockExpr(it) => { - if !it.may_carry_attributes() { - return None +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)); } - syntax.clone() + index += 1; + ControlFlow::Continue(()) }, - _ => return None, + ); + match result { + Some(Either::Left(it)) => it, + _ => { + panic!("used an incorrect `AttrId`; crate={krate:?}, attr_id={self:?}"); + } } - }; - - let attrs = ast::AttrDocCommentIter::from_syntax_node(&node).filter(|el| match el { - Either::Left(attr) => attr.kind().is_inner(), - Either::Right(comment) => comment.is_inner(), - }); - Some(attrs) -} + } -// Input subtree is: `(cfg, $(attr),+)` -// Split it up into a `cfg` subtree and the `attr` subtrees. -fn parse_cfg_attr_input( - subtree: &TopSubtree, -) -> Option<(tt::TokenTreesView<'_>, impl Iterator<Item = tt::TokenTreesView<'_>>)> { - let mut parts = subtree - .token_trees() - .split(|tt| matches!(tt, tt::TtElement::Leaf(tt::Leaf::Punct(Punct { char: ',', .. })))); - let cfg = parts.next()?; - Some((cfg, parts.filter(|it| !it.is_empty()))) + 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) + } } |