about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--compiler/rustc_codegen_ssa/src/mir/block.rs29
-rw-r--r--compiler/rustc_lint/src/early.rs8
-rw-r--r--compiler/rustc_lint/src/passes.rs2
-rw-r--r--library/alloc/src/ffi/c_str.rs5
-rw-r--r--library/alloc/src/rc.rs4
-rw-r--r--library/std/src/fs/tests.rs2
-rw-r--r--library/std/src/io/mod.rs2
-rw-r--r--library/std/src/lib.rs25
-rw-r--r--library/std/src/os/unix/fs/tests.rs4
-rw-r--r--library/std/src/os/unix/net/tests.rs2
-rw-r--r--library/std/src/process/tests.rs4
-rw-r--r--library/std/src/sys/io/io_slice/iovec.rs (renamed from library/std/src/sys/pal/hermit/io.rs)14
-rw-r--r--library/std/src/sys/io/io_slice/unsupported.rs (renamed from library/std/src/sys/pal/unsupported/io.rs)4
-rw-r--r--library/std/src/sys/io/io_slice/wasi.rs (renamed from library/std/src/sys/pal/wasi/io.rs)8
-rw-r--r--library/std/src/sys/io/io_slice/windows.rs (renamed from library/std/src/sys/pal/solid/io.rs)38
-rw-r--r--library/std/src/sys/io/is_terminal/hermit.rs6
-rw-r--r--library/std/src/sys/io/is_terminal/isatty.rs6
-rw-r--r--library/std/src/sys/io/is_terminal/unsupported.rs3
-rw-r--r--library/std/src/sys/io/is_terminal/windows.rs (renamed from library/std/src/sys/pal/windows/io.rs)84
-rw-r--r--library/std/src/sys/io/mod.rs44
-rw-r--r--library/std/src/sys/mod.rs1
-rw-r--r--library/std/src/sys/pal/hermit/mod.rs1
-rw-r--r--library/std/src/sys/pal/sgx/mod.rs2
-rw-r--r--library/std/src/sys/pal/sgx/stdio.rs2
-rw-r--r--library/std/src/sys/pal/solid/mod.rs1
-rw-r--r--library/std/src/sys/pal/teeos/mod.rs2
-rw-r--r--library/std/src/sys/pal/uefi/mod.rs2
-rw-r--r--library/std/src/sys/pal/unix/io.rs87
-rw-r--r--library/std/src/sys/pal/unix/kernel_copy/tests.rs2
-rw-r--r--library/std/src/sys/pal/unix/mod.rs1
-rw-r--r--library/std/src/sys/pal/unix/stdio.rs2
-rw-r--r--library/std/src/sys/pal/unsupported/mod.rs1
-rw-r--r--library/std/src/sys/pal/wasi/mod.rs1
-rw-r--r--library/std/src/sys/pal/wasi/stdio.rs2
-rw-r--r--library/std/src/sys/pal/wasip2/mod.rs2
-rw-r--r--library/std/src/sys/pal/wasm/mod.rs2
-rw-r--r--library/std/src/sys/pal/windows/mod.rs1
-rw-r--r--library/std/src/sys/pal/windows/process/tests.rs2
-rw-r--r--library/std/src/sys/pal/xous/mod.rs2
-rw-r--r--library/std/src/sys/pal/zkvm/mod.rs2
-rw-r--r--library/std/src/sys/pal/zkvm/stdio.rs2
-rw-r--r--library/std/src/sys_common/io.rs49
-rw-r--r--library/std/src/sys_common/mod.rs1
-rw-r--r--library/std/src/test_helpers.rs65
-rw-r--r--library/std/tests/common/mod.rs2
-rw-r--r--src/bootstrap/src/core/build_steps/tool.rs2
-rw-r--r--src/bootstrap/src/lib.rs2
-rw-r--r--src/ci/github-actions/jobs.yml4
-rw-r--r--src/tools/clippy/clippy_lints/src/declared_lints.rs4
-rw-r--r--src/tools/clippy/clippy_lints/src/doc/empty_line_after.rs345
-rw-r--r--src/tools/clippy/clippy_lints/src/doc/mod.rs81
-rw-r--r--src/tools/clippy/clippy_lints/src/empty_line_after.rs492
-rw-r--r--src/tools/clippy/clippy_lints/src/lib.rs2
-rw-r--r--src/tools/clippy/tests/ui/doc/unbalanced_ticks.rs16
-rw-r--r--src/tools/clippy/tests/ui/doc/unbalanced_ticks.stderr14
-rw-r--r--src/tools/clippy/tests/ui/empty_line_after/doc_comments.1.fixed9
-rw-r--r--src/tools/clippy/tests/ui/empty_line_after/doc_comments.2.fixed9
-rw-r--r--src/tools/clippy/tests/ui/empty_line_after/doc_comments.rs10
-rw-r--r--src/tools/clippy/tests/ui/empty_line_after/doc_comments.stderr51
-rw-r--r--src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr30
-rw-r--r--src/tools/clippy/tests/ui/suspicious_doc_comments.fixed1
-rw-r--r--src/tools/clippy/tests/ui/suspicious_doc_comments.rs1
-rw-r--r--src/tools/clippy/tests/ui/suspicious_doc_comments.stderr18
-rw-r--r--tests/codegen/terminating-catchpad.rs37
64 files changed, 857 insertions, 802 deletions
diff --git a/compiler/rustc_codegen_ssa/src/mir/block.rs b/compiler/rustc_codegen_ssa/src/mir/block.rs
index 4be363ca9a2..2eecf81a5d2 100644
--- a/compiler/rustc_codegen_ssa/src/mir/block.rs
+++ b/compiler/rustc_codegen_ssa/src/mir/block.rs
@@ -1708,15 +1708,32 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
             let mut cs_bx = Bx::build(self.cx, llbb);
             let cs = cs_bx.catch_switch(None, None, &[cp_llbb]);
 
-            // The "null" here is actually a RTTI type descriptor for the
-            // C++ personality function, but `catch (...)` has no type so
-            // it's null. The 64 here is actually a bitfield which
-            // represents that this is a catch-all block.
             bx = Bx::build(self.cx, cp_llbb);
             let null =
                 bx.const_null(bx.type_ptr_ext(bx.cx().data_layout().instruction_address_space));
-            let sixty_four = bx.const_i32(64);
-            funclet = Some(bx.catch_pad(cs, &[null, sixty_four, null]));
+
+            // The `null` in first argument here is actually a RTTI type
+            // descriptor for the C++ personality function, but `catch (...)`
+            // has no type so it's null.
+            let args = if base::wants_msvc_seh(self.cx.sess()) {
+                // This bitmask is a single `HT_IsStdDotDot` flag, which
+                // represents that this is a C++-style `catch (...)` block that
+                // only captures programmatic exceptions, not all SEH
+                // exceptions. The second `null` points to a non-existent
+                // `alloca` instruction, which an LLVM pass would inline into
+                // the initial SEH frame allocation.
+                let adjectives = bx.const_i32(0x40);
+                &[null, adjectives, null] as &[_]
+            } else {
+                // Specifying more arguments than necessary usually doesn't
+                // hurt, but the `WasmEHPrepare` LLVM pass does not recognize
+                // anything other than a single `null` as a `catch (...)` block,
+                // leading to problems down the line during instruction
+                // selection.
+                &[null] as &[_]
+            };
+
+            funclet = Some(bx.catch_pad(cs, args));
         } else {
             llbb = Bx::append_block(self.cx, self.llfn, "terminate");
             bx = Bx::build(self.cx, llbb);
diff --git a/compiler/rustc_lint/src/early.rs b/compiler/rustc_lint/src/early.rs
index bc7cd3d118c..723b894c43b 100644
--- a/compiler/rustc_lint/src/early.rs
+++ b/compiler/rustc_lint/src/early.rs
@@ -246,6 +246,14 @@ impl<'ast, 'ecx, 'tcx, T: EarlyLintPass> ast_visit::Visitor<'ast>
                 }
             }
             ast_visit::walk_assoc_item(cx, item, ctxt);
+            match ctxt {
+                ast_visit::AssocCtxt::Trait => {
+                    lint_callback!(cx, check_trait_item_post, item);
+                }
+                ast_visit::AssocCtxt::Impl => {
+                    lint_callback!(cx, check_impl_item_post, item);
+                }
+            }
         });
     }
 
diff --git a/compiler/rustc_lint/src/passes.rs b/compiler/rustc_lint/src/passes.rs
index 77bd13aacf7..409a23d1da0 100644
--- a/compiler/rustc_lint/src/passes.rs
+++ b/compiler/rustc_lint/src/passes.rs
@@ -162,7 +162,9 @@ macro_rules! early_lint_methods {
                 c: rustc_span::Span,
                 d_: rustc_ast::NodeId);
             fn check_trait_item(a: &rustc_ast::AssocItem);
+            fn check_trait_item_post(a: &rustc_ast::AssocItem);
             fn check_impl_item(a: &rustc_ast::AssocItem);
+            fn check_impl_item_post(a: &rustc_ast::AssocItem);
             fn check_variant(a: &rustc_ast::Variant);
             fn check_attribute(a: &rustc_ast::Attribute);
             fn check_attributes(a: &[rustc_ast::Attribute]);
diff --git a/library/alloc/src/ffi/c_str.rs b/library/alloc/src/ffi/c_str.rs
index c7d6d8a55c2..07c75677d05 100644
--- a/library/alloc/src/ffi/c_str.rs
+++ b/library/alloc/src/ffi/c_str.rs
@@ -965,8 +965,9 @@ impl Default for Rc<CStr> {
     /// This may or may not share an allocation with other Rcs on the same thread.
     #[inline]
     fn default() -> Self {
-        let c_str: &CStr = Default::default();
-        Rc::from(c_str)
+        let rc = Rc::<[u8]>::from(*b"\0");
+        // `[u8]` has the same layout as `CStr`, and it is `NUL` terminated.
+        unsafe { Rc::from_raw(Rc::into_raw(rc) as *const CStr) }
     }
 }
 
diff --git a/library/alloc/src/rc.rs b/library/alloc/src/rc.rs
index ae3318b839d..05e76ccf685 100644
--- a/library/alloc/src/rc.rs
+++ b/library/alloc/src/rc.rs
@@ -2369,7 +2369,9 @@ impl Default for Rc<str> {
     /// This may or may not share an allocation with other Rcs on the same thread.
     #[inline]
     fn default() -> Self {
-        Rc::from("")
+        let rc = Rc::<[u8]>::default();
+        // `[u8]` has the same layout as `str`.
+        unsafe { Rc::from_raw(Rc::into_raw(rc) as *const str) }
     }
 }
 
diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs
index 28f16da1ed8..e3810030ded 100644
--- a/library/std/src/fs/tests.rs
+++ b/library/std/src/fs/tests.rs
@@ -14,7 +14,7 @@ use crate::os::unix::fs::symlink as junction_point;
 use crate::os::windows::fs::{OpenOptionsExt, junction_point, symlink_dir, symlink_file};
 use crate::path::Path;
 use crate::sync::Arc;
-use crate::sys_common::io::test::{TempDir, tmpdir};
+use crate::test_helpers::{TempDir, tmpdir};
 use crate::time::{Duration, Instant, SystemTime};
 use crate::{env, str, thread};
 
diff --git a/library/std/src/io/mod.rs b/library/std/src/io/mod.rs
index cfd03b8e3d6..0ffad2c27a4 100644
--- a/library/std/src/io/mod.rs
+++ b/library/std/src/io/mod.rs
@@ -344,7 +344,7 @@ pub mod prelude;
 mod stdio;
 mod util;
 
-const DEFAULT_BUF_SIZE: usize = crate::sys_common::io::DEFAULT_BUF_SIZE;
+const DEFAULT_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
 
 pub(crate) use stdio::cleanup;
 
diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs
index 7c18226874c..954a4182fbd 100644
--- a/library/std/src/lib.rs
+++ b/library/std/src/lib.rs
@@ -739,27 +739,4 @@ mod sealed {
 
 #[cfg(test)]
 #[allow(dead_code)] // Not used in all configurations.
-pub(crate) mod test_helpers {
-    /// Test-only replacement for `rand::thread_rng()`, which is unusable for
-    /// us, as we want to allow running stdlib tests on tier-3 targets which may
-    /// not have `getrandom` support.
-    ///
-    /// Does a bit of a song and dance to ensure that the seed is different on
-    /// each call (as some tests sadly rely on this), but doesn't try that hard.
-    ///
-    /// This is duplicated in the `core`, `alloc` test suites (as well as
-    /// `std`'s integration tests), but figuring out a mechanism to share these
-    /// seems far more painful than copy-pasting a 7 line function a couple
-    /// times, given that even under a perma-unstable feature, I don't think we
-    /// want to expose types from `rand` from `std`.
-    #[track_caller]
-    pub(crate) fn test_rng() -> rand_xorshift::XorShiftRng {
-        use core::hash::{BuildHasher, Hash, Hasher};
-        let mut hasher = crate::hash::RandomState::new().build_hasher();
-        core::panic::Location::caller().hash(&mut hasher);
-        let hc64 = hasher.finish();
-        let seed_vec = hc64.to_le_bytes().into_iter().chain(0u8..8).collect::<Vec<u8>>();
-        let seed: [u8; 16] = seed_vec.as_slice().try_into().unwrap();
-        rand::SeedableRng::from_seed(seed)
-    }
-}
+pub(crate) mod test_helpers;
diff --git a/library/std/src/os/unix/fs/tests.rs b/library/std/src/os/unix/fs/tests.rs
index 67f607bd468..db9621c8c20 100644
--- a/library/std/src/os/unix/fs/tests.rs
+++ b/library/std/src/os/unix/fs/tests.rs
@@ -3,7 +3,7 @@ use super::*;
 #[test]
 fn read_vectored_at() {
     let msg = b"preadv is working!";
-    let dir = crate::sys_common::io::test::tmpdir();
+    let dir = crate::test_helpers::tmpdir();
 
     let filename = dir.join("preadv.txt");
     {
@@ -31,7 +31,7 @@ fn read_vectored_at() {
 #[test]
 fn write_vectored_at() {
     let msg = b"pwritev is not working!";
-    let dir = crate::sys_common::io::test::tmpdir();
+    let dir = crate::test_helpers::tmpdir();
 
     let filename = dir.join("preadv.txt");
     {
diff --git a/library/std/src/os/unix/net/tests.rs b/library/std/src/os/unix/net/tests.rs
index 21e2176185d..0398a535eb5 100644
--- a/library/std/src/os/unix/net/tests.rs
+++ b/library/std/src/os/unix/net/tests.rs
@@ -7,7 +7,7 @@ use crate::os::android::net::{SocketAddrExt, UnixSocketExt};
 use crate::os::linux::net::{SocketAddrExt, UnixSocketExt};
 #[cfg(any(target_os = "android", target_os = "linux"))]
 use crate::os::unix::io::AsRawFd;
-use crate::sys_common::io::test::tmpdir;
+use crate::test_helpers::tmpdir;
 use crate::thread;
 use crate::time::Duration;
 
diff --git a/library/std/src/process/tests.rs b/library/std/src/process/tests.rs
index 9b87259abef..69273d863eb 100644
--- a/library/std/src/process/tests.rs
+++ b/library/std/src/process/tests.rs
@@ -549,7 +549,7 @@ fn debug_print() {
 #[test]
 #[cfg(windows)]
 fn run_bat_script() {
-    let tempdir = crate::sys_common::io::test::tmpdir();
+    let tempdir = crate::test_helpers::tmpdir();
     let script_path = tempdir.join("hello.cmd");
 
     crate::fs::write(&script_path, "@echo Hello, %~1!").unwrap();
@@ -568,7 +568,7 @@ fn run_bat_script() {
 #[test]
 #[cfg(windows)]
 fn run_canonical_bat_script() {
-    let tempdir = crate::sys_common::io::test::tmpdir();
+    let tempdir = crate::test_helpers::tmpdir();
     let script_path = tempdir.join("hello.cmd");
 
     crate::fs::write(&script_path, "@echo Hello, %~1!").unwrap();
diff --git a/library/std/src/sys/pal/hermit/io.rs b/library/std/src/sys/io/io_slice/iovec.rs
index 0424a1ac55a..072191315f7 100644
--- a/library/std/src/sys/pal/hermit/io.rs
+++ b/library/std/src/sys/io/io_slice/iovec.rs
@@ -1,8 +1,13 @@
-use hermit_abi::{c_void, iovec};
+#[cfg(target_os = "hermit")]
+use hermit_abi::iovec;
+#[cfg(target_family = "unix")]
+use libc::iovec;
 
+use crate::ffi::c_void;
 use crate::marker::PhantomData;
-use crate::os::hermit::io::{AsFd, AsRawFd};
 use crate::slice;
+#[cfg(target_os = "solid_asp3")]
+use crate::sys::pal::abi::sockets::iovec;
 
 #[derive(Copy, Clone)]
 #[repr(transparent)]
@@ -80,8 +85,3 @@ impl<'a> IoSliceMut<'a> {
         unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
     }
 }
-
-pub fn is_terminal(fd: &impl AsFd) -> bool {
-    let fd = fd.as_fd();
-    hermit_abi::isatty(fd.as_raw_fd())
-}
diff --git a/library/std/src/sys/pal/unsupported/io.rs b/library/std/src/sys/io/io_slice/unsupported.rs
index 604735d32d5..1572cac6cd7 100644
--- a/library/std/src/sys/pal/unsupported/io.rs
+++ b/library/std/src/sys/io/io_slice/unsupported.rs
@@ -50,7 +50,3 @@ impl<'a> IoSliceMut<'a> {
         self.0
     }
 }
-
-pub fn is_terminal<T>(_: &T) -> bool {
-    false
-}
diff --git a/library/std/src/sys/pal/wasi/io.rs b/library/std/src/sys/io/io_slice/wasi.rs
index 57f81bc6257..87acbbd924e 100644
--- a/library/std/src/sys/pal/wasi/io.rs
+++ b/library/std/src/sys/io/io_slice/wasi.rs
@@ -1,7 +1,4 @@
-#![forbid(unsafe_op_in_unsafe_fn)]
-
 use crate::marker::PhantomData;
-use crate::os::fd::{AsFd, AsRawFd};
 use crate::slice;
 
 #[derive(Copy, Clone)]
@@ -77,8 +74,3 @@ impl<'a> IoSliceMut<'a> {
         unsafe { slice::from_raw_parts_mut(self.vec.buf as *mut u8, self.vec.buf_len) }
     }
 }
-
-pub fn is_terminal(fd: &impl AsFd) -> bool {
-    let fd = fd.as_fd();
-    unsafe { libc::isatty(fd.as_raw_fd()) != 0 }
-}
diff --git a/library/std/src/sys/pal/solid/io.rs b/library/std/src/sys/io/io_slice/windows.rs
index 9ef4b7049b6..c3d8ec87c19 100644
--- a/library/std/src/sys/pal/solid/io.rs
+++ b/library/std/src/sys/io/io_slice/windows.rs
@@ -1,86 +1,82 @@
-use libc::c_void;
-
-use super::abi::sockets::iovec;
 use crate::marker::PhantomData;
 use crate::slice;
+use crate::sys::c;
 
 #[derive(Copy, Clone)]
 #[repr(transparent)]
 pub struct IoSlice<'a> {
-    vec: iovec,
+    vec: c::WSABUF,
     _p: PhantomData<&'a [u8]>,
 }
 
 impl<'a> IoSlice<'a> {
     #[inline]
     pub fn new(buf: &'a [u8]) -> IoSlice<'a> {
+        assert!(buf.len() <= u32::MAX as usize);
         IoSlice {
-            vec: iovec { iov_base: buf.as_ptr() as *mut u8 as *mut c_void, iov_len: buf.len() },
+            vec: c::WSABUF { len: buf.len() as u32, buf: buf.as_ptr() as *mut u8 },
             _p: PhantomData,
         }
     }
 
     #[inline]
     pub fn advance(&mut self, n: usize) {
-        if self.vec.iov_len < n {
+        if (self.vec.len as usize) < n {
             panic!("advancing IoSlice beyond its length");
         }
 
         unsafe {
-            self.vec.iov_len -= n;
-            self.vec.iov_base = self.vec.iov_base.add(n);
+            self.vec.len -= n as u32;
+            self.vec.buf = self.vec.buf.add(n);
         }
     }
 
     #[inline]
     pub const fn as_slice(&self) -> &'a [u8] {
-        unsafe { slice::from_raw_parts(self.vec.iov_base as *mut u8, self.vec.iov_len) }
+        unsafe { slice::from_raw_parts(self.vec.buf, self.vec.len as usize) }
     }
 }
 
 #[repr(transparent)]
 pub struct IoSliceMut<'a> {
-    vec: iovec,
+    vec: c::WSABUF,
     _p: PhantomData<&'a mut [u8]>,
 }
 
 impl<'a> IoSliceMut<'a> {
     #[inline]
     pub fn new(buf: &'a mut [u8]) -> IoSliceMut<'a> {
+        assert!(buf.len() <= u32::MAX as usize);
         IoSliceMut {
-            vec: iovec { iov_base: buf.as_mut_ptr() as *mut c_void, iov_len: buf.len() },
+            vec: c::WSABUF { len: buf.len() as u32, buf: buf.as_mut_ptr() },
             _p: PhantomData,
         }
     }
 
     #[inline]
     pub fn advance(&mut self, n: usize) {
-        if self.vec.iov_len < n {
+        if (self.vec.len as usize) < n {
             panic!("advancing IoSliceMut beyond its length");
         }
 
         unsafe {
-            self.vec.iov_len -= n;
-            self.vec.iov_base = self.vec.iov_base.add(n);
+            self.vec.len -= n as u32;
+            self.vec.buf = self.vec.buf.add(n);
         }
     }
 
     #[inline]
     pub fn as_slice(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.vec.iov_base as *mut u8, self.vec.iov_len) }
+        unsafe { slice::from_raw_parts(self.vec.buf, self.vec.len as usize) }
     }
 
     #[inline]
     pub const fn into_slice(self) -> &'a mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
+        unsafe { slice::from_raw_parts_mut(self.vec.buf, self.vec.len as usize) }
     }
 
     #[inline]
     pub fn as_mut_slice(&mut self) -> &mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
+        unsafe { slice::from_raw_parts_mut(self.vec.buf, self.vec.len as usize) }
     }
 }
-
-pub fn is_terminal<T>(_: &T) -> bool {
-    false
-}
diff --git a/library/std/src/sys/io/is_terminal/hermit.rs b/library/std/src/sys/io/is_terminal/hermit.rs
new file mode 100644
index 00000000000..61bdb6f0a54
--- /dev/null
+++ b/library/std/src/sys/io/is_terminal/hermit.rs
@@ -0,0 +1,6 @@
+use crate::os::fd::{AsFd, AsRawFd};
+
+pub fn is_terminal(fd: &impl AsFd) -> bool {
+    let fd = fd.as_fd();
+    hermit_abi::isatty(fd.as_raw_fd())
+}
diff --git a/library/std/src/sys/io/is_terminal/isatty.rs b/library/std/src/sys/io/is_terminal/isatty.rs
new file mode 100644
index 00000000000..6e0b46211b9
--- /dev/null
+++ b/library/std/src/sys/io/is_terminal/isatty.rs
@@ -0,0 +1,6 @@
+use crate::os::fd::{AsFd, AsRawFd};
+
+pub fn is_terminal(fd: &impl AsFd) -> bool {
+    let fd = fd.as_fd();
+    unsafe { libc::isatty(fd.as_raw_fd()) != 0 }
+}
diff --git a/library/std/src/sys/io/is_terminal/unsupported.rs b/library/std/src/sys/io/is_terminal/unsupported.rs
new file mode 100644
index 00000000000..cee4add32fb
--- /dev/null
+++ b/library/std/src/sys/io/is_terminal/unsupported.rs
@@ -0,0 +1,3 @@
+pub fn is_terminal<T>(_: &T) -> bool {
+    false
+}
diff --git a/library/std/src/sys/pal/windows/io.rs b/library/std/src/sys/io/is_terminal/windows.rs
index f2865d2ffc1..3ec18fb47b9 100644
--- a/library/std/src/sys/pal/windows/io.rs
+++ b/library/std/src/sys/io/is_terminal/windows.rs
@@ -1,90 +1,8 @@
-use core::ffi::c_void;
-
-use crate::marker::PhantomData;
+use crate::ffi::c_void;
 use crate::mem::size_of;
 use crate::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
-use crate::slice;
 use crate::sys::c;
 
-#[derive(Copy, Clone)]
-#[repr(transparent)]
-pub struct IoSlice<'a> {
-    vec: c::WSABUF,
-    _p: PhantomData<&'a [u8]>,
-}
-
-impl<'a> IoSlice<'a> {
-    #[inline]
-    pub fn new(buf: &'a [u8]) -> IoSlice<'a> {
-        assert!(buf.len() <= u32::MAX as usize);
-        IoSlice {
-            vec: c::WSABUF { len: buf.len() as u32, buf: buf.as_ptr() as *mut u8 },
-            _p: PhantomData,
-        }
-    }
-
-    #[inline]
-    pub fn advance(&mut self, n: usize) {
-        if (self.vec.len as usize) < n {
-            panic!("advancing IoSlice beyond its length");
-        }
-
-        unsafe {
-            self.vec.len -= n as u32;
-            self.vec.buf = self.vec.buf.add(n);
-        }
-    }
-
-    #[inline]
-    pub const fn as_slice(&self) -> &'a [u8] {
-        unsafe { slice::from_raw_parts(self.vec.buf, self.vec.len as usize) }
-    }
-}
-
-#[repr(transparent)]
-pub struct IoSliceMut<'a> {
-    vec: c::WSABUF,
-    _p: PhantomData<&'a mut [u8]>,
-}
-
-impl<'a> IoSliceMut<'a> {
-    #[inline]
-    pub fn new(buf: &'a mut [u8]) -> IoSliceMut<'a> {
-        assert!(buf.len() <= u32::MAX as usize);
-        IoSliceMut {
-            vec: c::WSABUF { len: buf.len() as u32, buf: buf.as_mut_ptr() },
-            _p: PhantomData,
-        }
-    }
-
-    #[inline]
-    pub fn advance(&mut self, n: usize) {
-        if (self.vec.len as usize) < n {
-            panic!("advancing IoSliceMut beyond its length");
-        }
-
-        unsafe {
-            self.vec.len -= n as u32;
-            self.vec.buf = self.vec.buf.add(n);
-        }
-    }
-
-    #[inline]
-    pub fn as_slice(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.vec.buf, self.vec.len as usize) }
-    }
-
-    #[inline]
-    pub const fn into_slice(self) -> &'a mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.buf, self.vec.len as usize) }
-    }
-
-    #[inline]
-    pub fn as_mut_slice(&mut self) -> &mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.buf, self.vec.len as usize) }
-    }
-}
-
 pub fn is_terminal(h: &impl AsHandle) -> bool {
     handle_is_console(h.as_handle())
 }
