From 93630386128aeeb8d098e0b926362aa2a24b6474 Mon Sep 17 00:00:00 2001 From: Rohan Singla Date: Thu, 18 Jun 2026 15:23:02 +0530 Subject: [PATCH 01/11] bootstrap: fix RUSTFLAGS with spaces in paths via CARGO_ENCODED_RUSTFLAGS --- src/bootstrap/src/core/builder/cargo.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/bootstrap/src/core/builder/cargo.rs b/src/bootstrap/src/core/builder/cargo.rs index 24d7a24152a32..8e135a3c20a30 100644 --- a/src/bootstrap/src/core/builder/cargo.rs +++ b/src/bootstrap/src/core/builder/cargo.rs @@ -14,18 +14,18 @@ use crate::{ RemapScheme, TargetSelection, command, prepare_behaviour_dump_dir, t, }; -/// Represents flag values in `String` form with whitespace delimiter to pass it to the compiler -/// later. +/// Represents flag values passed to the compiler, stored as individual arguments to support +/// values that contain spaces (such as paths from `llvm-config --libdir`). /// /// `-Z crate-attr` flags will be applied recursively on the target code using the /// `rustc_parse::parser::Parser`. See `rustc_builtin_macros::cmdline_attrs::inject` for more /// information. #[derive(Debug, Clone)] -struct Rustflags(String, TargetSelection); +struct Rustflags(Vec, TargetSelection); impl Rustflags { fn new(target: TargetSelection) -> Rustflags { - Rustflags(String::new(), target) + Rustflags(Vec::new(), target) } /// By default, cargo will pick up on various variables in the environment. However, bootstrap @@ -51,11 +51,9 @@ impl Rustflags { } fn arg(&mut self, arg: &str) -> &mut Self { - assert_eq!(arg.split(' ').count(), 1); - if !self.0.is_empty() { - self.0.push(' '); + if !arg.is_empty() { + self.0.push(arg.to_owned()); } - self.0.push_str(arg); self } @@ -459,12 +457,14 @@ impl From for BootstrapCommand { let rustflags = &cargo.rustflags.0; if !rustflags.is_empty() { - cargo.command.env("RUSTFLAGS", rustflags); + // CARGO_ENCODED_RUSTFLAGS uses \x1f (unit separator) as delimiter, which allows + // individual flags to contain spaces (e.g. paths from `llvm-config --libdir`). + cargo.command.env("CARGO_ENCODED_RUSTFLAGS", rustflags.join("\x1f")); } let rustdocflags = &cargo.rustdocflags.0; if !rustdocflags.is_empty() { - cargo.command.env("RUSTDOCFLAGS", rustdocflags); + cargo.command.env("RUSTDOCFLAGS", rustdocflags.join(" ")); } let encoded_hostflags = cargo.hostflags.encode(); @@ -1181,8 +1181,9 @@ impl Builder<'_> { if (mode == Mode::ToolRustcPrivate || mode == Mode::Codegen) && let Some(llvm_config) = self.llvm_config(target) { - let llvm_libdir = + let llvm_libdir_raw = command(llvm_config).cached().arg("--libdir").run_capture_stdout(self).stdout(); + let llvm_libdir = llvm_libdir_raw.trim(); if target.is_msvc() { rustflags.arg(&format!("-Clink-arg=-LIBPATH:{llvm_libdir}")); } else { From 63d3395c4c69e71a942ae0cf7c1a9986f1f1120d Mon Sep 17 00:00:00 2001 From: Rohan Singla Date: Thu, 18 Jun 2026 16:42:33 +0530 Subject: [PATCH 02/11] bootstrap: also use CARGO_ENCODED_RUSTDOCFLAGS to support spaces in paths --- src/bootstrap/src/core/builder/cargo.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap/src/core/builder/cargo.rs b/src/bootstrap/src/core/builder/cargo.rs index 8e135a3c20a30..b025bfdecbf20 100644 --- a/src/bootstrap/src/core/builder/cargo.rs +++ b/src/bootstrap/src/core/builder/cargo.rs @@ -464,7 +464,7 @@ impl From for BootstrapCommand { let rustdocflags = &cargo.rustdocflags.0; if !rustdocflags.is_empty() { - cargo.command.env("RUSTDOCFLAGS", rustdocflags.join(" ")); + cargo.command.env("CARGO_ENCODED_RUSTDOCFLAGS", rustdocflags.join("\x1f")); } let encoded_hostflags = cargo.hostflags.encode(); From 386748b45ac3d0756fb2dcf997896f11c4eae617 Mon Sep 17 00:00:00 2001 From: Rohan Singla Date: Thu, 18 Jun 2026 17:44:33 +0530 Subject: [PATCH 03/11] =?UTF-8?q?=E2=8F=BA=20bootstrap:=20reset=20CARGO=5F?= =?UTF-8?q?ENCODED=5FRUSTFLAGS=20in=20Miri=20test=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tools/miri/tests/ui.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/miri/tests/ui.rs b/src/tools/miri/tests/ui.rs index 633452f6052d7..3e46ee17c8a65 100644 --- a/src/tools/miri/tests/ui.rs +++ b/src/tools/miri/tests/ui.rs @@ -182,8 +182,9 @@ fn miri_config( .map(Into::into) .collect(), envs: vec![ - // Reset `RUSTFLAGS` to work around . + // Reset `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS` to work around . ("RUSTFLAGS".into(), None), + ("CARGO_ENCODED_RUSTFLAGS".into(), None), // Reset `MIRIFLAGS` because it caused trouble in the past and should not be needed. ("MIRIFLAGS".into(), None), // Allow `cargo miri build`. From 8085cd3b44bf0c9683197c6082763a97c3dbb0f9 Mon Sep 17 00:00:00 2001 From: Rohan Singla Date: Thu, 18 Jun 2026 23:11:08 +0530 Subject: [PATCH 04/11] bootstrap: use \x1f-delimited String for Rustflags instead of Vec --- src/bootstrap/src/core/builder/cargo.rs | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/bootstrap/src/core/builder/cargo.rs b/src/bootstrap/src/core/builder/cargo.rs index b025bfdecbf20..cabcd5e86efa7 100644 --- a/src/bootstrap/src/core/builder/cargo.rs +++ b/src/bootstrap/src/core/builder/cargo.rs @@ -14,18 +14,21 @@ use crate::{ RemapScheme, TargetSelection, command, prepare_behaviour_dump_dir, t, }; -/// Represents flag values passed to the compiler, stored as individual arguments to support -/// values that contain spaces (such as paths from `llvm-config --libdir`). +/// Represents flag values in `String` form with a `\x1f` delimiter to pass to the compiler later. +/// +/// Flags are emitted via `CARGO_ENCODED_RUSTFLAGS` / `CARGO_ENCODED_RUSTDOCFLAGS`, +/// which use `\x1f` (ASCII Unit Separator) as the delimiter and therefore allow spaces +/// within individual flag values (e.g. paths from `llvm-config --libdir`). /// /// `-Z crate-attr` flags will be applied recursively on the target code using the /// `rustc_parse::parser::Parser`. See `rustc_builtin_macros::cmdline_attrs::inject` for more /// information. #[derive(Debug, Clone)] -struct Rustflags(Vec, TargetSelection); +struct Rustflags(String, TargetSelection); impl Rustflags { fn new(target: TargetSelection) -> Rustflags { - Rustflags(Vec::new(), target) + Rustflags(String::new(), target) } /// By default, cargo will pick up on various variables in the environment. However, bootstrap @@ -51,8 +54,15 @@ impl Rustflags { } fn arg(&mut self, arg: &str) -> &mut Self { + assert!( + !arg.contains('\x1f'), + "rustflag must not contain the ASCII unit separator (\\x1f): {arg:?}" + ); if !arg.is_empty() { - self.0.push(arg.to_owned()); + if !self.0.is_empty() { + self.0.push('\x1f'); + } + self.0.push_str(arg); } self } @@ -457,14 +467,12 @@ impl From for BootstrapCommand { let rustflags = &cargo.rustflags.0; if !rustflags.is_empty() { - // CARGO_ENCODED_RUSTFLAGS uses \x1f (unit separator) as delimiter, which allows - // individual flags to contain spaces (e.g. paths from `llvm-config --libdir`). - cargo.command.env("CARGO_ENCODED_RUSTFLAGS", rustflags.join("\x1f")); + cargo.command.env("CARGO_ENCODED_RUSTFLAGS", rustflags); } let rustdocflags = &cargo.rustdocflags.0; if !rustdocflags.is_empty() { - cargo.command.env("CARGO_ENCODED_RUSTDOCFLAGS", rustdocflags.join("\x1f")); + cargo.command.env("CARGO_ENCODED_RUSTDOCFLAGS", rustdocflags); } let encoded_hostflags = cargo.hostflags.encode(); From b4a7c3eaef31af895127408796c72744c18ec810 Mon Sep 17 00:00:00 2001 From: Rohan Singla Date: Mon, 22 Jun 2026 02:55:59 +0530 Subject: [PATCH 05/11] unset RUSTFLAGS/RUSTDOCFLAGS when setting encoded forms --- src/bootstrap/src/core/builder/cargo.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/bootstrap/src/core/builder/cargo.rs b/src/bootstrap/src/core/builder/cargo.rs index cabcd5e86efa7..b76dc77683f94 100644 --- a/src/bootstrap/src/core/builder/cargo.rs +++ b/src/bootstrap/src/core/builder/cargo.rs @@ -465,6 +465,13 @@ impl From for BootstrapCommand { cargo.command.args(cargo.args); + // Always unset the plain RUSTFLAGS/RUSTDOCFLAGS so that downstream + // tools (e.g. build.rs scripts) see only the encoded form. Any flags + // from the caller's environment have already been folded into the + // Rustflags struct via `propagate_cargo_env`. + cargo.command.env_remove("RUSTFLAGS"); + cargo.command.env_remove("RUSTDOCFLAGS"); + let rustflags = &cargo.rustflags.0; if !rustflags.is_empty() { cargo.command.env("CARGO_ENCODED_RUSTFLAGS", rustflags); From a13e2daa60bc70a10fb3c4d2b7b2b66723f1bfd2 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Tue, 23 Jun 2026 22:35:04 +0530 Subject: [PATCH 06/11] rustc: improve diagnostics for file-open failures Emit more targeted diagnostics when an input file cannot be opened, including dedicated messages for common error kinds and typo suggestions for missing files. Add run-make tests covering NotFound, PermissionDenied, IsADirectory, and non-existent directory cases. Signed-off-by: Usman Akinyemi --- compiler/rustc_parse/src/lib.rs | 41 +++++++++++- tests/run-make/input-file-errors/rmake.rs | 63 +++++++++++++++++++ tests/ui/modules/path-no-file-name.rs | 3 +- tests/ui/modules/path-no-file-name.stderr | 2 +- tests/ui/parser/issues/issue-5806.rs | 2 +- tests/ui/parser/issues/issue-5806.stderr | 2 +- .../ui/parser/mod_file_with_path_attr.stderr | 2 +- .../staged-api-invalid-path-108697.rs | 2 +- .../staged-api-invalid-path-108697.stderr | 2 +- 9 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 tests/run-make/input-file-errors/rmake.rs diff --git a/compiler/rustc_parse/src/lib.rs b/compiler/rustc_parse/src/lib.rs index 7805994b441b8..14debe267d3dc 100644 --- a/compiler/rustc_parse/src/lib.rs +++ b/compiler/rustc_parse/src/lib.rs @@ -20,8 +20,9 @@ use rustc_ast_pretty::pprust; use rustc_errors::{Diag, EmissionGuarantee, FatalError, PResult, pluralize}; pub use rustc_lexer::UNICODE_VERSION; use rustc_session::parse::ParseSess; +use rustc_span::edit_distance::find_best_match_for_name; use rustc_span::source_map::SourceMap; -use rustc_span::{FileName, SourceFile, Span}; +use rustc_span::{FileName, SourceFile, Span, Symbol}; pub const MACRO_ARGUMENTS: Option<&str> = Some("macro arguments"); @@ -105,6 +106,8 @@ pub fn new_parser_from_source_str( /// dropped. /// /// If a span is given, that is used on an error as the source of the problem. +/// +/// Error messages are tailored to the specific error kind. pub fn new_parser_from_file<'a>( psess: &'a ParseSess, path: &Path, @@ -113,8 +116,42 @@ pub fn new_parser_from_file<'a>( ) -> Result, Vec>> { let sm = psess.source_map(); let source_file = sm.load_file(path).unwrap_or_else(|e| { - let msg = format!("couldn't read `{}`: {}", path.display(), e); + use std::io::ErrorKind; + + let msg = match e.kind() { + ErrorKind::NotFound => format!("couldn't find file `{}`", path.display()), + ErrorKind::PermissionDenied => { + format!("permission denied when opening file `{}`", path.display()) + } + ErrorKind::IsADirectory => format!("`{}` is a directory", path.display()), + _ => format!("couldn't read `{}`: {}", path.display(), e), + }; + let mut err = psess.dcx().struct_fatal(msg); + + if e.kind() == ErrorKind::NotFound { + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + let parent = match path.parent() { + Some(p) if !p.as_os_str().is_empty() => p, + _ => Path::new("."), + }; + if let Ok(entries) = std::fs::read_dir(parent) { + let candidates: Vec = entries + .flatten() + .filter_map(|entry| entry.file_name().to_str().map(Symbol::intern)) + .collect(); + let lookup = Symbol::intern(file_name); + if let Some(suggestion) = find_best_match_for_name(&candidates, lookup, None) { + let suggested_path = if parent == Path::new(".") { + suggestion.as_str().to_string() + } else { + parent.join(suggestion.as_str()).display().to_string() + }; + err.help(format!("you might have meant to open `{}`", suggested_path)); + } + } + } + } if let Ok(contents) = std::fs::read(path) && let Err(utf8err) = std::str::from_utf8(&contents) { diff --git a/tests/run-make/input-file-errors/rmake.rs b/tests/run-make/input-file-errors/rmake.rs new file mode 100644 index 0000000000000..a3d8516af662c --- /dev/null +++ b/tests/run-make/input-file-errors/rmake.rs @@ -0,0 +1,63 @@ +// Tests that rustc produces helpful error messages when the input file +// cannot be opened, including specific messages for different error kinds +// and typo suggestions for NotFound errors. +// +// The permission-denied test requires Unix file mode bits and is skipped +// on Windows. It is also skipped on riscv64/arm because those CI runners +// run as root, which bypasses permission restrictions. + +//@ ignore-riscv64 +//@ ignore-arm +//@ ignore-windows +//@ needs-target-std + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +use run_make_support::{rfs, run_in_tmpdir, rustc}; + +fn main() { + // 1. NotFound — basic case: no "os error 2" in the output + run_in_tmpdir(|| { + rustc() + .input("fo.rs") + .run_fail() + .assert_stderr_contains("couldn't find file `fo.rs`") + .assert_stderr_not_contains("os error 2"); + }); + + // 2. NotFound with typo suggestion: foo.rs exists, compiling fo.rs should suggest foo.rs + run_in_tmpdir(|| { + rfs::write("foo.rs", b"fn main() {}"); + rustc() + .input("fo.rs") + .run_fail() + .assert_stderr_contains("couldn't find file `fo.rs`") + .assert_stderr_contains("you might have meant to open `foo.rs`"); + }); + + // 3. PermissionDenied — file exists but is unreadable + run_in_tmpdir(|| { + rfs::write("secret.rs", b"fn main() {}"); + + let mut perms = rfs::metadata("secret.rs").permissions(); + perms.set_mode(0o000); // no read, write, or execute + rfs::set_permissions("secret.rs", perms); + + // Run rustc before restoring permissions, store the result + let output = rustc().input("secret.rs").run_fail(); + + // Restore permissions so the tmpdir cleanup can delete the file + let mut perms = rfs::metadata("secret.rs").permissions(); + perms.set_mode(0o644); + rfs::set_permissions("secret.rs", perms); + + output.assert_stderr_contains("permission denied when opening file"); + }); + + // 4. IsADirectory — path points to a directory, not a file + run_in_tmpdir(|| { + rfs::create_dir("mydir.rs"); + rustc().input("mydir.rs").run_fail().assert_stderr_contains("is a directory"); + }); +} diff --git a/tests/ui/modules/path-no-file-name.rs b/tests/ui/modules/path-no-file-name.rs index 753a09501235a..4e7d79a192f31 100644 --- a/tests/ui/modules/path-no-file-name.rs +++ b/tests/ui/modules/path-no-file-name.rs @@ -2,6 +2,5 @@ //@ normalize-stderr: "os error \d+" -> "os error $$ACCESS_DENIED_CODE" #[path = "."] -mod m; //~ ERROR couldn't read - +mod m; //~ ERROR `$DIR/.` is a directory fn main() {} diff --git a/tests/ui/modules/path-no-file-name.stderr b/tests/ui/modules/path-no-file-name.stderr index 6274ecfed1365..9377966e2e5d6 100644 --- a/tests/ui/modules/path-no-file-name.stderr +++ b/tests/ui/modules/path-no-file-name.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/.`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE) +error: `$DIR/.` is a directory --> $DIR/path-no-file-name.rs:5:1 | LL | mod m; diff --git a/tests/ui/parser/issues/issue-5806.rs b/tests/ui/parser/issues/issue-5806.rs index 1a819e22197fe..e6eaead5d886b 100644 --- a/tests/ui/parser/issues/issue-5806.rs +++ b/tests/ui/parser/issues/issue-5806.rs @@ -2,6 +2,6 @@ //@ normalize-stderr: "os error \d+" -> "os error $$ACCESS_DENIED_CODE" #[path = "../parser"] -mod foo; //~ ERROR couldn't read +mod foo; //~ ERROR couldn't find file `$DIR/../parser` fn main() {} diff --git a/tests/ui/parser/issues/issue-5806.stderr b/tests/ui/parser/issues/issue-5806.stderr index 88cc982baf259..2de6c0ab08670 100644 --- a/tests/ui/parser/issues/issue-5806.stderr +++ b/tests/ui/parser/issues/issue-5806.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/../parser`: $ACCESS_DENIED_MSG (os error $ACCESS_DENIED_CODE) +error: couldn't find file `$DIR/../parser` --> $DIR/issue-5806.rs:5:1 | LL | mod foo; diff --git a/tests/ui/parser/mod_file_with_path_attr.stderr b/tests/ui/parser/mod_file_with_path_attr.stderr index ef8a715712bbd..986b0914b6c4f 100644 --- a/tests/ui/parser/mod_file_with_path_attr.stderr +++ b/tests/ui/parser/mod_file_with_path_attr.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/not_a_real_file.rs`: $FILE_NOT_FOUND_MSG (os error 2) +error: couldn't find file `$DIR/not_a_real_file.rs` --> $DIR/mod_file_with_path_attr.rs:4:1 | LL | mod m; diff --git a/tests/ui/unpretty/staged-api-invalid-path-108697.rs b/tests/ui/unpretty/staged-api-invalid-path-108697.rs index 8a806b10d9da1..b368bcbc074f9 100644 --- a/tests/ui/unpretty/staged-api-invalid-path-108697.rs +++ b/tests/ui/unpretty/staged-api-invalid-path-108697.rs @@ -6,4 +6,4 @@ #![feature(staged_api)] #[path = "lol"] mod foo; -//~^ ERROR couldn't read `$DIR/lol` +//~^ ERROR couldn't find file `$DIR/lol` diff --git a/tests/ui/unpretty/staged-api-invalid-path-108697.stderr b/tests/ui/unpretty/staged-api-invalid-path-108697.stderr index 188f4985ded56..a475dc2a60f47 100644 --- a/tests/ui/unpretty/staged-api-invalid-path-108697.stderr +++ b/tests/ui/unpretty/staged-api-invalid-path-108697.stderr @@ -1,4 +1,4 @@ -error: couldn't read `$DIR/lol`: $FILE_NOT_FOUND_MSG (os error 2) +error: couldn't find file `$DIR/lol` --> $DIR/staged-api-invalid-path-108697.rs:8:1 | LL | mod foo; From 9aaea562307a3b1866ba53d895fc13ab5073ea66 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Sat, 27 Jun 2026 16:59:46 -0300 Subject: [PATCH 07/11] Recover deferred closure calls after errors --- compiler/rustc_hir_typeck/src/callee.rs | 17 +++++--- .../src/error_reporting/traits/suggestions.rs | 10 +++++ ...rred-closure-call-recovery-issue-157951.rs | 10 +++++ ...-closure-call-recovery-issue-157951.stderr | 39 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.rs create mode 100644 tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.stderr diff --git a/compiler/rustc_hir_typeck/src/callee.rs b/compiler/rustc_hir_typeck/src/callee.rs index 57ab29ac752ad..57734d9be582d 100644 --- a/compiler/rustc_hir_typeck/src/callee.rs +++ b/compiler/rustc_hir_typeck/src/callee.rs @@ -9,11 +9,11 @@ use rustc_hir::{self as hir, HirId, LangItem, find_attr}; use rustc_hir_analysis::autoderef::Autoderef; use rustc_infer::infer::BoundRegionConversionTime; use rustc_infer::traits::{Obligation, ObligationCause, ObligationCauseCode}; +use rustc_middle::bug; use rustc_middle::ty::adjustment::{ Adjust, Adjustment, AllowTwoPhase, AutoBorrow, AutoBorrowMutability, }; use rustc_middle::ty::{self, FnSig, GenericArgsRef, Ty, TyCtxt, TypeVisitableExt, Unnormalized}; -use rustc_middle::{bug, span_bug}; use rustc_span::def_id::LocalDefId; use rustc_span::{Ident, Span, sym}; use rustc_target::spec::{AbiMap, AbiMapping}; @@ -1161,11 +1161,16 @@ impl<'a, 'tcx> DeferredCallResolution<'tcx> { ); } None => { - span_bug!( - self.call_expr.span, - "Expected to find a suitable `Fn`/`FnMut`/`FnOnce` implementation for `{}`", - self.closure_ty - ) + let guar = fcx.tainted_by_errors().unwrap_or_else(|| { + fcx.dcx().span_delayed_bug( + self.call_expr.span, + format!( + "Expected to find a suitable `Fn`/`FnMut`/`FnOnce` implementation for `{}`", + self.closure_ty + ), + ) + }); + fcx.write_resolution(self.call_expr.hir_id, Err(guar)); } } } diff --git a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs index c3f4a09b2d431..754fbe24a8464 100644 --- a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs +++ b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs @@ -2555,6 +2555,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { err.children.clear(); let mut span = obligation.cause.span; + let mut is_async_fn_return = false; if let DefKind::Closure = self.tcx.def_kind(obligation.cause.body_id) && let parent = self.tcx.local_parent(obligation.cause.body_id) && let DefKind::Fn | DefKind::AssocFn = self.tcx.def_kind(parent) @@ -2570,10 +2571,19 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { // and // async fn foo() -> dyn Display Box span = fn_sig.decl.output.span(); + is_async_fn_return = true; err.span(span); } let body = self.tcx.hir_body_owned_by(obligation.cause.body_id); + if !is_async_fn_return + && let Node::Expr(hir::Expr { kind: hir::ExprKind::Closure(closure), .. }) = + self.tcx.hir_node_by_def_id(obligation.cause.body_id) + && matches!(closure.fn_decl.output, hir::FnRetTy::DefaultReturn(_)) + { + return true; + } + let mut visitor = ReturnsVisitor::default(); visitor.visit_body(&body); diff --git a/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.rs b/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.rs new file mode 100644 index 0000000000000..d1e2a25b29521 --- /dev/null +++ b/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.rs @@ -0,0 +1,10 @@ +//@ compile-flags: -Znext-solver=globally +//@ check-fail + +fn main() { + let f = |f: dyn Fn()| f; + //~^ ERROR the size for values of type `(dyn Fn() + 'static)` cannot be known at compilation time + //~| ERROR return type cannot be a trait object without pointer indirection + f(); + //~^ ERROR this function takes 1 argument but 0 arguments were supplied +} diff --git a/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.stderr b/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.stderr new file mode 100644 index 0000000000000..a20e5f331d31a --- /dev/null +++ b/tests/ui/traits/next-solver/deferred-closure-call-recovery-issue-157951.stderr @@ -0,0 +1,39 @@ +error[E0277]: the size for values of type `(dyn Fn() + 'static)` cannot be known at compilation time + --> $DIR/deferred-closure-call-recovery-issue-157951.rs:5:17 + | +LL | let f = |f: dyn Fn()| f; + | ^^^^^^^^ doesn't have a size known at compile-time + | + = help: the trait `Sized` is not implemented for `(dyn Fn() + 'static)` + = help: unsized fn params are gated as an unstable feature +help: function arguments must have a statically known size, borrowed types always have a known size + | +LL | let f = |f: &dyn Fn()| f; + | + + +error[E0746]: return type cannot be a trait object without pointer indirection + --> $DIR/deferred-closure-call-recovery-issue-157951.rs:5:27 + | +LL | let f = |f: dyn Fn()| f; + | ^ doesn't have a size known at compile-time + +error[E0057]: this function takes 1 argument but 0 arguments were supplied + --> $DIR/deferred-closure-call-recovery-issue-157951.rs:8:5 + | +LL | f(); + | ^-- argument #1 of type `(dyn Fn() + 'static)` is missing + | +note: closure defined here + --> $DIR/deferred-closure-call-recovery-issue-157951.rs:5:13 + | +LL | let f = |f: dyn Fn()| f; + | ^^^^^^^^^^^^^ +help: provide the argument + | +LL | f(/* (dyn Fn() + 'static) */); + | ++++++++++++++++++++++++++ + +error: aborting due to 3 previous errors + +Some errors have detailed explanations: E0057, E0277, E0746. +For more information about an error, try `rustc --explain E0057`. From a89705b027241bcd6a63684da1ab46c587461c2c Mon Sep 17 00:00:00 2001 From: Mark Rousskov Date: Mon, 22 Jun 2026 10:28:15 -0400 Subject: [PATCH 08/11] Avoid panics bubbling out to proc macros Currently, rustc can emit a FatalError diagnostic during parsing of literals and tokenstreams. These are handled under the hood as a panic, which means that proc-macro code needed to catch_unwind if it wanted to fallibly parse some code. These still emit diagnostics, so in practice this isn't a full fix, but it at least makes the interface on the macro side a bit more uniform. This is primarily motivated by wasm proc macros which can't use catch_unwind and so this lets the test's output be the same with and without them. --- .../rustc_expand/src/proc_macro_server.rs | 21 +++++++----- .../auxiliary/nonfatal-parsing-body.rs | 34 ++++++++++--------- tests/ui/proc-macro/nonfatal-parsing.stderr | 18 +++++----- tests/ui/proc-macro/nonfatal-parsing.stdout | 6 +++- 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/compiler/rustc_expand/src/proc_macro_server.rs b/compiler/rustc_expand/src/proc_macro_server.rs index 41e550b8d6a3a..65af690611919 100644 --- a/compiler/rustc_expand/src/proc_macro_server.rs +++ b/compiler/rustc_expand/src/proc_macro_server.rs @@ -490,9 +490,11 @@ impl server::Server for Rustc<'_, '_> { fn literal_from_str(&mut self, s: &str) -> Result, String> { let name = FileName::proc_macro_source_code(s); - let mut parser = + let mut parser = rustc_errors::catch_fatal_errors(|| { new_parser_from_source_str(self.psess(), name, s.to_owned(), StripTokens::Nothing) - .map_err(cancel_diags_into_string)?; + }) + .map_err(|_| String::from("failed to parse to literal"))? + .map_err(cancel_diags_into_string)?; let first_span = parser.token.span.data(); let minus_present = parser.eat(exp!(Minus)); @@ -569,12 +571,15 @@ impl server::Server for Rustc<'_, '_> { } fn ts_from_str(&mut self, src: &str) -> Result { - source_str_to_stream( - self.psess(), - FileName::proc_macro_source_code(src), - src.to_string(), - Some(self.call_site), - ) + rustc_errors::catch_fatal_errors(|| { + source_str_to_stream( + self.psess(), + FileName::proc_macro_source_code(src), + src.to_string(), + Some(self.call_site), + ) + }) + .map_err(|_| String::from("failed to parse to tokenstream"))? .map_err(cancel_diags_into_string) } diff --git a/tests/ui/proc-macro/auxiliary/nonfatal-parsing-body.rs b/tests/ui/proc-macro/auxiliary/nonfatal-parsing-body.rs index 258f77067ce9c..b29c40770b0e7 100644 --- a/tests/ui/proc-macro/auxiliary/nonfatal-parsing-body.rs +++ b/tests/ui/proc-macro/auxiliary/nonfatal-parsing-body.rs @@ -7,12 +7,14 @@ use proc_macro::*; use self::Mode::*; // FIXME: all cases should become `NormalOk` or `NormalErr` +// +// And .stderr should be empty (no diagnostics should get emitted from fallible parsing in the proc +// macro). #[derive(PartialEq, Clone, Copy)] enum Mode { NormalOk, NormalErr, OtherError, - OtherWithPanic, } fn print_unspanned(s: &str) -> Result @@ -43,12 +45,11 @@ where assert!(t.is_err()); } OtherError => { - print_unspanned::(s); - } - OtherWithPanic => { - if catch_unwind(|| print_unspanned::(s)).is_ok() { - eprintln!("{s} did not panic"); - } + let t = print_unspanned::(s); + // For now we assert OK here, but in the future this should all move to NormalErr. + // Any case that's failing this should go to NormalErr, probably after verifying that a + // diagnostic did get emitted. + assert!(t.is_ok()); } } } @@ -136,9 +137,9 @@ pub fn run() { // FIXME: all of the cases below should return an Err and emit no diagnostics, but don't yet. // emits diagnostics and returns LexError - lit("r'r'", OtherError); - lit("c'r'", OtherError); - lit("\u{2000}", OtherError); + lit("r'r'", NormalErr); + lit("c'r'", NormalErr); + lit("\u{2000}", NormalErr); // emits diagnostic and returns a seemingly valid tokenstream stream("r'r'", OtherError); @@ -146,8 +147,8 @@ pub fn run() { stream("\u{2000}", OtherError); for parse in [stream as fn(&str, Mode), lit] { - // emits diagnostic(s), then panics - parse("r#", OtherWithPanic); + // emits diagnostic(s), then returns LexError + parse("r#", NormalErr); // emits diagnostic(s), then returns Ok(Literal { kind: ErrWithGuar, .. }) parse("0b2", OtherError); @@ -158,9 +159,10 @@ pub fn run() { "' '", OtherError, ); - parse(&format!("r{0}\"a\"{0}", "#".repeat(256)), OtherWithPanic); - - // emits diagnostic, then, when parsing as a lit, returns LexError, otherwise ErrWithGuar - parse("/*a*/ 0b2 //", OtherError); + parse(&format!("r{0}\"a\"{0}", "#".repeat(256)), NormalErr); } + + // emits diagnostic, then, when parsing as a lit, returns LexError, otherwise ErrWithGuar + lit("/*a*/ 0b2 //", NormalErr); + stream("/*a*/ 0b2 //", OtherError); } diff --git a/tests/ui/proc-macro/nonfatal-parsing.stderr b/tests/ui/proc-macro/nonfatal-parsing.stderr index cb6e243eebc9b..f015794e2ce67 100644 --- a/tests/ui/proc-macro/nonfatal-parsing.stderr +++ b/tests/ui/proc-macro/nonfatal-parsing.stderr @@ -130,15 +130,6 @@ LL | nonfatal_parsing::run!(); | = note: this error originates in the macro `nonfatal_parsing::run` (in Nightly builds, run with -Z macro-backtrace for more info) -error: invalid digit for a base 2 literal - --> $DIR/nonfatal-parsing.rs:15:5 - | -LL | nonfatal_parsing::run!(); - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - = note: this error originates in the macro `nonfatal_parsing::run` (in Nightly builds, run with -Z macro-backtrace for more info) - error: found invalid character; only `#` is allowed in raw string delimitation: \u{0} --> :1:1 | @@ -199,6 +190,15 @@ error: invalid digit for a base 2 literal LL | /*a*/ 0b2 // | ^ +error: invalid digit for a base 2 literal + --> $DIR/nonfatal-parsing.rs:15:5 + | +LL | nonfatal_parsing::run!(); + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` + = note: this error originates in the macro `nonfatal_parsing::run` (in Nightly builds, run with -Z macro-backtrace for more info) + error: aborting due to 22 previous errors For more information about this error, try `rustc --explain E0768`. diff --git a/tests/ui/proc-macro/nonfatal-parsing.stdout b/tests/ui/proc-macro/nonfatal-parsing.stdout index a46ef66f0f9d0..fcefde22541ba 100644 --- a/tests/ui/proc-macro/nonfatal-parsing.stdout +++ b/tests/ui/proc-macro/nonfatal-parsing.stdout @@ -52,15 +52,19 @@ Err(LexError("not a literal")) Ok(TokenStream [Ident { ident: "r", span: Span }, Literal { kind: Char, symbol: "r", suffix: None, span: Span }]) Ok(TokenStream [Ident { ident: "c", span: Span }, Literal { kind: Char, symbol: "r", suffix: None, span: Span }]) Ok(TokenStream []) +Err(LexError("failed to parse to tokenstream")) Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "0b2", suffix: None, span: Span }]) Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "0b", suffix: Some("f32"), span: Span }]) Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "0b0.0", suffix: Some("f32"), span: Span }]) Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "'''", suffix: None, span: Span }]) Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "'\n'", suffix: None, span: Span }]) -Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "0b2", suffix: None, span: Span }]) +Err(LexError("failed to parse to tokenstream")) +Err(LexError("failed to parse to literal")) Ok(Literal { kind: ErrWithGuar, symbol: "0b2", suffix: None, span: Span }) Ok(Literal { kind: ErrWithGuar, symbol: "0b", suffix: Some("f32"), span: Span }) Ok(Literal { kind: ErrWithGuar, symbol: "0b0.0", suffix: Some("f32"), span: Span }) Ok(Literal { kind: ErrWithGuar, symbol: "'''", suffix: None, span: Span }) Ok(Literal { kind: ErrWithGuar, symbol: "'\n'", suffix: None, span: Span }) +Err(LexError("failed to parse to literal")) Err(LexError("comment or whitespace around literal")) +Ok(TokenStream [Literal { kind: ErrWithGuar, symbol: "0b2", suffix: None, span: Span }]) From bd938c5bfb48fe83ef8f2f54f562c2394c17d89f Mon Sep 17 00:00:00 2001 From: Predrag Gruevski Date: Sat, 27 Jun 2026 01:13:26 -0400 Subject: [PATCH 09/11] Include default-stability info in rustdoc JSON. --- src/librustdoc/json/conversions.rs | 69 ++++++-- src/rustdoc-json-types/lib.rs | 39 ++++- src/tools/jsondoclint/src/validator.rs | 43 ++++- src/tools/jsondoclint/src/validator/tests.rs | 154 +++++++++++++++++- .../attrs/stability/default_body.rs | 76 +++++++++ 5 files changed, 364 insertions(+), 17 deletions(-) create mode 100644 tests/rustdoc-json/attrs/stability/default_body.rs diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 882220a0cae03..7083d730de58c 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -270,6 +270,18 @@ impl FromClean for Stability { } } +impl FromClean for Box { + fn from_clean(stab: &hir::DefaultBodyStability, _renderer: &JsonRenderer<'_>) -> Self { + let hir::StabilityLevel::Unstable { .. } = stab.level else { + bug!( + "unexpected stable default-body stability, \ + there's no stable equivalent of `#[rustc_default_body_unstable]`" + ) + }; + Box::new(ProvidedDefaultUnstable { feature: stab.feature.to_string() }) + } +} + impl FromClean for Option> { fn from_clean(generic_args: &clean::GenericArgs, renderer: &JsonRenderer<'_>) -> Self { use clean::GenericArgs::*; @@ -353,18 +365,23 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum EnumItem(e) => ItemEnum::Enum(e.into_json(renderer)), VariantItem(v) => ItemEnum::Variant(v.into_json(renderer)), FunctionItem(f) => { - ItemEnum::Function(from_clean_function(f, true, header.unwrap(), renderer)) + ItemEnum::Function(from_clean_function(f, true, None, header.unwrap(), renderer)) } ForeignFunctionItem(f, _) => { - ItemEnum::Function(from_clean_function(f, false, header.unwrap(), renderer)) + ItemEnum::Function(from_clean_function(f, false, None, header.unwrap(), renderer)) } TraitItem(t) => ItemEnum::Trait(t.into_json(renderer)), TraitAliasItem(t) => ItemEnum::TraitAlias(t.into_json(renderer)), - MethodItem(m, _) => { - ItemEnum::Function(from_clean_function(m, true, header.unwrap(), renderer)) - } + MethodItem(m, _) => ItemEnum::Function(from_clean_function( + m, + true, + default_body_stability_for_def_id(renderer.tcx, item.item_id.expect_def_id()) + .map(|stab| stab.into_json(renderer)), + header.unwrap(), + renderer, + )), RequiredMethodItem(m, _) => { - ItemEnum::Function(from_clean_function(m, false, header.unwrap(), renderer)) + ItemEnum::Function(from_clean_function(m, false, None, header.unwrap(), renderer)) } ImplItem(i) => ItemEnum::Impl(i.into_json(renderer)), StaticItem(s) => ItemEnum::Static(from_clean_static(s, rustc_hir::Safety::Safe, renderer)), @@ -385,23 +402,41 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum }) } // FIXME(generic_const_items): Add support for generic associated consts. - RequiredAssocConstItem(_generics, ty) => { - ItemEnum::AssocConst { type_: ty.into_json(renderer), value: None } - } + RequiredAssocConstItem(_generics, ty) => ItemEnum::AssocConst { + type_: ty.into_json(renderer), + value: None, + default_unstable: None, + }, // FIXME(generic_const_items): Add support for generic associated consts. - ProvidedAssocConstItem(ci) | ImplAssocConstItem(ci) => ItemEnum::AssocConst { + ProvidedAssocConstItem(ci) => ItemEnum::AssocConst { type_: ci.type_.into_json(renderer), value: Some(ci.kind.expr(renderer.tcx)), + default_unstable: default_body_stability_for_def_id( + renderer.tcx, + item.item_id.expect_def_id(), + ) + .map(|stab| stab.into_json(renderer)), + }, + ImplAssocConstItem(ci) => ItemEnum::AssocConst { + type_: ci.type_.into_json(renderer), + value: Some(ci.kind.expr(renderer.tcx)), + default_unstable: None, }, RequiredAssocTypeItem(g, b) => ItemEnum::AssocType { generics: g.into_json(renderer), bounds: b.into_json(renderer), type_: None, + default_unstable: None, }, AssocTypeItem(t, b) => ItemEnum::AssocType { generics: t.generics.into_json(renderer), bounds: b.into_json(renderer), type_: Some(t.item_type.as_ref().unwrap_or(&t.type_).into_json(renderer)), + default_unstable: default_body_stability_for_def_id( + renderer.tcx, + item.item_id.expect_def_id(), + ) + .map(|stab| stab.into_json(renderer)), }, // `convert_item` early returns `None` for stripped items, keywords, attributes and // "special" macro rules. @@ -815,6 +850,7 @@ impl FromClean for Impl { pub(crate) fn from_clean_function( clean::Function { decl, generics }: &clean::Function, has_body: bool, + default_unstable: Option>, header: rustc_hir::FnHeader, renderer: &JsonRenderer<'_>, ) -> Function { @@ -823,6 +859,7 @@ pub(crate) fn from_clean_function( generics: generics.into_json(renderer), header: header.into_json(renderer), has_body, + default_unstable, } } @@ -972,6 +1009,17 @@ impl FromClean for ItemKind { } } +fn default_body_stability_for_def_id( + tcx: TyCtxt<'_>, + def_id: DefId, +) -> Option { + let stability = tcx.lookup_default_body_stability(def_id)?; + match stability.level { + hir::StabilityLevel::Unstable { .. } => Some(stability), + hir::StabilityLevel::Stable { .. } => None, + } +} + fn const_stability_for_def_id(tcx: TyCtxt<'_>, def_id: DefId) -> Option { if !tcx.is_conditionally_const(def_id) { // The item cannot be conditionally-const. No const stability here. @@ -1040,6 +1088,7 @@ fn maybe_from_hir_attr(attr: &hir::Attribute, item_id: ItemId, tcx: TyCtxt<'_>) AK::Deprecated { .. } => return Vec::new(), // Handled separately into Item::deprecation. AK::Stability { .. } => return Vec::new(), // Handled separately into Item::stability AK::RustcConstStability { .. } => return Vec::new(), // Handled separately into Item::const_stability. + AK::RustcBodyStability { .. } => return Vec::new(), // Handled separately by `default_unstable`. AK::DocComment { .. } => unreachable!("doc comments stripped out earlier"), diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs index e6af2fa04bb10..be4921d888278 100644 --- a/src/rustdoc-json-types/lib.rs +++ b/src/rustdoc-json-types/lib.rs @@ -114,8 +114,8 @@ pub type FxHashMap = HashMap; // re-export for use in src/librustdoc // will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line // are deliberately not in a doc comment, because they need not be in public docs.) // -// Latest feature: Add `Item::const_stability`. -pub const FORMAT_VERSION: u32 = 59; +// Latest feature: Add default-body stability metadata. +pub const FORMAT_VERSION: u32 = 60; /// The root of the emitted JSON blob. /// @@ -288,6 +288,9 @@ pub struct Item { /// - `#[stable]` and `#[unstable]` attributes: see the [`Self::stability`] field instead. /// - `#[rustc_const_stable]` and `#[rustc_const_unstable]` attributes: /// see the [`Self::const_stability`] field instead. + /// - `#[rustc_default_body_unstable]` attributes: instead see `default_unstable` fields on + /// item kinds that can have unstable default values, such as [`Function::default_unstable`], + /// [`ItemEnum::AssocConst::default_unstable`], and [`ItemEnum::AssocType::default_unstable`]. /// /// Attributes appear in pretty-printed Rust form, regardless of their formatting /// in the original source code. For example: @@ -367,6 +370,19 @@ pub enum StabilityLevel { Unstable, } +/// Information about an unstable default provided by a trait item. +/// +/// Example unstable defaults include: +/// - a stable trait function or method whose body is not stable +/// - a stable trait associated type or const whose default value is not stable +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "rkyv_0_8", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] +#[cfg_attr(feature = "rkyv_0_8", rkyv(derive(Debug)))] +pub struct ProvidedDefaultUnstable { + /// The feature that must be enabled to use the provided default. + pub feature: String, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "rkyv_0_8", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] #[cfg_attr(feature = "rkyv_0_8", rkyv(derive(Debug)))] @@ -379,6 +395,9 @@ pub enum StabilityLevel { /// - `#[stable]` and `#[unstable]`. These are in [`Item::stability`] instead. /// - `#[rustc_const_stable]` and `#[rustc_const_unstable]`. These are in /// [`Item::const_stability`] instead. +/// - `#[rustc_default_body_unstable]`. These are in the `default_unstable` field on the appropriate +/// item kinds: [`Function::default_unstable`], [`ItemEnum::AssocConst::default_unstable`], +/// and [`ItemEnum::AssocType::default_unstable`]. pub enum Attribute { /// `#[non_exhaustive]` NonExhaustive, @@ -875,6 +894,11 @@ pub enum ItemEnum { /// // ^^^^^^^^^^ /// ``` value: Option, + /// Metadata about an unstable default value provided for the associated constant, if any. + /// + /// Empty if the associated constant has no default (see [`ItemEnum::AssocConst::value`]), + /// or if the default value is stable. + default_unstable: Option>, }, /// An associated type of a trait or a type. AssocType { @@ -899,6 +923,11 @@ pub enum ItemEnum { /// ``` #[serde(rename = "type")] type_: Option, + /// Metadata about an unstable default value provided for the associated type, if any. + /// + /// Empty if the associated type has no default (see [`ItemEnum::AssocType::type_`]), + /// or if the default value is stable. + default_unstable: Option>, }, } @@ -1188,6 +1217,12 @@ pub struct Function { pub header: FunctionHeader, /// Whether the function has a body, i.e. an implementation. pub has_body: bool, + /// Metadata about a possible unstable provided default implementation for trait methods. + /// + /// Only populated for function items inside traits. Empty if the trait method + /// does not have a default implementation (see [`Function::has_body`]), + /// or if its default implementation is stable. + pub default_unstable: Option>, } /// Generic parameters accepted by an item and `where` clauses imposed on it and the parameters. diff --git a/src/tools/jsondoclint/src/validator.rs b/src/tools/jsondoclint/src/validator.rs index 01deafe20b354..76a47c4f4fecf 100644 --- a/src/tools/jsondoclint/src/validator.rs +++ b/src/tools/jsondoclint/src/validator.rs @@ -97,7 +97,7 @@ impl<'a> Validator<'a> { ItemEnum::StructField(x) => self.check_struct_field(x), ItemEnum::Enum(x) => self.check_enum(x), ItemEnum::Variant(x) => self.check_variant(x, id), - ItemEnum::Function(x) => self.check_function(x), + ItemEnum::Function(x) => self.check_function(x, id), ItemEnum::Trait(x) => self.check_trait(x, id), ItemEnum::TraitAlias(x) => self.check_trait_alias(x), ItemEnum::Impl(x) => self.check_impl(x, id), @@ -114,12 +114,35 @@ impl<'a> Validator<'a> { ItemEnum::Module(x) => self.check_module(x, id), // FIXME: Why don't these have their own structs? ItemEnum::ExternCrate { .. } => {} - ItemEnum::AssocConst { type_, value: _ } => self.check_type(type_), - ItemEnum::AssocType { generics, bounds, type_ } => { + ItemEnum::AssocConst { type_, value, default_unstable } => { + self.check_type(type_); + if value.is_none() + && let Some(default_unstable) = default_unstable + { + self.fail( + id, + ErrorKind::Custom(format!( + "`default_unstable` must be `None` when `value` is `None`, but \ + assoc const id {} had `default_unstable` with feature `{}`", + id.0, default_unstable.feature + )), + ); + } + } + ItemEnum::AssocType { generics, bounds, type_, default_unstable } => { self.check_generics(generics); bounds.iter().for_each(|b| self.check_generic_bound(b)); if let Some(ty) = type_ { self.check_type(ty); + } else if let Some(default_unstable) = default_unstable { + self.fail( + id, + ErrorKind::Custom(format!( + "`default_unstable` must be `None` when `type_` is `None`, but \ + assoc type id {} had `default_unstable` with feature `{}`", + id.0, default_unstable.feature + )), + ); } } } @@ -194,9 +217,21 @@ impl<'a> Validator<'a> { } } - fn check_function(&mut self, x: &'a Function) { + fn check_function(&mut self, x: &'a Function, id: &Id) { self.check_generics(&x.generics); self.check_function_signature(&x.sig); + if !x.has_body + && let Some(default_unstable) = &x.default_unstable + { + self.fail( + id, + ErrorKind::Custom(format!( + "`default_unstable` must be `None` when `has_body == false`, but \ + function item id {} had `default_unstable` with feature `{}`", + id.0, default_unstable.feature + )), + ); + } } fn check_trait(&mut self, x: &'a Trait, id: &Id) { diff --git a/src/tools/jsondoclint/src/validator/tests.rs b/src/tools/jsondoclint/src/validator/tests.rs index ff2fae157f04c..f9b54a2cc0936 100644 --- a/src/tools/jsondoclint/src/validator/tests.rs +++ b/src/tools/jsondoclint/src/validator/tests.rs @@ -1,5 +1,7 @@ use rustc_hash::FxHashMap; -use rustdoc_json_types::{Abi, FORMAT_VERSION, FunctionHeader, Item, ItemKind, Visibility}; +use rustdoc_json_types::{ + Abi, FORMAT_VERSION, FunctionHeader, Item, ItemKind, ProvidedDefaultUnstable, Visibility, +}; use super::*; use crate::json_find::SelectorPart; @@ -221,6 +223,7 @@ fn errors_on_missing_path() { abi: Abi::Rust, }, has_body: true, + default_unstable: None, }), }, ), @@ -246,6 +249,155 @@ fn errors_on_missing_path() { ); } +fn krate_with_trait_item(inner: ItemEnum) -> Crate { + let item_id = Id(2); + Crate { + root: Id(0), + crate_version: None, + includes_private: false, + index: FxHashMap::from_iter([ + ( + Id(0), + Item { + id: Id(0), + crate_id: 0, + name: Some("root".to_owned()), + span: None, + visibility: Visibility::Public, + docs: None, + links: FxHashMap::default(), + attrs: Vec::new(), + deprecation: None, + stability: None, + const_stability: None, + inner: ItemEnum::Module(Module { + is_crate: true, + items: vec![Id(1)], + is_stripped: false, + }), + }, + ), + ( + Id(1), + Item { + id: Id(1), + crate_id: 0, + name: Some("Trait".to_owned()), + span: None, + visibility: Visibility::Public, + docs: None, + links: FxHashMap::default(), + attrs: Vec::new(), + deprecation: None, + stability: None, + const_stability: None, + inner: ItemEnum::Trait(Trait { + is_auto: false, + is_unsafe: false, + is_dyn_compatible: true, + items: vec![item_id], + generics: Generics { params: vec![], where_predicates: vec![] }, + bounds: vec![], + implementations: vec![], + }), + }, + ), + ( + item_id, + Item { + id: item_id, + crate_id: 0, + name: Some("TraitItem".to_owned()), + span: None, + visibility: Visibility::Public, + docs: None, + links: FxHashMap::default(), + attrs: Vec::new(), + deprecation: None, + stability: None, + const_stability: None, + inner, + }, + ), + ]), + paths: FxHashMap::default(), + external_crates: FxHashMap::default(), + target: rustdoc_json_types::Target { triple: "".to_string(), target_features: vec![] }, + format_version: FORMAT_VERSION, + } +} + +#[test] +fn errors_on_default_unstable_without_function_body() { + let krate = krate_with_trait_item(ItemEnum::Function(Function { + sig: FunctionSignature { inputs: vec![], output: None, is_c_variadic: false }, + generics: Generics { params: vec![], where_predicates: vec![] }, + header: FunctionHeader { + is_const: false, + is_unsafe: false, + is_async: false, + abi: Abi::Rust, + }, + has_body: false, + default_unstable: Some(Box::new(ProvidedDefaultUnstable { feature: "feature".to_owned() })), + })); + + check( + &krate, + &[Error { + id: Id(2), + kind: ErrorKind::Custom( + "`default_unstable` must be `None` when `has_body == false`, but \ + function item id 2 had `default_unstable` with feature `feature`" + .to_owned(), + ), + }], + ); +} + +#[test] +fn errors_on_default_unstable_without_assoc_const_value() { + let krate = krate_with_trait_item(ItemEnum::AssocConst { + type_: Type::Primitive("usize".to_owned()), + value: None, + default_unstable: Some(Box::new(ProvidedDefaultUnstable { feature: "feature".to_owned() })), + }); + + check( + &krate, + &[Error { + id: Id(2), + kind: ErrorKind::Custom( + "`default_unstable` must be `None` when `value` is `None`, but \ + assoc const id 2 had `default_unstable` with feature `feature`" + .to_owned(), + ), + }], + ); +} + +#[test] +fn errors_on_default_unstable_without_assoc_type_default() { + let krate = krate_with_trait_item(ItemEnum::AssocType { + generics: Generics { params: vec![], where_predicates: vec![] }, + bounds: vec![], + type_: None, + default_unstable: Some(Box::new(ProvidedDefaultUnstable { feature: "feature".to_owned() })), + }); + + check( + &krate, + &[Error { + id: Id(2), + kind: ErrorKind::Custom( + "`default_unstable` must be `None` when `type_` is `None`, but \ + assoc type id 2 had `default_unstable` with feature `feature`" + .to_owned(), + ), + }], + ); +} + #[test] #[should_panic = "LOCAL_CRATE_ID is wrong"] fn checks_local_crate_id_is_correct() { diff --git a/tests/rustdoc-json/attrs/stability/default_body.rs b/tests/rustdoc-json/attrs/stability/default_body.rs new file mode 100644 index 0000000000000..5a70e47d6c505 --- /dev/null +++ b/tests/rustdoc-json/attrs/stability/default_body.rs @@ -0,0 +1,76 @@ +#![feature(staged_api, rustc_attrs, associated_type_defaults)] + +#[stable(feature = "default_body_trait_feature", since = "1.0.0")] +pub trait TraitWithDefaults { + //@ is "$.index[?(@.docs=='method with unstable default body')].inner.function.has_body" true + //@ is "$.index[?(@.docs=='method with unstable default body')].inner.function.default_unstable.feature" '"method_default_body_feature"' + //@ is "$.index[?(@.docs=='method with unstable default body')].attrs" [] + /// method with unstable default body + #[stable(feature = "default_body_method_feature", since = "1.1.0")] + #[rustc_default_body_unstable(feature = "method_default_body_feature", issue = "none")] + fn method_with_unstable_default() {} + + //@ is "$.index[?(@.docs=='required method without default body')].inner.function.has_body" false + //@ is "$.index[?(@.docs=='required method without default body')].inner.function.default_unstable" null + /// required method without default body + #[stable(feature = "required_method_feature", since = "1.2.0")] + fn required_method(); + + //@ is "$.index[?(@.docs=='method with stable default body')].inner.function.has_body" true + //@ is "$.index[?(@.docs=='method with stable default body')].inner.function.default_unstable" null + /// method with stable default body + #[stable(feature = "stable_default_method_feature", since = "1.3.0")] + fn method_with_stable_default() {} + + //@ is "$.index[?(@.docs=='associated constant with unstable default value')].inner.assoc_const.value" '"0"' + //@ is "$.index[?(@.docs=='associated constant with unstable default value')].inner.assoc_const.default_unstable.feature" '"assoc_const_default_value_feature"' + //@ is "$.index[?(@.docs=='associated constant with unstable default value')].attrs" [] + /// associated constant with unstable default value + #[stable(feature = "assoc_const_with_unstable_default_feature", since = "1.4.0")] + #[rustc_default_body_unstable(feature = "assoc_const_default_value_feature", issue = "none")] + const UNSTABLE_DEFAULT_CONST: usize = 0; + + //@ is "$.index[?(@.docs=='required associated constant')].inner.assoc_const.value" null + //@ is "$.index[?(@.docs=='required associated constant')].inner.assoc_const.default_unstable" null + /// required associated constant + #[stable(feature = "required_assoc_const_feature", since = "1.5.0")] + const REQUIRED_CONST: usize; + + //@ is "$.index[?(@.docs=='associated type with unstable default type')].inner.assoc_type.default_unstable.feature" '"assoc_type_default_type_feature"' + //@ is "$.index[?(@.docs=='associated type with unstable default type')].attrs" [] + /// associated type with unstable default type + #[stable(feature = "assoc_type_with_unstable_default_feature", since = "1.6.0")] + #[rustc_default_body_unstable(feature = "assoc_type_default_type_feature", issue = "none")] + type UnstableDefaultType = usize; + + //@ is "$.index[?(@.docs=='required associated type')].inner.assoc_type.type" null + //@ is "$.index[?(@.docs=='required associated type')].inner.assoc_type.default_unstable" null + /// required associated type + #[stable(feature = "required_assoc_type_feature", since = "1.7.0")] + type RequiredType; +} + +#[stable(feature = "default_body_impl_target_feature", since = "2.0.0")] +pub struct ImplTarget; + +// Impl items provide their own definitions, so they do not use the trait's unstable defaults. +#[stable(feature = "default_body_impl_feature", since = "2.1.0")] +impl TraitWithDefaults for ImplTarget { + //@ is "$.index[?(@.docs=='impl override for unstable default body')].inner.function.default_unstable" null + /// impl override for unstable default body + fn method_with_unstable_default() {} + + fn required_method() {} + + //@ is "$.index[?(@.docs=='impl override for unstable default value')].inner.assoc_const.default_unstable" null + /// impl override for unstable default value + const UNSTABLE_DEFAULT_CONST: usize = 1; + + const REQUIRED_CONST: usize = 2; + + //@ is "$.index[?(@.docs=='impl override for unstable default type')].inner.assoc_type.default_unstable" null + /// impl override for unstable default type + type UnstableDefaultType = u8; + + type RequiredType = (); +} From 7f2574f608d339d42035ef200338af0652b4dd44 Mon Sep 17 00:00:00 2001 From: Justin Schilleman Date: Sun, 28 Jun 2026 22:02:48 +0000 Subject: [PATCH 10/11] Move attribute and keyword docs from `std` to `core` * refactor: move attribute and keywords docs files to core * fix references to `std` * tidy fixes * ignore doc tests w/ explicit_tail_calls * revert `unsafe` example and ignore specifically WASM for `become` doc tests * add explicit note about doube including the docs in `core` and `std` * missed refactoring of doc test ignore * conditionally exclude doc-test containing threading for `wasm-wasip1` * Change exclusion to just `wasi` target_os Co-authored-by: Justin Schilleman <97192655+jschillem@users.noreply.github.com> --- library/{std => core}/src/attribute_docs.rs | 0 library/{std => core}/src/keyword_docs.rs | 59 ++++++++++----------- library/core/src/lib.rs | 13 +++++ library/std/src/lib.rs | 21 ++++---- 4 files changed, 53 insertions(+), 40 deletions(-) rename library/{std => core}/src/attribute_docs.rs (100%) rename library/{std => core}/src/keyword_docs.rs (98%) diff --git a/library/std/src/attribute_docs.rs b/library/core/src/attribute_docs.rs similarity index 100% rename from library/std/src/attribute_docs.rs rename to library/core/src/attribute_docs.rs diff --git a/library/std/src/keyword_docs.rs b/library/core/src/keyword_docs.rs similarity index 98% rename from library/std/src/keyword_docs.rs rename to library/core/src/keyword_docs.rs index 5f94a13dad22a..3b0e3f21a0ac4 100644 --- a/library/std/src/keyword_docs.rs +++ b/library/core/src/keyword_docs.rs @@ -195,7 +195,7 @@ mod break_keyword {} /// to be most things that would be reasonable to have in a constant (barring `const fn`s). For /// example, you can't have a [`File`] as a `const`. /// -/// [`File`]: crate::fs::File +/// [`File`]: ../std/fs/struct.File.html /// /// The only lifetime allowed in a constant is `'static`, which is the lifetime that encompasses /// all others in a Rust program. For example, if you wanted to define a constant string, it would @@ -484,7 +484,7 @@ mod extern_keyword {} #[doc(keyword = "false")] // -/// A value of type [`bool`] representing logical **false**. +/// A value of type [`prim@bool`] representing logical **false**. /// /// `false` is the logical opposite of [`true`]. /// @@ -1060,7 +1060,8 @@ mod mod_keyword {} /// /// `move` is often used when [threads] are involved. /// -/// ```rust +#[cfg_attr(target_os = "wasi", doc = "```rust,ignore (thread::spawn not supported)")] +#[cfg_attr(not(target_os = "wasi"), doc = "```rust")] /// let data = vec![1, 2, 3]; /// /// std::thread::spawn(move || { @@ -1235,31 +1236,18 @@ mod ref_keyword {} /// `return` returns from the function immediately (an "early return"): /// /// ```no_run -/// use std::fs::File; -/// use std::io::{Error, ErrorKind, Read, Result}; +/// fn main() -> Result<(), &'static str> { +/// let contents = "Hello, world!"; /// -/// fn main() -> Result<()> { -/// let mut file = match File::open("foo.txt") { -/// Ok(f) => f, -/// Err(e) => return Err(e), -/// }; +/// if contents.contains("impossible!") { +/// return Err("oh no!"); +/// } /// -/// let mut contents = String::new(); -/// let size = match file.read_to_string(&mut contents) { -/// Ok(s) => s, -/// Err(e) => return Err(e), -/// }; -/// -/// if contents.contains("impossible!") { -/// return Err(Error::new(ErrorKind::Other, "oh no!")); -/// } +/// if contents.len() > 9000 { +/// return Err("over 9000!"); +/// } /// -/// if size > 9000 { -/// return Err(Error::new(ErrorKind::Other, "over 9000!")); -/// } -/// -/// assert_eq!(contents, "Hello, world!"); -/// Ok(()) +/// Ok(()) /// } /// ``` /// @@ -1306,7 +1294,11 @@ mod return_keyword {} /// manner to computed goto). /// /// Example of using `become` to implement functional-style `fold`: -/// ``` +#[cfg_attr( + target_family = "wasm", + doc = "```ignore (tail-call target feature not enabled by default on wasm)" +)] +#[cfg_attr(not(target_family = "wasm"), doc = "```")] /// #![feature(explicit_tail_calls)] /// #![expect(incomplete_features)] /// @@ -1360,7 +1352,11 @@ mod return_keyword {} /// (unless it's coerced to a function pointer) /// /// It is possible to tail-call a function pointer: -/// ``` +#[cfg_attr( + target_family = "wasm", + doc = "```ignore (tail-call target feature not enabled by default on wasm)" +)] +#[cfg_attr(not(target_family = "wasm"), doc = "```")] /// #![feature(explicit_tail_calls)] /// #![expect(incomplete_features)] /// @@ -1631,8 +1627,8 @@ mod self_upper_keyword {} /// [`extern`]: keyword.extern.html /// [`mut`]: keyword.mut.html /// [`unsafe`]: keyword.unsafe.html -/// [`Mutex`]: sync::Mutex -/// [`OnceLock`]: sync::OnceLock +/// [`Mutex`]: ../std/sync/struct.Mutex.html +/// [`OnceLock`]: ../std/sync/struct.OnceLock.html /// [`RefCell`]: cell::RefCell /// [atomic]: sync::atomic /// [Reference]: ../reference/items/static-items.html @@ -1959,7 +1955,7 @@ mod trait_keyword {} #[doc(keyword = "true")] // -/// A value of type [`bool`] representing logical **true**. +/// A value of type [`prim@bool`] representing logical **true**. /// /// Logically `true` is not equal to [`false`]. /// @@ -2312,6 +2308,7 @@ mod type_keyword {} /// [`static`]: keyword.static.html /// [`union`]: keyword.union.html /// [`impl`]: keyword.impl.html +/// [`Vec::set_len`]: ../std/vec/struct.Vec.html#method.set_len /// [raw pointers]: ../reference/types/pointer.html /// [memory safety]: ../book/ch19-01-unsafe-rust.html /// [Rustonomicon]: ../nomicon/index.html @@ -2502,7 +2499,7 @@ mod use_keyword {} /// ``` /// /// `where` is available anywhere generic and lifetime parameters are available, -/// as can be seen with the [`Cow`](crate::borrow::Cow) type from the standard +/// as can be seen with the [`Cow`](../std/borrow/enum.Cow.html) type from the standard /// library: /// /// ```rust diff --git a/library/core/src/lib.rs b/library/core/src/lib.rs index a26304c46ecea..124e871a32a3a 100644 --- a/library/core/src/lib.rs +++ b/library/core/src/lib.rs @@ -397,4 +397,17 @@ pub mod simd { pub use crate::core_simd::simd::*; } +// Include private modules that exist solely to provide rustdoc +// documentation for built-in attributes. Using `include!` because rustdoc +// only looks for these modules at the crate level. +include!("attribute_docs.rs"); + +// Include a number of private modules that exist solely to provide +// the rustdoc documentation for the existing keywords. Using `include!` +// because rustdoc only looks for these modules at the crate level. +include!("keyword_docs.rs"); + +// Include a number of private modules that exist solely to provide +// the rustdoc documentation for primitive types. Using `include!` +// because rustdoc only looks for these modules at the crate level. include!("primitive_docs.rs"); diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 1b27be8e2dde3..f24ce720e944f 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -778,20 +778,23 @@ pub mod from { pub use core::from::From; } -// Include a number of private modules that exist solely to provide -// the rustdoc documentation for primitive types. Using `include!` -// because rustdoc only looks for these modules at the crate level. -include!("../../core/src/primitive_docs.rs"); +// We include the following files here *again* (they are already included in libcore) +// so that they show up in search results for the std crate, and to avoid breaking +// existing links: + +// documentation for built-in attributes. Using `include!` because rustdoc +// only looks for these modules at the crate level. +include!("../../core/src/attribute_docs.rs"); // Include a number of private modules that exist solely to provide // the rustdoc documentation for the existing keywords. Using `include!` // because rustdoc only looks for these modules at the crate level. -include!("keyword_docs.rs"); +include!("../../core/src/keyword_docs.rs"); -// Include private modules that exist solely to provide rustdoc -// documentation for built-in attributes. Using `include!` because rustdoc -// only looks for these modules at the crate level. -include!("attribute_docs.rs"); +// Include a number of private modules that exist solely to provide +// the rustdoc documentation for primitive types. Using `include!` +// because rustdoc only looks for these modules at the crate level. +include!("../../core/src/primitive_docs.rs"); // This is required to avoid an unstable error when `restricted-std` is not // enabled. The use of #![feature(restricted_std)] in rustc-std-workspace-std From cca8cd1736adb36920f6721e540bd9df4298c80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Mon, 29 Jun 2026 12:32:03 +0200 Subject: [PATCH 11/11] Use doctest attribute `ignore-wasm` instead of manual `cfg_attr` --- library/core/src/keyword_docs.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/library/core/src/keyword_docs.rs b/library/core/src/keyword_docs.rs index 3b0e3f21a0ac4..596765be5e2dd 100644 --- a/library/core/src/keyword_docs.rs +++ b/library/core/src/keyword_docs.rs @@ -1294,11 +1294,8 @@ mod return_keyword {} /// manner to computed goto). /// /// Example of using `become` to implement functional-style `fold`: -#[cfg_attr( - target_family = "wasm", - doc = "```ignore (tail-call target feature not enabled by default on wasm)" -)] -#[cfg_attr(not(target_family = "wasm"), doc = "```")] +/// +/// ```ignore-wasm (tail-call target feature not enabled by default on wasm) /// #![feature(explicit_tail_calls)] /// #![expect(incomplete_features)] /// @@ -1352,11 +1349,8 @@ mod return_keyword {} /// (unless it's coerced to a function pointer) /// /// It is possible to tail-call a function pointer: -#[cfg_attr( - target_family = "wasm", - doc = "```ignore (tail-call target feature not enabled by default on wasm)" -)] -#[cfg_attr(not(target_family = "wasm"), doc = "```")] +/// +/// ```ignore-wasm (tail-call target feature not enabled by default on wasm) /// #![feature(explicit_tail_calls)] /// #![expect(incomplete_features)] ///