Repository: elastio/bon (Rust)

Issue:
`#[builder(const)]` does not support visibility modifier
The new `const` attribute for `#[builder]` does not seem compatible with a visibility modifier. For example, the following will not compile:

```
#[derive(Builder)]
#[builder(const)]
pub struct Example {
    x: u32,
}
```

with error that begins with
```
error: bug in the `bon` crate: the macro generated code that contains syntax errors; please report this issue at our Github repository: https://github.com/elastio/bon;
       syntax error in generated code: Error(
           "expected identifier or `_`",
       );
```

Removing the `pub` visibility modifier compiles fine, but of course, then `Example` cannot be used outside of the module.

I am using `bon = "3.6.1"`. Rust version:
```
$ rustc --version
rustc 1.88.0-nightly (2da29dbe8 2025-04-14)
```


Discussion:
Hi, thank you for the report! `bon` generated code with `const` and the visibility tokens in wrong order. The fix #288 was merged and released in a new patch `3.6.2`



Patch:
diff --git a/bon-macros/src/builder/builder_gen/getters.rs b/bon-macros/src/builder/builder_gen/getters.rs
index b77623c2..ef099170 100644
--- a/bon-macros/src/builder/builder_gen/getters.rs
+++ b/bon-macros/src/builder/builder_gen/getters.rs
@@ -63,7 +63,7 @@ impl<'a> GettersCtx<'a> {
             )]
             #[inline(always)]
             #[must_use = "this method has no side effects; it only returns a value"]
-            #const_ #vis fn #name(&self) -> #return_ty
+            #vis #const_ fn #name(&self) -> #return_ty
             where
                 #state_var::#member_pascal: #state_mod::IsSet,
             {
diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs
index a68cf427..18544cb0 100644
--- a/bon-macros/src/builder/builder_gen/setters/mod.rs
+++ b/bon-macros/src/builder/builder_gen/setters/mod.rs
@@ -463,7 +463,7 @@ impl<'a> SettersCtx<'a> {
                 clippy::missing_const_for_fn,
             )]
             #[inline(always)]