diff --git a/library/std/src/sys/io/mod.rs b/library/std/src/sys/io/mod.rs
new file mode 100644
index 00000000000..e00b479109f
--- /dev/null
+++ b/library/std/src/sys/io/mod.rs
@@ -0,0 +1,44 @@
+#![forbid(unsafe_op_in_unsafe_fn)]
+
+mod io_slice {
+    cfg_if::cfg_if! {
+        if #[cfg(any(target_family = "unix", target_os = "hermit", target_os = "solid_asp3"))] {
+            mod iovec;
+            pub use iovec::*;
+        } else if #[cfg(target_os = "windows")] {
+            mod windows;
+            pub use windows::*;
+        } else if #[cfg(target_os = "wasi")] {
+            mod wasi;
+            pub use wasi::*;
+        } else {
+            mod unsupported;
+            pub use unsupported::*;
+        }
+    }
+}
+
+mod is_terminal {
+    cfg_if::cfg_if! {
+        if #[cfg(any(target_family = "unix", target_os = "wasi"))] {
+            mod isatty;
+            pub use isatty::*;
+        } else if #[cfg(target_os = "windows")] {
+            mod windows;
+            pub use windows::*;
+        } else if #[cfg(target_os = "hermit")] {
+            mod hermit;
+            pub use hermit::*;
+        } else {
+            mod unsupported;
+            pub use unsupported::*;
+        }
+    }
+}
+
+pub use io_slice::{IoSlice, IoSliceMut};
+pub use is_terminal::is_terminal;
+
+// Bare metal platforms usually have very small amounts of RAM
+// (in the order of hundreds of KB)
+pub const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") { 512 } else { 8 * 1024 };
diff --git a/library/std/src/sys/mod.rs b/library/std/src/sys/mod.rs
index 39a0bc6e337..1032fcba5e2 100644
--- a/library/std/src/sys/mod.rs
+++ b/library/std/src/sys/mod.rs
@@ -12,6 +12,7 @@ pub mod anonymous_pipe;
 pub mod backtrace;
 pub mod cmath;
 pub mod exit_guard;
+pub mod io;
 pub mod net;
 pub mod os_str;
 pub mod path;
diff --git a/library/std/src/sys/pal/hermit/mod.rs b/library/std/src/sys/pal/hermit/mod.rs
index 032007aa4dc..3d555ad5050 100644
--- a/library/std/src/sys/pal/hermit/mod.rs
+++ b/library/std/src/sys/pal/hermit/mod.rs
@@ -23,7 +23,6 @@ pub mod env;
 pub mod fd;
 pub mod fs;
 pub mod futex;
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/sgx/mod.rs b/library/std/src/sys/pal/sgx/mod.rs
index 0f5935d0c71..9a04fa4b97e 100644
--- a/library/std/src/sys/pal/sgx/mod.rs
+++ b/library/std/src/sys/pal/sgx/mod.rs
@@ -14,8 +14,6 @@ pub mod env;
 pub mod fd;
 #[path = "../unsupported/fs.rs"]
 pub mod fs;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 mod libunwind_integration;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
diff --git a/library/std/src/sys/pal/sgx/stdio.rs b/library/std/src/sys/pal/sgx/stdio.rs
index 2e680e740fd..e79a3d971c6 100644
--- a/library/std/src/sys/pal/sgx/stdio.rs
+++ b/library/std/src/sys/pal/sgx/stdio.rs
@@ -62,7 +62,7 @@ impl io::Write for Stderr {
     }
 }
 
-pub const STDIN_BUF_SIZE: usize = crate::sys_common::io::DEFAULT_BUF_SIZE;
+pub const STDIN_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
 
 pub fn is_ebadf(err: &io::Error) -> bool {
     // FIXME: Rust normally maps Unix EBADF to `Uncategorized`
diff --git a/library/std/src/sys/pal/solid/mod.rs b/library/std/src/sys/pal/solid/mod.rs
index caf848a4e9b..06af7bfade0 100644
--- a/library/std/src/sys/pal/solid/mod.rs
+++ b/library/std/src/sys/pal/solid/mod.rs
@@ -23,7 +23,6 @@ pub mod env;
 // `crate::sys::error`
 pub(crate) mod error;
 pub mod fs;
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/teeos/mod.rs b/library/std/src/sys/pal/teeos/mod.rs
index a9904e66664..f850fefc8f2 100644
--- a/library/std/src/sys/pal/teeos/mod.rs
+++ b/library/std/src/sys/pal/teeos/mod.rs
@@ -13,8 +13,6 @@ pub mod env;
 //pub mod fd;
 #[path = "../unsupported/fs.rs"]
 pub mod fs;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/uefi/mod.rs b/library/std/src/sys/pal/uefi/mod.rs
index 07025a304bf..4766e2ef0a9 100644
--- a/library/std/src/sys/pal/uefi/mod.rs
+++ b/library/std/src/sys/pal/uefi/mod.rs
@@ -17,8 +17,6 @@ pub mod args;
 pub mod env;
 pub mod fs;
 pub mod helpers;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/unix/io.rs b/library/std/src/sys/pal/unix/io.rs
deleted file mode 100644
index 0d5a152dc0d..00000000000
--- a/library/std/src/sys/pal/unix/io.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use libc::{c_void, iovec};
-
-use crate::marker::PhantomData;
-use crate::os::fd::{AsFd, AsRawFd};
-use crate::slice;
-
-#[derive(Copy, Clone)]
-#[repr(transparent)]
-pub struct IoSlice<'a> {
-    vec: iovec,
-    _p: PhantomData<&'a [u8]>,
-}
-
-impl<'a> IoSlice<'a> {
-    #[inline]
-    pub fn new(buf: &'a [u8]) -> IoSlice<'a> {
-        IoSlice {
-            vec: iovec { iov_base: buf.as_ptr() as *mut u8 as *mut c_void, iov_len: buf.len() },
-            _p: PhantomData,
-        }
-    }
-
-    #[inline]
-    pub fn advance(&mut self, n: usize) {
-        if self.vec.iov_len < n {
-            panic!("advancing IoSlice beyond its length");
-        }
-
-        unsafe {
-            self.vec.iov_len -= n;
-            self.vec.iov_base = self.vec.iov_base.add(n);
-        }
-    }
-
-    #[inline]
-    pub const fn as_slice(&self) -> &'a [u8] {
-        unsafe { slice::from_raw_parts(self.vec.iov_base as *mut u8, self.vec.iov_len) }
-    }
-}
-
-#[repr(transparent)]
-pub struct IoSliceMut<'a> {
-    vec: iovec,
-    _p: PhantomData<&'a mut [u8]>,
-}
-
-impl<'a> IoSliceMut<'a> {
-    #[inline]
-    pub fn new(buf: &'a mut [u8]) -> IoSliceMut<'a> {
-        IoSliceMut {
-            vec: iovec { iov_base: buf.as_mut_ptr() as *mut c_void, iov_len: buf.len() },
-            _p: PhantomData,
-        }
-    }
-
-    #[inline]
-    pub fn advance(&mut self, n: usize) {
-        if self.vec.iov_len < n {
-            panic!("advancing IoSliceMut beyond its length");
-        }
-
-        unsafe {
-            self.vec.iov_len -= n;
-            self.vec.iov_base = self.vec.iov_base.add(n);
-        }
-    }
-
-    #[inline]
-    pub fn as_slice(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.vec.iov_base as *mut u8, self.vec.iov_len) }
-    }
-
-    #[inline]
-    pub const fn into_slice(self) -> &'a mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
-    }
-
-    #[inline]
-    pub fn as_mut_slice(&mut self) -> &mut [u8] {
-        unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
-    }
-}
-
-pub fn is_terminal(fd: &impl AsFd) -> bool {
-    let fd = fd.as_fd();
-    unsafe { libc::isatty(fd.as_raw_fd()) != 0 }
-}
diff --git a/library/std/src/sys/pal/unix/kernel_copy/tests.rs b/library/std/src/sys/pal/unix/kernel_copy/tests.rs
index 1350d743ff6..54d8f8ed2ed 100644
--- a/library/std/src/sys/pal/unix/kernel_copy/tests.rs
+++ b/library/std/src/sys/pal/unix/kernel_copy/tests.rs
@@ -2,7 +2,7 @@ use crate::fs::OpenOptions;
 use crate::io;
 use crate::io::{BufRead, Read, Result, Seek, SeekFrom, Write};
 use crate::os::unix::io::AsRawFd;
