From c77768590dbc2e63acf853ab86177de3fb4e5bfb Mon Sep 17 00:00:00 2001 From: "Andrew V. Teylu" Date: Thu, 19 Mar 2026 10:27:40 +0000 Subject: [PATCH] Fix whitespace after fragment specifiers in macro pretty printing When a macro-generating-macro captures fragment specifier tokens (like `$x:ident`) as `tt` metavariables and replays them before a keyword (like `where`), the pretty printer concatenates them into an invalid fragment specifier (e.g. `$x:identwhere` instead of `$x:ident where`). This happens because `tt` captures preserve the original token spacing. When the fragment specifier name (e.g. `ident`) was originally the last token before a closing delimiter, it retains `JointHidden` spacing. The `print_tts` function only checks `space_between` for `Spacing::Alone` tokens, so `JointHidden` tokens skip the space check entirely, causing adjacent identifier-like tokens to merge. The fix adds a check in `print_tts` to insert a space between adjacent identifier-like tokens (identifiers and keywords) regardless of the original spacing, preventing them from being concatenated into invalid tokens. This is similar to the existing `space_between` mechanism that prevents token merging for `Spacing::Alone` tokens, extended to also handle `Joint`/`JointHidden` cases where two identifier-like tokens would merge. Signed-off-by: Andrew V. Teylu --- compiler/rustc_ast_pretty/src/pprust/state.rs | 20 ++++++++++++++++ .../macro-fragment-specifier-whitespace.pp | 24 +++++++++++++++++++ .../macro-fragment-specifier-whitespace.rs | 19 +++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 tests/pretty/macro-fragment-specifier-whitespace.pp create mode 100644 tests/pretty/macro-fragment-specifier-whitespace.rs diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs index 4ba5dc541342..2ff730e3331c 100644 --- a/compiler/rustc_ast_pretty/src/pprust/state.rs +++ b/compiler/rustc_ast_pretty/src/pprust/state.rs @@ -329,6 +329,19 @@ fn print_crate_inner<'a>( /// - #63896: `#[allow(unused,` must be printed rather than `#[allow(unused ,` /// - #73345: `#[allow(unused)]` must be printed rather than `# [allow(unused)]` /// +/// Returns `true` if both token trees are identifier-like tokens that would +/// merge into a single token if printed without a space between them. +/// E.g. `ident` + `where` would merge into `identwhere`. +fn idents_would_merge(tt1: &TokenTree, tt2: &TokenTree) -> bool { + fn is_ident_like(tt: &TokenTree) -> bool { + matches!( + tt, + TokenTree::Token(Token { kind: token::Ident(..) | token::NtIdent(..), .. }, _,) + ) + } + is_ident_like(tt1) && is_ident_like(tt2) +} + fn space_between(tt1: &TokenTree, tt2: &TokenTree) -> bool { use Delimiter::*; use TokenTree::{Delimited as Del, Token as Tok}; @@ -794,6 +807,13 @@ fn print_tts(&mut self, tts: &TokenStream, convert_dollar_crate: bool) { if let Some(next) = iter.peek() { if spacing == Spacing::Alone && space_between(tt, next) { self.space(); + } else if spacing != Spacing::Alone && idents_would_merge(tt, next) { + // When tokens from macro `tt` captures preserve their + // original `Joint`/`JointHidden` spacing, adjacent + // identifier-like tokens can be concatenated without a + // space (e.g. `$x:identwhere`). Insert a space to + // prevent this. + self.space(); } } } diff --git a/tests/pretty/macro-fragment-specifier-whitespace.pp b/tests/pretty/macro-fragment-specifier-whitespace.pp new file mode 100644 index 000000000000..ee5a0f7a7c05 --- /dev/null +++ b/tests/pretty/macro-fragment-specifier-whitespace.pp @@ -0,0 +1,24 @@ +#![feature(prelude_import)] +#![no_std] +extern crate std; +#[prelude_import] +use ::std::prelude::rust_2015::*; +//@ pretty-mode:expanded +//@ pp-exact:macro-fragment-specifier-whitespace.pp + +// Test that fragment specifier names in macro definitions are properly +// separated from the following keyword/identifier token when pretty-printed. +// This is a regression test for a bug where `$x:ident` followed by `where` +// was pretty-printed as `$x:identwhere` (an invalid fragment specifier). + +macro_rules! outer { + ($d:tt $($params:tt)*) => + { + #[macro_export] macro_rules! inner + { ($($params)* where $d($rest:tt)*) => {}; } + }; +} +#[macro_export] +macro_rules! inner { ($x:ident where $ ($rest : tt)*) => {}; } + +fn main() {} diff --git a/tests/pretty/macro-fragment-specifier-whitespace.rs b/tests/pretty/macro-fragment-specifier-whitespace.rs new file mode 100644 index 000000000000..54c6debd9a27 --- /dev/null +++ b/tests/pretty/macro-fragment-specifier-whitespace.rs @@ -0,0 +1,19 @@ +//@ pretty-mode:expanded +//@ pp-exact:macro-fragment-specifier-whitespace.pp + +// Test that fragment specifier names in macro definitions are properly +// separated from the following keyword/identifier token when pretty-printed. +// This is a regression test for a bug where `$x:ident` followed by `where` +// was pretty-printed as `$x:identwhere` (an invalid fragment specifier). + +macro_rules! outer { + ($d:tt $($params:tt)*) => { + #[macro_export] + macro_rules! inner { + ($($params)* where $d($rest:tt)*) => {}; + } + }; +} +outer!($ $x:ident); + +fn main() {}