Repository: rust-analyzer/rust-analyzer
Pull request #9128 (2021-06-03 13:38:28)

feat: expand procedural attribute macros
This adds experimental support for attribute macros. They can be enabled by setting `rust-analyzer.experimental.procAttrMacros` to `true`.

Known issues:
* Tokens aren't remapped, presumably because we edit the input syntax tree (this causes IDE features to not work inside items with attribute macros on them)
* Macro errors aren't reported correctly

Closes https://github.com/rust-analyzer/rust-analyzer/issues/8971
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/8964 / https://github.com/la10736/rstest/issues/120
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/2984
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/5412
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/6029
Fixes https://github.com/rust-analyzer/rust-analyzer/issues/6687

https://github.com/rust-analyzer/rust-analyzer/issues/6740 is still not fixed – we now expand `#[proc_macro_hack]`, but fail to expand the resulting `proc_macro_call!()` macro.

jonas-schievink: bors r+
bors[bot]: Build failed:
  * [Rust (ubuntu-latest)](https://github.com/rust-analyzer/rust-analyzer/runs/2738765061)

Patch:
diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index d3ef29db4e..b43d61d0e8 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -534,6 +534,18 @@ impl Module {
                                 Some(derive_name.clone()),
                             )
                         }
+                        MacroCallKind::Attr { ast_id, invoc_attr_index, attr_name, .. } => {
+                            let node = ast_id.to_node(db.upcast());
+                            let attr =
+                                node.attrs().nth((*invoc_attr_index) as usize).unwrap_or_else(
+                                    || panic!("cannot find attribute #{}", invoc_attr_index),
+                                );
+                            (
+                                ast_id.file_id,
+                                SyntaxNodePtr::from(AstPtr::new(&attr)),
+                                Some(attr_name.clone()),
+                            )
+                        }
                     };
                     sink.push(UnresolvedProcMacro {
                         file,
@@ -558,7 +570,9 @@ impl Module {
                             let node = ast_id.to_node(db.upcast());
                             (ast_id.file_id, SyntaxNodePtr::from(AstPtr::new(&node)))
                         }
-                        MacroCallKind::Derive { ast_id, .. } => {
+                        MacroCallKind::Derive { ast_id, .. }
+                        | MacroCallKind::Attr { ast_id, .. } => {
+                            // FIXME: point to the attribute instead, this creates very large diagnostics
                             let node = ast_id.to_node(db.upcast());
                             (ast_id.file_id, SyntaxNodePtr::from(AstPtr::new(&node)))
                         }
diff --git a/crates/hir_def/src/builtin_attr.rs b/crates/hir_def/src/builtin_attr.rs
index d5d7f0f473..39c7f84f7e 100644
--- a/crates/hir_def/src/builtin_attr.rs
+++ b/crates/hir_def/src/builtin_attr.rs
@@ -2,7 +2,7 @@
 //!
 //! The actual definitions were copied from rustc's `compiler/rustc_feature/src/builtin_attrs.rs`.
 //!
-//! It was last synchronized with upstream commit 2225ee1b62ff089917434aefd9b2bf509cfa087f.
+//! It was last synchronized with upstream commit 835150e70288535bc57bb624792229b9dc94991d.
 //!
 //! The macros were adjusted to only expand to the attribute name, since that is all we need to do
 //! name resolution, and `BUILTIN_ATTRIBUTES` is almost entirely unchanged from the original, to
@@ -58,7 +58,6 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
     ungated!(reexport_test_harness_main, Normal, template!(NameValueStr: "name")),
 
     // Macros:
-    ungated!(derive, Normal, template!(List: "Trait1, Trait2, ...")),
     ungated!(automatically_derived, Normal, template!(Word)),
     // FIXME(#14407)
     ungated!(macro_use, Normal, template!(Word, List: "name1, name2, ...")),
@@ -98,8 +97,8 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         template!(List: r#"name = "...", /*opt*/ kind = "dylib|static|...", /*opt*/ wasm_import_module = "...""#),
     ),
     ungated!(link_name, AssumedUsed, template!(NameValueStr: "name")),
-    ungated!(no_link, Normal, template!(Word)),
-    ungated!(repr, Normal, template!(List: "C")),
+    ungated!(no_link, AssumedUsed, template!(Word)),
+    ungated!(repr, AssumedUsed, template!(List: "C")),
     ungated!(export_name, AssumedUsed, template!(NameValueStr: "name")),
     ungated!(link_section, AssumedUsed, template!(NameValueStr: "name")),
     ungated!(no_mangle, AssumedUsed, template!(Word)),
@@ -112,6 +111,10 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         const_eval_limit, CrateLevel, template!(NameValueStr: "N"), const_eval_limit,
         experimental!(const_eval_limit)
     ),
+    gated!(
+        move_size_limit, CrateLevel, template!(NameValueStr: "N"), large_assignments,
+        experimental!(move_size_limit)
+    ),
 
     // Entry point:
     ungated!(main, Normal, template!(Word)),
@@ -140,6 +143,7 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         template!(List: "address, memory, thread"),
         experimental!(no_sanitize)
     ),
+    gated!(no_coverage, AssumedUsed, template!(Word), experimental!(no_coverage)),
 
     // FIXME: #14408 assume docs are used since rustdoc looks at them.
     ungated!(doc, AssumedUsed, template!(List: "hidden|inline|...", NameValueStr: "string")),
@@ -150,11 +154,6 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
 
     // Linking:
     gated!(naked, AssumedUsed, template!(Word), naked_functions, experimental!(naked)),
-    gated!(
-        link_args, Normal, template!(NameValueStr: "args"),
-        "the `link_args` attribute is experimental and not portable across platforms, \
-        it is recommended to use `#[link(name = \"foo\")] instead",
-    ),
     gated!(
         link_ordinal, AssumedUsed, template!(List: "ordinal"), raw_dylib,
         experimental!(link_ordinal)
@@ -172,7 +171,7 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         "custom test frameworks are an unstable feature",
     ),
     // RFC #1268
-    gated!(marker, Normal, template!(Word), marker_trait_attr, experimental!(marker)),
+    gated!(marker, AssumedUsed, template!(Word), marker_trait_attr, experimental!(marker)),
     gated!(
         thread_local, AssumedUsed, template!(Word),
         "`#[thread_local]` is an experimental feature, and does not currently handle destructors",
@@ -291,7 +290,7 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
     // Internal attributes, Macro related:
     // ==========================================================================
 
-    rustc_attr!(rustc_builtin_macro, AssumedUsed, template!(Word), IMPL_DETAIL),
+    rustc_attr!(rustc_builtin_macro, AssumedUsed, template!(Word, NameValueStr: "name"), IMPL_DETAIL),
     rustc_attr!(rustc_proc_macro_decls, Normal, template!(Word), INTERNAL_UNSTABLE),
     rustc_attr!(
         rustc_macro_transparency, AssumedUsed,
@@ -319,7 +318,7 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
     // ==========================================================================
 
     rustc_attr!(rustc_promotable, AssumedUsed, template!(Word), IMPL_DETAIL),
-    rustc_attr!(rustc_args_required_const, AssumedUsed, template!(List: "N"), INTERNAL_UNSTABLE),
+    rustc_attr!(rustc_legacy_const_generics, AssumedUsed, template!(List: "N"), INTERNAL_UNSTABLE),
 
     // ==========================================================================
     // Internal attributes, Layout related:
@@ -380,6 +379,15 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         rustc_specialization_trait, Normal, template!(Word),
         "the `#[rustc_specialization_trait]` attribute is used to check specializations"
     ),
+    rustc_attr!(
+        rustc_main, Normal, template!(Word),
+        "the `#[rustc_main]` attribute is used internally to specify test entry point function",
+    ),
+    rustc_attr!(
+        rustc_skip_array_during_method_dispatch, Normal, template!(Word),
+        "the `#[rustc_skip_array_during_method_dispatch]` attribute is used to exclude a trait \
+        from method dispatch when the receiver is an array, for compatibility in editions < 2021."
+    ),
 
     // ==========================================================================
     // Internal attributes, Testing:
@@ -387,6 +395,7 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
 
     rustc_attr!(TEST, rustc_outlives, Normal, template!(Word)),
     rustc_attr!(TEST, rustc_capture_analysis, Normal, template!(Word)),
+    rustc_attr!(TEST, rustc_insignificant_dtor, Normal, template!(Word)),
     rustc_attr!(TEST, rustc_variance, Normal, template!(Word)),
     rustc_attr!(TEST, rustc_layout, Normal, template!(List: "field1, field2, ...")),
     rustc_attr!(TEST, rustc_regions, Normal, template!(Word)),
@@ -395,12 +404,9 @@ pub const INERT_ATTRIBUTES: &[BuiltinAttribute] = &[
         template!(Word, List: "delay_span_bug_from_inside_query")
     ),
     rustc_attr!(TEST, rustc_dump_user_substs, AssumedUsed, template!(Word)),
+    rustc_attr!(TEST, rustc_evaluate_where_clauses, AssumedUsed, template!(Word)),
     rustc_attr!(TEST, rustc_if_this_changed, AssumedUsed, template!(Word, List: "DepNode")),
     rustc_attr!(TEST, rustc_then_this_would_need, AssumedUsed, template!(List: "DepNode")),
-    rustc_attr!(
-        TEST, rustc_dirty, AssumedUsed,
-        template!(List: r#"cfg = "...", /*opt*/ label = "...", /*opt*/ except = "...""#),
-    ),
     rustc_attr!(
         TEST, rustc_clean, AssumedUsed,
         template!(List: r#"cfg = "...", /*opt*/ label = "...", /*opt*/ except = "...""#),
diff --git a/crates/hir_def/src/db.rs b/crates/hir_def/src/db.rs
index 7eadc8e0d6..c977971cd4 100644
--- a/crates/hir_def/src/db.rs
+++ b/crates/hir_def/src/db.rs
@@ -51,6 +51,9 @@ pub trait InternDatabase: SourceDatabase {
 
 #[salsa::query_group(DefDatabaseStorage)]
 pub trait DefDatabase: InternDatabase + AstDatabase + Upcast<dyn AstDatabase> {
+    #[salsa::input]
+    fn enable_proc_attr_macros(&self) -> bool;
+
     #[salsa::invoke(ItemTree::file_item_tree_query)]
     fn file_item_tree(&self, file_id: HirFileId) -> Arc<ItemTree>;
 
diff --git a/crates/hir_def/src/lib.rs b/crates/hir_def/src/lib.rs
index 9aa95720af..987485accc 100644
--- a/crates/hir_def/src/lib.rs
+++ b/crates/hir_def/src/lib.rs
@@ -55,6 +55,7 @@ use std::{
     sync::Arc,
 };
 
+use attr::Attr;
 use base_db::{impl_intern_key, salsa, CrateId};
 use hir_expand::{
     ast_id_map::FileAstId,
@@ -768,3 +769,42 @@ fn derive_macro_as_call_id(
         .into();
     Ok(res)
 }
+
+fn attr_macro_as_call_id(
+    item_attr: &AstIdWithPath<ast::Item>,
+    macro_attr: &Attr,
+    db: &dyn db::DefDatabase,
+    krate: CrateId,
+    resolver: impl Fn(path::ModPath) -> Option<MacroDefId>,
+) -> Result<MacroCallId, UnresolvedMacro> {
+    let def: MacroDefId = resolver(item_attr.path.clone())
+        .ok_or_else(|| UnresolvedMacro { path: item_attr.path.clone() })?;
+    let last_segment = item_attr
+        .path
+        .segments()
+        .last()
+        .ok_or_else(|| UnresolvedMacro { path: item_attr.path.clone() })?;
+    let mut arg = match &macro_attr.input {
+        Some(input) => match &**input {
+            attr::AttrInput::Literal(_) => tt::Subtree::default(),
+            attr::AttrInput::TokenTree(tt) => tt.clone(),
+        },
+        None => tt::Subtree::default(),
+    };
+    // The parentheses are always disposed here.
+    arg.delimiter = None;
+
+    let res = def
+        .as_lazy_macro(
+            db.upcast(),
+            krate,
+            MacroCallKind::Attr {
+                ast_id: item_attr.ast_id,
+                attr_name: last_segment.to_string(),
+                attr_args: arg,
+                invoc_attr_index: macro_attr.id.ast_index,
+            },
+        )
+        .into();
+    Ok(res)
+}
diff --git a/crates/hir_def/src/nameres/collector.rs b/crates/hir_def/src/nameres/collector.rs
index 6b41921ae4..b2ce739bdb 100644
--- a/crates/hir_def/src/nameres/collector.rs
+++ b/crates/hir_def/src/nameres/collector.rs
@@ -23,7 +23,7 @@ use syntax::ast;
 
 use crate::{
     attr::{Attr, AttrId, AttrInput, Attrs},
-    builtin_attr,
+    attr_macro_as_call_id, builtin_attr,
     db::DefDatabase,
     derive_macro_as_call_id,
     intern::Interned,
@@ -223,7 +223,7 @@ struct MacroDirective {
 enum MacroDirectiveKind {
     FnLike { ast_id: AstIdWithPath<ast::MacroCall>, fragment: FragmentKind },
     Derive { ast_id: AstIdWithPath<ast::Item>, derive_attr: AttrId },
-    Attr { ast_id: AstIdWithPath<ast::Item>, attr: AttrId, mod_item: ModItem },
+    Attr { ast_id: AstIdWithPath<ast::Item>, attr: Attr, mod_item: ModItem },
 }
 
 struct DefData<'a> {
@@ -419,7 +419,7 @@ impl DefCollector<'_> {
         let mut unresolved_macros = std::mem::replace(&mut self.unresolved_macros, Vec::new());
         let pos = unresolved_macros.iter().position(|directive| {
             if let MacroDirectiveKind::Attr { ast_id, mod_item, attr } = &directive.kind {
-                self.skip_attrs.insert(ast_id.ast_id.with_value(*mod_item), *attr);
+                self.skip_attrs.insert(ast_id.ast_id.with_value(*mod_item), attr.id);
 
                 let file_id = ast_id.ast_id.file_id;
                 let item_tree = self.db.file_item_tree(file_id);
@@ -1050,7 +1050,7 @@ impl DefCollector<'_> {
                                 let file_id = ast_id.ast_id.file_id;
                                 let item_tree = self.db.file_item_tree(file_id);
                                 let mod_dir = self.mod_dirs[&directive.module_id].clone();
-                                self.skip_attrs.insert(InFile::new(file_id, *mod_item), *attr);
+                                self.skip_attrs.insert(InFile::new(file_id, *mod_item), attr.id);
                                 ModCollector {
                                     def_collector: &mut *self,
                                     macro_depth: directive.depth,
@@ -1067,8 +1067,56 @@ impl DefCollector<'_> {
                         }
                     }
 
+                    if !self.db.enable_proc_attr_macros() {
+                        return true;
+                    }
+
                     // Not resolved to a derive helper, so try to resolve as a macro.
-                    // FIXME: not yet :)
+                    match attr_macro_as_call_id(
+                        ast_id,
+                        attr,
+                        self.db,
+                        self.def_map.krate,
+                        &resolver,
+                    ) {
+                        Ok(call_id) => {
+                            let loc: MacroCallLoc = self.db.lookup_intern_macro(call_id);
+                            if let MacroDefKind::ProcMacro(exp, ..) = &loc.def.kind {
+                                if exp.is_dummy() {
+                                    // Proc macros that cannot be expanded are treated as not
+                                    // resolved, in order to fall back later.
+                                    self.def_map.diagnostics.push(
+                                        DefDiagnostic::unresolved_proc_macro(
+                                            directive.module_id,
+                                            loc.kind,
+                                        ),
+                                    );
+
+                                    let file_id = ast_id.ast_id.file_id;
+                                    let item_tree = self.db.file_item_tree(file_id);
+                                    let mod_dir = self.mod_dirs[&directive.module_id].clone();
+                                    self.skip_attrs
+                                        .insert(InFile::new(file_id, *mod_item), attr.id);
+                                    ModCollector {
+                                        def_collector: &mut *self,
+                                        macro_depth: directive.depth,
+                                        module_id: directive.module_id,
+                                        file_id,
+                                        item_tree: &item_tree,
+                                        mod_dir,
+                                    }
+                                    .collect(&[*mod_item]);
+
+                                    // Remove the macro directive.
+                                    return false;
+                                }
+                            }
+                            resolved.push((directive.module_id, call_id, directive.depth));
+                            res = ReachedFixedPoint::No;
+                            return false;
+                        }
+                        Err(UnresolvedMacro { .. }) => (),
+                    }
                 }
             }
 
@@ -1628,7 +1676,7 @@ impl ModCollector<'_, '_> {
                 self.def_collector.unresolved_macros.push(MacroDirective {
                     module_id: self.module_id,
                     depth: self.macro_depth + 1,
-                    kind: MacroDirectiveKind::Attr { ast_id, attr: attr.id, mod_item },
+                    kind: MacroDirectiveKind::Attr { ast_id, attr: attr.clone(), mod_item },
                 });
 
                 return Err(());
diff --git a/crates/hir_expand/src/db.rs b/crates/hir_expand/src/db.rs
index e8f4af3095..3ebe194e40 100644
--- a/crates/hir_expand/src/db.rs
+++ b/crates/hir_expand/src/db.rs
@@ -13,8 +13,8 @@ use syntax::{
 
 use crate::{
     ast_id_map::AstIdMap, hygiene::HygieneFrame, input::process_macro_input, BuiltinDeriveExpander,
-    BuiltinFnLikeExpander, HirFileId, HirFileIdRepr, MacroCallId, MacroCallLoc, MacroDefId,
-    MacroDefKind, MacroFile, ProcMacroExpander,
+    BuiltinFnLikeExpander, HirFileId, HirFileIdRepr, MacroCallId, MacroCallKind, MacroCallLoc,
+    MacroDefId, MacroDefKind, MacroFile, ProcMacroExpander,
 };
 
 /// Total limit on the number of tokens produced by any macro invocation.
@@ -377,7 +377,12 @@ fn expand_proc_macro(
         _ => unreachable!(),
     };
 
-    expander.expand(db, loc.krate, &macro_arg.0)
+    let attr_arg = match &loc.kind {
+        MacroCallKind::Attr { attr_args, .. } => Some(attr_args),
+        _ => None,
+    };
+
+    expander.expand(db, loc.krate, &macro_arg.0, attr_arg)
 }
 
 fn is_self_replicating(from: &SyntaxNode, to: &SyntaxNode) -> bool {
diff --git a/crates/hir_expand/src/input.rs b/crates/hir_expand/src/input.rs
index fe4790e7b6..40116a479b 100644
--- a/crates/hir_expand/src/input.rs
+++ b/crates/hir_expand/src/input.rs
@@ -28,6 +28,14 @@ pub(crate) fn process_macro_input(
 
             remove_derives_up_to(item, derive_attr_index as usize).syntax().clone()
         }
+        MacroCallKind::Attr { invoc_attr_index, .. } => {
+            let item = match ast::Item::cast(node.clone()) {
+                Some(item) => item,
+                None => return node,
+            };
+
+            remove_attr_invoc(item, invoc_attr_index as usize).syntax().clone()
+        }
     }
 }
 
@@ -46,6 +54,17 @@ fn remove_derives_up_to(item: ast::Item, attr_index: usize) -> ast::Item {
     item
 }
 
+/// Removes the attribute invoking an attribute macro from `item`.
+fn remove_attr_invoc(item: ast::Item, attr_index: usize) -> ast::Item {
+    let item = item.clone_for_update();
+    let attr = item
+        .attrs()
+        .nth(attr_index)
+        .unwrap_or_else(|| panic!("cannot find attribute #{}", attr_index));
+    attr.syntax().detach();
+    item
+}
+
 #[cfg(test)]
 mod tests {
     use base_db::fixture::WithFixture;
diff --git a/crates/hir_expand/src/lib.rs b/crates/hir_expand/src/lib.rs
index 90d8ae2401..618f26b954 100644
--- a/crates/hir_expand/src/lib.rs
+++ b/crates/hir_expand/src/lib.rs
@@ -258,14 +258,29 @@ pub enum MacroCallKind {
         /// out-of-line modules, which may have attributes spread across 2 files!
         derive_attr_index: u32,
     },
+    Attr {
+        ast_id: AstId<ast::Item>,
+        attr_name: String,
+        attr_args: tt::Subtree,
+        /// Syntactical index of the invoking `#[attribute]`.
+        ///
+        /// Outer attributes are counted first, then inner attributes. This does not support
+        /// out-of-line modules, which may have attributes spread across 2 files!
+        invoc_attr_index: u32,
+    },
 }
 
+// FIXME: attribute indices do not account for `cfg_attr`, which means that we'll strip the whole
+// `cfg_attr` instead of just one of the attributes it expands to
+
 impl MacroCallKind {
     /// Returns the file containing the macro invocation.
     fn file_id(&self) -> HirFileId {
         match self {
             MacroCallKind::FnLike { ast_id, .. } => ast_id.file_id,
-            MacroCallKind::Derive { ast_id, .. } => ast_id.file_id,
+            MacroCallKind::Derive { ast_id, .. } | MacroCallKind::Attr { ast_id, .. } => {
+                ast_id.file_id
+            }
         }
     }
 
@@ -274,7 +289,7 @@ impl MacroCallKind {
             MacroCallKind::FnLike { ast_id, .. } => {
                 ast_id.with_value(ast_id.to_node(db).syntax().clone())
             }
-            MacroCallKind::Derive { ast_id, .. } => {
+            MacroCallKind::Derive { ast_id, .. } | MacroCallKind::Attr { ast_id, .. } => {
                 ast_id.with_value(ast_id.to_node(db).syntax().clone())
             }
         }
@@ -285,7 +300,9 @@ impl MacroCallKind {
             MacroCallKind::FnLike { ast_id, .. } => {
                 Some(ast_id.to_node(db).token_tree()?.syntax().clone())
             }
-            MacroCallKind::Derive { ast_id, .. } => Some(ast_id.to_node(db).syntax().clone()),
+            MacroCallKind::Derive { ast_id, .. } | MacroCallKind::Attr { ast_id, .. } => {
+                Some(ast_id.to_node(db).syntax().clone())
+            }
         }
     }
 
@@ -293,6 +310,7 @@ impl MacroCallKind {
         match self {
             MacroCallKind::FnLike { fragment, .. } => *fragment,
             MacroCallKind::Derive { .. } => FragmentKind::Items,
+            MacroCallKind::Attr { .. } => FragmentKind::Items, // is this always correct?
         }
     }
 }
diff --git a/crates/hir_expand/src/proc_macro.rs b/crates/hir_expand/src/proc_macro.rs
index d5643393ae..dbe1b446e0 100644
--- a/crates/hir_expand/src/proc_macro.rs
+++ b/crates/hir_expand/src/proc_macro.rs
@@ -28,11 +28,16 @@ impl ProcMacroExpander {
         Self { krate, proc_macro_id: None }
     }
 
+    pub fn is_dummy(&self) -> bool {
+        self.proc_macro_id.is_none()
+    }
+
     pub fn expand(
         self,
         db: &dyn AstDatabase,
         calling_crate: CrateId,
         tt: &tt::Subtree,
+        attr_arg: Option<&tt::Subtree>,
     ) -> Result<tt::Subtree, mbe::ExpandError> {
         match self.proc_macro_id {
             Some(id) => {
@@ -46,7 +51,7 @@ impl ProcMacroExpander {
                 // Proc macros have access to the environment variables of the invoking crate.
                 let env = &krate_graph[calling_crate].env;
 
-                proc_macro.expander.expand(&tt, None, &env).map_err(mbe::ExpandError::from)
+                proc_macro.expander.expand(&tt, attr_arg, &env).map_err(mbe::ExpandError::from)
             }
             None => Err(mbe::ExpandError::UnresolvedProcMacro),
         }
diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs
index 1f900aef4b..105607dca4 100644
--- a/crates/ide_db/src/lib.rs
+++ b/crates/ide_db/src/lib.rs
@@ -93,6 +93,7 @@ impl RootDatabase {
         db.set_crate_graph_with_durability(Default::default(), Durability::HIGH);
         db.set_local_roots_with_durability(Default::default(), Durability::HIGH);
         db.set_library_roots_with_durability(Default::default(), Durability::HIGH);
+        db.set_enable_proc_attr_macros(Default::default());
         db.update_lru_capacity(lru_capacity);
         db
     }
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index c33cdb7409..d1f3c1b06f 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -126,6 +126,9 @@ config_data! {
         /// and a blue icon in the `Problems Panel`.
         diagnostics_warningsAsInfo: Vec<String> = "[]",
 
+        /// Expand attribute macros.
+        experimental_procAttrMacros: bool = "false",
+
         /// Controls file watching implementation.
         files_watcher: String = "\"client\"",
         /// These directories will be ignored by rust-analyzer.
@@ -546,6 +549,9 @@ impl Config {
         let path = self.data.procMacro_server.clone().or_else(|| std::env::current_exe().ok())?;
         Some((path, vec!["proc-macro".into()]))
     }
+    pub fn expand_proc_attr_macros(&self) -> bool {
+        self.data.experimental_procAttrMacros
+    }
     pub fn files(&self) -> FilesConfig {
         FilesConfig {
             watcher: match self.data.files_watcher.as_str() {
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index ea9dbf7fcd..582a896677 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -119,12 +119,12 @@ impl GlobalState {
 
         let analysis_host = AnalysisHost::new(config.lru_capacity());
         let (flycheck_sender, flycheck_receiver) = unbounded();
-        GlobalState {
+        let mut this = GlobalState {
             sender,
             req_queue: ReqQueue::default(),
             task_pool,
             loader,
-            config: Arc::new(config),
+            config: Arc::new(config.clone()),
             analysis_host,
             diagnostics: Default::default(),
             mem_docs: FxHashMap::default(),
@@ -151,7 +151,10 @@ impl GlobalState {
 
             fetch_build_data_queue: OpQueue::default(),
             latest_requests: Default::default(),
-        }
+        };
+        // Apply any required database inputs from the config.
+        this.update_configuration(config);
+        this
     }
 
     pub(crate) fn process_changes(&mut self) -> bool {
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index 93b5ff55f7..bd31d1d13d 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -2,6 +2,7 @@
 use std::{mem, sync::Arc};
 
 use flycheck::{FlycheckConfig, FlycheckHandle};
+use hir::db::DefDatabase;
 use ide::Change;
 use ide_db::base_db::{CrateGraph, SourceRoot, VfsPath};
 use project_model::{BuildDataCollector, BuildDataResult, ProcMacroClient, ProjectWorkspace};
@@ -47,6 +48,11 @@ impl GlobalState {
         } else if self.config.flycheck() != old_config.flycheck() {
             self.reload_flycheck();
         }
+
+        // Apply experimental feature flags.
+        self.analysis_host
+            .raw_database_mut()
+            .set_enable_proc_attr_macros(self.config.expand_proc_attr_macros());
     }
     pub(crate) fn maybe_refresh(&mut self, changes: &[(AbsPathBuf, ChangeKind)]) {
         if !changes.iter().any(|(path, kind)| is_interesting(path, *kind)) {
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index 7f405b4d7a..4eec8455df 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -181,6 +181,11 @@ List of warnings that should be displayed with info severity.
 The warnings will be indicated by a blue squiggly underline in code
 and a blue icon in the `Problems Panel`.
 --
+[[rust-analyzer.experimental.procAttrMacros]]rust-analyzer.experimental.procAttrMacros (default: `false`)::
++
+--
+Expand attribute macros.
+--
 [[rust-analyzer.files.watcher]]rust-analyzer.files.watcher (default: `"client"`)::
 +
 --
diff --git a/editors/code/package.json b/editors/code/package.json
index 4a5070d021..bffc1e05b7 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -617,6 +617,11 @@
                         "type": "string"
                     }
                 },
+                "rust-analyzer.experimental.procAttrMacros": {
+                    "markdownDescription": "Expand attribute macros.",
+                    "default": false,
+                    "type": "boolean"
+                },
                 "rust-analyzer.files.watcher": {
                     "markdownDescription": "Controls file watching implementation.",
                     "default": "client",


Test patch:
diff --git a/crates/hir_def/src/test_db.rs b/crates/hir_def/src/test_db.rs
index e840fe5e80..b20b066e24 100644
--- a/crates/hir_def/src/test_db.rs
+++ b/crates/hir_def/src/test_db.rs
@@ -30,12 +30,19 @@ use crate::{
     crate::db::InternDatabaseStorage,
     crate::db::DefDatabaseStorage
 )]
-#[derive(Default)]
 pub(crate) struct TestDB {
     storage: salsa::Storage<TestDB>,
     events: Mutex<Option<Vec<salsa::Event>>>,
 }
 
+impl Default for TestDB {
+    fn default() -> Self {
+        let mut this = Self { storage: Default::default(), events: Default::default() };
+        this.set_enable_proc_attr_macros(true);
+        this
+    }
+}
+
 impl Upcast<dyn AstDatabase> for TestDB {
     fn upcast(&self) -> &(dyn AstDatabase + 'static) {
         &*self
diff --git a/crates/hir_ty/src/test_db.rs b/crates/hir_ty/src/test_db.rs
index 381b98ba80..4640ea8211 100644
--- a/crates/hir_ty/src/test_db.rs
+++ b/crates/hir_ty/src/test_db.rs
@@ -22,11 +22,19 @@ use test_utils::extract_annotations;
     hir_def::db::DefDatabaseStorage,
     crate::db::HirDatabaseStorage
 )]
-#[derive(Default)]
 pub(crate) struct TestDB {
     storage: salsa::Storage<TestDB>,
     events: Mutex<Option<Vec<salsa::Event>>>,
 }
+
+impl Default for TestDB {
+    fn default() -> Self {
+        let mut this = Self { storage: Default::default(), events: Default::default() };
+        this.set_enable_proc_attr_macros(true);
+        this
+    }
+}
+
 impl fmt::Debug for TestDB {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("TestDB").finish()