-use crate::sys_common::io::test::tmpdir;
+use crate::test_helpers::tmpdir;
 
 #[test]
 fn copy_specialization() -> Result<()> {
diff --git a/library/std/src/sys/pal/unix/mod.rs b/library/std/src/sys/pal/unix/mod.rs
index 6862399b942..027df6c5691 100644
--- a/library/std/src/sys/pal/unix/mod.rs
+++ b/library/std/src/sys/pal/unix/mod.rs
@@ -11,7 +11,6 @@ pub mod env;
 pub mod fd;
 pub mod fs;
 pub mod futex;
-pub mod io;
 #[cfg(any(target_os = "linux", target_os = "android"))]
 pub mod kernel_copy;
 #[cfg(target_os = "linux")]
diff --git a/library/std/src/sys/pal/unix/stdio.rs b/library/std/src/sys/pal/unix/stdio.rs
index 97e75f1b5b6..8c2f61a40de 100644
--- a/library/std/src/sys/pal/unix/stdio.rs
+++ b/library/std/src/sys/pal/unix/stdio.rs
@@ -92,7 +92,7 @@ pub fn is_ebadf(err: &io::Error) -> bool {
     err.raw_os_error() == Some(libc::EBADF as i32)
 }
 
-pub const STDIN_BUF_SIZE: usize = crate::sys_common::io::DEFAULT_BUF_SIZE;
+pub const STDIN_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
 
 pub fn panic_output() -> Option<impl io::Write> {
     Some(Stderr::new())
diff --git a/library/std/src/sys/pal/unsupported/mod.rs b/library/std/src/sys/pal/unsupported/mod.rs
index 4941dd4918c..b1aaeb1b4c8 100644
--- a/library/std/src/sys/pal/unsupported/mod.rs
+++ b/library/std/src/sys/pal/unsupported/mod.rs
@@ -3,7 +3,6 @@
 pub mod args;
 pub mod env;
 pub mod fs;
-pub mod io;
 pub mod os;
 pub mod pipe;
 pub mod process;
diff --git a/library/std/src/sys/pal/wasi/mod.rs b/library/std/src/sys/pal/wasi/mod.rs
index 312ad28ab51..f4588a60ea9 100644
--- a/library/std/src/sys/pal/wasi/mod.rs
+++ b/library/std/src/sys/pal/wasi/mod.rs
@@ -20,7 +20,6 @@ pub mod fs;
 #[allow(unused)]
 #[path = "../wasm/atomics/futex.rs"]
 pub mod futex;
-pub mod io;
 
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
diff --git a/library/std/src/sys/pal/wasi/stdio.rs b/library/std/src/sys/pal/wasi/stdio.rs
index ca49f871e19..d08b772e5fc 100644
--- a/library/std/src/sys/pal/wasi/stdio.rs
+++ b/library/std/src/sys/pal/wasi/stdio.rs
@@ -101,7 +101,7 @@ impl io::Write for Stderr {
     }
 }
 
-pub const STDIN_BUF_SIZE: usize = crate::sys_common::io::DEFAULT_BUF_SIZE;
+pub const STDIN_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
 
 pub fn is_ebadf(err: &io::Error) -> bool {
     err.raw_os_error() == Some(wasi::ERRNO_BADF.raw().into())
diff --git a/library/std/src/sys/pal/wasip2/mod.rs b/library/std/src/sys/pal/wasip2/mod.rs
index 234e946d3b8..72c9742b2e5 100644
--- a/library/std/src/sys/pal/wasip2/mod.rs
+++ b/library/std/src/sys/pal/wasip2/mod.rs
@@ -17,8 +17,6 @@ pub mod fs;
 #[allow(unused)]
 #[path = "../wasm/atomics/futex.rs"]
 pub mod futex;
-#[path = "../wasi/io.rs"]
-pub mod io;
 
 #[path = "../wasi/os.rs"]
 pub mod os;
diff --git a/library/std/src/sys/pal/wasm/mod.rs b/library/std/src/sys/pal/wasm/mod.rs
index 1280f353200..32d59c4d0f7 100644
--- a/library/std/src/sys/pal/wasm/mod.rs
+++ b/library/std/src/sys/pal/wasm/mod.rs
@@ -21,8 +21,6 @@ pub mod args;
 pub mod env;
 #[path = "../unsupported/fs.rs"]
 pub mod fs;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 #[path = "../unsupported/os.rs"]
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
diff --git a/library/std/src/sys/pal/windows/mod.rs b/library/std/src/sys/pal/windows/mod.rs
index f9aa049ca9a..1eca346b76c 100644
--- a/library/std/src/sys/pal/windows/mod.rs
+++ b/library/std/src/sys/pal/windows/mod.rs
@@ -21,7 +21,6 @@ pub mod fs;
 #[cfg(not(target_vendor = "win7"))]
 pub mod futex;
 pub mod handle;
-pub mod io;
 pub mod os;
 pub mod pipe;
 pub mod process;
diff --git a/library/std/src/sys/pal/windows/process/tests.rs b/library/std/src/sys/pal/windows/process/tests.rs
index 1bcc5fa6b20..9a1eaf42fd9 100644
--- a/library/std/src/sys/pal/windows/process/tests.rs
+++ b/library/std/src/sys/pal/windows/process/tests.rs
@@ -158,7 +158,7 @@ fn windows_exe_resolver() {
     use super::resolve_exe;
     use crate::io;
     use crate::sys::fs::symlink;
-    use crate::sys_common::io::test::tmpdir;
+    use crate::test_helpers::tmpdir;
 
     let env_paths = || env::var_os("PATH");
 
diff --git a/library/std/src/sys/pal/xous/mod.rs b/library/std/src/sys/pal/xous/mod.rs
index 8ba2b6e2f20..1bd0e67f371 100644
--- a/library/std/src/sys/pal/xous/mod.rs
+++ b/library/std/src/sys/pal/xous/mod.rs
@@ -5,8 +5,6 @@ pub mod args;
 pub mod env;
 #[path = "../unsupported/fs.rs"]
 pub mod fs;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/zkvm/mod.rs b/library/std/src/sys/pal/zkvm/mod.rs
index 9e9ae861070..054c867f90d 100644
--- a/library/std/src/sys/pal/zkvm/mod.rs
+++ b/library/std/src/sys/pal/zkvm/mod.rs
@@ -16,8 +16,6 @@ pub mod args;
 pub mod env;
 #[path = "../unsupported/fs.rs"]
 pub mod fs;
-#[path = "../unsupported/io.rs"]
-pub mod io;
 pub mod os;
 #[path = "../unsupported/pipe.rs"]
 pub mod pipe;
diff --git a/library/std/src/sys/pal/zkvm/stdio.rs b/library/std/src/sys/pal/zkvm/stdio.rs
index dd218c8894c..5f1d06dd1d7 100644
--- a/library/std/src/sys/pal/zkvm/stdio.rs
+++ b/library/std/src/sys/pal/zkvm/stdio.rs
@@ -54,7 +54,7 @@ impl io::Write for Stderr {
     }
 }
 
-pub const STDIN_BUF_SIZE: usize = crate::sys_common::io::DEFAULT_BUF_SIZE;
+pub const STDIN_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
 
 pub fn is_ebadf(_err: &io::Error) -> bool {
     true
diff --git a/library/std/src/sys_common/io.rs b/library/std/src/sys_common/io.rs
deleted file mode 100644
index 6f6f282d432..00000000000
--- a/library/std/src/sys_common/io.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Bare metal platforms usually have very small amounts of RAM
-// (in the order of hundreds of KB)
-pub const DEFAULT_BUF_SIZE: usize = if cfg!(target_os = "espidf") { 512 } else { 8 * 1024 };
-
-#[cfg(test)]
-#[allow(dead_code)] // not used on emscripten and wasi
-pub mod test {
-    use rand::RngCore;
-
-    use crate::path::{Path, PathBuf};
-    use crate::{env, fs, thread};
-
-    pub struct TempDir(PathBuf);
-
-    impl TempDir {
-        pub fn join(&self, path: &str) -> PathBuf {
-            let TempDir(ref p) = *self;
-            p.join(path)
-        }
-
-        pub fn path(&self) -> &Path {
-            let TempDir(ref p) = *self;
-            p
-        }
-    }
-
-    impl Drop for TempDir {
-        fn drop(&mut self) {
-            // Gee, seeing how we're testing the fs module I sure hope that we
-            // at least implement this correctly!
-            let TempDir(ref p) = *self;
-            let result = fs::remove_dir_all(p);
-            // Avoid panicking while panicking as this causes the process to
-            // immediately abort, without displaying test results.
-            if !thread::panicking() {
-                result.unwrap();
-            }
-        }
-    }
-
-    #[track_caller] // for `test_rng`
-    pub fn tmpdir() -> TempDir {
-        let p = env::temp_dir();
-        let mut r = crate::test_helpers::test_rng();
-        let ret = p.join(&format!("rust-{}", r.next_u32()));
-        fs::create_dir(&ret).unwrap();
-        TempDir(ret)
-    }
-}
diff --git a/library/std/src/sys_common/mod.rs b/library/std/src/sys_common/mod.rs
index 959fbd4ca0a..4dc67d26bd8 100644
--- a/library/std/src/sys_common/mod.rs
+++ b/library/std/src/sys_common/mod.rs
@@ -21,7 +21,6 @@
 mod tests;
 
 pub mod fs;
-pub mod io;
 pub mod process;
 pub mod wstr;
 pub mod wtf8;
diff --git a/library/std/src/test_helpers.rs b/library/std/src/test_helpers.rs
new file mode 100644
index 00000000000..7c20f38c863
--- /dev/null
+++ b/library/std/src/test_helpers.rs
@@ -0,0 +1,65 @@
+use rand::{RngCore, SeedableRng};
+
+use crate::hash::{BuildHasher, Hash, Hasher, RandomState};
+use crate::panic::Location;
+use crate::path::{Path, PathBuf};
+use crate::{env, fs, thread};
+
+/// Test-only replacement for `rand::thread_rng()`, which is unusable for
+/// us, as we want to allow running stdlib tests on tier-3 targets which may
+/// not have `getrandom` support.
+///
+/// Does a bit of a song and dance to ensure that the seed is different on
+/// each call (as some tests sadly rely on this), but doesn't try that hard.
+///
+/// This is duplicated in the `core`, `alloc` test suites (as well as
+/// `std`'s integration tests), but figuring out a mechanism to share these
+/// seems far more painful than copy-pasting a 7 line function a couple
+/// times, given that even under a perma-unstable feature, I don't think we
+/// want to expose types from `rand` from `std`.
+#[track_caller]
+pub(crate) fn test_rng() -> rand_xorshift::XorShiftRng {
+    let mut hasher = RandomState::new().build_hasher();
+    Location::caller().hash(&mut hasher);
+    let hc64 = hasher.finish();
+    let seed_vec = hc64.to_le_bytes().into_iter().chain(0u8..8).collect::<Vec<u8>>();
+    let seed: [u8; 16] = seed_vec.as_slice().try_into().unwrap();
+    SeedableRng::from_seed(seed)
+}
+
+pub struct TempDir(PathBuf);
+
+impl TempDir {
+    pub fn join(&self, path: &str) -> PathBuf {
+        let TempDir(ref p) = *self;
+        p.join(path)
+    }
+
+    pub fn path(&self) -> &Path {
+        let TempDir(ref p) = *self;
+        p
+    }
+}
+
+impl Drop for TempDir {
+    fn drop(&mut self) {
+        // Gee, seeing how we're testing the fs module I sure hope that we
+        // at least implement this correctly!
+        let TempDir(ref p) = *self;
+        let result = fs::remove_dir_all(p);
+        // Avoid panicking while panicking as this causes the process to
+        // immediately abort, without displaying test results.
+        if !thread::panicking() {
+            result.unwrap();
+        }
+    }
+}
+
+#[track_caller] // for `test_rng`
+pub fn tmpdir() -> TempDir {
+    let p = env::temp_dir();
+    let mut r = test_rng();
+    let ret = p.join(&format!("rust-{}", r.next_u32()));
+    fs::create_dir(&ret).unwrap();
+    TempDir(ret)
+}
diff --git a/library/std/tests/common/mod.rs b/library/std/tests/common/mod.rs
index 7cf70c725e4..1e8e4cced6c 100644
--- a/library/std/tests/common/mod.rs
+++ b/library/std/tests/common/mod.rs
@@ -18,7 +18,7 @@ pub(crate) fn test_rng() -> rand_xorshift::XorShiftRng {
     rand::SeedableRng::from_seed(seed)
 }
 
-// Copied from std::sys_common::io
+// Copied from std::test_helpers
 pub(crate) struct TempDir(PathBuf);
 
 impl TempDir {
diff --git a/src/bootstrap/src/core/build_steps/tool.rs b/src/bootstrap/src/core/build_steps/tool.rs
index 75fac88252b..620daec5114 100644
--- a/src/bootstrap/src/core/build_steps/tool.rs
+++ b/src/bootstrap/src/core/build_steps/tool.rs
@@ -582,7 +582,7 @@ impl Step for Rustdoc {
             if !target_compiler.is_snapshot(builder) {
                 panic!("rustdoc in stage 0 must be snapshot rustdoc");
             }
-            return builder.initial_rustc.with_file_name(exe("rustdoc", target_compiler.host));
+            return builder.initial_rustdoc.clone();
         }
         let target = target_compiler.host;
 
diff --git a/src/bootstrap/src/lib.rs b/src/bootstrap/src/lib.rs
index 2dd83d5938e..4e097b67503 100644
--- a/src/bootstrap/src/lib.rs
+++ b/src/bootstrap/src/lib.rs
@@ -154,6 +154,7 @@ pub struct Build {
     targets: Vec<TargetSelection>,
 
     initial_rustc: PathBuf,
+    initial_rustdoc: PathBuf,
     initial_cargo: PathBuf,
     initial_lld: PathBuf,
     initial_relative_libdir: PathBuf,
@@ -354,6 +355,7 @@ impl Build {
             initial_lld,
             initial_relative_libdir,
             initial_rustc: config.initial_rustc.clone(),
+            initial_rustdoc: config.initial_rustc.with_file_name(exe("rustdoc", config.build)),
             initial_cargo: config.initial_cargo.clone(),
             initial_sysroot: config.initial_sysroot.clone(),
             local_rebuild: config.local_rebuild,
diff --git a/src/ci/github-actions/jobs.yml b/src/ci/github-actions/jobs.yml
index 92458bfaa34..341d82599fd 100644
--- a/src/ci/github-actions/jobs.yml
+++ b/src/ci/github-actions/jobs.yml
@@ -167,10 +167,10 @@ auto:
     <<: *job-linux-4c
 
   - name: dist-loongarch64-linux
-    <<: *job-linux-4c
+    <<: *job-linux-4c-largedisk
 
   - name: dist-loongarch64-musl
-    <<: *job-linux-4c
+    <<: *job-linux-4c-largedisk
 
   - name: dist-ohos
     <<: *job-linux-4c
diff --git a/src/tools/clippy/clippy_lints/src/declared_lints.rs b/src/tools/clippy/clippy_lints/src/declared_lints.rs
index 9fbeab5bf2e..5c5978b5559 100644
--- a/src/tools/clippy/clippy_lints/src/declared_lints.rs
+++ b/src/tools/clippy/clippy_lints/src/declared_lints.rs
@@ -144,8 +144,6 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::doc::DOC_NESTED_REFDEFS_INFO,
     crate::doc::DOC_OVERINDENTED_LIST_ITEMS_INFO,
     crate::doc::EMPTY_DOCS_INFO,
-    crate::doc::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
-    crate::doc::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
     crate::doc::MISSING_ERRORS_DOC_INFO,
     crate::doc::MISSING_PANICS_DOC_INFO,
     crate::doc::MISSING_SAFETY_DOC_INFO,
@@ -162,6 +160,8 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO,
     crate::empty_drop::EMPTY_DROP_INFO,
     crate::empty_enum::EMPTY_ENUM_INFO,
+    crate::empty_line_after::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
+    crate::empty_line_after::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
     crate::empty_with_brackets::EMPTY_ENUM_VARIANTS_WITH_BRACKETS_INFO,
     crate::empty_with_brackets::EMPTY_STRUCTS_WITH_BRACKETS_INFO,
     crate::endian_bytes::BIG_ENDIAN_BYTES_INFO,
diff --git a/src/tools/clippy/clippy_lints/src/doc/empty_line_after.rs b/src/tools/clippy/clippy_lints/src/doc/empty_line_after.rs
deleted file mode 100644
index 6e85c6af642..00000000000
--- a/src/tools/clippy/clippy_lints/src/doc/empty_line_after.rs
+++ /dev/null
@@ -1,345 +0,0 @@
-use clippy_utils::diagnostics::span_lint_and_then;
-use clippy_utils::source::{SpanRangeExt, snippet_indent};
-use clippy_utils::tokenize_with_text;
-use itertools::Itertools;
-use rustc_ast::AttrStyle;
-use rustc_ast::token::CommentKind;
-use rustc_errors::{Applicability, Diag, SuggestionStyle};
-use rustc_hir::{AttrKind, Attribute, ItemKind, Node};
-use rustc_lexer::TokenKind;
-use rustc_lint::LateContext;
-use rustc_span::{BytePos, ExpnKind, InnerSpan, Span, SpanData};
-
-use super::{EMPTY_LINE_AFTER_DOC_COMMENTS, EMPTY_LINE_AFTER_OUTER_ATTR};
-
-#[derive(Debug, PartialEq, Clone, Copy)]
-enum StopKind {
-    Attr,
-    Doc(CommentKind),
-}
-
-impl StopKind {
-    fn is_doc(self) -> bool {
-        matches!(self, StopKind::Doc(_))
-    }
-}
-
-#[derive(Debug)]
-struct Stop {
-    span: Span,
-    kind: StopKind,
-    first: usize,
-    last: usize,
-}
-
-impl Stop {
-    fn convert_to_inner(&self) -> (Span, String) {
-        let inner = match self.kind {
-            // #|[...]
-            StopKind::Attr => InnerSpan::new(1, 1),
-            // /// or /**
-            //   ^      ^
-            StopKind::Doc(_) => InnerSpan::new(2, 3),
-        };
-        (self.span.from_inner(inner), "!".into())
-    }
-
-    fn comment_out(&self, cx: &LateContext<'_>, suggestions: &mut Vec<(Span, String)>) {
-        match self.kind {
-            StopKind::Attr => {
-                if cx.tcx.sess.source_map().is_multiline(self.span) {
-                    suggestions.extend([
-                        (self.span.shrink_to_lo(), "/* ".into()),
-                        (self.span.shrink_to_hi(), " */".into()),
-                    ]);
-                } else {
-                    suggestions.push((self.span.shrink_to_lo(), "// ".into()));
-                }
-            },
-            StopKind::Doc(CommentKind::Line) => suggestions.push((self.span.shrink_to_lo(), "// ".into())),
-            StopKind::Doc(CommentKind::Block) => {
-                // /** outer */  /*! inner */
-                //  ^             ^
-                let asterisk = self.span.from_inner(InnerSpan::new(1, 2));
-                suggestions.push((asterisk, String::new()));
-            },
-        }
-    }
-
-    fn from_attr(cx: &LateContext<'_>, attr: &Attribute) -> Option<Self> {
-        let SpanData { lo, hi, .. } = attr.span.data();
-        let file = cx.tcx.sess.source_map().lookup_source_file(lo);
-
-        Some(Self {
-            span: attr.span,
-            kind: match attr.kind {
-                AttrKind::Normal(_) => StopKind::Attr,
-                AttrKind::DocComment(comment_kind, _) => StopKind::Doc(comment_kind),
-            },
-            first: file.lookup_line(file.relative_position(lo))?,
-            last: file.lookup_line(file.relative_position(hi))?,
-        })
-    }
-}
-
-/// Represents a set of attrs/doc comments separated by 1 or more empty lines
-///
-/// ```ignore
-/// /// chunk 1 docs
-/// // not an empty line so also part of chunk 1
-/// #[chunk_1_attrs] // <-- prev_stop
-///
-/// /* gap */
-///
-/// /// chunk 2 docs // <-- next_stop
-/// #[chunk_2_attrs]
-/// ```
-struct Gap<'a> {
-    /// The span of individual empty lines including the newline at the end of the line
-    empty_lines: Vec<Span>,
-    has_comment: bool,
-    next_stop: &'a Stop,
-    prev_stop: &'a Stop,
-    /// The chunk that includes [`prev_stop`](Self::prev_stop)
-    prev_chunk: &'a [Stop],
-}
-
-impl<'a> Gap<'a> {
-    fn new(cx: &LateContext<'_>, prev_chunk: &'a [Stop], next_chunk: &'a [Stop]) -> Option<Self> {
-        let prev_stop = prev_chunk.last()?;
-        let next_stop = next_chunk.first()?;
-        let gap_span = prev_stop.span.between(next_stop.span);
-        let gap_snippet = gap_span.get_source_text(cx)?;
-
-        let mut has_comment = false;
-        let mut empty_lines = Vec::new();
-
-        for (token, source, inner_span) in tokenize_with_text(&gap_snippet) {
-            match token {
-                TokenKind::BlockComment {
-                    doc_style: None,
-                    terminated: true,
-                }
-                | TokenKind::LineComment { doc_style: None } => has_comment = true,
-                TokenKind::Whitespace => {
-                    let newlines = source.bytes().positions(|b| b == b'\n');
-                    empty_lines.extend(
-                        newlines
-                            .tuple_windows()
-                            .map(|(a, b)| InnerSpan::new(inner_span.start + a + 1, inner_span.start + b))
-                            .map(|inner_span| gap_span.from_inner(inner_span)),
-                    );
-                },
-                // Ignore cfg_attr'd out attributes as they may contain empty lines, could also be from macro
-                // shenanigans
-                _ => return None,
-            }
-        }
-
-        (!empty_lines.is_empty()).then_some(Self {
-            empty_lines,
-            has_comment,
-            next_stop,
-            prev_stop,
-            prev_chunk,
-        })
-    }
-
-    fn contiguous_empty_lines(&self) -> impl Iterator<Item = Span> + '_ {
-        self.empty_lines
-            // The `+ BytePos(1)` means "next line", because each empty line span is "N:1-N:1".
-            .chunk_by(|a, b| a.hi() + BytePos(1) == b.lo())
-            .map(|chunk| {
-                let first = chunk.first().expect("at least one empty line");
-                let last = chunk.last().expect("at least one empty line");
-                // The BytePos subtraction here is safe, as before an empty line, there must be at least one
-                // attribute/comment. The span needs to start at the end of the previous line.
-                first.with_lo(first.lo() - BytePos(1)).with_hi(last.hi())
-            })
-    }
-}
-
-/// If the node the attributes/docs apply to is the first in the module/crate suggest converting
-/// them to inner attributes/docs
-fn suggest_inner(cx: &LateContext<'_>, diag: &mut Diag<'_, ()>, kind: StopKind, gaps: &[Gap<'_>]) {
-    let Some(owner) = cx.last_node_with_lint_attrs.as_owner() else {
-        return;
-    };
-    let parent_desc = match cx.tcx.parent_hir_node(owner.into()) {
-        Node::Item(item)
-            if let ItemKind::Mod(parent_mod) = item.kind
-                && let [first, ..] = parent_mod.item_ids
-                && first.owner_id == owner =>
-        {
-            "parent module"
-        },
-        Node::Crate(crate_mod)
-            if let Some(first) = crate_mod
-                .item_ids
-                .iter()
-                .map(|&id| cx.tcx.hir().item(id))
-                // skip prelude imports
-                .find(|item| !matches!(item.span.ctxt().outer_expn_data().kind, ExpnKind::AstPass(_)))
-                && first.owner_id == owner =>
-        {
-            "crate"
-        },
-        _ => return,
-    };
-
-    diag.multipart_suggestion_verbose(
-        match kind {
-            StopKind::Attr => format!("if the attribute should apply to the {parent_desc} use an inner attribute"),
-            StopKind::Doc(_) => format!("if the comment should document the {parent_desc} use an inner doc comment"),
-        },
-        gaps.iter()
-            .flat_map(|gap| gap.prev_chunk)
-            .map(Stop::convert_to_inner)
-            .collect(),
-        Applicability::MaybeIncorrect,
-    );
-}
-
-fn check_gaps(cx: &LateContext<'_>, gaps: &[Gap<'_>]) -> bool {
-    let Some(first_gap) = gaps.first() else {
-        return false;
-    };
-    let empty_lines = || gaps.iter().flat_map(|gap| gap.empty_lines.iter().copied());
-    let contiguous_empty_lines = || gaps.iter().flat_map(Gap::contiguous_empty_lines);
-    let mut has_comment = false;
-    let mut has_attr = false;
-    for gap in gaps {
-        has_comment |= gap.has_comment;
-        if !has_attr {
-            has_attr = gap.prev_chunk.iter().any(|stop| stop.kind == StopKind::Attr);
-        }
-    }
-    let kind = first_gap.prev_stop.kind;
-    let (lint, kind_desc) = match kind {
-        StopKind::Attr => (EMPTY_LINE_AFTER_OUTER_ATTR, "outer attribute"),
-        StopKind::Doc(_) => (EMPTY_LINE_AFTER_DOC_COMMENTS, "doc comment"),
-    };
-    let (lines, are, them) = if empty_lines().nth(1).is_some() {
-        ("lines", "are", "them")
-    } else {
-        ("line", "is", "it")
-    };
-    span_lint_and_then(
-        cx,
-        lint,
-        first_gap.prev_stop.span.to(empty_lines().last().unwrap()),
-        format!("empty {lines} after {kind_desc}"),
-        |diag| {
-            if let Some(owner) = cx.last_node_with_lint_attrs.as_owner() {
-                let def_id = owner.to_def_id();
-                let def_descr = cx.tcx.def_descr(def_id);
-                diag.span_label(
-                    cx.tcx.def_span(def_id),
-                    match kind {
-                        StopKind::Attr => format!("the attribute applies to this {def_descr}"),
-                        StopKind::Doc(_) => format!("the comment documents this {def_descr}"),
-                    },
-                );
-            }
-
-            diag.multipart_suggestion_with_style(
-                format!("if the empty {lines} {are} unintentional remove {them}"),
-                contiguous_empty_lines()
-                    .map(|empty_lines| (empty_lines, String::new()))
-                    .collect(),
-                Applicability::MaybeIncorrect,
-                SuggestionStyle::HideCodeAlways,
-            );
-
-            if has_comment && kind.is_doc() {
-                // Likely doc comments that applied to some now commented out code
-                //
-                // /// Old docs for Foo
-                // // struct Foo;
-
-                let mut suggestions = Vec::new();
-                for stop in gaps.iter().flat_map(|gap| gap.prev_chunk) {
-                    stop.comment_out(cx, &mut suggestions);
-                }
-                let name = match cx.tcx.hir().opt_name(cx.last_node_with_lint_attrs) {
-                    Some(name) => format!("`{name}`"),
-                    None => "this".into(),
-                };
-                diag.multipart_suggestion_verbose(
-                    format!("if the doc comment should not document {name} comment it out"),
-                    suggestions,
-                    Applicability::MaybeIncorrect,
-                );
-            } else {
-                suggest_inner(cx, diag, kind, gaps);
-            }
-
-            if kind == StopKind::Doc(CommentKind::Line)
-                && gaps
-                    .iter()
-                    .all(|gap| !gap.has_comment && gap.next_stop.kind == StopKind::Doc(CommentKind::Line))
-            {
-                // Commentless empty gaps between line doc comments, possibly intended to be part of the markdown
-
-                let indent = snippet_indent(cx, first_gap.prev_stop.span).unwrap_or_default();
-                diag.multipart_suggestion_verbose(
-                    format!("if the documentation should include the empty {lines} include {them} in the comment"),
-                    empty_lines()
-                        .map(|empty_line| (empty_line, format!("{indent}///")))
-                        .collect(),
-                    Applicability::MaybeIncorrect,
-                );
-            }
-        },
-    );
-    kind.is_doc()
-}
-
-/// Returns `true` if [`EMPTY_LINE_AFTER_DOC_COMMENTS`] triggered, used to skip other doc comment
-/// lints where they would be confusing
-///
-/// [`EMPTY_LINE_AFTER_OUTER_ATTR`] is also here to share an implementation but does not return
-/// `true` if it triggers
-pub(super) fn check(cx: &LateContext<'_>, attrs: &[Attribute]) -> bool {
-    let mut outer = attrs
-        .iter()
-        .filter(|attr| attr.style == AttrStyle::Outer && !attr.span.from_expansion())
-        .map(|attr| Stop::from_attr(cx, attr))
-        .collect::<Option<Vec<_>>>()
-        .unwrap_or_default();
-
-    if outer.is_empty() {
-        return false;
-    }
-
-    // Push a fake attribute Stop for the item itself so we check for gaps between the last outer
-    // attr/doc comment and the item they apply to
-    let span = cx.tcx.hir().span(cx.last_node_with_lint_attrs);
-    if !span.from_expansion()
-        && let Ok(line) = cx.tcx.sess.source_map().lookup_line(span.lo())
-    {
-        outer.push(Stop {
-            span,
-            kind: StopKind::Attr,
-            first: line.line,
-            // last doesn't need to be accurate here, we don't compare it with anything
-            last: line.line,
-        });
-    }
-
-    let mut gaps = Vec::new();
-    let mut last = 0;
-    for pos in outer
-        .array_windows()
-        .positions(|[a, b]| b.first.saturating_sub(a.last) > 1)
-    {
-        // we want to be after the first stop in the window
-        let pos = pos + 1;
-        if let Some(gap) = Gap::new(cx, &outer[last..pos], &outer[pos..]) {
-            last = pos;
-            gaps.push(gap);
-        }
-    }
-
-    check_gaps(cx, &gaps)
-}
diff --git a/src/tools/clippy/clippy_lints/src/doc/mod.rs b/src/tools/clippy/clippy_lints/src/doc/mod.rs
index 3d8ce7becdb..42e1f7fd950 100644
--- a/src/tools/clippy/clippy_lints/src/doc/mod.rs
+++ b/src/tools/clippy/clippy_lints/src/doc/mod.rs
@@ -33,7 +33,6 @@ use rustc_span::{Span, sym};
 use std::ops::Range;
 use url::Url;
 
-mod empty_line_after;
 mod include_in_doc_without_cfg;
 mod link_with_quotes;
 mod markdown;
@@ -493,82 +492,6 @@ declare_clippy_lint! {
 
 declare_clippy_lint! {
     /// ### What it does
-    /// Checks for empty lines after outer attributes
-    ///
-    /// ### Why is this bad?
-    /// The attribute may have meant to be an inner attribute (`#![attr]`). If
-    /// it was meant to be an outer attribute (`#[attr]`) then the empty line
-    /// should be removed
-    ///
-    /// ### Example
-    /// ```no_run
-    /// #[allow(dead_code)]
-    ///
-    /// fn not_quite_good_code() {}
-    /// ```
-    ///
-    /// Use instead:
-    /// ```no_run
-    /// // Good (as inner attribute)
-    /// #![allow(dead_code)]
-    ///
-    /// fn this_is_fine() {}
-    ///
-    /// // or
-    ///
-    /// // Good (as outer attribute)
-    /// #[allow(dead_code)]
-    /// fn this_is_fine_too() {}
-    /// ```
-    #[clippy::version = "pre 1.29.0"]
-    pub EMPTY_LINE_AFTER_OUTER_ATTR,
-    suspicious,
-    "empty line after outer attribute"
-}
-
-declare_clippy_lint! {
-    /// ### What it does
-    /// Checks for empty lines after doc comments.
-    ///
-    /// ### Why is this bad?
-    /// The doc comment may have meant to be an inner doc comment, regular
-    /// comment or applied to some old code that is now commented out. If it was
-    /// intended to be a doc comment, then the empty line should be removed.
-    ///
-    /// ### Example
-    /// ```no_run
-    /// /// Some doc comment with a blank line after it.
-    ///
-    /// fn f() {}
-    ///
-    /// /// Docs for `old_code`
-    /// // fn old_code() {}
-    ///
-    /// fn new_code() {}
-    /// ```
-    ///
-    /// Use instead:
-    /// ```no_run
-    /// //! Convert it to an inner doc comment
-    ///
-    /// // Or a regular comment
-    ///
-    /// /// Or remove the empty line
-    /// fn f() {}
-    ///
-    /// // /// Docs for `old_code`
-    /// // fn old_code() {}
-    ///
-    /// fn new_code() {}
-    /// ```
-    #[clippy::version = "1.70.0"]
-    pub EMPTY_LINE_AFTER_DOC_COMMENTS,
-    suspicious,
-    "empty line after doc comments"
-}
-
-declare_clippy_lint! {
-    /// ### What it does
     /// Checks if included files in doc comments are included only for `cfg(doc)`.
     ///
     /// ### Why restrict this?
@@ -650,8 +573,6 @@ impl_lint_pass!(Documentation => [
     EMPTY_DOCS,
     DOC_LAZY_CONTINUATION,
     DOC_OVERINDENTED_LIST_ITEMS,
-    EMPTY_LINE_AFTER_OUTER_ATTR,
-    EMPTY_LINE_AFTER_DOC_COMMENTS,
     TOO_LONG_FIRST_DOC_PARAGRAPH,
     DOC_INCLUDE_WITHOUT_CFG,
 ]);
@@ -784,7 +705,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
     }
 
     include_in_doc_without_cfg::check(cx, attrs);
-    if suspicious_doc_comments::check(cx, attrs) || empty_line_after::check(cx, attrs) || is_doc_hidden(attrs) {
+    if suspicious_doc_comments::check(cx, attrs) || is_doc_hidden(attrs) {
         return None;
     }
 
diff --git a/src/tools/clippy/clippy_lints/src/empty_line_after.rs b/src/tools/clippy/clippy_lints/src/empty_line_after.rs
new file mode 100644
index 00000000000..578ff6e38a6
--- /dev/null
+++ b/src/tools/clippy/clippy_lints/src/empty_line_after.rs
@@ -0,0 +1,492 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::source::{SpanRangeExt, snippet_indent};
+use clippy_utils::tokenize_with_text;
+use itertools::Itertools;
+use rustc_ast::token::CommentKind;
+use rustc_ast::{AssocItemKind, AttrKind, AttrStyle, Attribute, Crate, Item, ItemKind, ModKind, NodeId};
+use rustc_errors::{Applicability, Diag, SuggestionStyle};
+use rustc_lexer::TokenKind;
+use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
+use rustc_session::impl_lint_pass;
+use rustc_span::symbol::kw;
+use rustc_span::{BytePos, ExpnKind, Ident, InnerSpan, Span, SpanData, Symbol};
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for empty lines after outer attributes
+    ///
+    /// ### Why is this bad?
+    /// The attribute may have meant to be an inner attribute (`#![attr]`). If
+    /// it was meant to be an outer attribute (`#[attr]`) then the empty line
+    /// should be removed
+    ///
+    /// ### Example
+    /// ```no_run
+    /// #[allow(dead_code)]
+    ///
+    /// fn not_quite_good_code() {}
+    /// ```
+    ///
+    /// Use instead:
+    /// ```no_run
+    /// // Good (as inner attribute)
+    /// #![allow(dead_code)]
+    ///
+    /// fn this_is_fine() {}
+    ///
+    /// // or
+    ///
+    /// // Good (as outer attribute)
+    /// #[allow(dead_code)]
+    /// fn this_is_fine_too() {}
+    /// ```
+    #[clippy::version = "pre 1.29.0"]
+    pub EMPTY_LINE_AFTER_OUTER_ATTR,
+    suspicious,
+    "empty line after outer attribute"
+}
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for empty lines after doc comments.
+    ///
+    /// ### Why is this bad?
+    /// The doc comment may have meant to be an inner doc comment, regular
+    /// comment or applied to some old code that is now commented out. If it was
+    /// intended to be a doc comment, then the empty line should be removed.
+    ///
+    /// ### Example
+    /// ```no_run
+    /// /// Some doc comment with a blank line after it.
+    ///
+    /// fn f() {}
+    ///
+    /// /// Docs for `old_code`
+    /// // fn old_code() {}
+    ///
+    /// fn new_code() {}
+    /// ```
+    ///
+    /// Use instead:
+    /// ```no_run
+    /// //! Convert it to an inner doc comment
+    ///
+    /// // Or a regular comment
+    ///
+    /// /// Or remove the empty line
+    /// fn f() {}
+    ///
+    /// // /// Docs for `old_code`
+    /// // fn old_code() {}
+    ///
+    /// fn new_code() {}
+    /// ```
+    #[clippy::version = "1.70.0"]
+    pub EMPTY_LINE_AFTER_DOC_COMMENTS,
+    suspicious,
+    "empty line after doc comments"
+}
+
+#[derive(Debug)]
+struct ItemInfo {
+    kind: &'static str,
+    name: Symbol,
+    span: Span,
+    mod_items: Option<NodeId>,
+}
+
+pub struct EmptyLineAfter {
+    items: Vec<ItemInfo>,
+}
+
+impl_lint_pass!(EmptyLineAfter => [
+    EMPTY_LINE_AFTER_OUTER_ATTR,
+    EMPTY_LINE_AFTER_DOC_COMMENTS,
+]);
+
+impl EmptyLineAfter {
+    pub fn new() -> Self {
+        Self { items: Vec::new() }
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+enum StopKind {
+    Attr,
+    Doc(CommentKind),
+}
+
+impl StopKind {
+    fn is_doc(self) -> bool {
+        matches!(self, StopKind::Doc(_))
+    }
+}
+
+#[derive(Debug)]
+struct Stop {
+    span: Span,
+    kind: StopKind,
+    first: usize,
+    last: usize,
+}
+
+impl Stop {
+    fn convert_to_inner(&self) -> (Span, String) {
+        let inner = match self.kind {
+            // #![...]
+            StopKind::Attr => InnerSpan::new(1, 1),
+            // /// or /**
+            //   ^      ^
+            StopKind::Doc(_) => InnerSpan::new(2, 3),
+        };
+        (self.span.from_inner(inner), "!".into())
+    }
+
+    fn comment_out(&self, cx: &EarlyContext<'_>, suggestions: &mut Vec<(Span, String)>) {
+        match self.kind {
+            StopKind::Attr => {
+                if cx.sess().source_map().is_multiline(self.span) {
+                    suggestions.extend([
+                        (self.span.shrink_to_lo(), "/* ".into()),
+                        (self.span.shrink_to_hi(), " */".into()),
+                    ]);
+                } else {
+                    suggestions.push((self.span.shrink_to_lo(), "// ".into()));
+                }
+            },
+            StopKind::Doc(CommentKind::Line) => suggestions.push((self.span.shrink_to_lo(), "// ".into())),
+            StopKind::Doc(CommentKind::Block) => {
+                // /** outer */  /*! inner */
+                //  ^             ^
+                let asterisk = self.span.from_inner(InnerSpan::new(1, 2));
+                suggestions.push((asterisk, String::new()));
+            },
+        }
+    }
+
+    fn from_attr(cx: &EarlyContext<'_>, attr: &Attribute) -> Option<Self> {
+        let SpanData { lo, hi, .. } = attr.span.data();
+        let file = cx.sess().source_map().lookup_source_file(lo);
+
+        Some(Self {
+            span: attr.span,
+            kind: match attr.kind {
+                AttrKind::Normal(_) => StopKind::Attr,
+                AttrKind::DocComment(comment_kind, _) => StopKind::Doc(comment_kind),
+            },
+            first: file.lookup_line(file.relative_position(lo))?,
+            last: file.lookup_line(file.relative_position(hi))?,
+        })
+    }
+}
+
+/// Represents a set of attrs/doc comments separated by 1 or more empty lines
+///
+/// ```ignore
+/// /// chunk 1 docs
+/// // not an empty line so also part of chunk 1
+/// #[chunk_1_attrs] // <-- prev_stop
+///
+/// /* gap */
+///
+/// /// chunk 2 docs // <-- next_stop
+/// #[chunk_2_attrs]
+/// ```
+struct Gap<'a> {
+    /// The span of individual empty lines including the newline at the end of the line
+    empty_lines: Vec<Span>,
+    has_comment: bool,
+    next_stop: &'a Stop,
+    prev_stop: &'a Stop,
+    /// The chunk that includes [`prev_stop`](Self::prev_stop)
+    prev_chunk: &'a [Stop],
+}
+
+impl<'a> Gap<'a> {
+    fn new(cx: &EarlyContext<'_>, prev_chunk: &'a [Stop], next_chunk: &'a [Stop]) -> Option<Self> {
+        let prev_stop = prev_chunk.last()?;
+        let next_stop = next_chunk.first()?;
+        let gap_span = prev_stop.span.between(next_stop.span);
+        let gap_snippet = gap_span.get_source_text(cx)?;
+
+        let mut has_comment = false;
+        let mut empty_lines = Vec::new();
+
+        for (token, source, inner_span) in tokenize_with_text(&gap_snippet) {
+            match token {
+                TokenKind::BlockComment {
+                    doc_style: None,
+                    terminated: true,
+                }
+                | TokenKind::LineComment { doc_style: None } => has_comment = true,
+                TokenKind::Whitespace => {
+                    let newlines = source.bytes().positions(|b| b == b'\n');
+                    empty_lines.extend(
+                        newlines
+                            .tuple_windows()
+                            .map(|(a, b)| InnerSpan::new(inner_span.start + a + 1, inner_span.start + b))
+                            .map(|inner_span| gap_span.from_inner(inner_span)),
+                    );
+                },
+                // Ignore cfg_attr'd out attributes as they may contain empty lines, could also be from macro
+                // shenanigans
+                _ => return None,
+            }
+        }
+
+        (!empty_lines.is_empty()).then_some(Self {
+            empty_lines,
+            has_comment,
+            next_stop,
+            prev_stop,
+            prev_chunk,
+        })
+    }
+
+    fn contiguous_empty_lines(&self) -> impl Iterator<Item = Span> + '_ {
+        self.empty_lines
+            // The `+ BytePos(1)` means "next line", because each empty line span is "N:1-N:1".
+            .chunk_by(|a, b| a.hi() + BytePos(1) == b.lo())
+            .map(|chunk| {
+                let first = chunk.first().expect("at least one empty line");
+                let last = chunk.last().expect("at least one empty line");
+                // The BytePos subtraction here is safe, as before an empty line, there must be at least one
+                // attribute/comment. The span needs to start at the end of the previous line.
+                first.with_lo(first.lo() - BytePos(1)).with_hi(last.hi())
+            })
+    }
+}
+
+impl EmptyLineAfter {
+    fn check_gaps(&self, cx: &EarlyContext<'_>, gaps: &[Gap<'_>], id: NodeId) {
+        let Some(first_gap) = gaps.first() else {
+            return;
+        };
+        let empty_lines = || gaps.iter().flat_map(|gap| gap.empty_lines.iter().copied());
+        let contiguous_empty_lines = || gaps.iter().flat_map(Gap::contiguous_empty_lines);
+        let mut has_comment = false;
+        let mut has_attr = false;
+        for gap in gaps {
+            has_comment |= gap.has_comment;
+            if !has_attr {
+                has_attr = gap.prev_chunk.iter().any(|stop| stop.kind == StopKind::Attr);
+            }
+        }
+        let kind = first_gap.prev_stop.kind;
+        let (lint, kind_desc) = match kind {
+            StopKind::Attr => (EMPTY_LINE_AFTER_OUTER_ATTR, "outer attribute"),
+            StopKind::Doc(_) => (EMPTY_LINE_AFTER_DOC_COMMENTS, "doc comment"),
+        };
+        let (lines, are, them) = if empty_lines().nth(1).is_some() {
+            ("lines", "are", "them")
+        } else {
+            ("line", "is", "it")
+        };
+        span_lint_and_then(
+            cx,
+            lint,
+            first_gap.prev_stop.span.to(empty_lines().last().unwrap()),
+            format!("empty {lines} after {kind_desc}"),
+            |diag| {
+                let info = self.items.last().unwrap();
+                diag.span_label(info.span, match kind {
+                    StopKind::Attr => format!("the attribute applies to this {}", info.kind),
+                    StopKind::Doc(_) => format!("the comment documents this {}", info.kind),
+                });
+
+                diag.multipart_suggestion_with_style(
+                    format!("if the empty {lines} {are} unintentional, remove {them}"),
+                    contiguous_empty_lines()
+                        .map(|empty_lines| (empty_lines, String::new()))
+                        .collect(),
+                    Applicability::MaybeIncorrect,
+                    SuggestionStyle::HideCodeAlways,
+                );
+
+                if has_comment && kind.is_doc() {
+                    // Likely doc comments that applied to some now commented out code
+                    //
+                    // /// Old docs for Foo
+                    // // struct Foo;
+
+                    let mut suggestions = Vec::new();
+                    for stop in gaps.iter().flat_map(|gap| gap.prev_chunk) {
+                        stop.comment_out(cx, &mut suggestions);
+                    }
+                    diag.multipart_suggestion_verbose(
+                        format!("if the doc comment should not document `{}` comment it out", info.name),
+                        suggestions,
+                        Applicability::MaybeIncorrect,
+                    );
+                } else {
+                    self.suggest_inner(diag, kind, gaps, id);
+                }
+
+                if kind == StopKind::Doc(CommentKind::Line)
+                    && gaps
+                        .iter()
+                        .all(|gap| !gap.has_comment && gap.next_stop.kind == StopKind::Doc(CommentKind::Line))
+                {
+                    // Commentless empty gaps between line doc comments, possibly intended to be part of the markdown
+
+                    let indent = snippet_indent(cx, first_gap.prev_stop.span).unwrap_or_default();
+                    diag.multipart_suggestion_verbose(
+                        format!("if the documentation should include the empty {lines} include {them} in the comment"),
+                        empty_lines()
+                            .map(|empty_line| (empty_line, format!("{indent}///")))
+                            .collect(),
+                        Applicability::MaybeIncorrect,
+                    );
+                }
+            },
+        );
+    }
+
+    /// If the node the attributes/docs apply to is the first in the module/crate suggest converting
+    /// them to inner attributes/docs
+    fn suggest_inner(&self, diag: &mut Diag<'_, ()>, kind: StopKind, gaps: &[Gap<'_>], id: NodeId) {
+        if let Some(parent) = self.items.iter().rev().nth(1)
+            && (parent.kind == "module" || parent.kind == "crate")
+            && parent.mod_items == Some(id)
+        {
+            let desc = if parent.kind == "module" {
+                "parent module"
+            } else {
+                parent.kind
+            };
+            diag.multipart_suggestion_verbose(
+                match kind {
+                    StopKind::Attr => format!("if the attribute should apply to the {desc} use an inner attribute"),
+                    StopKind::Doc(_) => format!("if the comment should document the {desc} use an inner doc comment"),
+                },
+                gaps.iter()
+                    .flat_map(|gap| gap.prev_chunk)
+                    .map(Stop::convert_to_inner)
+                    .collect(),
+                Applicability::MaybeIncorrect,
+            );
+        }
+    }
+
+    fn check_item_kind(
+        &mut self,
+        cx: &EarlyContext<'_>,
+        kind: &ItemKind,
+        ident: &Ident,
+        span: Span,
+        attrs: &[Attribute],
+        id: NodeId,
+    ) {
+        self.items.push(ItemInfo {
+            kind: kind.descr(),
+            name: ident.name,
+            span: if span.contains(ident.span) {
+                span.with_hi(ident.span.hi())
+            } else {
+                span.with_hi(span.lo())
+            },
+            mod_items: match kind {
+                ItemKind::Mod(_, ModKind::Loaded(items, _, _, _)) => items
+                    .iter()
+                    .filter(|i| !matches!(i.span.ctxt().outer_expn_data().kind, ExpnKind::AstPass(_)))
+                    .map(|i| i.id)
+                    .next(),
+                _ => None,
+            },
+        });
+
+        let mut outer = attrs
+            .iter()
+            .filter(|attr| attr.style == AttrStyle::Outer && !attr.span.from_expansion())
+            .map(|attr| Stop::from_attr(cx, attr))
+            .collect::<Option<Vec<_>>>()
+            .unwrap_or_default();
+
+        if outer.is_empty() {
+            return;
+        }
+
+        // Push a fake attribute Stop for the item itself so we check for gaps between the last outer
+        // attr/doc comment and the item they apply to
+        let span = self.items.last().unwrap().span;
+        if !span.from_expansion()
+            && let Ok(line) = cx.sess().source_map().lookup_line(span.lo())
+        {
+            outer.push(Stop {
+                span,
+                kind: StopKind::Attr,
+                first: line.line,
+                // last doesn't need to be accurate here, we don't compare it with anything
+                last: line.line,
+            });
+        }
+
+        let mut gaps = Vec::new();
+        let mut last = 0;
+        for pos in outer
+            .array_windows()
+            .positions(|[a, b]| b.first.saturating_sub(a.last) > 1)
+        {
+            // we want to be after the first stop in the window
+            let pos = pos + 1;
+            if let Some(gap) = Gap::new(cx, &outer[last..pos], &outer[pos..]) {
+                last = pos;
+                gaps.push(gap);
+            }
+        }
+
+        self.check_gaps(cx, &gaps, id);
+    }
+}
+
+impl EarlyLintPass for EmptyLineAfter {
+    fn check_crate(&mut self, _: &EarlyContext<'_>, krate: &Crate) {
+        self.items.push(ItemInfo {
+            kind: "crate",
+            name: kw::Crate,
+            span: krate.spans.inner_span.with_hi(krate.spans.inner_span.lo()),
+            mod_items: krate
+                .items
+                .iter()
+                .filter(|i| !matches!(i.span.ctxt().outer_expn_data().kind, ExpnKind::AstPass(_)))
+                .map(|i| i.id)
+                .next(),
+        });
+    }
+
+    fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) {
+        self.items.pop();
+    }
+    fn check_impl_item_post(&mut self, _: &EarlyContext<'_>, _: &Item<AssocItemKind>) {
+        self.items.pop();
+    }
+    fn check_trait_item_post(&mut self, _: &EarlyContext<'_>, _: &Item<AssocItemKind>) {
+        self.items.pop();
+    }
+
+    fn check_impl_item(&mut self, cx: &EarlyContext<'_>, item: &Item<AssocItemKind>) {
+        self.check_item_kind(
+            cx,
+            &item.kind.clone().into(),
+            &item.ident,
+            item.span,
+            &item.attrs,
+            item.id,
+        );
+    }
+
+    fn check_trait_item(&mut self, cx: &EarlyContext<'_>, item: &Item<AssocItemKind>) {
+        self.check_item_kind(
+            cx,
+            &item.kind.clone().into(),
+            &item.ident,
+            item.span,
+            &item.attrs,
+            item.id,
+        );
+    }
+
+    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
+        self.check_item_kind(cx, &item.kind, &item.ident, item.span, &item.attrs, item.id);
+    }
+}
diff --git a/src/tools/clippy/clippy_lints/src/lib.rs b/src/tools/clippy/clippy_lints/src/lib.rs
index 8887ab7ec0d..13218331a67 100644
--- a/src/tools/clippy/clippy_lints/src/lib.rs
+++ b/src/tools/clippy/clippy_lints/src/lib.rs
@@ -126,6 +126,7 @@ mod duplicate_mod;
 mod else_if_without_else;
 mod empty_drop;
 mod empty_enum;
