diff --git a/bon-macros/src/builder/builder_gen/getters.rs b/bon-macros/src/builder/builder_gen/getters.rs index ef099170..09794902 100644 --- a/bon-macros/src/builder/builder_gen/getters.rs +++ b/bon-macros/src/builder/builder_gen/getters.rs @@ -53,8 +53,14 @@ 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_)); - Ok(quote! { + // 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( // This is intentional. We want the builder syntax to compile away @@ -63,7 +69,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_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..3e86e504 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.ident.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..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 { @@ -90,6 +91,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 { @@ -303,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/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 18544cb0..987ae277 100644 --- a/bon-macros/src/builder/builder_gen/setters/mod.rs +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -449,8 +449,14 @@ impl<'a> SettersCtx<'a> { 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_)); - quote! { + // 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( // This is intentional. We want the builder syntax to compile away @@ -463,7 +469,7 @@ impl<'a> SettersCtx<'a> { clippy::missing_const_for_fn, )] #[inline(always)] - #vis #const_ 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-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 { 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 { 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, }