-            #const_ #vis fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type
+            #vis #const_ fn #name(#maybe_mut self, #( #pats: #types ),*) -> #return_type
             #where_clause
             {
                 #body


Test patch:
diff --git a/bon/tests/integration/builder/attr_const.rs b/bon/tests/integration/builder/attr_const.rs
index 8f391e61..149b7ca8 100644
--- a/bon/tests/integration/builder/attr_const.rs
+++ b/bon/tests/integration/builder/attr_const.rs
@@ -1,104 +1,66 @@
 #[rustversion::since(1.61.0)]
 
 mod msrv_1_61 {
-    use crate::prelude::*;
-
-    #[test]
-    const fn test_struct() {
-        #[derive(Builder)]
-        #[builder(const)]
-        struct Sut {
-            #[builder(start_fn)]
-            x1: u32,
-
-            #[builder(finish_fn)]
-            x2: u32,
-
-            x3: u32,
-
-            x4: Option<u32>,
-
-            #[builder(default = x1 + 99)]
-            x5: u32,
-
-            #[builder(with = |a: u32, b: u32| a + b)]
-            x6: u32,
-
-            #[builder(with = |a: u32, b: u32| -> Result<_, ()> { Ok(a + b) })]
-            x7: Option<u32>,
-
-            #[builder(skip = 10)]
-            x8: u32,
-            //
-            // This doesn't work because Rust complains about this in setters that
-            // consume `self` and return a new instance of the builder:
-            // ```
-            // destructor of `builder::attr_const::test_struct::SutBuilder<S>`
-            // cannot be evaluated at compile-time
-            // ```
-            // x7: Vec<String>,
-            // x8: String,
-        }
 
-        const ACTUAL: Sut = Sut::builder(1).x3(2).x4(3).x6(4, 5).build(6);
-
-        #[allow(clippy::assertions_on_constants)]
-        {
-            assert!(ACTUAL.x1 == 1);
-            assert!(ACTUAL.x2 == 6);
-            assert!(ACTUAL.x3 == 2);
-            assert!(matches!(ACTUAL.x4, Some(3)));
-            assert!(ACTUAL.x5 == 100);
-            assert!(ACTUAL.x6 == 9);
-            assert!(ACTUAL.x7.is_none());
-            assert!(ACTUAL.x8 == 10);
-        }
-    }
-    #[test]
-    const fn test_function() {
-        type Output = (u32, u32, u32, Option<u32>, u32, u32, Option<u32>);
+    mod smoke {
 
-        #[builder(const)]
-        const fn sut(
-            #[builder(start_fn)] x1: u32,
+        use crate::prelude::*;
 
-            #[builder(finish_fn)] x2: u32,
+        #[test]
+        const fn test_struct() {
+            #[derive(Builder)]
+            #[builder(const)]
+            struct Sut {
+                #[builder(start_fn)]
+                x1: u32,
 
-            x3: u32,
+                #[builder(finish_fn)]
+                x2: u32,
 
-            x4: Option<u32>,
+                x3: u32,
 
-            #[builder(default = x1 + 99)] x5: u32,
+                x4: Option<u32>,
 
-            #[builder(with = |a: u32, b: u32| a + b)] x6: u32,
+                #[builder(default = x1 + 99)]
+                x5: u32,
+
+                #[builder(with = |a: u32, b: u32| a + b)]
+                x6: u32,
+
+                #[builder(with = |a: u32, b: u32| -> Result<_, ()> { Ok(a + b) })]
+                x7: Option<u32>,
+
+                #[builder(skip = 10)]
+                x8: u32,
+                //
+                // This doesn't work because Rust complains about this in setters that
+                // consume `self` and return a new instance of the builder:
+                // ```
+                // destructor of `builder::attr_const::test_struct::SutBuilder<S>`
+                // cannot be evaluated at compile-time
+                // ```
+                // x7: Vec<String>,
+                // x8: String,
+            }
 
-            #[builder(with = |a: u32, b: u32| -> Result<_, ()> { Ok(a + b) })] x7: Option<u32>,
-        ) -> Output {
-            (x1, x2, x3, x4, x5, x6, x7)
+            const ACTUAL: Sut = Sut::builder(1).x3(2).x4(3).x6(4, 5).build(6);
+
+            #[allow(clippy::assertions_on_constants)]
+            {
+                assert!(ACTUAL.x1 == 1);
+                assert!(ACTUAL.x2 == 6);
+                assert!(ACTUAL.x3 == 2);
+                assert!(matches!(ACTUAL.x4, Some(3)));
+                assert!(ACTUAL.x5 == 100);
+                assert!(ACTUAL.x6 == 9);
+                assert!(ACTUAL.x7.is_none());
+                assert!(ACTUAL.x8 == 10);
+            }
         }
+        #[test]
+        const fn test_function() {
+            type Output = (u32, u32, u32, Option<u32>, u32, u32, Option<u32>);
 
-        const ACTUAL: Output = sut(1).x3(2).x4(3).x6(4, 5).call(6);
-
-        #[allow(clippy::assertions_on_constants)]
-        {
-            assert!(ACTUAL.0 == 1);
-            assert!(ACTUAL.1 == 6);
-            assert!(ACTUAL.2 == 2);
-            assert!(matches!(ACTUAL.3, Some(3)));
-            assert!(ACTUAL.4 == 100);
-            assert!(ACTUAL.5 == 9);
-            assert!(ACTUAL.6.is_none());
-        }
-    }
-
-    #[test]
-    const fn test_method() {
-        type Output = (u32, u32, u32, Option<u32>, u32, u32, Option<u32>);
-
-        struct Sut;
-
-        #[bon]
-        impl Sut {
             #[builder(const)]
             const fn sut(
                 #[builder(start_fn)] x1: u32,
@@ -117,19 +79,89 @@ mod msrv_1_61 {
             ) -> Output {
                 (x1, x2, x3, x4, x5, x6, x7)
             }
+
+            const ACTUAL: Output = sut(1).x3(2).x4(3).x6(4, 5).call(6);
+
+            #[allow(clippy::assertions_on_constants)]
+            {
+                assert!(ACTUAL.0 == 1);
+                assert!(ACTUAL.1 == 6);
+                assert!(ACTUAL.2 == 2);
+                assert!(matches!(ACTUAL.3, Some(3)));
+                assert!(ACTUAL.4 == 100);
+                assert!(ACTUAL.5 == 9);
+                assert!(ACTUAL.6.is_none());
+            }
         }
 
-        const ACTUAL: Output = Sut::sut(1).x3(2).x4(3).x6(4, 5).call(6);
-
-        #[allow(clippy::assertions_on_constants)]
-        {
-            assert!(ACTUAL.0 == 1);
-            assert!(ACTUAL.1 == 6);
-            assert!(ACTUAL.2 == 2);
-            assert!(matches!(ACTUAL.3, Some(3)));
-            assert!(ACTUAL.4 == 100);
-            assert!(ACTUAL.5 == 9);
-            assert!(ACTUAL.6.is_none());
+        #[test]
+        const fn test_method() {
+            type Output = (u32, u32, u32, Option<u32>, u32, u32, Option<u32>);
+
+            struct Sut;
+
+            #[bon]
+            impl Sut {
+                #[builder(const)]
+                const fn sut(
+                    #[builder(start_fn)] x1: u32,
+
+                    #[builder(finish_fn)] x2: u32,
+
+                    x3: u32,
+
+                    x4: Option<u32>,
+
+                    #[builder(default = x1 + 99)] x5: u32,
+
+                    #[builder(with = |a: u32, b: u32| a + b)] x6: u32,
+
+                    #[builder(with = |a: u32, b: u32| -> Result<_, ()> { Ok(a + b) })] x7: Option<
+                        u32,
+                    >,
+                ) -> Output {
+                    (x1, x2, x3, x4, x5, x6, x7)
+                }
+            }
+
+            const ACTUAL: Output = Sut::sut(1).x3(2).x4(3).x6(4, 5).call(6);
+
+            #[allow(clippy::assertions_on_constants)]
+            {
+                assert!(ACTUAL.0 == 1);
+                assert!(ACTUAL.1 == 6);
+                assert!(ACTUAL.2 == 2);
+                assert!(matches!(ACTUAL.3, Some(3)));
+                assert!(ACTUAL.4 == 100);
+                assert!(ACTUAL.5 == 9);
+                assert!(ACTUAL.6.is_none());
+            }
+        }
+    }
+
+    // Tests for the following bug: https://github.com/elastio/bon/issues/287
+    mod visibility {
+        use crate::prelude::*;
+
+        #[test]
+        const fn test_function() {
+            #[builder(const)]
+            pub const fn sut(#[builder(getter)] _x: u32) {}
+
+            sut().x(1).call();
+        }
+
+        #[test]
+        const fn test_method() {
+            struct Sut;
+
+            #[bon]
+            impl Sut {
+                #[builder(const)]
+                pub const fn sut(#[builder(getter)] _x: u32) {}
+            }
+
+            Sut::sut().x(1).call();
         }
     }
 }