+mod empty_line_after;
 mod empty_with_brackets;
 mod endian_bytes;
 mod entry;
@@ -973,6 +974,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
     store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
     store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
     store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
+    store.register_early_pass(|| Box::new(empty_line_after::EmptyLineAfter::new()));
     store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
     store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
     store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
diff --git a/src/tools/clippy/tests/ui/doc/unbalanced_ticks.rs b/src/tools/clippy/tests/ui/doc/unbalanced_ticks.rs
index 04446787b6c..a065654e319 100644
--- a/src/tools/clippy/tests/ui/doc/unbalanced_ticks.rs
+++ b/src/tools/clippy/tests/ui/doc/unbalanced_ticks.rs
@@ -66,3 +66,19 @@ fn escape_3() {}
 
 /// Backslashes ` \` within code blocks don't count.
 fn escape_4() {}
+
+trait Foo {
+    fn bar();
+}
+
+struct Bar;
+impl Foo for Bar {
+    // NOTE: false positive
+    /// Returns an `Option<Month>` from a i64, assuming a 1-index, January = 1.
+    ///
+    /// `Month::from_i64(n: i64)`: | `1`                  | `2`                   | ... | `12`
+    /// ---------------------------| -------------------- | --------------------- | ... | -----
+    /// ``:                        | Some(Month::January) | Some(Month::February) | ... |
+    /// Some(Month::December)
+    fn bar() {}
+}
diff --git a/src/tools/clippy/tests/ui/doc/unbalanced_ticks.stderr b/src/tools/clippy/tests/ui/doc/unbalanced_ticks.stderr
index 50324010e97..c9fd25eb1a1 100644
--- a/src/tools/clippy/tests/ui/doc/unbalanced_ticks.stderr
+++ b/src/tools/clippy/tests/ui/doc/unbalanced_ticks.stderr
@@ -94,5 +94,17 @@ LL | /// Escaped \` ` backticks don't count, but unescaped backticks do.
    |
    = help: a backtick may be missing a pair
 
