From 6ee6a80cf165bef454ec693a5d726418cfcb4e8a Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:33:05 -0400 Subject: [PATCH] Strip the `r#` prefix from raw identifiers in `derive(Debug)` `#[builder(derive(Debug))]` printed the `r#` escape as if it were part of the name. A builder whose type and members use raw identifiers formatted as `r#type { r#struct: 1, r#enum: 0, while: 2 }`, while the std `#[derive(Debug)]` prints those same names as `type`, `struct` and `enum`. Named members were already correct: `derive_debug` in `bon-macros/src/builder/builder_gen/builder_derives/debug.rs` formats them with `MemberName::snake_raw_str`, which is the name with the prefix removed. The three other names in the generated `fmt` came from `Ident::to_string()`, which keeps it: * the builder struct name passed to `Formatter::debug_struct`, reachable through `#[builder(builder_type = r#type)]` * `#[builder(start_fn)]` member names * `#[builder(field)]` member names All three now use `IdentExt::raw_name()`, the helper `snake_raw_str` is itself built from. `#[builder(name = ...)]` cannot be combined with `start_fn` or with `field`, so for those members the original identifier is the only name they can have and stripping the prefix is the whole fix. --- .../builder_gen/builder_derives/debug.rs | 9 ++++--- bon/tests/integration/builder/raw_idents.rs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/builder_derives/debug.rs b/bon-macros/src/builder/builder_gen/builder_derives/debug.rs index fcac1576..3f55445a 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/debug.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/debug.rs @@ -7,11 +7,14 @@ impl BuilderGenCtx { pub(super) fn derive_debug(&self, derive: &DeriveConfig) -> TokenStream { let bon = &self.bon; + // The `r#` prefix is a syntactic escape, not a part of the identifier's + // name, so it's stripped from every name that ends up in the `Debug` + // output, just like the std `#[derive(Debug)]` does. let format_members = self.members.iter().filter_map(|member| { match member { Member::StartFn(member) => { let member_ident = &member.ident; - let member_ident_str = member_ident.to_string(); + let member_ident_str = member_ident.raw_name(); let member_ty = &member.ty.norm; Some(quote! { output.field( @@ -24,7 +27,7 @@ impl BuilderGenCtx { } Member::Field(member) => { let member_ident = &member.ident; - let member_ident_str = member_ident.to_string(); + let member_ident_str = member_ident.raw_name(); let member_ty = &member.norm_ty; Some(quote! { output.field( @@ -77,7 +80,7 @@ impl BuilderGenCtx { let generic_args = &self.generics.args; let builder_ident = &self.builder_type.ident; let state_var = &self.state_var; - let builder_ident_str = builder_ident.to_string(); + let builder_ident_str = builder_ident.raw_name(); quote! { #[automatically_derived] diff --git a/bon/tests/integration/builder/raw_idents.rs b/bon/tests/integration/builder/raw_idents.rs index 791beb12..ee778ac3 100644 --- a/bon/tests/integration/builder/raw_idents.rs +++ b/bon/tests/integration/builder/raw_idents.rs @@ -105,3 +105,27 @@ fn test_self_underscore_bug_237() { assert_eq!(builder.build().self_, 42); } + +// The `r#` prefix is a syntactic escape, not a part of the identifier's name, +// so it must not appear in the `Debug` output, just like it doesn't appear in +// the output of the std `#[derive(Debug)]`. +#[test] +#[allow(non_camel_case_types)] +fn test_derive_debug() { + #[derive(Builder)] + #[builder(builder_type = r#type, state_mod = r#mod, derive(Debug))] + #[allow(dead_code)] + struct Sut { + #[builder(start_fn)] + r#struct: u32, + + #[builder(field)] + r#enum: u32, + + r#while: u32, + } + + let builder = Sut::builder(1).r#while(2); + + assert_debug_eq(&builder, expect!["type { struct: 1, enum: 0, while: 2 }"]); +}