Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions compiler/rustc_builtin_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod iter;
mod log_syntax;
mod offload;
mod pattern_type;
mod rustdoc_skip_body;
mod source_util;
mod test;
mod test_binder_constraints;
Expand Down Expand Up @@ -125,6 +126,7 @@ pub fn register_builtin_macros(resolver: &mut dyn ResolverExpand) {
eii_shared_macro: eii::eii_shared_macro,
global_allocator: global_allocator::expand,
offload_kernel: offload::expand_kernel,
rustdoc_skip_body: rustdoc_skip_body::expand,
test: test::expand_test,
test_case: test::expand_test_case,
unsafe_eii: eii::unsafe_eii,
Expand Down
93 changes: 93 additions & 0 deletions compiler/rustc_builtin_macros/src/rustdoc_skip_body.rs

@bjorn3 bjorn3 Oct 26, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"everybody loops" returns?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the question so I'll answer to "Is it an issue to use this attribute macro if the function returns something other than ()". Answer: loop returns !, so it covers every types. It's a little trick we used at some point in rustdoc to remove function bodies (before we realized you could have impl inside any block).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to the old everybody loops pass that rustdoc used to have, but then removed because it broke with RPIT. Seems like we have pretty much gone full circle except now you have to opt-in to the loop replacement instead.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! The expansion from a function when the code would not compile with rustdoc: so if `cfg(doc)`,
//! expands to a `loop {}`.

use rustc_ast::token::{self, Delimiter, IdentKind};
use rustc_ast::tokenstream::{DelimSpacing, DelimSpan, Spacing, TokenStream, TokenTree};
use rustc_ast::{
AssocItemKind, AttrArgs, AttrItem, Attribute, DelimArgs, ExprKind, Fn, ItemKind, MetaItem,
Safety,
};
use rustc_expand::base::*;
use rustc_span::{Ident, Span, sym};
use thin_vec::ThinVec;

pub(crate) fn expand(
cx: &mut ExtCtxt<'_>,
attr_path_sp: Span,
meta_item: &MetaItem,
mut item: Annotatable,
) -> Vec<Annotatable> {
if !meta_item.is_word() {
cx.dcx().struct_span_err(attr_path_sp, "`rustdoc_skip_body` doesn't take arguments").emit();
}
let mut is_expected_item = false;
match item {
Annotatable::Item(ref mut item) => {
if let ItemKind::Fn(ref mut fn_) = item.kind {
expand_macro(cx, attr_path_sp, fn_);
is_expected_item = true;
}
}
Annotatable::AssocItem(ref mut item, _) => {
if let AssocItemKind::Fn(ref mut fn_) = item.kind {
expand_macro(cx, attr_path_sp, fn_);
is_expected_item = true;
}
}
_ => {}
}
if !is_expected_item {
cx.dcx()
.struct_span_err(attr_path_sp, "`rustdoc_skip_body` can only be used on functions")
.emit();
}
vec![item]
}

fn cfg_not_doc(cx: &mut ExtCtxt<'_>, sp: Span) -> Attribute {
// `not(doc)`
let not_doc = TokenStream::new(vec![
TokenTree::token_alone(token::Ident(sym::not, IdentKind::Normal), sp),
TokenTree::Delimited(
DelimSpan::from_single(sp),
DelimSpacing::new(Spacing::JointHidden, Spacing::Alone),
Delimiter::Parenthesis,
TokenStream::token_alone(token::Ident(sym::doc, IdentKind::Normal), sp),
),
]);

cx.attr_nested(
AttrItem {
unsafety: Safety::Default,
path: cx.path_ident(sp, Ident::new(sym::cfg, sp)),
args: AttrArgs::Delimited(DelimArgs {
dspan: DelimSpan::from_single(sp),
delim: Delimiter::Parenthesis,
tokens: not_doc,
}),
span: sp,
},
sp,
)
}

fn expand_macro(cx: &mut ExtCtxt<'_>, sp: Span, fn_: &mut Fn) {
let Some(old_body) = fn_.body.take() else {
// No body... Well, no possibility to not compile I guess so all good.
return;
};
let mut old_body = cx.expr_block(old_body);
// We add on the function's body `#[cfg(not(doc))]`.
old_body.attrs.push(cfg_not_doc(cx, sp));

// We create the `#[cfg(doc)] loop {}` "block".
let mut loop_expr = cx.expr(sp, ExprKind::Loop(cx.block(sp, ThinVec::new()), None, sp));
loop_expr.attrs.push(cx.attr_nested_word(sym::cfg, sym::doc, sp));

let mut stmts = ThinVec::new();
stmts.push(cx.stmt_expr(loop_expr));
stmts.push(cx.stmt_expr(old_body));

// We create a new function body containing both blocks.
fn_.body = Some(cx.block(sp, stmts));
}
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,7 @@ symbols! {
rustdoc,
rustdoc_internals,
rustdoc_missing_doc_code_examples,
rustdoc_skip_body,
rustfmt,
rvalue_static_promotion,
rvector,
Expand Down
14 changes: 14 additions & 0 deletions library/std/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,17 @@ macro_rules! hash_map {
map
}}
}