-error: aborting due to 10 previous errors
+error: backticks are unbalanced
+  --> tests/ui/doc/unbalanced_ticks.rs:79:9
+   |
+LL |       /// `Month::from_i64(n: i64)`: | `1`                  | `2`                   | ... | `12`
+   |  _________^
+LL | |     /// ---------------------------| -------------------- | --------------------- | ... | -----
+LL | |     /// ``:                        | Some(Month::January) | Some(Month::February) | ... |
+LL | |     /// Some(Month::December)
+   | |_____________________________^
+   |
+   = help: a backtick may be missing a pair
+
+error: aborting due to 11 previous errors
 
diff --git a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.1.fixed b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.1.fixed
index fd6a94b6a80..3772b465fdb 100644
--- a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.1.fixed
+++ b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.1.fixed
@@ -132,4 +132,13 @@ pub struct BlockComment;
 ))]
 fn empty_line_in_cfg_attr() {}
 
+trait Foo {
+    fn bar();
+}
+
+impl Foo for LineComment {
+    /// comment on assoc item
+    fn bar() {}
+}
+
 fn main() {}
diff --git a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.2.fixed b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.2.fixed
index 7a57dcd9233..3028d03b669 100644
--- a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.2.fixed
+++ b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.2.fixed
@@ -141,4 +141,13 @@ pub struct BlockComment;
 ))]
 fn empty_line_in_cfg_attr() {}
 
