diff --git a/crates/hir_def/src/item_scope.rs b/crates/hir_def/src/item_scope.rs
index f1e9dfd5b14..99820c275cf 100644
--- a/crates/hir_def/src/item_scope.rs
+++ b/crates/hir_def/src/item_scope.rs
@@ -5,10 +5,12 @@ use std::collections::hash_map::Entry;
 
 use base_db::CrateId;
 use hir_expand::name::Name;
+use hir_expand::MacroDefKind;
 use once_cell::sync::Lazy;
 use rustc_hash::{FxHashMap, FxHashSet};
 use test_utils::mark;
 
+use crate::ModuleId;
 use crate::{
     db::DefDatabase, per_ns::PerNs, visibility::Visibility, AdtId, BuiltinType, HasModule, ImplId,
     LocalModuleId, Lookup, MacroDefId, ModuleDefId, TraitId,
@@ -265,6 +267,29 @@ impl ItemScope {
     pub(crate) fn collect_legacy_macros(&self) -> FxHashMap<Name, MacroDefId> {
         self.legacy_macros.clone()
     }
+
+    /// Marks everything that is not a procedural macro as private to `this_module`.
+    pub(crate) fn censor_non_proc_macros(&mut self, this_module: ModuleId) {
+        for vis in self
+            .types
+            .values_mut()
+            .chain(self.values.values_mut())
+            .map(|(_, v)| v)
+            .chain(self.unnamed_trait_imports.values_mut())
+        {
+            *vis = Visibility::Module(this_module);
+        }
+
+        for (mac, vis) in self.macros.values_mut() {
+            if let MacroDefKind::ProcMacro(_) = mac.kind {
+                // FIXME: Technically this is insufficient since reexports of proc macros are also
+                // forbidden. Practically nobody does that.
+                continue;
+            }
+
+            *vis = Visibility::Module(this_module);
+        }
+    }
 }
 
 impl PerNs {
diff --git a/crates/hir_def/src/nameres/collector.rs b/crates/hir_def/src/nameres/collector.rs
index 28ef494881f..f98a42643af 100644
--- a/crates/hir_def/src/nameres/collector.rs
+++ b/crates/hir_def/src/nameres/collector.rs
@@ -87,6 +87,7 @@ pub(super) fn collect_defs(db: &dyn DefDatabase, mut def_map: CrateDefMap) -> Cr
         mod_dirs: FxHashMap::default(),
         cfg_options,
         proc_macros,
+        exports_proc_macros: false,
         from_glob_import: Default::default(),
     };
     collector.collect();
@@ -203,6 +204,7 @@ struct DefCollector<'a> {
     mod_dirs: FxHashMap<LocalModuleId, ModDir>,
     cfg_options: &'a CfgOptions,
     proc_macros: Vec<(Name, ProcMacroExpander)>,
+    exports_proc_macros: bool,
     from_glob_import: PerNsGlobImports,
 }
 
@@ -260,9 +262,25 @@ impl DefCollector<'_> {
             self.record_resolved_import(directive)
         }
         self.unresolved_imports = unresolved_imports;
+
+        // FIXME: This condition should instead check if this is a `proc-macro` type crate.
+        if self.exports_proc_macros {
+            // A crate exporting procedural macros is not allowed to export anything else.
+            //
+            // Additionally, while the proc macro entry points must be `pub`, they are not publicly
+            // exported in type/value namespace. This function reduces the visibility of all items
+            // in the crate root that aren't proc macros.
+            let root = self.def_map.root;
+            let root = &mut self.def_map.modules[root];
+            root.scope.censor_non_proc_macros(ModuleId {
+                krate: self.def_map.krate,
+                local_id: self.def_map.root,
+            });
+        }
     }
 
     fn resolve_proc_macro(&mut self, name: &Name) {
+        self.exports_proc_macros = true;
         let macro_def = match self.proc_macros.iter().find(|(n, _)| n == name) {
             Some((_, expander)) => MacroDefId {
                 ast_id: None,
@@ -1310,6 +1328,7 @@ mod tests {
             mod_dirs: FxHashMap::default(),
             cfg_options: &CfgOptions::default(),
             proc_macros: Default::default(),
+            exports_proc_macros: false,
             from_glob_import: Default::default(),
         };
         collector.collect();
diff --git a/crates/hir_def/src/nameres/tests/macros.rs b/crates/hir_def/src/nameres/tests/macros.rs
index 98cb5a0fdc3..0851c3b7d89 100644
--- a/crates/hir_def/src/nameres/tests/macros.rs
+++ b/crates/hir_def/src/nameres/tests/macros.rs
@@ -699,3 +699,44 @@ fn resolves_proc_macros() {
         "#]],
     );
 }
+
+#[test]
+fn proc_macro_censoring() {
+    // Make sure that only proc macros are publicly exported from proc-macro crates.
+
+    check(
+        r"
+        //- /main.rs crate:main deps:macros
+        pub use macros::*;
+
+        //- /macros.rs crate:macros
+        pub struct TokenStream;
+
+        #[proc_macro]
+        pub fn function_like_macro(args: TokenStream) -> TokenStream {
+            args
+        }
+
+        #[proc_macro_attribute]
+        pub fn attribute_macro(_args: TokenStream, item: TokenStream) -> TokenStream {
+            item
+        }
+
+        #[proc_macro_derive(DummyTrait)]
+        pub fn derive_macro(_item: TokenStream) -> TokenStream {
+            TokenStream
+        }
+
+        #[macro_export]
+        macro_rules! mbe {
+            () => {};
+        }
+        ",
+        expect![[r#"
+            crate
+            DummyTrait: m
+            attribute_macro: m
+            function_like_macro: m
+        "#]],
+    );
+}