/// Attribute macro to replace a function's body with `loop {}` under rustdoc.
///
/// This allows target-specific functions to be documented on other targets,
/// even when their bodies reference items unavailable on the target used to
/// build the documentation. Use this with `#[cfg(any(doc, ...))]` to include
/// such functions in documentation builds.
///
/// The function's signature must still be valid on the documentation target.
#[rustc_builtin_macro]
#[allow(unused_macros)]
pub(crate) macro rustdoc_skip_body($item:item) {
/* compiler built-in */
}
Comment on lines +464 to +477

@Amanieu Amanieu Oct 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[rustc_builtin_macro]
pub(crate) macro no_code_when_rustdoc($item:item) {
/* compiler built-in */
}
/// Attribute macro to replace a function's body with `loop {}` under rustdoc.
///
/// This allows target-specific functions to be documented on other targets,
/// even when their bodies reference items unavailable on the target used to
/// build the documentation. Use this with `#[cfg(any(doc, ...))]` to include
/// such functions in documentation builds.
///
/// The function's signature must still be valid on the documentation target.
#[rustc_builtin_macro]
pub(crate) macro no_code_when_rustdoc($item:item) {
/* compiler built-in */
}

Also, I think rustdoc_skip_body would be a better name for this attribute: it clearly indicates that the function body is ignored by rustdoc.

View changes since the review

24 changes: 24 additions & 0 deletions library/std/src/os/darwin/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#![stable(feature = "metadata_ext", since = "1.1.0")]

use crate::fs::{self, Metadata};
use crate::macros::rustdoc_skip_body;
use crate::sys::{AsInner, AsInnerMut, IntoInner};
use crate::time::SystemTime;