+trait Foo {
+    fn bar();
+}
+
+impl Foo for LineComment {
+    /// comment on assoc item
+    fn bar() {}
+}
+
 fn main() {}
diff --git a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.rs b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.rs
index 1da761a5c3d..ae4ebc271fa 100644
--- a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.rs
+++ b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.rs
@@ -144,4 +144,14 @@ pub struct BlockComment;
 ))]
 fn empty_line_in_cfg_attr() {}
 
+trait Foo {
+    fn bar();
+}
+
+impl Foo for LineComment {
+    /// comment on assoc item
+
+    fn bar() {}
+}
+
 fn main() {}
diff --git a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.stderr b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.stderr
index c5d5f3d3759..7b197ae67e0 100644
--- a/src/tools/clippy/tests/ui/empty_line_after/doc_comments.stderr
+++ b/src/tools/clippy/tests/ui/empty_line_after/doc_comments.stderr
@@ -5,11 +5,11 @@ LL | / /// for the crate
 LL | |
    | |_^
 LL |   fn first_in_crate() {}
-   |   ------------------- the comment documents this function
+   |   ----------------- the comment documents this function
    |
    = note: `-D clippy::empty-line-after-doc-comments` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::empty_line_after_doc_comments)]`
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the comment should document the crate use an inner doc comment
    |
 LL ~ //! Meant to be an
@@ -24,9 +24,9 @@ LL | /     /// for the module
 LL | |
    | |_^
 LL |       fn first_in_module() {}
-   |       -------------------- the comment documents this function
+   |       ------------------ the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the comment should document the parent module use an inner doc comment
    |
 LL ~     //! Meant to be an
@@ -42,9 +42,9 @@ LL | |
    | |_^
 LL |       /// Blank line
 LL |       fn indented() {}
-   |       ------------- the comment documents this function
+   |       ----------- the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the documentation should include the empty line include it in the comment
    |
 LL |     ///
@@ -57,9 +57,9 @@ LL | / /// This should produce a warning
 LL | |
    | |_^
 LL |   fn with_doc_and_newline() {}
-   |   ------------------------- the comment documents this function
+   |   ----------------------- the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty lines after doc comment
   --> tests/ui/empty_line_after/doc_comments.rs:44:1
@@ -72,9 +72,9 @@ LL | |
    | |_^
 ...
 LL |   fn three_attributes() {}
-   |   --------------------- the comment documents this function
+   |   ------------------- the comment documents this function
    |
-   = help: if the empty lines are unintentional remove them
+   = help: if the empty lines are unintentional, remove them
 
 error: empty line after doc comment
   --> tests/ui/empty_line_after/doc_comments.rs:56:5
@@ -84,9 +84,9 @@ LL | |     // fn old_code() {}
 LL | |
    | |_^
 LL |       fn new_code() {}
-   |       ------------- the comment documents this function
+   |       ----------- the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the doc comment should not document `new_code` comment it out
    |
 LL |     // /// docs for `old_code`
@@ -106,7 +106,7 @@ LL | |
 LL |       struct Multiple;
    |       --------------- the comment documents this struct
    |
-   = help: if the empty lines are unintentional remove them
+   = help: if the empty lines are unintentional, remove them
 help: if the doc comment should not document `Multiple` comment it out
    |
 LL ~     // /// Docs
@@ -126,9 +126,9 @@ LL | |      */
 LL | |
    | |_^
 LL |       fn first_in_module() {}
-   |       -------------------- the comment documents this function
+   |       ------------------ the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the comment should document the parent module use an inner doc comment
    |
 LL |     /*!
@@ -145,9 +145,9 @@ LL | |
    | |_^
 ...
 LL |       fn new_code() {}
-   |       ------------- the comment documents this function
+   |       ----------- the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the doc comment should not document `new_code` comment it out
    |
 LL -     /**
@@ -163,13 +163,24 @@ LL | |
    | |_^
 LL |       /// Docs for `new_code2`
 LL |       fn new_code2() {}
-   |       -------------- the comment documents this function
+   |       ------------ the comment documents this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the doc comment should not document `new_code2` comment it out
    |
 LL |     // /// Docs for `old_code2`
    |     ++
 
-error: aborting due to 10 previous errors
+error: empty line after doc comment
+  --> tests/ui/empty_line_after/doc_comments.rs:152:5
+   |
+LL | /     /// comment on assoc item
+LL | |
+   | |_^
+LL |       fn bar() {}
+   |       ------ the comment documents this function
+   |
+   = help: if the empty line is unintentional, remove it
+
+error: aborting due to 11 previous errors
 
diff --git a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr
index a95306e2fa3..519ba6e6761 100644
--- a/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr
+++ b/src/tools/clippy/tests/ui/empty_line_after/outer_attribute.stderr
@@ -5,11 +5,11 @@ LL | / #[crate_type = "lib"]
 LL | |
    | |_^
 LL |   fn first_in_crate() {}
-   |   ------------------- the attribute applies to this function
+   |   ----------------- the attribute applies to this function
    |
    = note: `-D clippy::empty-line-after-outer-attr` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::empty_line_after_outer_attr)]`
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 help: if the attribute should apply to the crate use an inner attribute
    |
 LL | #![crate_type = "lib"]
