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.rs400
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();