//! Processes out `#[cfg]` and `#[cfg_attr]` attributes from the input for the derive macro use std::{cell::OnceCell, ops::ControlFlow}; use ::tt::TextRange; use base_db::Crate; use cfg::CfgExpr; use parser::T; use smallvec::SmallVec; use syntax::{ AstNode, PreorderWithTokens, SyntaxElement, SyntaxNode, SyntaxToken, WalkEvent, ast::{self, HasAttrs, TokenTreeChildren}, }; use syntax_bridge::DocCommentDesugarMode; use crate::{ attrs::{AttrId, Meta, expand_cfg_attr, is_item_tree_filtered_attr}, db::ExpandDatabase, fixup::{self, SyntaxFixupUndoInfo}, span_map::SpanMapRef, tt::{self, DelimSpan, Span}, }; struct ItemIsCfgedOut; #[derive(Debug)] struct ExpandedAttrToProcess { range: TextRange, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum NextExpandedAttrState { NotStarted, InTheMiddle, } #[derive(Debug)] struct AstAttrToProcess { range: TextRange, expanded_attrs: SmallVec<[ExpandedAttrToProcess; 1]>, expanded_attrs_idx: usize, next_expanded_attr: NextExpandedAttrState, pound_span: Span, brackets_span: DelimSpan, /// If `Some`, this is an inner attribute. excl_span: Option, } fn macro_input_callback( db: &dyn ExpandDatabase, is_derive: bool, censor_item_tree_attr_ids: &[AttrId], krate: Crate, default_span: Span, span_map: SpanMapRef<'_>, ) -> impl FnMut(&mut PreorderWithTokens, &WalkEvent) -> (bool, Vec) { let cfg_options = OnceCell::new(); let cfg_options = move || *cfg_options.get_or_init(|| krate.cfg_options(db)); let mut should_strip_attr = { let mut item_tree_attr_id = 0; let mut censor_item_tree_attr_ids_index = 0; move || { let mut result = false; if let Some(&next_censor_attr_id) = censor_item_tree_attr_ids.get(censor_item_tree_attr_ids_index) && next_censor_attr_id.item_tree_index() == item_tree_attr_id { censor_item_tree_attr_ids_index += 1; result = true; } item_tree_attr_id += 1; result } }; let mut attrs = Vec::new(); let mut attrs_idx = 0; let mut has_inner_attrs_owner = false; let mut in_attr = false; let mut done_with_attrs = false; let mut did_top_attrs = false; move |preorder, event| { match event { WalkEvent::Enter(SyntaxElement::Node(node)) => { if done_with_attrs { return (true, Vec::new()); } if ast::Attr::can_cast(node.kind()) { in_attr = true; let node_range = node.text_range(); while attrs .get(attrs_idx) .is_some_and(|it: &AstAttrToProcess| it.range != node_range) { attrs_idx += 1; } } else if !in_attr && let Some(has_attrs) = ast::AnyHasAttrs::cast(node.clone()) { // Attributes of the form `key = value` have `ast::Expr` in them, which returns `Some` for // `AnyHasAttrs::cast()`, so we also need to check `in_attr`. if has_inner_attrs_owner { has_inner_attrs_owner = false; return (true, Vec::new()); } if did_top_attrs && !is_derive { // Derives need all attributes handled, but attribute macros need only the top attributes handled. done_with_attrs = true; return (true, Vec::new()); } did_top_attrs = true; if let Some(inner_attrs_node) = has_attrs.inner_attributes_node() && inner_attrs_node != *node { has_inner_attrs_owner = true; } let node_attrs = ast::attrs_including_inner(&has_attrs); attrs.clear(); node_attrs.clone().for_each(|attr| { let span_for = |token: Option| { token .map(|token| span_map.span_for_range(token.text_range())) .unwrap_or(default_span) }; attrs.push(AstAttrToProcess { range: attr.syntax().text_range(), pound_span: span_for(attr.pound_token()), brackets_span: DelimSpan { open: span_for(attr.l_brack_token()), close: span_for(attr.r_brack_token()), }, excl_span: attr .excl_token() .map(|token| span_map.span_for_range(token.text_range())), expanded_attrs: SmallVec::new(), expanded_attrs_idx: 0, next_expanded_attr: NextExpandedAttrState::NotStarted, }); }); attrs_idx = 0; let strip_current_item = expand_cfg_attr( node_attrs, &cfg_options, |attr, _container, range, top_attr| { // Find the attr. while attrs[attrs_idx].range != top_attr.syntax().text_range() { attrs_idx += 1; } let mut strip_current_attr = false; match attr { Meta::NamedKeyValue { name, .. } => { if name .is_none_or(|name| !is_item_tree_filtered_attr(name.text())) { strip_current_attr = should_strip_attr(); } } Meta::TokenTree { path, tt } => { if path.is1("cfg") { let cfg_expr = CfgExpr::parse_from_ast( &mut TokenTreeChildren::new(&tt).peekable(), ); if cfg_options().check(&cfg_expr) == Some(false) { return ControlFlow::Break(ItemIsCfgedOut); } strip_current_attr = true; } else if path.segments.len() != 1 || !is_item_tree_filtered_attr(path.segments[0].text()) { strip_current_attr = should_strip_attr(); } } Meta::Path { path } => { if path.segments.len() != 1 || !is_item_tree_filtered_attr(path.segments[0].text()) { strip_current_attr = should_strip_attr(); } } } if !strip_current_attr { attrs[attrs_idx] .expanded_attrs .push(ExpandedAttrToProcess { range }); } ControlFlow::Continue(()) }, ); attrs_idx = 0; if strip_current_item.is_some() { preorder.skip_subtree(); attrs.clear(); 'eat_comma: { // If there is a comma after this node, eat it too. let mut events_until_comma = 0; for event in preorder.clone() { match event { WalkEvent::Enter(SyntaxElement::Node(_)) | WalkEvent::Leave(_) => {} WalkEvent::Enter(SyntaxElement::Token(token)) => { let kind = token.kind(); if kind == T![,] { break; } else if !kind.is_trivia() { break 'eat_comma; } } } events_until_comma += 1; } preorder.nth(events_until_comma); } return (false, Vec::new()); } } } WalkEvent::Leave(SyntaxElement::Node(node)) => { if ast::Attr::can_cast(node.kind()) { in_attr = false; attrs_idx += 1; } } WalkEvent::Enter(SyntaxElement::Token(token)) => { if !in_attr { return (true, Vec::new()); } let Some(ast_attr) = attrs.get_mut(attrs_idx) else { return (true, Vec::new()); }; let token_range = token.text_range(); let Some(expanded_attr) = ast_attr.expanded_attrs.get(ast_attr.expanded_attrs_idx) else { // No expanded attributes in this `ast::Attr`, or we finished them all already, either way // the remaining tokens should be discarded. return (false, Vec::new()); }; match ast_attr.next_expanded_attr { NextExpandedAttrState::NotStarted => { if token_range.start() >= expanded_attr.range.start() { // We started the next attribute. let mut insert_tokens = Vec::with_capacity(3); insert_tokens.push(tt::Leaf::Punct(tt::Punct { char: '#', spacing: tt::Spacing::Alone, span: ast_attr.pound_span, })); if let Some(span) = ast_attr.excl_span { insert_tokens.push(tt::Leaf::Punct(tt::Punct { char: '!', spacing: tt::Spacing::Alone, span, })); } insert_tokens.push(tt::Leaf::Punct(tt::Punct { char: '[', spacing: tt::Spacing::Alone, span: ast_attr.brackets_span.open, })); ast_attr.next_expanded_attr = NextExpandedAttrState::InTheMiddle; return (true, insert_tokens); } else { // Before any attribute or between the attributes. return (false, Vec::new()); } } NextExpandedAttrState::InTheMiddle => { if token_range.start() >= expanded_attr.range.end() { // Finished the current attribute. let insert_tokens = vec![tt::Leaf::Punct(tt::Punct { char: ']', spacing: tt::Spacing::Alone, span: ast_attr.brackets_span.close, })]; ast_attr.next_expanded_attr = NextExpandedAttrState::NotStarted; ast_attr.expanded_attrs_idx += 1; // It's safe to ignore the current token because between attributes // there is always at least one token we skip - either the closing bracket // in `#[]` or the comma in case of multiple attrs in `cfg_attr` expansion. return (false, insert_tokens); } else { // Still in the middle. return (true, Vec::new()); } } } } WalkEvent::Leave(SyntaxElement::Token(_)) => {} } (true, Vec::new()) } } pub(crate) fn attr_macro_input_to_token_tree( db: &dyn ExpandDatabase, node: &SyntaxNode, span_map: SpanMapRef<'_>, span: Span, is_derive: bool, censor_item_tree_attr_ids: &[AttrId], krate: Crate, ) -> (tt::TopSubtree, SyntaxFixupUndoInfo) { let fixups = fixup::fixup_syntax(span_map, node, span, DocCommentDesugarMode::ProcMacro); ( syntax_bridge::syntax_node_to_token_tree_modified( node, span_map, fixups.append, fixups.remove, span, DocCommentDesugarMode::ProcMacro, macro_input_callback(db, is_derive, censor_item_tree_attr_ids, krate, span, span_map), ), fixups.undo_info, ) } pub fn check_cfg_attr_value( db: &dyn ExpandDatabase, attr: &ast::TokenTree, krate: Crate, ) -> Option { let cfg_expr = CfgExpr::parse_from_ast(&mut TokenTreeChildren::new(attr).peekable()); krate.cfg_options(db).check(&cfg_expr) }