diff --git a/src/librustc_expand/expand.rs b/src/librustc_expand/expand.rs
index 371d1f744dd25..ab372a41e6942 100644
--- a/src/librustc_expand/expand.rs
+++ b/src/librustc_expand/expand.rs
@@ -14,6 +14,8 @@ use rustc_parse::configure;
 use rustc_parse::parser::Parser;
 use rustc_parse::validate_attr;
 use rustc_parse::DirectoryOwnership;
+use rustc_session::lint::builtin::UNUSED_DOC_COMMENTS;
+use rustc_session::lint::BuiltinLintDiagnostics;
 use rustc_session::parse::{feature_err, ParseSess};
 use rustc_span::source_map::respan;
 use rustc_span::symbol::{sym, Symbol};
@@ -1090,6 +1092,16 @@ impl<'a, 'b> InvocationCollector<'a, 'b> {
                     .note("this may become a hard error in a future release")
                     .emit();
             }
+
+            if attr.doc_str().is_some() {
+                self.cx.parse_sess.buffer_lint_with_diagnostic(
+                    &UNUSED_DOC_COMMENTS,
+                    attr.span,
+                    ast::CRATE_NODE_ID,
+                    "unused doc comment",
+                    BuiltinLintDiagnostics::UnusedDocComment(attr.span),
+                );
+            }
         }
     }
 }
diff --git a/src/librustc_lint/builtin.rs b/src/librustc_lint/builtin.rs
index 93fca43d67c1f..54b5b922ac729 100644
--- a/src/librustc_lint/builtin.rs
+++ b/src/librustc_lint/builtin.rs
@@ -738,87 +738,56 @@ impl EarlyLintPass for DeprecatedAttr {
     }
 }
 
