From a5995da68369827aca90d39a8918518175fa690a Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Sat, 10 Oct 2026 14:05:05 +0800 Subject: [PATCH 1/4] Extend `rustfmt_with_extra` with stdin piping support Since I didn't want to use a tempdir/tempfile just for piping a simple source file. --- tests/rustfmt/main.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/rustfmt/main.rs b/tests/rustfmt/main.rs index 2220df2b617..139836e8f68 100644 --- a/tests/rustfmt/main.rs +++ b/tests/rustfmt/main.rs @@ -3,16 +3,19 @@ use indoc::panicdoc; use std::env; use std::fs::{File, remove_file}; +use std::io::Write; use std::path::Path; -use std::process::Command; +use std::process::{Command, Stdio}; use rustfmt_config_proc_macro::{nightly_only_test, rustfmt_only_ci_test, stable_only_test}; /// Run the rustfmt executable with environment vars set and return its output. +/// If `input` is given, it is piped to the executable's stdin. fn rustfmt_with_extra( args: &[&str], working_dir: Option<&str>, envs: &[(&str, &str)], + input: Option<&str>, ) -> (String, String) { let rustfmt_exe = env!("CARGO_BIN_EXE_rustfmt"); let bin_dir = Path::new(rustfmt_exe).parent().unwrap(); @@ -29,7 +32,19 @@ fn rustfmt_with_extra( if let Some(working_dir) = working_dir { cmd.current_dir(working_dir); } - match cmd.output() { + cmd.stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + match cmd.spawn().and_then(|mut child| { + if let Some(input) = input { + child.stdin.take().unwrap().write_all(input.as_bytes())?; + } + child.wait_with_output() + }) { Ok(output) => ( String::from_utf8(output.stdout).expect("utf-8"), String::from_utf8(output.stderr).expect("utf-8"), @@ -39,7 +54,7 @@ fn rustfmt_with_extra( } fn rustfmt(args: &[&str]) -> (String, String) { - rustfmt_with_extra(args, None, &[]) + rustfmt_with_extra(args, None, &[], None) } macro_rules! assert_that { @@ -401,7 +416,7 @@ fn rustfmt_allow_not_a_dir_errors() { let args = [empty_rs.to_str().unwrap()]; let envs = &[("HOME", fake_home_str)]; - let (stdout, stderr) = rustfmt_with_extra(&args, Some(fake_home_str), envs); + let (stdout, stderr) = rustfmt_with_extra(&args, Some(fake_home_str), envs, None); // Should pass without any errors assert_eq!(stdout, ""); From 73040e941de88b31af9469f1de3374161a83ceb5 Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Sat, 10 Oct 2026 14:05:51 +0800 Subject: [PATCH 2/4] Add regression test for `cfg_select` invalid fn item recovery ICE This ICEs on latest main. This has to be a binary test not the usual {source, target} test, since the latter doesn't have an installed ICE hook and the test would just vacuously pass. --- tests/rustfmt/main.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/rustfmt/main.rs b/tests/rustfmt/main.rs index 139836e8f68..5fbf64d8862 100644 --- a/tests/rustfmt/main.rs +++ b/tests/rustfmt/main.rs @@ -422,3 +422,16 @@ fn rustfmt_allow_not_a_dir_errors() { assert_eq!(stdout, ""); assert_eq!(stderr, ""); } + +// FIXME(cfg_select): ungate when `cfg_select!` formatting can be enabled on stable. +#[nightly_only_test] +#[test] +fn cfg_select_parser_recovery_ice_7087() { + // See also https://github.com/rust-lang/rustfmt/issues/7087 + let source = "cfg_select! { true => { fn foo {} } }\n"; + let panic_re = regex::Regex::new("thread.*panicked").unwrap(); + let (stdout, stderr) = rustfmt_with_extra(&[], None, &[], Some(source)); + assert!(!panic_re.is_match(&stderr)); + // Invalid arm should cause `cfg_select!` to be re-emitted unmodified. + assert_eq!(source, stdout); +} From 686491b245eb5dbdf2248e2d1cd90911710d0c63 Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Sat, 10 Oct 2026 14:59:50 +0800 Subject: [PATCH 3/4] Add regression test for `parse_items_from_cfg_select_inner` error count leak Since rustc's parser session state is global. We don't reset it properly in `parse_items_from_cfg_select_inner`, so an valid `cfg_select!` followed by an invalid `cfg_select!` can inhibit formatting. --- .../cfg-select-valid-then-invalid-item-error-count-leak.rs | 6 ++++++ .../cfg-select-valid-then-invalid-item-error-count-leak.rs | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/source/cfg-select-valid-then-invalid-item-error-count-leak.rs create mode 100644 tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs diff --git a/tests/source/cfg-select-valid-then-invalid-item-error-count-leak.rs b/tests/source/cfg-select-valid-then-invalid-item-error-count-leak.rs new file mode 100644 index 00000000000..399fcd45e09 --- /dev/null +++ b/tests/source/cfg-select-valid-then-invalid-item-error-count-leak.rs @@ -0,0 +1,6 @@ +//@ rustfmt-unstable: true +cfg_select! { + unix => { fn bar() {} } +} + +cfg_select! { unix => { fn foo {} } } diff --git a/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs b/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs new file mode 100644 index 00000000000..399fcd45e09 --- /dev/null +++ b/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs @@ -0,0 +1,6 @@ +//@ rustfmt-unstable: true +cfg_select! { + unix => { fn bar() {} } +} + +cfg_select! { unix => { fn foo {} } } From 14c3546d84b0717f45e35d63b90844a1b7a9471f Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Sat, 10 Oct 2026 14:26:37 +0800 Subject: [PATCH 4/4] Fix `cfg_select!` formatting ICE and error count leaks rustc's parser can produce an `Ok(expr)` with recoveries in the form of diagnostics even if the `expr` is technically invalid. One such case is an fn item that forgot the arg list parentheses, i.e. `fn foo {}`. To properly handle this, we check for actual parser errors and abort if there are parser errors (i.e. we re-emit the original macro invocation unchanged). ICE reported in . We also reset error counts to avoid tainting other parsing sessions. Since error count is a parser session global state, this was accidentally leaking previously. For example, if you have a `cfg_select!` containing valid item syntax followed by an `cfg_select!` containing invalid item syntax, the invalid `cfg_select!` instance unintentionally inhibits the formatting of the earlier valid one. So fix this by properly resetting error counts. --- src/parse/macros/cfg_select.rs | 37 ++++++++++++++++--- ...alid-then-invalid-item-error-count-leak.rs | 4 +- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/parse/macros/cfg_select.rs b/src/parse/macros/cfg_select.rs index de8f61d2412..234070105e6 100644 --- a/src/parse/macros/cfg_select.rs +++ b/src/parse/macros/cfg_select.rs @@ -17,15 +17,22 @@ use crate::parse::macros::build_stream_parser; use crate::parse::session::ParseSess; use crate::spanned::Spanned; +// NB: parser recovery can return `Ok(expr)` (or other non-terminals) and still emit errors. We need +// to properly reset the shared error count. + pub(crate) fn parse_items_from_cfg_select<'a>( psess: &'a ParseSess, mac: &'a ast::MacCall, ) -> Result, &'static str> { - match catch_unwind(AssertUnwindSafe(|| { + let result = catch_unwind(AssertUnwindSafe(|| { parse_items_from_cfg_select_inner(psess, mac) - })) { - Ok(Ok(items)) => Ok(items), - Ok(err @ Err(_)) => err, + })); + let dcx = psess.inner().dcx(); + let recovered = dcx.has_errors().is_some(); + dcx.reset_err_count(); + match result { + Ok(Ok(_)) if recovered => Err("cfg_select! parsed with recovery"), + Ok(res) => res, Err(..) => Err("failed to parse cfg_select!"), } } @@ -71,7 +78,6 @@ fn parse_items_from_cfg_select_inner<'a>( } Err(err) => { err.cancel(); - parser.psess.dcx().reset_err_count(); return Err( "Expected item inside cfg_select block, but failed to parse it as an item", ); @@ -159,6 +165,24 @@ pub(crate) fn parse_cfg_select_arms( psess: &ParseSess, ts: TokenStream, ) -> Option> { + let result = catch_unwind(AssertUnwindSafe(|| parse_cfg_select_arms_inner(psess, ts))); + let dcx = psess.inner().dcx(); + let recovered = dcx.has_errors().is_some(); + dcx.reset_err_count(); + match result { + Ok(Some(_)) if recovered => { + debug!("cfg_select! arms parsed with recovery"); + None + } + Ok(arms) => arms, + Err(..) => { + debug!("failed to parse cfg_select! arms"); + None + } + } +} + +fn parse_cfg_select_arms_inner(psess: &ParseSess, ts: TokenStream) -> Option> { let mut cfg_select_predicates = vec![]; let mut parser = build_stream_parser(psess.inner(), ts); @@ -170,12 +194,13 @@ pub(crate) fn parse_cfg_select_arms( debug!("Failed to parse cfg entry in cfg_select! predicate"); return None; }; + CfgSelectFormatPredicate::Cfg(meta_item) }; if let Err(e) = parser.expect(exp!(FatArrow)) { e.cancel(); - debug!("Expected to find a `=>` after cfg_selec! predicate."); + debug!("Expected to find a `=>` after cfg_select! predicate."); return None; }; diff --git a/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs b/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs index 399fcd45e09..d4337d26120 100644 --- a/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs +++ b/tests/target/cfg-select-valid-then-invalid-item-error-count-leak.rs @@ -1,6 +1,8 @@ //@ rustfmt-unstable: true cfg_select! { - unix => { fn bar() {} } + unix => { + fn bar() {} + } } cfg_select! { unix => { fn foo {} } }