From e2e829cd098d4319b1f8a9b727b89a7a19b9c31e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 3 Oct 2025 11:47:55 +0000 Subject: [PATCH 1/8] Make rustdoc source links for setters/getters/other_items reference original member --- .../src/builder/builder_gen/input_fn/mod.rs | 1 + .../src/builder/builder_gen/input_struct.rs | 2 ++ .../src/builder/builder_gen/member/mod.rs | 10 +++++++- .../src/builder/builder_gen/member/named.rs | 3 +++ .../src/builder/builder_gen/setters/mod.rs | 25 ++++++++++++++++--- bon-macros/src/util/ident.rs | 3 ++- 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/input_fn/mod.rs b/bon-macros/src/builder/builder_gen/input_fn/mod.rs index 0df8292f..3f712985 100644 --- a/bon-macros/src/builder/builder_gen/input_fn/mod.rs +++ b/bon-macros/src/builder/builder_gen/input_fn/mod.rs @@ -296,6 +296,7 @@ impl<'a> FnInputCtx<'a> { attrs: &arg.norm.attrs, ident: pat.ident.clone(), ty, + span: pat.ident.span(), }) }) .collect::>>()?; diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index be8343fa..d394384c 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -8,6 +8,7 @@ use crate::normalization::{GenericsNamespace, SyntaxVariant}; use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; use std::borrow::Cow; +use syn::spanned::Spanned; use syn::visit::Visit; use syn::visit_mut::VisitMut; @@ -110,6 +111,7 @@ impl StructInputCtx { attrs: &norm_field.attrs, ident, ty, + span: orig_field.span(), }) }) .collect::>>()?; diff --git a/bon-macros/src/builder/builder_gen/member/mod.rs b/bon-macros/src/builder/builder_gen/member/mod.rs index 57e8114b..3722de64 100644 --- a/bon-macros/src/builder/builder_gen/member/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/mod.rs @@ -95,6 +95,7 @@ pub(crate) struct RawMember<'a> { pub(crate) attrs: &'a [syn::Attribute], pub(crate) ident: syn::Ident, pub(crate) ty: SyntaxVariant>, + pub(crate) span: Span, } impl Member { @@ -157,7 +158,12 @@ impl Member { let mut named_count = 0; for (member, config) in members { - let RawMember { attrs, ident, ty } = member; + let RawMember { + attrs, + ident, + ty, + span: _, + } = member; if let Some(value) = config.skip { output.push(Self::Skip(SkipMember { @@ -206,6 +212,7 @@ impl Member { ty, config, docs, + span: member.span, }; member.merge_on_config(on)?; @@ -278,6 +285,7 @@ impl PosFnMember { attrs: _, ident, ty, + span: _, } = member; let mut me = Self { diff --git a/bon-macros/src/builder/builder_gen/member/named.rs b/bon-macros/src/builder/builder_gen/member/named.rs index a04c5a32..b93baa4f 100644 --- a/bon-macros/src/builder/builder_gen/member/named.rs +++ b/bon-macros/src/builder/builder_gen/member/named.rs @@ -90,6 +90,9 @@ pub(crate) struct NamedMember { /// Parameters configured by the user explicitly via attributes pub(crate) config: MemberConfig, + + /// Preserve a span the documentation for the member's methods should link to. + pub(crate) span: Span, } impl NamedMember { diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index 18544cb0..58830fca 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -445,12 +445,27 @@ impl<'a> SettersCtx<'a> { } }); - let SetterItem { name, vis, docs } = item; + let SetterItem { + name, + vis, + docs, + span, + } = item; let pats = imp.inputs.iter().map(|(pat, _)| pat); let types = imp.inputs.iter().map(|(_, ty)| ty); let const_ = &self.base.const_; - quote! { + let fn_prefix = quote_spanned! {span => + #vis #const_ fn + } + .into_iter() + .map(|mut token| { + token.set_span(span); + token + }) + .collect::(); + + quote_spanned! {span=> #( #docs )* #[allow( // This is intentional. We want the builder syntax to compile away @@ -463,7 +478,7 @@ impl<'a> SettersCtx<'a> { clippy::missing_const_for_fn, )] #[inline(always)] - #vis #const_ fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type + #fn_prefix #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type #where_clause { #body @@ -504,6 +519,7 @@ struct SetterItem { name: syn::Ident, vis: syn::Visibility, docs: Vec, + span: Span, } impl SettersItems { @@ -531,6 +547,7 @@ impl SettersItems { name: common_name.unwrap_or(&member.name.snake).clone(), vis: common_vis.unwrap_or(&builder_type.vis).clone(), docs, + span: member.span, }); } @@ -618,6 +635,7 @@ impl SettersItems { .clone(), docs: some_fn_docs, + span: member.span, }; let option_fn = config.and_then(|config| config.fns.option_fn.as_deref()); @@ -631,6 +649,7 @@ impl SettersItems { .clone(), docs: option_fn_docs, + span: member.span, }; Self::Optional(OptionalSettersItems { some_fn, option_fn }) diff --git a/bon-macros/src/util/ident.rs b/bon-macros/src/util/ident.rs index 12a85fbc..bb5a94aa 100644 --- a/bon-macros/src/util/ident.rs +++ b/bon-macros/src/util/ident.rs @@ -1,5 +1,6 @@ use crate::util::prelude::*; use ident_case::RenameRule; +use syn::spanned::Spanned; pub(crate) trait IdentExt { /// Converts the ident (assumed to be in `snake_case`) to `PascalCase` without @@ -45,7 +46,7 @@ impl IdentExt for syn::Ident { renamed.push('_'); } - Self::new(&renamed, Span::call_site()) + Self::new(&renamed, renamed.span()) } fn pascal_to_snake_case(&self) -> Self { From 4ee8ee19495f3802777192a9331bacdd6c5e5618 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 3 Oct 2025 11:49:36 +0000 Subject: [PATCH 2/8] Comment --- bon-macros/src/builder/builder_gen/setters/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index 58830fca..0684749a 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -458,6 +458,9 @@ impl<'a> SettersCtx<'a> { let fn_prefix = quote_spanned! {span => #vis #const_ fn } + // Respan the tokens to make sure the function's signature begins with + // the member's span. This is important to make rustdoc's source links + // point to the original member's location. .into_iter() .map(|mut token| { token.set_span(span); From f61e16c5448deec4bd861930c4ac541831e54469 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 3 Oct 2025 11:57:35 +0000 Subject: [PATCH 3/8] A bit better --- .../src/builder/builder_gen/setters/mod.rs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index 0684749a..996373fe 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -455,18 +455,16 @@ impl<'a> SettersCtx<'a> { let types = imp.inputs.iter().map(|(_, ty)| ty); let const_ = &self.base.const_; - let fn_prefix = quote_spanned! {span => - #vis #const_ fn - } - // Respan the tokens to make sure the function's signature begins with - // the member's span. This is important to make rustdoc's source links - // point to the original member's location. - .into_iter() - .map(|mut token| { - token.set_span(span); - token - }) - .collect::(); + let fn_modifiers = quote!(#vis #const_) + // Respan the tokens to make sure the function's signature begins with + // the member's span. This is important to make rustdoc's source links + // point to the original member's location. + .into_iter() + .map(|mut token| { + token.set_span(span); + token + }) + .collect::(); quote_spanned! {span=> #( #docs )* @@ -481,7 +479,7 @@ impl<'a> SettersCtx<'a> { clippy::missing_const_for_fn, )] #[inline(always)] - #fn_prefix #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type + #fn_modifiers fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type #where_clause { #body From c9712b389ae74c40b07b779146fa8b06beed62fa Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 3 Oct 2025 12:00:51 +0000 Subject: [PATCH 4/8] Even better --- .../src/builder/builder_gen/setters/mod.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index 996373fe..c901dbb1 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -455,16 +455,13 @@ impl<'a> SettersCtx<'a> { let types = imp.inputs.iter().map(|(_, ty)| ty); let const_ = &self.base.const_; - let fn_modifiers = quote!(#vis #const_) - // Respan the tokens to make sure the function's signature begins with - // the member's span. This is important to make rustdoc's source links - // point to the original member's location. - .into_iter() - .map(|mut token| { - token.set_span(span); - token - }) - .collect::(); + // Respan the tokens to make sure the function's signature begins with + // the member's span. This is important to make rustdoc's source links + // point to the original member's location. + let fn_modifiers = quote!(#vis #const_).into_iter().map(|mut token| { + token.set_span(span); + token + }); quote_spanned! {span=> #( #docs )* @@ -479,7 +476,7 @@ impl<'a> SettersCtx<'a> { clippy::missing_const_for_fn, )] #[inline(always)] - #fn_modifiers fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type + #(#fn_modifiers)* fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type #where_clause { #body From c507c8ed5c75bc2b55d25f2a776c4dde60a3dc64 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 4 Oct 2025 18:43:05 +0000 Subject: [PATCH 5/8] Add respan for getters --- bon-macros/src/builder/builder_gen/getters.rs | 6 +++-- .../src/builder/builder_gen/input_struct.rs | 2 +- .../src/builder/builder_gen/member/named.rs | 11 ++++++++++ .../src/builder/builder_gen/setters/mod.rs | 22 +++---------------- bon-sandbox/src/attr_getter.rs | 2 +- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/getters.rs b/bon-macros/src/builder/builder_gen/getters.rs index ef099170..8bf89930 100644 --- a/bon-macros/src/builder/builder_gen/getters.rs +++ b/bon-macros/src/builder/builder_gen/getters.rs @@ -54,7 +54,9 @@ impl<'a> GettersCtx<'a> { let state_mod = &self.base.state_mod.ident; let const_ = &self.base.const_; - Ok(quote! { + let fn_modifiers = self.member.respan(quote!(#vis #const_)); + + Ok(quote_spanned! {self.member.span=> #( #docs )* #[allow( // This is intentional. We want the builder syntax to compile away @@ -63,7 +65,7 @@ impl<'a> GettersCtx<'a> { )] #[inline(always)] #[must_use = "this method has no side effects; it only returns a value"] - #vis #const_ fn #name(&self) -> #return_ty + #(#fn_modifiers)* fn #name(&self) -> #return_ty where #state_var::#member_pascal: #state_mod::IsSet, { diff --git a/bon-macros/src/builder/builder_gen/input_struct.rs b/bon-macros/src/builder/builder_gen/input_struct.rs index d394384c..3e86e504 100644 --- a/bon-macros/src/builder/builder_gen/input_struct.rs +++ b/bon-macros/src/builder/builder_gen/input_struct.rs @@ -111,7 +111,7 @@ impl StructInputCtx { attrs: &norm_field.attrs, ident, ty, - span: orig_field.span(), + span: orig_field.ident.span(), }) }) .collect::>>()?; diff --git a/bon-macros/src/builder/builder_gen/member/named.rs b/bon-macros/src/builder/builder_gen/member/named.rs index b93baa4f..c97dbd83 100644 --- a/bon-macros/src/builder/builder_gen/member/named.rs +++ b/bon-macros/src/builder/builder_gen/member/named.rs @@ -5,6 +5,7 @@ use crate::builder::builder_gen::top_level_config::OnConfig; use crate::normalization::SyntaxVariant; use crate::parsing::{ItemSigConfig, SpannedKey}; use crate::util::prelude::*; +use proc_macro2::TokenTree; #[derive(Debug)] pub(crate) struct MemberName { @@ -306,4 +307,14 @@ impl NamedMember { Ok(()) } + + /// Respan the tokens with the member's span. This is important to make + /// rustdoc's source links point to the original member's location. + pub(crate) fn respan(&self, tokens: TokenStream) -> impl Iterator { + let span = self.span; + tokens.into_iter().map(move |mut token| { + token.set_span(span); + token + }) + } } diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index c901dbb1..0eefa2bc 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -445,25 +445,13 @@ impl<'a> SettersCtx<'a> { } }); - let SetterItem { - name, - vis, - docs, - span, - } = item; + let SetterItem { name, vis, docs } = item; let pats = imp.inputs.iter().map(|(pat, _)| pat); let types = imp.inputs.iter().map(|(_, ty)| ty); let const_ = &self.base.const_; + let fn_modifiers = self.member.respan(quote!(#vis #const_)); - // Respan the tokens to make sure the function's signature begins with - // the member's span. This is important to make rustdoc's source links - // point to the original member's location. - let fn_modifiers = quote!(#vis #const_).into_iter().map(|mut token| { - token.set_span(span); - token - }); - - quote_spanned! {span=> + quote_spanned! {self.member.span=> #( #docs )* #[allow( // This is intentional. We want the builder syntax to compile away @@ -517,7 +505,6 @@ struct SetterItem { name: syn::Ident, vis: syn::Visibility, docs: Vec, - span: Span, } impl SettersItems { @@ -545,7 +532,6 @@ impl SettersItems { name: common_name.unwrap_or(&member.name.snake).clone(), vis: common_vis.unwrap_or(&builder_type.vis).clone(), docs, - span: member.span, }); } @@ -633,7 +619,6 @@ impl SettersItems { .clone(), docs: some_fn_docs, - span: member.span, }; let option_fn = config.and_then(|config| config.fns.option_fn.as_deref()); @@ -647,7 +632,6 @@ impl SettersItems { .clone(), docs: option_fn_docs, - span: member.span, }; Self::Optional(OptionalSettersItems { some_fn, option_fn }) diff --git a/bon-sandbox/src/attr_getter.rs b/bon-sandbox/src/attr_getter.rs index 5ac89dcf..2de4bb3f 100644 --- a/bon-sandbox/src/attr_getter.rs +++ b/bon-sandbox/src/attr_getter.rs @@ -10,7 +10,7 @@ pub struct FullName { #[builder(getter)] first_name: String, - #[builder(getter(name = get_the_last_name, vis = "pub(crate)", doc { + #[builder(getter(name = get_the_last_name, doc { /// Docs on the getter }))] last_name: String, From a19a988f2d3f071723eda91f3cd3770d78d6373a Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 4 Oct 2025 18:44:59 +0000 Subject: [PATCH 6/8] Undo test change --- bon-sandbox/src/attr_getter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bon-sandbox/src/attr_getter.rs b/bon-sandbox/src/attr_getter.rs index 2de4bb3f..5ac89dcf 100644 --- a/bon-sandbox/src/attr_getter.rs +++ b/bon-sandbox/src/attr_getter.rs @@ -10,7 +10,7 @@ pub struct FullName { #[builder(getter)] first_name: String, - #[builder(getter(name = get_the_last_name, doc { + #[builder(getter(name = get_the_last_name, vis = "pub(crate)", doc { /// Docs on the getter }))] last_name: String, From a471c1aa9af51bcfc601b3cfb900e3d517a7ea9d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 4 Oct 2025 19:13:39 +0000 Subject: [PATCH 7/8] Fix some breakages --- bon-macros/src/builder/builder_gen/getters.rs | 8 ++++++-- bon-macros/src/builder/builder_gen/mod.rs | 3 +++ bon-macros/src/builder/builder_gen/setters/mod.rs | 7 ++++++- bon-sandbox/src/attr_default.rs | 2 +- bon-sandbox/src/state_mod/comprehensive.rs | 3 +++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/bon-macros/src/builder/builder_gen/getters.rs b/bon-macros/src/builder/builder_gen/getters.rs index 8bf89930..09794902 100644 --- a/bon-macros/src/builder/builder_gen/getters.rs +++ b/bon-macros/src/builder/builder_gen/getters.rs @@ -53,9 +53,13 @@ impl<'a> GettersCtx<'a> { let member_pascal = &self.member.name.pascal; let state_mod = &self.base.state_mod.ident; let const_ = &self.base.const_; - let fn_modifiers = self.member.respan(quote!(#vis #const_)); + // It's important to keep the span of `self` the same across all + // references to it. Otherwise `self`s that have different spans will + // be treated as totally different symbols due to the hygiene rules. + let self_ = quote!(self); + Ok(quote_spanned! {self.member.span=> #( #docs )* #[allow( @@ -65,7 +69,7 @@ impl<'a> GettersCtx<'a> { )] #[inline(always)] #[must_use = "this method has no side effects; it only returns a value"] - #(#fn_modifiers)* fn #name(&self) -> #return_ty + #(#fn_modifiers)* fn #name(&#self_) -> #return_ty where #state_var::#member_pascal: #state_mod::IsSet, { diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index d2fbba5f..082cd175 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -136,6 +136,9 @@ impl BuilderGenCtx { Ok(quote! { #allows + // Ignore dead code warnings because some setter/getter methods may + // not be used + #[allow(dead_code)] #[automatically_derived] impl< #(#generics_decl,)* diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs index 0eefa2bc..987ae277 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -451,6 +451,11 @@ impl<'a> SettersCtx<'a> { let const_ = &self.base.const_; let fn_modifiers = self.member.respan(quote!(#vis #const_)); + // It's important to keep the span of `self` the same across all + // references to it. Otherwise `self`s that have different spans will + // be treated as totally different symbols due to the hygiene rules. + let self_ = quote!(self); + quote_spanned! {self.member.span=> #( #docs )* #[allow( @@ -464,7 +469,7 @@ impl<'a> SettersCtx<'a> { clippy::missing_const_for_fn, )] #[inline(always)] - #(#fn_modifiers)* fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type + #(#fn_modifiers)* fn #name(#maybe_mut #self_, #( #pats: #types ),*) -> #return_type #where_clause { #body diff --git a/bon-sandbox/src/attr_default.rs b/bon-sandbox/src/attr_default.rs index e9c4a13b..9b0bf078 100644 --- a/bon-sandbox/src/attr_default.rs +++ b/bon-sandbox/src/attr_default.rs @@ -28,7 +28,7 @@ pub struct Example { standard_string_default: String, } -struct Point { +pub struct Point { x: u32, y: u32, } diff --git a/bon-sandbox/src/state_mod/comprehensive.rs b/bon-sandbox/src/state_mod/comprehensive.rs index c13181f1..944c3aeb 100644 --- a/bon-sandbox/src/state_mod/comprehensive.rs +++ b/bon-sandbox/src/state_mod/comprehensive.rs @@ -34,6 +34,9 @@ pub struct Example { #[builder(required)] required_option: Option, + /// # Errors + /// + /// Non-integer strings will be rejected. #[builder(with = |x: &str| -> Result<_, std::num::ParseIntError> { x.parse() })] with: u32, } From c7c5c0f725e70529b9c81fc9baf48163a263818b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 4 Oct 2025 19:15:25 +0000 Subject: [PATCH 8/8] Update snapshot --- bon-macros/tests/snapshots/setters_docs_and_vis.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bon-macros/tests/snapshots/setters_docs_and_vis.rs b/bon-macros/tests/snapshots/setters_docs_and_vis.rs index b48321df..465820de 100644 --- a/bon-macros/tests/snapshots/setters_docs_and_vis.rs +++ b/bon-macros/tests/snapshots/setters_docs_and_vis.rs @@ -1,4 +1,5 @@ #[allow(unused_parens)] +#[allow(dead_code)] #[automatically_derived] #[allow(deprecated)] impl SutBuilder {