-declare_lint! {
-    pub UNUSED_DOC_COMMENTS,
-    Warn,
-    "detects doc comments that aren't used by rustdoc"
-}
+fn warn_if_doc(cx: &EarlyContext<'_>, node_span: Span, node_kind: &str, attrs: &[ast::Attribute]) {
+    let mut attrs = attrs.into_iter().peekable();
 
-declare_lint_pass!(UnusedDocComment => [UNUSED_DOC_COMMENTS]);
+    // Accumulate a single span for sugared doc comments.
+    let mut sugared_span: Option<Span> = None;
 
-impl UnusedDocComment {
-    fn warn_if_doc(
-        &self,
-        cx: &EarlyContext<'_>,
-        node_span: Span,
-        node_kind: &str,
-        is_macro_expansion: bool,
-        attrs: &[ast::Attribute],
-    ) {
-        let mut attrs = attrs.into_iter().peekable();
-
-        // Accumulate a single span for sugared doc comments.
-        let mut sugared_span: Option<Span> = None;
-
-        while let Some(attr) = attrs.next() {
-            if attr.is_doc_comment() {
-                sugared_span = Some(
-                    sugared_span.map_or_else(|| attr.span, |span| span.with_hi(attr.span.hi())),
-                );
-            }
+    while let Some(attr) = attrs.next() {
+        if attr.is_doc_comment() {
+            sugared_span =
+                Some(sugared_span.map_or_else(|| attr.span, |span| span.with_hi(attr.span.hi())));
+        }
 
-            if attrs.peek().map(|next_attr| next_attr.is_doc_comment()).unwrap_or_default() {
-                continue;
-            }
+        if attrs.peek().map(|next_attr| next_attr.is_doc_comment()).unwrap_or_default() {
+            continue;
+        }
 
-            let span = sugared_span.take().unwrap_or_else(|| attr.span);
+        let span = sugared_span.take().unwrap_or_else(|| attr.span);
 
-            if attr.is_doc_comment() || attr.check_name(sym::doc) {
-                cx.struct_span_lint(UNUSED_DOC_COMMENTS, span, |lint| {
-                    let mut err = lint.build("unused doc comment");
-                    err.span_label(
-                        node_span,
-                        format!("rustdoc does not generate documentation for {}", node_kind),
-                    );
-                    if is_macro_expansion {
-                        err.help(
-                            "to document an item produced by a macro, \
-                                  the macro must produce the documentation as part of its expansion",
-                        );
-                    }
-                    err.emit();
-                });
-            }
+        if attr.is_doc_comment() || attr.check_name(sym::doc) {
+            cx.struct_span_lint(UNUSED_DOC_COMMENTS, span, |lint| {
+                let mut err = lint.build("unused doc comment");
+                err.span_label(
+                    node_span,
+                    format!("rustdoc does not generate documentation for {}", node_kind),
+                );
+                err.emit();
+            });
         }
     }
 }
 
 impl EarlyLintPass for UnusedDocComment {
-    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
-        if let ast::ItemKind::Mac(..) = item.kind {
-            self.warn_if_doc(cx, item.span, "macro expansions", true, &item.attrs);
-        }
-    }
-
     fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &ast::Stmt) {
-        let (kind, is_macro_expansion) = match stmt.kind {
-            ast::StmtKind::Local(..) => ("statements", false),
-            ast::StmtKind::Item(..) => ("inner items", false),
-            ast::StmtKind::Mac(..) => ("macro expansions", true),
+        let kind = match stmt.kind {
+            ast::StmtKind::Local(..) => "statements",
+            ast::StmtKind::Item(..) => "inner items",
             // expressions will be reported by `check_expr`.
-            ast::StmtKind::Semi(..) | ast::StmtKind::Expr(..) => return,
+            ast::StmtKind::Semi(..) | ast::StmtKind::Expr(..) | ast::StmtKind::Mac(..) => return,
         };
 
-        self.warn_if_doc(cx, stmt.span, kind, is_macro_expansion, stmt.kind.attrs());
+        warn_if_doc(cx, stmt.span, kind, stmt.kind.attrs());
     }
 
     fn check_arm(&mut self, cx: &EarlyContext<'_>, arm: &ast::Arm) {
         let arm_span = arm.pat.span.with_hi(arm.body.span.hi());
-        self.warn_if_doc(cx, arm_span, "match arms", false, &arm.attrs);
+        warn_if_doc(cx, arm_span, "match arms", &arm.attrs);
     }
 
     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
-        self.warn_if_doc(cx, expr.span, "expressions", false, &expr.attrs);
+        warn_if_doc(cx, expr.span, "expressions", &expr.attrs);
     }
 }
 
diff --git a/src/librustc_lint/context.rs b/src/librustc_lint/context.rs
index 8e8beefa72f13..adad1198b096b 100644
--- a/src/librustc_lint/context.rs
+++ b/src/librustc_lint/context.rs
@@ -565,6 +565,11 @@ pub trait LintContext: Sized {
                 BuiltinLintDiagnostics::DeprecatedMacro(suggestion, span) => {
                     stability::deprecation_suggestion(&mut db, suggestion, span)
                 }
+                BuiltinLintDiagnostics::UnusedDocComment(span) => {
+                    db.span_label(span, "rustdoc does not generate documentation for macros");
+                    db.help("to document an item produced by a macro, \
+                                  the macro must produce the documentation as part of its expansion");
+                }
             }
             // Rewrap `db`, and pass control to the user.
             decorate(LintDiagnosticBuilder::new(db));
diff --git a/src/librustc_lint/lib.rs b/src/librustc_lint/lib.rs
index 2204e104803b3..d3670056c1a44 100644
--- a/src/librustc_lint/lib.rs
+++ b/src/librustc_lint/lib.rs
@@ -94,7 +94,7 @@ fn lint_mod(tcx: TyCtxt<'_>, module_def_id: DefId) {
 
 macro_rules! pre_expansion_lint_passes {
     ($macro:path, $args:tt) => {
-        $macro!($args, [KeywordIdents: KeywordIdents, UnusedDocComment: UnusedDocComment,]);
+        $macro!($args, [KeywordIdents: KeywordIdents,]);
     };
 }
 
@@ -114,6 +114,7 @@ macro_rules! early_lint_passes {
                 NonAsciiIdents: NonAsciiIdents,
                 IncompleteFeatures: IncompleteFeatures,
                 RedundantSemicolon: RedundantSemicolon,
+                UnusedDocComment: UnusedDocComment,
             ]
         );
     };
diff --git a/src/librustc_session/lint.rs b/src/librustc_session/lint.rs
index 983dfb19919dd..6d4f1ff5b4877 100644
--- a/src/librustc_session/lint.rs
+++ b/src/librustc_session/lint.rs
@@ -190,6 +190,7 @@ pub enum BuiltinLintDiagnostics {
     UnusedImports(String, Vec<(Span, String)>),
     RedundantImport(Vec<(Span, bool)>, Ident),
     DeprecatedMacro(Option<Symbol>, Span),
+    UnusedDocComment(Span),
 }
 
 /// Lints that are buffered up early on in the `Session` before the
diff --git a/src/librustc_session/lint/builtin.rs b/src/librustc_session/lint/builtin.rs
index 5a360b40d61b5..5e3367f869657 100644
--- a/src/librustc_session/lint/builtin.rs
+++ b/src/librustc_session/lint/builtin.rs
@@ -557,3 +557,11 @@ declare_lint_pass! {
         INLINE_NO_SANITIZE,
     ]
 }