Expand Down Expand Up @@ -88,69 +89,91 @@ pub trait MetadataExt {
impl MetadataExt for Metadata {
#[allow(deprecated)]
#[cfg(any(doc, target_os = "macos", target_os = "ios"))]
#[rustdoc_skip_body]
fn as_raw_stat(&self) -> &super::raw::stat {
unsafe { &*(self.as_inner().as_inner() as *const libc::stat as *const super::raw::stat) }
}
#[rustdoc_skip_body]
fn st_dev(&self) -> u64 {
self.as_inner().as_inner().st_dev as u64
}
#[rustdoc_skip_body]
fn st_ino(&self) -> u64 {
self.as_inner().as_inner().st_ino as u64
}
#[rustdoc_skip_body]
fn st_mode(&self) -> u32 {
self.as_inner().as_inner().st_mode as u32
}
#[rustdoc_skip_body]
fn st_nlink(&self) -> u64 {
self.as_inner().as_inner().st_nlink as u64
}
#[rustdoc_skip_body]
fn st_uid(&self) -> u32 {
self.as_inner().as_inner().st_uid as u32
}
#[rustdoc_skip_body]
fn st_gid(&self) -> u32 {
self.as_inner().as_inner().st_gid as u32
}
#[rustdoc_skip_body]
fn st_rdev(&self) -> u64 {
self.as_inner().as_inner().st_rdev as u64
}
#[rustdoc_skip_body]
fn st_size(&self) -> u64 {
self.as_inner().as_inner().st_size as u64
}
#[rustdoc_skip_body]
fn st_atime(&self) -> i64 {
self.as_inner().as_inner().st_atime as i64
}
#[rustdoc_skip_body]
fn st_atime_nsec(&self) -> i64 {
self.as_inner().as_inner().st_atime_nsec as i64
}
#[rustdoc_skip_body]
fn st_mtime(&self) -> i64 {
self.as_inner().as_inner().st_mtime as i64
}
#[rustdoc_skip_body]
fn st_mtime_nsec(&self) -> i64 {
self.as_inner().as_inner().st_mtime_nsec as i64
}
#[rustdoc_skip_body]
fn st_ctime(&self) -> i64 {
self.as_inner().as_inner().st_ctime as i64
}
#[rustdoc_skip_body]
fn st_ctime_nsec(&self) -> i64 {
self.as_inner().as_inner().st_ctime_nsec as i64
}
#[rustdoc_skip_body]
fn st_birthtime(&self) -> i64 {
self.as_inner().as_inner().st_birthtime as i64
}
#[rustdoc_skip_body]
fn st_birthtime_nsec(&self) -> i64 {
self.as_inner().as_inner().st_birthtime_nsec as i64
}
#[rustdoc_skip_body]
fn st_blksize(&self) -> u64 {
self.as_inner().as_inner().st_blksize as u64
}
#[rustdoc_skip_body]
fn st_blocks(&self) -> u64 {
self.as_inner().as_inner().st_blocks as u64
}
#[rustdoc_skip_body]
fn st_gen(&self) -> u32 {
self.as_inner().as_inner().st_gen as u32
}
#[rustdoc_skip_body]
fn st_flags(&self) -> u32 {
self.as_inner().as_inner().st_flags as u32
}
#[rustdoc_skip_body]
fn st_lspare(&self) -> u32 {
self.as_inner().as_inner().st_lspare as u32
}
Expand All @@ -171,6 +194,7 @@ pub impl(self) trait FileTimesExt {

#[stable(feature = "file_set_times", since = "1.75.0")]
impl FileTimesExt for fs::FileTimes {
#[rustdoc_skip_body]
fn set_created(mut self, t: SystemTime) -> Self {
self.as_inner_mut().set_created(t.into_inner());
self
Expand Down
19 changes: 19 additions & 0 deletions library/std/src/os/fd/owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use super::raw::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use crate::alloc::Allocator;
#[cfg(not(target_os = "trusty"))]
use crate::fs;
use crate::macros::rustdoc_skip_body;
use crate::marker::PhantomData;
use crate::mem::ManuallyDrop;
#[cfg(not(any(
Expand Down Expand Up @@ -136,6 +137,7 @@ impl BorrowedFd<'_> {
target_os = "motor"
)))]
#[stable(feature = "io_safety", since = "1.63.0")]
#[rustdoc_skip_body]
pub fn try_clone_to_owned(&self) -> io::Result<OwnedFd> {
// We want to atomically duplicate this file descriptor and set the
// CLOEXEC flag, and currently that's done via F_DUPFD_CLOEXEC. This
Expand Down Expand Up @@ -225,6 +227,7 @@ impl FromRawFd for OwnedFd {
#[stable(feature = "io_safety", since = "1.63.0")]
impl Drop for OwnedFd {
#[inline]
#[rustdoc_skip_body]
fn drop(&mut self) {
unsafe {
// Note that errors are ignored when closing a file descriptor. According to POSIX 2024,
Expand Down Expand Up @@ -344,6 +347,7 @@ impl AsFd for OwnedFd {
#[cfg(not(target_os = "trusty"))]
impl AsFd for fs::File {
#[inline]
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().as_fd()
}
Expand All @@ -354,6 +358,7 @@ impl AsFd for fs::File {
impl From<fs::File> for OwnedFd {
/// Takes ownership of a [`File`](fs::File)'s underlying file descriptor.
#[inline]
#[rustdoc_skip_body]
fn from(file: fs::File) -> OwnedFd {
file.into_inner().into_inner().into_inner()
}
Expand All @@ -365,6 +370,7 @@ impl From<OwnedFd> for fs::File {
/// Returns a [`File`](fs::File) that takes ownership of the given
/// file descriptor.
#[inline]
#[rustdoc_skip_body]
fn from(owned_fd: OwnedFd) -> Self {
Self::from_inner(FromInner::from_inner(FromInner::from_inner(owned_fd)))
}
Expand All @@ -374,6 +380,7 @@ impl From<OwnedFd> for fs::File {
#[cfg(not(target_os = "trusty"))]
impl AsFd for crate::net::TcpStream {
#[inline]
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().socket().as_fd()
}
Expand All @@ -384,6 +391,7 @@ impl AsFd for crate::net::TcpStream {
impl From<crate::net::TcpStream> for OwnedFd {
/// Takes ownership of a [`TcpStream`](crate::net::TcpStream)'s socket file descriptor.
#[inline]
#[rustdoc_skip_body]
fn from(tcp_stream: crate::net::TcpStream) -> OwnedFd {
tcp_stream.into_inner().into_socket().into_inner().into_inner()
}
Expand All @@ -393,6 +401,7 @@ impl From<crate::net::TcpStream> for OwnedFd {
#[cfg(not(target_os = "trusty"))]
impl From<OwnedFd> for crate::net::TcpStream {
#[inline]
#[rustdoc_skip_body]
fn from(owned_fd: OwnedFd) -> Self {
Self::from_inner(FromInner::from_inner(FromInner::from_inner(FromInner::from_inner(
owned_fd,
Expand All @@ -404,6 +413,7 @@ impl From<OwnedFd> for crate::net::TcpStream {
#[cfg(not(target_os = "trusty"))]
impl AsFd for crate::net::TcpListener {
#[inline]
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().socket().as_fd()
}
Expand All @@ -414,6 +424,7 @@ impl AsFd for crate::net::TcpListener {
impl From<crate::net::TcpListener> for OwnedFd {
/// Takes ownership of a [`TcpListener`](crate::net::TcpListener)'s socket file descriptor.
#[inline]
#[rustdoc_skip_body]
fn from(tcp_listener: crate::net::TcpListener) -> OwnedFd {
tcp_listener.into_inner().into_socket().into_inner().into_inner()
}
Expand All @@ -423,6 +434,7 @@ impl From<crate::net::TcpListener> for OwnedFd {
#[cfg(not(target_os = "trusty"))]
impl From<OwnedFd> for crate::net::TcpListener {
#[inline]
#[rustdoc_skip_body]
fn from(owned_fd: OwnedFd) -> Self {
Self::from_inner(FromInner::from_inner(FromInner::from_inner(FromInner::from_inner(
owned_fd,
Expand All @@ -434,6 +446,7 @@ impl From<OwnedFd> for crate::net::TcpListener {
#[cfg(not(target_os = "trusty"))]
impl AsFd for crate::net::UdpSocket {
#[inline]
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().socket().as_fd()
}
Expand All @@ -444,6 +457,7 @@ impl AsFd for crate::net::UdpSocket {
impl From<crate::net::UdpSocket> for OwnedFd {
/// Takes ownership of a [`UdpSocket`](crate::net::UdpSocket)'s file descriptor.
#[inline]
#[rustdoc_skip_body]
fn from(udp_socket: crate::net::UdpSocket) -> OwnedFd {
udp_socket.into_inner().into_socket().into_inner().into_inner()
}
Expand All @@ -453,6 +467,7 @@ impl From<crate::net::UdpSocket> for OwnedFd {
#[cfg(not(target_os = "trusty"))]
impl From<OwnedFd> for crate::net::UdpSocket {
#[inline]
#[rustdoc_skip_body]
fn from(owned_fd: OwnedFd) -> Self {
Self::from_inner(FromInner::from_inner(FromInner::from_inner(FromInner::from_inner(
owned_fd,
Expand Down Expand Up @@ -613,6 +628,7 @@ impl<'a> AsFd for io::StderrLock<'a> {
#[stable(feature = "anonymous_pipe", since = "1.87.0")]
#[cfg(not(target_os = "trusty"))]
impl AsFd for io::PipeReader {
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
Expand All @@ -621,6 +637,7 @@ impl AsFd for io::PipeReader {
#[stable(feature = "anonymous_pipe", since = "1.87.0")]
#[cfg(not(target_os = "trusty"))]
impl From<io::PipeReader> for OwnedFd {
#[rustdoc_skip_body]
fn from(pipe: io::PipeReader) -> Self {
pipe.0.into_inner()
}
Expand All @@ -629,6 +646,7 @@ impl From<io::PipeReader> for OwnedFd {
#[stable(feature = "anonymous_pipe", since = "1.87.0")]
#[cfg(not(target_os = "trusty"))]
impl AsFd for io::PipeWriter {
#[rustdoc_skip_body]
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
Expand All @@ -637,6 +655,7 @@ impl AsFd for io::PipeWriter {
#[stable(feature = "anonymous_pipe", since = "1.87.0")]
#[cfg(not(target_os = "trusty"))]
impl From<io::PipeWriter> for OwnedFd {
#[rustdoc_skip_body]
fn from(pipe: io::PipeWriter) -> Self {
pipe.0.into_inner()
}
Expand Down
Loading
Loading