@@ -23,9 +23,9 @@ LL | |
    | |_^
 LL |   /// some comment
 LL |   fn with_one_newline_and_comment() {}
-   |   --------------------------------- the attribute applies to this function
+   |   ------------------------------- the attribute applies to this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty line after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:23:1
@@ -34,9 +34,9 @@ LL | / #[inline]
 LL | |
    | |_^
 LL |   fn with_one_newline() {}
-   |   --------------------- the attribute applies to this function
+   |   ------------------- the attribute applies to this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty lines after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:30:5
@@ -46,9 +46,9 @@ LL | |
 LL | |
    | |_^
 LL |       fn with_two_newlines() {}
-   |       ---------------------- the attribute applies to this function
+   |       -------------------- the attribute applies to this function
    |
-   = help: if the empty lines are unintentional remove them
+   = help: if the empty lines are unintentional, remove them
 help: if the attribute should apply to the parent module use an inner attribute
    |
 LL |     #![crate_type = "lib"]
@@ -63,7 +63,7 @@ LL | |
 LL |   enum Baz {
    |   -------- the attribute applies to this enum
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty line after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:45:1
@@ -74,7 +74,7 @@ LL | |
 LL |   struct Foo {
    |   ---------- the attribute applies to this struct
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty line after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:53:1
@@ -85,7 +85,7 @@ LL | |
 LL |   mod foo {}
    |   ------- the attribute applies to this module
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty line after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:58:1
@@ -95,9 +95,9 @@ LL | | // Still lint cases where the empty line does not immediately follow the
 LL | |
    | |_^
 LL |   fn comment_before_empty_line() {}
-   |   ------------------------------ the attribute applies to this function
+   |   ---------------------------- the attribute applies to this function
    |
-   = help: if the empty line is unintentional remove it
+   = help: if the empty line is unintentional, remove it
 
 error: empty lines after outer attribute
   --> tests/ui/empty_line_after/outer_attribute.rs:64:1
@@ -107,9 +107,9 @@ LL | / #[allow(unused)]
 LL | |
    | |_^
 LL |   pub fn isolated_comment() {}
-   |   ------------------------- the attribute applies to this function
+   |   ----------------------- the attribute applies to this function
    |
-   = help: if the empty lines are unintentional remove them
+   = help: if the empty lines are unintentional, remove them
 
 error: aborting due to 9 previous errors
 
diff --git a/src/tools/clippy/tests/ui/suspicious_doc_comments.fixed b/src/tools/clippy/tests/ui/suspicious_doc_comments.fixed
index 614fc03571e..d3df6a41cb1 100644
--- a/src/tools/clippy/tests/ui/suspicious_doc_comments.fixed
+++ b/src/tools/clippy/tests/ui/suspicious_doc_comments.fixed
@@ -1,5 +1,6 @@
 #![allow(unused)]
 #![warn(clippy::suspicious_doc_comments)]
+#![allow(clippy::empty_line_after_doc_comments)]
 
 //! Real module documentation.
 //! Fake module documentation.
diff --git a/src/tools/clippy/tests/ui/suspicious_doc_comments.rs b/src/tools/clippy/tests/ui/suspicious_doc_comments.rs
index 7dcba0fefc9..04db2b199c0 100644
--- a/src/tools/clippy/tests/ui/suspicious_doc_comments.rs
+++ b/src/tools/clippy/tests/ui/suspicious_doc_comments.rs
@@ -1,5 +1,6 @@
 #![allow(unused)]
 #![warn(clippy::suspicious_doc_comments)]
+#![allow(clippy::empty_line_after_doc_comments)]
 
 //! Real module documentation.
 ///! Fake module documentation.
diff --git a/src/tools/clippy/tests/ui/suspicious_doc_comments.stderr b/src/tools/clippy/tests/ui/suspicious_doc_comments.stderr
index f12053b1595..c34e39cd0fc 100644
--- a/src/tools/clippy/tests/ui/suspicious_doc_comments.stderr
+++ b/src/tools/clippy/tests/ui/suspicious_doc_comments.stderr
@@ -1,5 +1,5 @@
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:5:1
+  --> tests/ui/suspicious_doc_comments.rs:6:1
    |
 LL | ///! Fake module documentation.
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -12,7 +12,7 @@ LL | //! Fake module documentation.
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:9:5
+  --> tests/ui/suspicious_doc_comments.rs:10:5
    |
 LL |     ///! This module contains useful functions.
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -23,7 +23,7 @@ LL |     //! This module contains useful functions.
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:21:5
+  --> tests/ui/suspicious_doc_comments.rs:22:5
    |
 LL | /     /**! This module contains useful functions.
 LL | |      */
@@ -36,7 +36,7 @@ LL +      */
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:35:5
+  --> tests/ui/suspicious_doc_comments.rs:36:5
    |
 LL | /     ///! This module
 LL | |     ///! contains
@@ -51,7 +51,7 @@ LL ~     //! useful functions.
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:43:5
+  --> tests/ui/suspicious_doc_comments.rs:44:5
    |
 LL | /     ///! a
 LL | |     ///! b
@@ -64,7 +64,7 @@ LL ~     //! b
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:51:5
+  --> tests/ui/suspicious_doc_comments.rs:52:5
    |
 LL |     ///! a
    |     ^^^^^^
@@ -75,7 +75,7 @@ LL |     //! a
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:57:5
+  --> tests/ui/suspicious_doc_comments.rs:58:5
    |
 LL | /     ///! a
 LL | |
@@ -90,7 +90,7 @@ LL ~     //! b
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:69:5
+  --> tests/ui/suspicious_doc_comments.rs:70:5
    |
 LL |     ///! Very cool macro
    |     ^^^^^^^^^^^^^^^^^^^^
@@ -101,7 +101,7 @@ LL |     //! Very cool macro
    |
 
 error: this is an outer doc comment and does not apply to the parent module or crate
-  --> tests/ui/suspicious_doc_comments.rs:76:5
+  --> tests/ui/suspicious_doc_comments.rs:77:5
    |
 LL |     ///! Huh.
    |     ^^^^^^^^^
diff --git a/tests/codegen/terminating-catchpad.rs b/tests/codegen/terminating-catchpad.rs
new file mode 100644
index 00000000000..17d88796300
--- /dev/null
+++ b/tests/codegen/terminating-catchpad.rs
@@ -0,0 +1,37 @@
+//@ revisions: emscripten wasi seh
+//@[emscripten] compile-flags: --target wasm32-unknown-emscripten -Z emscripten-wasm-eh
+//@[wasi] compile-flags: --target wasm32-wasip1 -C panic=unwind
+//@[seh] compile-flags: --target x86_64-pc-windows-msvc
+//@[emscripten] needs-llvm-components: webassembly
+//@[wasi] needs-llvm-components: webassembly
+//@[seh] needs-llvm-components: x86
+
+// Ensure a catch-all generates:
+// - `catchpad ... [ptr null]` on Wasm (otherwise LLVM gets confused)
+// - `catchpad ... [ptr null, i32 64, ptr null]` on Windows (otherwise we catch SEH exceptions)
+
+#![feature(no_core, lang_items, rustc_attrs)]
+#![crate_type = "lib"]
+#![no_std]
+#![no_core]
+
+#[lang = "sized"]
+trait Sized {}
+
+unsafe extern "C-unwind" {
+    safe fn unwinds();
+}
+
+#[lang = "panic_cannot_unwind"]
+fn panic_cannot_unwind() -> ! {
+    loop {}
+}
+
+#[no_mangle]
+#[rustc_nounwind]
+pub fn doesnt_unwind() {
+    // emscripten: %catchpad = catchpad within %catchswitch [ptr null]
+    // wasi: %catchpad = catchpad within %catchswitch [ptr null]
+    // seh: %catchpad = catchpad within %catchswitch [ptr null, i32 64, ptr null]
+    unwinds();
+}