+
+declare_lint! {
+    pub UNUSED_DOC_COMMENTS,
+    Warn,
+    "detects doc comments that aren't used by rustdoc"
+}
+
+declare_lint_pass!(UnusedDocComment => [UNUSED_DOC_COMMENTS]);
diff --git a/src/librustc_session/parse.rs b/src/librustc_session/parse.rs
index 6a4871b6da059..30888e343ed81 100644
--- a/src/librustc_session/parse.rs
+++ b/src/librustc_session/parse.rs
@@ -176,6 +176,25 @@ impl ParseSess {
         });
     }
 
+    pub fn buffer_lint_with_diagnostic(
+        &self,
+        lint: &'static Lint,
+        span: impl Into<MultiSpan>,
+        node_id: NodeId,
+        msg: &str,
+        diagnostic: BuiltinLintDiagnostics,
+    ) {
+        self.buffered_lints.with_lock(|buffered_lints| {
+            buffered_lints.push(BufferedEarlyLint {
+                span: span.into(),
+                node_id,
+                msg: msg.into(),
+                lint_id: LintId::of(lint),
+                diagnostic,
+            });
+        });
+    }
+
     /// Extend an error with a suggestion to wrap an expression with parentheses to allow the
     /// parser to continue parsing the following operation as part of the same expression.
     pub fn expr_parentheses_needed(
diff --git a/src/libstd/sys/wasi/fd.rs b/src/libstd/sys/wasi/fd.rs
index 00327c1743c3d..8458ded5db0b2 100644
--- a/src/libstd/sys/wasi/fd.rs
+++ b/src/libstd/sys/wasi/fd.rs
@@ -13,19 +13,15 @@ pub struct WasiFd {
 fn iovec<'a>(a: &'a mut [IoSliceMut<'_>]) -> &'a [wasi::Iovec] {
     assert_eq!(mem::size_of::<IoSliceMut<'_>>(), mem::size_of::<wasi::Iovec>());
     assert_eq!(mem::align_of::<IoSliceMut<'_>>(), mem::align_of::<wasi::Iovec>());
-    /// SAFETY: `IoSliceMut` and `IoVec` have exactly the same memory layout
-    unsafe {
-        mem::transmute(a)
-    }
+    // SAFETY: `IoSliceMut` and `IoVec` have exactly the same memory layout
+    unsafe { mem::transmute(a) }
 }
 
 fn ciovec<'a>(a: &'a [IoSlice<'_>]) -> &'a [wasi::Ciovec] {
     assert_eq!(mem::size_of::<IoSlice<'_>>(), mem::size_of::<wasi::Ciovec>());
     assert_eq!(mem::align_of::<IoSlice<'_>>(), mem::align_of::<wasi::Ciovec>());
-    /// SAFETY: `IoSlice` and `CIoVec` have exactly the same memory layout
-    unsafe {
-        mem::transmute(a)
-    }
+    // SAFETY: `IoSlice` and `CIoVec` have exactly the same memory layout
+    unsafe { mem::transmute(a) }
 }
 
 impl WasiFd {
diff --git a/src/test/ui/useless-comment.stderr b/src/test/ui/useless-comment.stderr
index e5e4290d0e1b8..92817321a88f0 100644
--- a/src/test/ui/useless-comment.stderr
+++ b/src/test/ui/useless-comment.stderr
@@ -2,9 +2,7 @@ error: unused doc comment
   --> $DIR/useless-comment.rs:9:1
    |
 LL | /// foo
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-LL | mac!();
-   | ------- rustdoc does not generate documentation for macro expansions
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ rustdoc does not generate documentation for macros
    |
 note: the lint level is defined here
   --> $DIR/useless-comment.rs:3:9
@@ -13,6 +11,14 @@ LL | #![deny(unused_doc_comments)]
    |         ^^^^^^^^^^^^^^^^^^^
    = help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
 
+error: unused doc comment
+  --> $DIR/useless-comment.rs:32:5
+   |
+LL |     /// bar
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ rustdoc does not generate documentation for macros
+   |
+   = help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
+
 error: unused doc comment
   --> $DIR/useless-comment.rs:13:5
    |
@@ -68,16 +74,6 @@ LL |     #[doc = "bar"]
 LL |     3;
    |     - rustdoc does not generate documentation for expressions
 
-error: unused doc comment
-  --> $DIR/useless-comment.rs:32:5
-   |
-LL |     /// bar
-   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-LL |     mac!();
-   |     ------- rustdoc does not generate documentation for macro expansions
-   |
-   = help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
-
 error: unused doc comment
   --> $DIR/useless-comment.rs:35:13
    |