From aed714815be5affea9a89a15ea728f4e1ab7e283 Mon Sep 17 00:00:00 2001 From: Vitalii Kryvenko Date: Sun, 20 Oct 2024 03:50:39 +0300 Subject: [PATCH] Flexible builder extension and type signature API (#145) --- .github/workflows/ci.yml | 44 +- Cargo.lock | 114 +- Cargo.toml | 17 +- benchmarks/compilation/Cargo.toml | 28 + benchmarks/compilation/README.md | 25 + benchmarks/compilation/codegen/Cargo.toml | 16 + benchmarks/compilation/codegen/src/main.rs | 64 + benchmarks/compilation/results.md | 12 + benchmarks/compilation/run.sh | 25 + benchmarks/compilation/src/lib.rs | 22 + .../compilation/src/structs_100_fields_10.rs | 1700 +++++++++++++++++ .../compilation/src/structs_10_fields_50.rs | 570 ++++++ benchmarks/run.sh | 24 - benchmarks/{ => runtime}/Cargo.toml | 6 +- benchmarks/{ => runtime}/README.md | 6 +- benchmarks/{ => runtime}/benches/criterion.rs | 6 +- benchmarks/{ => runtime}/benches/iai.rs | 2 +- benchmarks/runtime/run.sh | 22 + benchmarks/{ => runtime}/src/args_10.rs | 0 benchmarks/{ => runtime}/src/args_10_alloc.rs | 0 .../{ => runtime}/src/args_10_structs.rs | 0 benchmarks/{ => runtime}/src/args_20.rs | 0 benchmarks/{ => runtime}/src/args_3.rs | 0 benchmarks/{ => runtime}/src/args_5.rs | 0 benchmarks/{ => runtime}/src/lib.rs | 0 bon-macros/Cargo.toml | 18 +- bon-macros/src/bon.rs | 20 +- .../builder/builder_gen/builder_derives.rs | 209 +- .../src/builder/builder_gen/builder_params.rs | 223 --- .../builder/builder_gen/builder_params/mod.rs | 87 + .../builder_gen/builder_params/on_params.rs | 103 + .../src/builder/builder_gen/finish_fn.rs | 150 ++ .../{input_func.rs => input_fn.rs} | 317 +-- .../src/builder/builder_gen/input_struct.rs | 200 +- .../builder_gen/member/into_conversion.rs | 99 +- .../src/builder/builder_gen/member/mod.rs | 218 +-- .../src/builder/builder_gen/member/named.rs | 278 +++ .../builder_gen/member/params/blanket.rs | 81 + .../member/{params.rs => params/mod.rs} | 120 +- .../member/params/setter_closure.rs | 120 ++ .../builder_gen/member/params/setters.rs | 48 + bon-macros/src/builder/builder_gen/mod.rs | 780 ++------ bon-macros/src/builder/builder_gen/models.rs | 447 +++++ .../src/builder/builder_gen/setter_methods.rs | 242 --- .../src/builder/builder_gen/setters/mod.rs | 534 ++++++ .../src/builder/builder_gen/state_mod.rs | 280 +++ bon-macros/src/builder/item_fn.rs | 46 + bon-macros/src/builder/item_func.rs | 37 - bon-macros/src/builder/item_impl.rs | 68 +- bon-macros/src/builder/item_struct.rs | 9 +- bon-macros/src/builder/mod.rs | 53 +- bon-macros/src/collections/map.rs | 3 +- bon-macros/src/collections/set.rs | 3 +- bon-macros/src/error.rs | 108 +- bon-macros/src/lib.rs | 47 +- bon-macros/src/normalization/cfg/mod.rs | 15 +- bon-macros/src/normalization/cfg/parse.rs | 8 +- .../src/normalization/generics_namespace.rs | 75 + bon-macros/src/normalization/impl_traits.rs | 24 +- bon-macros/src/normalization/lifetimes.rs | 59 +- bon-macros/src/normalization/mod.rs | 10 +- .../src/normalization/syntax_variant.rs | 28 + bon-macros/src/parsing/docs.rs | 69 + bon-macros/src/parsing/item_params.rs | 71 + bon-macros/src/parsing/mod.rs | 83 + bon-macros/src/parsing/simple_closure.rs | 97 + bon-macros/src/parsing/spanned_key.rs | 47 + bon-macros/src/tests/attr_setters.rs | 158 ++ bon-macros/src/tests/mod.rs | 19 + bon-macros/src/tests/syntax_errors.rs | 60 + bon-macros/src/util/attrs.rs | 10 +- bon-macros/src/util/generic_param.rs | 19 + bon-macros/src/util/ide.rs | 9 +- bon-macros/src/util/ident.rs | 32 +- bon-macros/src/util/meta_list.rs | 66 + bon-macros/src/util/mod.rs | 10 +- bon-macros/src/util/ty/match_types.rs | 16 +- bon-macros/src/util/ty/mod.rs | 91 +- bon-macros/src/util/visibility.rs | 131 ++ .../tests/snapshots/bon_incomplete_if.rs | 1 + .../tests/snapshots/setters_docs_and_vis.rs | 222 +++ bon/Cargo.toml | 42 +- bon/src/builder_state.rs | 38 + bon/src/collections.rs | 151 ++ bon/src/lib.rs | 174 +- bon/src/private/cfg_eval.rs | 137 ++ bon/src/private/deprecations.rs | 6 - bon/src/private/derives.rs | 21 + bon/src/private/mod.rs | 248 +-- bon/tests/integration/builder/attr_default.rs | 10 +- bon/tests/integration/builder/attr_derive.rs | 350 ++++ .../builder/attr_expose_positional_fn.rs | 3 +- bon/tests/integration/builder/attr_into.rs | 12 +- .../integration/builder/attr_overwritable.rs | 103 + bon/tests/integration/builder/attr_setters.rs | 436 +++++ .../integration/builder/attr_transparent.rs | 127 ++ .../integration/builder/attr_with/mod.rs | 11 + .../builder/attr_with/multi_arg.rs | 115 ++ .../builder/attr_with/overwritable.rs | 119 ++ .../builder/attr_with/single_arg.rs | 459 +++++ .../integration/builder/builder_derives.rs | 236 --- bon/tests/integration/builder/cfgs.rs | 18 +- bon/tests/integration/builder/init_order.rs | 3 +- bon/tests/integration/builder/legacy.rs | 40 - bon/tests/integration/builder/mod.rs | 16 +- .../builder/name_conflicts/builder_state.rs | 335 ++++ .../builder/name_conflicts/generics.rs | 114 ++ .../member_and_type_named_the_same.rs} | 1 - .../name_conflicts/member_names_state.rs | 45 + .../integration/builder/name_conflicts/mod.rs | 4 + .../integration/builder/positional_members.rs | 34 +- bon/tests/integration/builder/raw_idents.rs | 13 +- bon/tests/integration/main.rs | 1 + .../integration/ui/compile_fail/attr_bon.rs | 20 + .../ui/compile_fail/attr_bon.stderr | 11 + .../ui/compile_fail/attr_builder.rs | 69 + .../ui/compile_fail/attr_builder.stderr | 85 + .../ui/compile_fail/attr_derive.rs | 69 + .../ui/compile_fail/attr_derive.stderr | 454 +++++ .../integration/ui/compile_fail/attr_into.rs | 6 + .../ui/compile_fail/attr_into.stderr | 5 + .../integration/ui/compile_fail/attr_on.rs | 27 + .../ui/compile_fail/attr_on.stderr | 51 + .../ui/compile_fail/attr_setters.rs | 75 + .../ui/compile_fail/attr_setters.stderr | 53 + .../integration/ui/compile_fail/attr_skip.rs | 40 + .../ui/compile_fail/attr_skip.stderr | 53 + .../ui/compile_fail/attr_start_finish_fn.rs | 11 + .../compile_fail/attr_start_finish_fn.stderr | 11 + .../ui/compile_fail/attr_transparent.rs | 49 + .../ui/compile_fail/attr_transparent.stderr | 59 + .../integration/ui/compile_fail/attr_with.rs | 84 + .../ui/compile_fail/attr_with.stderr | 179 ++ .../ui/compile_fail/broken_intra_doc_links.rs | 74 + .../broken_intra_doc_links.stderr | 53 + .../ui/compile_fail/builder_derives.rs | 21 - .../ui/compile_fail/builder_derives.stderr | 107 -- .../ui/compile_fail/collections.rs | 10 + .../ui/compile_fail/collections.stderr | 23 + .../ui/compile_fail/derive_builder.rs | 12 + .../ui/compile_fail/derive_builder.stderr | 19 + .../diagnostic_on_unimplemented.rs | 18 + .../diagnostic_on_unimplemented.stderr | 61 + .../integration/ui/compile_fail/errors.rs | 132 -- .../integration/ui/compile_fail/errors.stderr | 264 --- .../ui/compile_fail/name_conflicts.rs | 21 + .../ui/compile_fail/name_conflicts.stderr | 30 + .../ui/compile_fail/positional_members.stderr | 16 +- .../ui/compile_fail/warnings.stderr | 8 +- .../ui/compile_fail/wrong_delimiters.rs | 58 + .../ui/compile_fail/wrong_delimiters.stderr | 107 ++ bon/tests/integration/ui/mod.rs | 2 +- e2e-tests/src/attr_with.rs | 16 + e2e-tests/src/lib.rs | 43 +- e2e-tests/src/reexports.rs | 13 + e2e-tests/src/state_mod_pub.rs | 25 + rust-toolchain | 2 +- scripts/install/hyperfine.sh | 17 + scripts/test-msrv.sh | 3 +- website/.vitepress/config.mts | 10 +- website/.vitepress/theme/utils/versioning.ts | 2 +- .../blog/bon-builder-generator-v2-release.md | 6 +- website/blog/bon-builder-v2-1-release.md | 2 +- website/changelog.md | 58 + website/guide/overview.md | 2 +- website/guide/patterns/fallible-builders.md | 4 +- website/guide/positional-members.md | 12 +- website/reference/builder.md | 57 +- 168 files changed, 12025 insertions(+), 3197 deletions(-) create mode 100644 benchmarks/compilation/Cargo.toml create mode 100644 benchmarks/compilation/README.md create mode 100644 benchmarks/compilation/codegen/Cargo.toml create mode 100644 benchmarks/compilation/codegen/src/main.rs create mode 100644 benchmarks/compilation/results.md create mode 100755 benchmarks/compilation/run.sh create mode 100644 benchmarks/compilation/src/lib.rs create mode 100644 benchmarks/compilation/src/structs_100_fields_10.rs create mode 100644 benchmarks/compilation/src/structs_10_fields_50.rs delete mode 100755 benchmarks/run.sh rename benchmarks/{ => runtime}/Cargo.toml (74%) rename benchmarks/{ => runtime}/README.md (82%) rename benchmarks/{ => runtime}/benches/criterion.rs (77%) rename benchmarks/{ => runtime}/benches/iai.rs (54%) create mode 100755 benchmarks/runtime/run.sh rename benchmarks/{ => runtime}/src/args_10.rs (100%) rename benchmarks/{ => runtime}/src/args_10_alloc.rs (100%) rename benchmarks/{ => runtime}/src/args_10_structs.rs (100%) rename benchmarks/{ => runtime}/src/args_20.rs (100%) rename benchmarks/{ => runtime}/src/args_3.rs (100%) rename benchmarks/{ => runtime}/src/args_5.rs (100%) rename benchmarks/{ => runtime}/src/lib.rs (100%) delete mode 100644 bon-macros/src/builder/builder_gen/builder_params.rs create mode 100644 bon-macros/src/builder/builder_gen/builder_params/mod.rs create mode 100644 bon-macros/src/builder/builder_gen/builder_params/on_params.rs create mode 100644 bon-macros/src/builder/builder_gen/finish_fn.rs rename bon-macros/src/builder/builder_gen/{input_func.rs => input_fn.rs} (60%) create mode 100644 bon-macros/src/builder/builder_gen/member/named.rs create mode 100644 bon-macros/src/builder/builder_gen/member/params/blanket.rs rename bon-macros/src/builder/builder_gen/member/{params.rs => params/mod.rs} (53%) create mode 100644 bon-macros/src/builder/builder_gen/member/params/setter_closure.rs create mode 100644 bon-macros/src/builder/builder_gen/member/params/setters.rs create mode 100644 bon-macros/src/builder/builder_gen/models.rs delete mode 100644 bon-macros/src/builder/builder_gen/setter_methods.rs create mode 100644 bon-macros/src/builder/builder_gen/setters/mod.rs create mode 100644 bon-macros/src/builder/builder_gen/state_mod.rs create mode 100644 bon-macros/src/builder/item_fn.rs delete mode 100644 bon-macros/src/builder/item_func.rs create mode 100644 bon-macros/src/normalization/generics_namespace.rs create mode 100644 bon-macros/src/normalization/syntax_variant.rs create mode 100644 bon-macros/src/parsing/docs.rs create mode 100644 bon-macros/src/parsing/item_params.rs create mode 100644 bon-macros/src/parsing/mod.rs create mode 100644 bon-macros/src/parsing/simple_closure.rs create mode 100644 bon-macros/src/parsing/spanned_key.rs create mode 100644 bon-macros/src/tests/attr_setters.rs create mode 100644 bon-macros/src/tests/mod.rs create mode 100644 bon-macros/src/tests/syntax_errors.rs create mode 100644 bon-macros/src/util/generic_param.rs create mode 100644 bon-macros/src/util/meta_list.rs create mode 100644 bon-macros/src/util/visibility.rs create mode 100644 bon-macros/tests/snapshots/bon_incomplete_if.rs create mode 100644 bon-macros/tests/snapshots/setters_docs_and_vis.rs create mode 100644 bon/src/builder_state.rs create mode 100644 bon/src/collections.rs create mode 100644 bon/src/private/cfg_eval.rs delete mode 100644 bon/src/private/deprecations.rs create mode 100644 bon/src/private/derives.rs create mode 100644 bon/tests/integration/builder/attr_derive.rs create mode 100644 bon/tests/integration/builder/attr_overwritable.rs create mode 100644 bon/tests/integration/builder/attr_setters.rs create mode 100644 bon/tests/integration/builder/attr_transparent.rs create mode 100644 bon/tests/integration/builder/attr_with/mod.rs create mode 100644 bon/tests/integration/builder/attr_with/multi_arg.rs create mode 100644 bon/tests/integration/builder/attr_with/overwritable.rs create mode 100644 bon/tests/integration/builder/attr_with/single_arg.rs delete mode 100644 bon/tests/integration/builder/builder_derives.rs delete mode 100644 bon/tests/integration/builder/legacy.rs create mode 100644 bon/tests/integration/builder/name_conflicts/builder_state.rs create mode 100644 bon/tests/integration/builder/name_conflicts/generics.rs rename bon/tests/integration/builder/{name_conflicts.rs => name_conflicts/member_and_type_named_the_same.rs} (99%) create mode 100644 bon/tests/integration/builder/name_conflicts/member_names_state.rs create mode 100644 bon/tests/integration/builder/name_conflicts/mod.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_bon.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_bon.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_builder.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_builder.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_derive.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_derive.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_into.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_into.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_on.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_on.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_setters.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_setters.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_skip.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_skip.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_transparent.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_transparent.stderr create mode 100644 bon/tests/integration/ui/compile_fail/attr_with.rs create mode 100644 bon/tests/integration/ui/compile_fail/attr_with.stderr create mode 100644 bon/tests/integration/ui/compile_fail/broken_intra_doc_links.rs create mode 100644 bon/tests/integration/ui/compile_fail/broken_intra_doc_links.stderr delete mode 100644 bon/tests/integration/ui/compile_fail/builder_derives.rs delete mode 100644 bon/tests/integration/ui/compile_fail/builder_derives.stderr create mode 100644 bon/tests/integration/ui/compile_fail/collections.rs create mode 100644 bon/tests/integration/ui/compile_fail/collections.stderr create mode 100644 bon/tests/integration/ui/compile_fail/derive_builder.rs create mode 100644 bon/tests/integration/ui/compile_fail/derive_builder.stderr create mode 100644 bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs create mode 100644 bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.stderr delete mode 100644 bon/tests/integration/ui/compile_fail/errors.rs delete mode 100644 bon/tests/integration/ui/compile_fail/errors.stderr create mode 100644 bon/tests/integration/ui/compile_fail/name_conflicts.rs create mode 100644 bon/tests/integration/ui/compile_fail/name_conflicts.stderr create mode 100644 bon/tests/integration/ui/compile_fail/wrong_delimiters.rs create mode 100644 bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr create mode 100644 e2e-tests/src/attr_with.rs create mode 100644 e2e-tests/src/reexports.rs create mode 100644 e2e-tests/src/state_mod_pub.rs create mode 100755 scripts/install/hyperfine.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d609688..e0f87940 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ env: jobs: # Sanity-check that benchmarks work - benchmarks: + runtime-benchmarks: runs-on: ubuntu-latest strategy: matrix: @@ -41,7 +41,16 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 - run: sudo apt-get install -y valgrind - - run: ./benchmarks/run.sh ${{ matrix.benchmark }} + - run: cd ./benchmarks/runtime && ./run.sh ${{ matrix.benchmark }} + + compilation-benchmarks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: ./scripts/install/hyperfine.sh + + - run: cd ./benchmarks/compilation && ./run.sh cargo-lock: runs-on: ubuntu-latest @@ -75,6 +84,12 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # rust-src is required to make sure compile errors on CI are rendered + # the same as locally. The `rust-src` component is installed by default + # locally, and with its presence compile error messages can show snippets + # of rust standard library code. + components: rust-src - run: cargo clippy --all-features --all-targets --locked @@ -82,6 +97,8 @@ jobs: - run: cargo test --locked --all-features --doc - run: cd bon && cargo test --locked --no-default-features --features= - run: cd bon && cargo test --locked --no-default-features --features=alloc + - run: cd bon && cargo test --locked --no-default-features --features=implied-bounds + - run: cd bon && cargo test --locked --no-default-features --features=alloc,implied-bounds test-msrv: runs-on: ${{ matrix.os }}-latest @@ -119,13 +136,28 @@ jobs: toolchain: ${{ matrix.toolchain }} components: clippy - - run: cargo +${{ matrix.toolchain }} clippy --all-features --all-targets --locked + - run: | + cargo +${{ matrix.toolchain }} clippy --all-features --all-targets --locked \ + -- \ + --allow edition-2024-expr-fragment-specifier \ + --allow impl-trait-overcaptures + + cargo-miri: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly-2024-10-14 + components: miri + + - run: | + cargo miri test --locked --all-features --all-targets \ + --workspace --exclude runtime-benchmarks env: - # There is no need for us to stick with the edition 2021 meaning for - # the `expr` fragment in macro rules. RUSTFLAGS: >- --deny warnings - --allow unknown-lints --allow edition-2024-expr-fragment-specifier --allow impl-trait-overcaptures diff --git a/Cargo.lock b/Cargo.lock index e803efcc..8774fd16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,16 +74,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "benchmarks" -version = "0.1.0" -dependencies = [ - "bon", - "cfg-if", - "criterion", - "iai", -] - [[package]] name = "bon" version = "2.3.0" @@ -110,7 +100,9 @@ name = "bon-macros" version = "2.3.0" dependencies = [ "darling", + "expect-test", "ident_case", + "prettyplease", "proc-macro2", "quote", "syn", @@ -207,6 +199,25 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "compilation-benchmarks" +version = "0.1.0" +dependencies = [ + "bon", + "cfg-if", + "derive_builder", + "typed-builder", +] + +[[package]] +name = "compilation-benchmarks-codegen" +version = "0.1.0" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", +] + [[package]] name = "countme" version = "3.0.1" @@ -330,6 +341,37 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "dissimilar" version = "1.0.9" @@ -658,11 +700,21 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -812,6 +864,16 @@ dependencies = [ "text-size", ] +[[package]] +name = "runtime-benchmarks" +version = "0.1.0" +dependencies = [ + "bon", + "cfg-if", + "criterion", + "iai", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -912,9 +974,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.72" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -1081,9 +1143,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1e5645f2ee8025c2f1d75e1138f2dd034d74e6ba54620f3c569ba2a2a1ea06" +checksum = "207aa50d36c4be8d8c6ea829478be44a372c6a77669937bb39c698e52f1491e8" dependencies = [ "glob", "serde", @@ -1093,6 +1155,26 @@ dependencies = [ "toml", ] +[[package]] +name = "typed-builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e14ed59dc8b7b26cacb2a92bad2e8b1f098806063898ab42a3bd121d7d45e75" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index f680af81..29da93e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,13 @@ [workspace] -members = ["benchmarks", "bon", "bon-cli", "bon-macros", "e2e-tests"] +members = [ + "benchmarks/compilation", + "benchmarks/compilation/codegen", + "benchmarks/runtime", + "bon", + "bon-cli", + "bon-macros", + "e2e-tests", +] resolver = "2" # This config is used for the benchmarks @@ -130,8 +138,7 @@ wildcard_dependencies = "warn" zero_sized_map_values = "warn" # Priorities are used not because we override lints from these lint groups -# but just to order them in order from the less noisy to the more noisy in -# the output +# but just to order them from the less noisy to the more noisy in the output nursery = { level = "warn", priority = -2 } pedantic = { level = "warn", priority = -1 } @@ -176,5 +183,5 @@ rust_2024_compatibility = { level = "warn", priority = 1 } # [workspace.lints.rust] # must_not_suspend = "warn" # rust_2024_incompatible_pat = "warn" -# lossy_provenance_casts = "warn" -# fuzzy_provenance_casts = "warn" +# lossy_provenance_casts = "warn" +# fuzzy_provenance_casts = "warn" diff --git a/benchmarks/compilation/Cargo.toml b/benchmarks/compilation/Cargo.toml new file mode 100644 index 00000000..63579d58 --- /dev/null +++ b/benchmarks/compilation/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "compilation-benchmarks" + +publish = false + +description = """ + Crate for benchmarking of the compilation time of code generated by + proc-macros in the `bon` crate and other alternatives. +""" + +edition = "2021" +version = "0.1.0" + +[dependencies] +bon = { path = "../../bon", optional = true } +cfg-if = "1.0" +derive_builder = { version = "0.20", optional = true } +typed-builder = { version = "0.20", optional = true } + +[lints] +workspace = true + +[features] +bon-overwritable = ["bon"] +default = [] + +structs_100_fields_10 = [] +structs_10_fields_50 = [] diff --git a/benchmarks/compilation/README.md b/benchmarks/compilation/README.md new file mode 100644 index 00000000..085c455e --- /dev/null +++ b/benchmarks/compilation/README.md @@ -0,0 +1,25 @@ +# Benchmarks + +This is a collection of compilation time benchmarks for the code generated by `bon` crate and some other alternative builder macro crates. + +## Dependencies + +If you'd like to run the benchmarks yourself, first you need to install the following: + +- [`hyperfine`](https://github.com/sharkdp/hyperfine) CLI + +If you are on Linux, just run the following commands to install the dependencies: + +```bash +./scripts/install/hyperfine.sh +``` + +## Running the benchmarks + +Once you have all the [dependencies](#dependencies) installed you can run the selected benchmark from this directory like this: + +```bash +./run.sh {benchmark_name} +``` + +The `{benchmark_name}` corresponds to the modules in this crate. diff --git a/benchmarks/compilation/codegen/Cargo.toml b/benchmarks/compilation/codegen/Cargo.toml new file mode 100644 index 00000000..7d305a8a --- /dev/null +++ b/benchmarks/compilation/codegen/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "compilation-benchmarks-codegen" + +publish = false + +description = """ + Generates code for the compilation benchmarks. +""" + +edition = "2021" +version = "0.1.0" + +[dependencies] +anyhow = "1.0" +proc-macro2 = "1.0" +quote = "1.0" diff --git a/benchmarks/compilation/codegen/src/main.rs b/benchmarks/compilation/codegen/src/main.rs new file mode 100644 index 00000000..084b4411 --- /dev/null +++ b/benchmarks/compilation/codegen/src/main.rs @@ -0,0 +1,64 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::path::PathBuf; + +fn main() -> anyhow::Result<()> { + let bench_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?) + .parent() + .unwrap() + .to_path_buf(); + + let src_dir = bench_dir.join("src"); + + let structs_number = 100; + let fields_number = 10; + + std::fs::write( + src_dir.join(format!( + "structs_{structs_number}_fields_{fields_number}.rs" + )), + structs_n_fields_n(structs_number, fields_number).to_string(), + )?; + + println!("Running cargo fmt in {}", bench_dir.display()); + + let status = std::process::Command::new("cargo") + .arg("fmt") + .current_dir(&bench_dir) + .spawn()? + .wait()?; + + anyhow::ensure!(status.success(), "cargo fmt failed: {status}"); + + Ok(()) +} + +fn structs_n_fields_n(structs_number: usize, fields_number: usize) -> TokenStream { + let field_names = (1..=fields_number) + .map(|i| format_ident!("x{i}")) + .collect::>(); + + (1..=structs_number) + .map(move |i| { + let struct_name = format_ident!("Struct{i}"); + + quote! { + #[cfg_attr( + any( + feature = "bon", + feature = "typed-builder", + feature = "derive_builder", + ), + derive(crate::Builder), + )] + #[cfg_attr( + feature = "bon-overwritable", + builder(on(_, overwritable)), + )] + pub struct #struct_name { + #( #field_names: i32, )* + } + } + }) + .collect() +} diff --git a/benchmarks/compilation/results.md b/benchmarks/compilation/results.md new file mode 100644 index 00000000..a9a5ad25 --- /dev/null +++ b/benchmarks/compilation/results.md @@ -0,0 +1,12 @@ +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `structs_100_fields_10 bon` | 2.319 ± 0.021 | 2.278 | 2.350 | 22.80 ± 2.99 | +| `structs_100_fields_10 bon-overwritable` | 2.240 ± 0.021 | 2.203 | 2.274 | 22.03 ± 2.89 | +| `structs_100_fields_10 typed-builder` | 1.849 ± 0.011 | 1.838 | 1.878 | 18.18 ± 2.38 | +| `structs_100_fields_10 derive_builder` | 1.022 ± 0.016 | 0.995 | 1.043 | 10.05 ± 1.32 | +| `structs_100_fields_10 ` | 0.104 ± 0.012 | 0.088 | 0.124 | 1.02 ± 0.18 | +| `structs_10_fields_50 bon` | 2.063 ± 0.017 | 2.044 | 2.100 | 20.28 ± 2.66 | +| `structs_10_fields_50 bon-overwritable` | 2.029 ± 0.029 | 1.998 | 2.102 | 19.95 ± 2.62 | +| `structs_10_fields_50 typed-builder` | 2.076 ± 0.016 | 2.048 | 2.101 | 20.41 ± 2.67 | +| `structs_10_fields_50 derive_builder` | 0.432 ± 0.016 | 0.400 | 0.458 | 4.25 ± 0.58 | +| `structs_10_fields_50 ` | 0.102 ± 0.013 | 0.084 | 0.130 | 1.00 | diff --git a/benchmarks/compilation/run.sh b/benchmarks/compilation/run.sh new file mode 100755 index 00000000..74d5dc8d --- /dev/null +++ b/benchmarks/compilation/run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +macros=( + bon + bon-overwritable + typed-builder + derive_builder +) + +suites=( + structs_100_fields_10 + structs_10_fields_50 +) + +hyperfine \ + --setup 'cargo build -p compilation-benchmarks --features={suite},{macro}' \ + --prepare 'cargo clean -p compilation-benchmarks' \ + --shell=none \ + --export-markdown results.md \ + --parameter-list macro "$(IFS=, ; echo "${macros[*]}")," \ + --parameter-list suite "$(IFS=, ; echo "${suites[*]}")" \ + --command-name '{suite} {macro}' \ + 'cargo build -p compilation-benchmarks --features={suite},{macro}' diff --git a/benchmarks/compilation/src/lib.rs b/benchmarks/compilation/src/lib.rs new file mode 100644 index 00000000..7674dd09 --- /dev/null +++ b/benchmarks/compilation/src/lib.rs @@ -0,0 +1,22 @@ +#![allow( + missing_docs, + missing_debug_implementations, + dead_code, + rustdoc::missing_crate_level_docs +)] + +cfg_if::cfg_if! { + if #[cfg(feature = "bon")] { + use bon::Builder; + } else if #[cfg(feature = "typed-builder")] { + use typed_builder::TypedBuilder as Builder; + } else if #[cfg(feature = "derive_builder")] { + use derive_builder::Builder; + } +} + +#[cfg(feature = "structs_100_fields_10")] +pub mod structs_100_fields_10; + +#[cfg(feature = "structs_10_fields_50")] +pub mod structs_10_fields_50; diff --git a/benchmarks/compilation/src/structs_100_fields_10.rs b/benchmarks/compilation/src/structs_100_fields_10.rs new file mode 100644 index 00000000..a7e1992b --- /dev/null +++ b/benchmarks/compilation/src/structs_100_fields_10.rs @@ -0,0 +1,1700 @@ +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct1 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct2 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct3 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct4 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct5 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct6 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct7 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct8 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct9 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct10 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct11 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct12 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct13 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct14 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct15 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct16 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct17 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct18 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct19 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct20 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct21 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct22 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct23 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct24 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct25 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct26 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct27 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct28 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct29 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct30 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct31 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct32 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct33 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct34 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct35 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct36 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct37 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct38 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct39 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct40 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct41 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct42 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct43 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct44 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct45 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct46 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct47 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct48 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct49 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct50 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct51 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct52 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct53 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct54 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct55 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct56 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct57 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct58 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct59 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct60 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct61 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct62 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct63 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct64 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct65 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct66 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct67 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct68 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct69 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct70 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct71 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct72 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct73 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct74 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct75 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct76 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct77 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct78 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct79 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct80 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct81 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct82 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct83 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct84 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct85 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct86 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct87 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct88 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct89 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct90 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct91 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct92 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct93 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct94 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct95 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct96 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct97 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct98 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct99 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct100 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, +} diff --git a/benchmarks/compilation/src/structs_10_fields_50.rs b/benchmarks/compilation/src/structs_10_fields_50.rs new file mode 100644 index 00000000..8f3edea1 --- /dev/null +++ b/benchmarks/compilation/src/structs_10_fields_50.rs @@ -0,0 +1,570 @@ +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct1 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct2 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct3 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct4 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct5 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct6 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct7 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct8 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct9 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} +#[cfg_attr( + any(feature = "bon", feature = "typed-builder", feature = "derive_builder",), + derive(crate::Builder) +)] +#[cfg_attr(feature = "bon-overwritable", builder(on(_, overwritable)))] +pub struct Struct10 { + x1: i32, + x2: i32, + x3: i32, + x4: i32, + x5: i32, + x6: i32, + x7: i32, + x8: i32, + x9: i32, + x10: i32, + x11: i32, + x12: i32, + x13: i32, + x14: i32, + x15: i32, + x16: i32, + x17: i32, + x18: i32, + x19: i32, + x20: i32, + x21: i32, + x22: i32, + x23: i32, + x24: i32, + x25: i32, + x26: i32, + x27: i32, + x28: i32, + x29: i32, + x30: i32, + x31: i32, + x32: i32, + x33: i32, + x34: i32, + x35: i32, + x36: i32, + x37: i32, + x38: i32, + x39: i32, + x40: i32, + x41: i32, + x42: i32, + x43: i32, + x44: i32, + x45: i32, + x46: i32, + x47: i32, + x48: i32, + x49: i32, + x50: i32, +} diff --git a/benchmarks/run.sh b/benchmarks/run.sh deleted file mode 100755 index 865c5814..00000000 --- a/benchmarks/run.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -bench=${1:-args_10_structs} - -export CARGO_INCREMENTAL=0 - -cargo clean - -cargo build --features "$bench" --release -p benchmarks - -cd benchmarks - -cargo asm --features "$bench" --no-color "benchmarks::$bench::builder_bench" > builder.dbg.s || true -cargo asm --features "$bench" --no-color "benchmarks::$bench::regular_bench" > regular.dbg.s || true - -# If vscode is present, show diff: - if command -v code; then - code --diff regular.dbg.s builder.dbg.s -fi - -cargo bench --features "$bench" -p benchmarks --profile release --bench iai -cargo bench --features "$bench" -p benchmarks --profile release --bench criterion diff --git a/benchmarks/Cargo.toml b/benchmarks/runtime/Cargo.toml similarity index 74% rename from benchmarks/Cargo.toml rename to benchmarks/runtime/Cargo.toml index 93830bdc..27fff5a9 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/runtime/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "benchmarks" +name = "runtime-benchmarks" publish = false description = """ - Crate for manual benchmarking of the code generated by proc-macros in the `bon` crate + Crate for benchmarking of the code generated by proc-macros in the `bon` crate. """ edition = "2021" @@ -31,7 +31,7 @@ args_10_structs = [] args_20 = [] [dependencies] -bon = { path = "../bon" } +bon = { path = "../../bon" } cfg-if = "1.0" [dev-dependencies] diff --git a/benchmarks/README.md b/benchmarks/runtime/README.md similarity index 82% rename from benchmarks/README.md rename to benchmarks/runtime/README.md index 28bc5947..2c26674a 100644 --- a/benchmarks/README.md +++ b/benchmarks/runtime/README.md @@ -1,6 +1,6 @@ # Benchmarks -This is a collection of benchmarks for the code generated by `bon` crate. +This is a collection of runtime benchmarks for the code generated by `bon` crate. ## Dependencies @@ -18,10 +18,10 @@ sudo apt install valgrind ## Running the benchmarks -Once you have all the [dependencies](#dependencies) installed you can run the selected benchmark like this: +Once you have all the [dependencies](#dependencies) installed you can run the selected benchmark from this directory like this: ```bash -./benchmarks/run.sh {benchmark_name} +./run.sh {benchmark_name} ``` The `{benchmark_name}` corresponds to the modules in this crate. The number in the benchmark name represents the number of arguments that are passed to the function. diff --git a/benchmarks/benches/criterion.rs b/benchmarks/runtime/benches/criterion.rs similarity index 77% rename from benchmarks/benches/criterion.rs rename to benchmarks/runtime/benches/criterion.rs index 2d753e09..bbaace13 100644 --- a/benchmarks/benches/criterion.rs +++ b/benchmarks/runtime/benches/criterion.rs @@ -1,10 +1,10 @@ #![allow(missing_docs)] -use benchmarks::{builder_bench, regular_bench}; +use runtime_benchmarks::{builder_bench, regular_bench}; fn criterion_bench(c: &mut criterion::Criterion) { - let builder_bench_addr: fn() -> u32 = benchmarks::bench::builder_bench; - let regular_bebch_addr: fn() -> u32 = benchmarks::bench::regular_bench; + let builder_bench_addr: fn() -> u32 = runtime_benchmarks::bench::builder_bench; + let regular_bebch_addr: fn() -> u32 = runtime_benchmarks::bench::regular_bench; let equal = if builder_bench_addr == regular_bebch_addr { "equal" diff --git a/benchmarks/benches/iai.rs b/benchmarks/runtime/benches/iai.rs similarity index 54% rename from benchmarks/benches/iai.rs rename to benchmarks/runtime/benches/iai.rs index 831b8785..b2fb3d68 100644 --- a/benchmarks/benches/iai.rs +++ b/benchmarks/runtime/benches/iai.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use benchmarks::{builder_bench, regular_bench}; +use runtime_benchmarks::{builder_bench, regular_bench}; iai::main!(builder_bench, regular_bench); diff --git a/benchmarks/runtime/run.sh b/benchmarks/runtime/run.sh new file mode 100755 index 00000000..0d33461d --- /dev/null +++ b/benchmarks/runtime/run.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +bench=${1:-args_10_structs} + +export CARGO_INCREMENTAL=0 + +cargo clean + +cargo build --features "$bench" --release -p runtime-benchmarks + +cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::builder_bench" > builder.dbg.s || true +cargo asm --features "$bench" --no-color "runtime_benchmarks::$bench::regular_bench" > regular.dbg.s || true + +# If vscode is present, show diff: +if command -v code; then + code --diff regular.dbg.s builder.dbg.s +fi + +cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench iai +cargo bench --features "$bench" -p runtime-benchmarks --profile release --bench criterion diff --git a/benchmarks/src/args_10.rs b/benchmarks/runtime/src/args_10.rs similarity index 100% rename from benchmarks/src/args_10.rs rename to benchmarks/runtime/src/args_10.rs diff --git a/benchmarks/src/args_10_alloc.rs b/benchmarks/runtime/src/args_10_alloc.rs similarity index 100% rename from benchmarks/src/args_10_alloc.rs rename to benchmarks/runtime/src/args_10_alloc.rs diff --git a/benchmarks/src/args_10_structs.rs b/benchmarks/runtime/src/args_10_structs.rs similarity index 100% rename from benchmarks/src/args_10_structs.rs rename to benchmarks/runtime/src/args_10_structs.rs diff --git a/benchmarks/src/args_20.rs b/benchmarks/runtime/src/args_20.rs similarity index 100% rename from benchmarks/src/args_20.rs rename to benchmarks/runtime/src/args_20.rs diff --git a/benchmarks/src/args_3.rs b/benchmarks/runtime/src/args_3.rs similarity index 100% rename from benchmarks/src/args_3.rs rename to benchmarks/runtime/src/args_3.rs diff --git a/benchmarks/src/args_5.rs b/benchmarks/runtime/src/args_5.rs similarity index 100% rename from benchmarks/src/args_5.rs rename to benchmarks/runtime/src/args_5.rs diff --git a/benchmarks/src/lib.rs b/benchmarks/runtime/src/lib.rs similarity index 100% rename from benchmarks/src/lib.rs rename to benchmarks/runtime/src/lib.rs diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index b1269908..9ea686bd 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -48,8 +48,22 @@ darling = "0.20.10" # conversions to share the same dependency. ident_case = "1" -proc-macro2 = "1" -quote = "1" +# This version still supports our MSRV and it is where this bug was fixed: +# https://github.com/dtolnay/proc-macro2/issues/470 +proc-macro2 = "1.0.88" + +quote = "1" # This is the highest version that supports our MSRV syn = { version = "2.0.56", features = ["full", "visit-mut", "visit"] } + +prettyplease = "0.2" + +[features] +default = [] + +# See the docs on this feature in the `bon`'s crate `Cargo.toml` +implied-bounds = [] + +[dev-dependencies] +expect-test = "1.4.1" diff --git a/bon-macros/src/bon.rs b/bon-macros/src/bon.rs index cb4c9670..51b115f7 100644 --- a/bon-macros/src/bon.rs +++ b/bon-macros/src/bon.rs @@ -2,12 +2,12 @@ use crate::builder; use crate::normalization::{ExpandCfg, ExpansionOutput}; use crate::util::prelude::*; -pub(crate) fn generate(params: TokenStream2, item: TokenStream2) -> TokenStream2 { - try_generate(params, item.clone()) - .unwrap_or_else(|err| crate::error::error_into_token_stream(err, item)) +pub(crate) fn generate(params: TokenStream, item: TokenStream) -> TokenStream { + crate::error::with_fallback(item.clone(), || try_generate(params, item)) + .unwrap_or_else(std::convert::identity) } -pub(crate) fn try_generate(params: TokenStream2, item: TokenStream2) -> Result { +pub(crate) fn try_generate(params: TokenStream, item: TokenStream) -> Result { let item: syn::Item = syn::parse2(item)?; let macro_path = syn::parse_quote!(::bon::bon); @@ -17,11 +17,19 @@ pub(crate) fn try_generate(params: TokenStream2, item: TokenStream2) -> Result item, + let (params, item) = match ctx.expand_cfg()? { + ExpansionOutput::Expanded { params, item } => (params, item), ExpansionOutput::Recurse(output) => return Ok(output), }; + if !params.is_empty() { + bail!( + ¶ms, + "`#[bon]` attribute does not accept any parameters yet, \ + but it will in future releases" + ); + } + match item { syn::Item::Impl(item_impl) => builder::item_impl::generate(item_impl), _ => bail!( diff --git a/bon-macros/src/builder/builder_gen/builder_derives.rs b/bon-macros/src/builder/builder_gen/builder_derives.rs index f72638db..8d7d334a 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives.rs @@ -1,123 +1,174 @@ -use super::builder_params::BuilderDerives; +use super::builder_params::{BuilderDerive, BuilderDerives}; use super::BuilderGenCtx; use crate::builder::builder_gen::Member; use crate::util::prelude::*; -use quote::quote; +use darling::ast::GenericParamExt; impl BuilderGenCtx { - pub(crate) fn builder_derives(&self) -> TokenStream2 { + pub(crate) fn builder_derives(&self) -> TokenStream { let BuilderDerives { clone, debug } = &self.builder_type.derives; - let mut tokens = TokenStream2::new(); + let mut tokens = TokenStream::new(); - if clone.is_present() { - tokens.extend(self.derive_clone()); + if let Some(derive) = clone { + tokens.extend(self.derive_clone(derive)); } - if debug.is_present() { - tokens.extend(self.derive_debug()); + if let Some(derive) = debug { + tokens.extend(self.derive_debug(derive)); } tokens } - fn builder_component_types(&self) -> impl Iterator { - let receiver_ty = self - .receiver() - .map(|receiver| &receiver.without_self_keyword); + /// We follow the logic of the standard `#[derive(...)]` macros such as `Clone` and `Debug`. + /// They add bounds of their respective traits to every generic type parameter on the struct + /// without trying to analyze if that bound is actually required for the derive to work, so + /// it's a conservative approach. + /// + /// However, the user can also override these bounds using the `bounds(...)` attribute for + /// the specific derive. + fn where_clause_for_derive( + &self, + target_trait_bounds: &TokenStream, + derive: &BuilderDerive, + ) -> TokenStream { + let derive_specific_predicates = derive + .bounds + .as_ref() + .map(ToTokens::to_token_stream) + .unwrap_or_else(|| { + let bounds = self + .generics + .decl_without_defaults + .iter() + .filter_map(syn::GenericParam::as_type_param) + .map(|param| { + let ident = ¶m.ident; + quote! { + #ident: #target_trait_bounds + } + }); + + quote! { + #( #bounds, )* + } + }); - let member_types = self.named_members().map(|member| &member.norm_ty); + let inherent_item_predicates = self.generics.where_clause_predicates(); - std::iter::empty() - .chain(receiver_ty) - .chain(member_types) - .map(Box::as_ref) + quote! { + where + #( #inherent_item_predicates, )* + #derive_specific_predicates + } } - fn derive_clone(&self) -> TokenStream2 { + fn derive_clone(&self, derive: &BuilderDerive) -> TokenStream { let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; let builder_ident = &self.builder_type.ident; + let phantom_field = &self.ident_pool.phantom; + let receiver_field = &self.ident_pool.receiver; + let start_fn_args_field = &self.ident_pool.start_fn_args; + let named_members_field = &self.ident_pool.named_members; + let clone = quote!(::core::clone::Clone); - let clone_receiver = self.receiver().map(|_| { + let clone_receiver = self.receiver().map(|receiver| { + let ty = &receiver.without_self_keyword; quote! { - __private_receiver: #clone::clone(&self.__private_receiver), + #receiver_field: <#ty as #clone>::clone(&self.#receiver_field), } }); let clone_start_fn_args = self.start_fn_args().next().map(|_| { + let types = self.start_fn_args().map(|arg| &arg.base.ty.norm); + let indices = self.start_fn_args().map(|arg| &arg.index); + quote! { - __private_start_fn_args: #clone::clone(&self.__private_start_fn_args), + // We clone named members individually instead of cloning + // the entire tuple to improve error messages in case if + // one of the members doesn't implement `Clone`. This avoids + // a sentence that say smth like + // ``` + // required for `(...big type...)` to implement `Clone` + // ``` + #start_fn_args_field: ( + #( <#types as #clone>::clone(&self.#start_fn_args_field.#indices), )* + ), } }); - let builder_where_clause_predicates = self.generics.where_clause_predicates(); + let where_clause = self.where_clause_for_derive(&clone, derive); + let state_mod = &self.state_mod.ident; + + let clone_named_members = self.named_members().map(|member| { + let member_index = &member.index; - let builder_component_types = self.builder_component_types(); + // The type hint here is necessary to get better error messages + // that point directly to the type that doesn't implement `Clone` + // in the input code using the span info from the type hint. + let ty = member.underlying_norm_ty(); + + quote! { + ::bon::private::derives::clone_member::<#ty>( + &self.#named_members_field.#member_index + ) + } + }); + + let state_var = &self.state_var; quote! { #[automatically_derived] - impl < + impl< #(#generics_decl,)* - ___State + #state_var: #state_mod::State > - #clone for #builder_ident < + #clone for #builder_ident< #(#generic_args,)* - ___State + #state_var > - where - #(#builder_where_clause_predicates,)* - ___State: #clone, + #where_clause { fn clone(&self) -> Self { - #(::bon::private::assert_clone::<#builder_component_types>();)* Self { - __private_phantom: ::core::marker::PhantomData, + #phantom_field: ::core::marker::PhantomData, #clone_receiver #clone_start_fn_args - __private_named_members: self.__private_named_members.clone(), + + // We clone named members individually instead of cloning + // the entire tuple to improve error messages in case if + // one of the members doesn't implement `Clone`. This avoids + // a sentence that say smth like + // ``` + // required for `(...big type...)` to implement `Clone` + // ``` + #named_members_field: ( #( #clone_named_members, )* ), } } } } } - fn derive_debug(&self) -> TokenStream2 { - let generics_decl = &self.generics.decl_without_defaults; - let generic_args = &self.generics.args; - let builder_ident = &self.builder_type.ident; - - let debug = quote!(::core::fmt::Debug); - - let format_receiver = self.receiver().map(|_| { - quote! { - output.field("self", &self.__private_receiver); - } - }); - - let builder_where_clause_predicates = self.generics.where_clause_predicates(); - let builder_component_types = self.builder_component_types(); - - let builder_ident_str = builder_ident.to_string(); - - let state_type_vars = self - .named_members() - .map(|member| &member.generic_var_ident) - .collect::>(); + fn derive_debug(&self, derive: &BuilderDerive) -> TokenStream { + let receiver_field = &self.ident_pool.receiver; + let start_fn_args_field = &self.ident_pool.start_fn_args; + let named_members_field = &self.ident_pool.named_members; let format_members = self.members.iter().filter_map(|member| { match member { Member::Named(member) => { let member_index = &member.index; - let member_ident_str = member.orig_ident.to_string(); + let member_ident_str = &member.name.snake_raw_str; + let member_ty = member.underlying_norm_ty(); Some(quote! { - // Skip members that are not set to reduce noise - if self.__private_named_members.#member_index.is_set() { + if let Some(value) = &self.#named_members_field.#member_index { output.field( #member_ident_str, - &self.__private_named_members.#member_index + ::bon::private::derives::as_dyn_debug::<#member_ty>(value) ); } }) @@ -125,10 +176,13 @@ impl BuilderGenCtx { Member::StartFnArg(member) => { let member_index = &member.index; let member_ident_str = member.base.ident.to_string(); + let member_ty = &member.base.ty.norm; Some(quote! { output.field( #member_ident_str, - &self.__private_start_fn_args.#member_index + ::bon::private::derives::as_dyn_debug::<#member_ty>( + &self.#start_fn_args_field.#member_index + ) ); }) } @@ -140,23 +194,40 @@ impl BuilderGenCtx { } }); + let format_receiver = self.receiver().map(|receiver| { + let ty = &receiver.without_self_keyword; + quote! { + output.field( + "self", + ::bon::private::derives::as_dyn_debug::<#ty>( + &self.#receiver_field + ) + ); + } + }); + + let debug = quote!(::core::fmt::Debug); + let where_clause = self.where_clause_for_derive(&debug, derive); + let state_mod = &self.state_mod.ident; + let generics_decl = &self.generics.decl_without_defaults; + 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(); + quote! { #[automatically_derived] - impl < + impl< #(#generics_decl,)* - #(#state_type_vars,)* + #state_var: #state_mod::State > - #debug for #builder_ident < + #debug for #builder_ident< #(#generic_args,)* - (#(#state_type_vars,)*) + #state_var > - where - #(#builder_where_clause_predicates,)* - #(#state_type_vars: ::bon::private::MemberState + ::core::fmt::Debug,)* + #where_clause { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - #(::bon::private::assert_debug::<#builder_component_types>();)* - let mut output = f.debug_struct(#builder_ident_str); #format_receiver diff --git a/bon-macros/src/builder/builder_gen/builder_params.rs b/bon-macros/src/builder/builder_gen/builder_params.rs deleted file mode 100644 index 2fd4ed3a..00000000 --- a/bon-macros/src/builder/builder_gen/builder_params.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::util::prelude::*; -use darling::FromMeta; -use proc_macro2::Span; -use quote::quote; -use syn::parse::Parse; -use syn::spanned::Spanned; -use syn::visit::Visit; - -fn parse_finish_fn(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - allow_vis: false, - reject_self_mentions: Some("builder struct's impl block"), - } - .parse() -} - -fn parse_builder_type(meta: &syn::Meta) -> Result { - ItemParamsParsing { - meta, - allow_vis: false, - reject_self_mentions: Some("builder struct"), - } - .parse() -} - -#[derive(Debug, FromMeta)] -pub(crate) struct BuilderParams { - #[darling(default, with = parse_finish_fn)] - pub(crate) finish_fn: ItemParams, - - #[darling(default, with = parse_builder_type)] - pub(crate) builder_type: ItemParams, - - #[darling(multiple)] - pub(crate) on: Vec, - - /// Specifies the derives to apply to the builder. - #[darling(default)] - pub(crate) derive: BuilderDerives, -} - -#[derive(Debug, Clone, Default, FromMeta)] -pub(crate) struct BuilderDerives { - #[darling(rename = "Clone")] - pub(crate) clone: darling::util::Flag, - - #[darling(rename = "Debug")] - pub(crate) debug: darling::util::Flag, -} - -#[derive(Debug)] -pub(crate) struct OnParams { - pub(crate) type_pattern: syn::Type, - pub(crate) into: darling::util::Flag, -} - -impl Parse for OnParams { - fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { - let type_pattern = input.parse()?; - - let _ = input.parse::()?; - let rest: TokenStream2 = input.parse()?; - - #[derive(FromMeta)] - struct Parsed { - into: darling::util::Flag, - } - - let Parsed { into } = Parsed::from_meta(&syn::parse_quote!(on(#rest)))?; - - if !into.is_present() { - return Err(syn::Error::new_spanned( - &rest, - "this #[builder(on(type_pattern, ...))] contains no options to override \ - the default behavior for the selected setters like `into`, so it \ - does nothing", - )); - } - - struct FindAttr { - attr: Option, - } - - impl Visit<'_> for FindAttr { - fn visit_attribute(&mut self, attr: &'_ syn::Attribute) { - self.attr.get_or_insert(attr.span()); - } - } - - let mut find_attr = FindAttr { attr: None }; - find_attr.visit_type(&type_pattern); - let attr_in_type_pattern = find_attr.attr; - - if let Some(attr) = attr_in_type_pattern { - return Err(syn::Error::new( - attr, - "nested attributes are not allowed in the type pattern of \ - #[builder(on(type_pattern, ...))]", - )); - } - - // Validate that the pattern. The validation is done in the process - // of matching the types. To make sure that matching traverses the - // full pattern we match it with itself. - let type_pattern_matches_itself = type_pattern.matches(&type_pattern)?; - - assert!( - type_pattern_matches_itself, - "BUG: the type pattern does not match itself: {type_pattern:#?}" - ); - - Ok(Self { type_pattern, into }) - } -} - -impl FromMeta for OnParams { - fn from_meta(meta: &syn::Meta) -> Result { - let meta = match meta { - syn::Meta::List(meta) => meta, - _ => bail!( - meta, - "Expected an attribute of form `on(type_pattern, ...)`" - ), - }; - - let me = syn::parse2(meta.tokens.clone())?; - - Ok(me) - } -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct ItemParams { - pub(crate) name: Option, - pub(crate) vis: Option, - pub(crate) docs: Option>, -} - -pub(crate) struct ItemParamsParsing<'a> { - pub(crate) meta: &'a syn::Meta, - pub(crate) allow_vis: bool, - pub(crate) reject_self_mentions: Option<&'static str>, -} - -impl ItemParamsParsing<'_> { - pub(crate) fn parse(self) -> Result { - let params = Self::params_from_meta(self.meta)?; - - if !self.allow_vis { - if let Some(vis) = ¶ms.vis { - bail!(vis, "visibility can't be overridden for this item"); - } - } - - if let Some(context) = self.reject_self_mentions { - if let Some(docs) = ¶ms.docs { - super::reject_self_mentions_in_docs(context, docs)?; - } - } - - Ok(params) - } - - fn params_from_meta(meta: &syn::Meta) -> Result { - if let syn::Meta::NameValue(meta) = meta { - let val = &meta.value; - let name = syn::parse2(quote!(#val))?; - - return Ok(ItemParams { - name: Some(name), - vis: None, - docs: None, - }); - } - - #[derive(Debug, FromMeta)] - struct Full { - name: Option, - vis: Option, - docs: Option, - } - - let full = Full::from_meta(meta)?; - - let is_empty = matches!( - full, - Full { - name: None, - vis: None, - docs: None, - } - ); - - if is_empty { - bail!(meta, "expected at least one parameter in parentheses"); - } - - let docs = full - .docs - .map(|docs| { - let docs = docs.require_list()?; - let docs = docs.parse_args_with(syn::Attribute::parse_outer)?; - - for attr in &docs { - if !attr.is_doc() { - bail!(attr, "expected a doc comment"); - } - } - - Ok(docs) - }) - .transpose()?; - - let params = ItemParams { - name: full.name, - vis: full.vis, - docs, - }; - - Ok(params) - } -} diff --git a/bon-macros/src/builder/builder_gen/builder_params/mod.rs b/bon-macros/src/builder/builder_gen/builder_params/mod.rs new file mode 100644 index 00000000..67e9413b --- /dev/null +++ b/bon-macros/src/builder/builder_gen/builder_params/mod.rs @@ -0,0 +1,87 @@ +mod on_params; + +pub(crate) use on_params::OnParams; + +use crate::parsing::{ItemParams, ItemParamsParsing}; +use crate::util::prelude::*; +use darling::FromMeta; +use syn::punctuated::Punctuated; + +fn parse_finish_fn(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + reject_self_mentions: Some("builder struct's impl block"), + } + .parse() +} + +fn parse_builder_type(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + reject_self_mentions: Some("builder struct"), + } + .parse() +} + +fn parse_state_mod(meta: &syn::Meta) -> Result { + ItemParamsParsing { + meta, + reject_self_mentions: Some("builder's state module"), + } + .parse() +} + +#[derive(Debug, FromMeta)] +pub(crate) struct BuilderParams { + #[darling(default, with = parse_finish_fn)] + pub(crate) finish_fn: ItemParams, + + #[darling(default, with = parse_builder_type)] + pub(crate) builder_type: ItemParams, + + #[darling(default, with = parse_state_mod)] + pub(crate) state_mod: ItemParams, + + #[darling(multiple, with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) on: Vec, + + /// Specifies the derives to apply to the builder. + #[darling(default, with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) derive: BuilderDerives, +} + +#[derive(Debug, Clone, Default, FromMeta)] +pub(crate) struct BuilderDerives { + #[darling(rename = "Clone")] + pub(crate) clone: Option, + + #[darling(rename = "Debug")] + pub(crate) debug: Option, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct BuilderDerive { + pub(crate) bounds: Option>, +} + +impl FromMeta for BuilderDerive { + fn from_meta(meta: &syn::Meta) -> Result { + if let syn::Meta::Path(_) = meta { + return Ok(Self { bounds: None }); + } + + meta.require_list()?.require_parens_delim()?; + + #[derive(FromMeta)] + struct Parsed { + #[darling(with = crate::parsing::parse_paren_meta_list_with_terminated)] + bounds: Punctuated, + } + + let Parsed { bounds } = Parsed::from_meta(meta)?; + + Ok(Self { + bounds: Some(bounds), + }) + } +} diff --git a/bon-macros/src/builder/builder_gen/builder_params/on_params.rs b/bon-macros/src/builder/builder_gen/builder_params/on_params.rs new file mode 100644 index 00000000..1acf1d23 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/builder_params/on_params.rs @@ -0,0 +1,103 @@ +use crate::util::prelude::*; +use darling::FromMeta; +use syn::parse::Parse; +use syn::spanned::Spanned; +use syn::visit::Visit; + +#[derive(Debug)] +pub(crate) struct OnParams { + pub(crate) type_pattern: syn::Type, + pub(crate) into: darling::util::Flag, + pub(crate) overwritable: darling::util::Flag, +} + +impl Parse for OnParams { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let type_pattern = input.parse()?; + + let _ = input.parse::()?; + let rest: TokenStream = input.parse()?; + + #[derive(FromMeta)] + struct Parsed { + into: darling::util::Flag, + overwritable: darling::util::Flag, + } + + let parsed = Parsed::from_meta(&syn::parse_quote!(on(#rest)))?; + + { + // Validate that at least some option was enabled. + // This lives in a separate block to make sure that if a new + // field is added to `Parsed` and unused here, then a compiler + // warning is emitted. + let Parsed { into, overwritable } = &parsed; + let flags = [("into", into), ("overwritable", overwritable)]; + + if flags.iter().all(|(_, flag)| !flag.is_present()) { + let flags = flags.iter().map(|(name, _)| format!("`{name}`")).join(", "); + let err = format!( + "this #[builder(on(type_pattern, ...))] contains no options \ + to override the default behavior for the selected setters \ + like {flags}, so it does nothing" + ); + + return Err(syn::Error::new_spanned(&rest, err)); + } + } + + struct FindAttr { + attr: Option, + } + + impl Visit<'_> for FindAttr { + fn visit_attribute(&mut self, attr: &'_ syn::Attribute) { + self.attr.get_or_insert(attr.span()); + } + } + + let mut find_attr = FindAttr { attr: None }; + find_attr.visit_type(&type_pattern); + + if let Some(attr) = find_attr.attr { + return Err(syn::Error::new( + attr, + "nested attributes are not allowed in the type pattern of \ + #[builder(on(type_pattern, ...))]", + )); + } + + // The validation is done in the process of matching the types. To make + // sure that matching traverses the full pattern we match it with itself. + let type_pattern_matches_itself = type_pattern.matches(&type_pattern)?; + + assert!( + type_pattern_matches_itself, + "BUG: the type pattern does not match itself: {type_pattern:#?}" + ); + + let Parsed { into, overwritable } = parsed; + + Ok(Self { + type_pattern, + into, + overwritable, + }) + } +} + +impl FromMeta for OnParams { + fn from_meta(meta: &syn::Meta) -> Result { + let meta = match meta { + syn::Meta::List(meta) => meta, + _ => bail!( + meta, + "expected an attribute of form `on(type_pattern, ...)`" + ), + }; + + let me = syn::parse2(meta.tokens.clone())?; + + Ok(me) + } +} diff --git a/bon-macros/src/builder/builder_gen/finish_fn.rs b/bon-macros/src/builder/builder_gen/finish_fn.rs new file mode 100644 index 00000000..4a274853 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/finish_fn.rs @@ -0,0 +1,150 @@ +use super::member::{Member, PositionalFnArgMember}; +use crate::util::prelude::*; + +impl super::BuilderGenCtx { + fn finish_fn_member_expr(&self, member: &Member) -> TokenStream { + let member = match member { + Member::Named(member) => member, + Member::Skipped(member) => { + return member + .value + .as_ref() + .as_ref() + .map(|value| quote! { (|| #value)() }) + .unwrap_or_else(|| quote! { ::core::default::Default::default() }); + } + Member::StartFnArg(member) => { + let index = &member.index; + let start_fn_args_field = &self.ident_pool.start_fn_args; + + return quote! { self.#start_fn_args_field.#index }; + } + Member::FinishFnArg(member) => { + return member.init_expr(); + } + }; + + let index = &member.index; + + let named_members_field = &self.ident_pool.named_members; + let member_field = quote! { + self.#named_members_field.#index + }; + + let param_default = member + .params + .default + .as_ref() + .map(|default| default.value.as_ref()); + + match param_default { + Some(Some(default)) => { + let has_into = member.params.into.is_present(); + let default = if has_into { + quote! { Into::into((|| #default)()) } + } else { + quote! { #default } + }; + + quote! { + ::core::option::Option::unwrap_or_else(#member_field, || #default) + } + } + Some(None) => { + quote! { + ::core::option::Option::unwrap_or_default(#member_field) + } + } + None => { + // For `Option` the default value is always `None`. So we can just return + // the value of the member field itself (which is already an `Option`). + if member.is_special_option_ty() { + return member_field; + } + + quote! { + unsafe { + // SAFETY: we know that the member is set because we are in + // the `finish` function where this method uses the trait + // bound of `IsSet` for every required member. It's also + // not possible to intervene with the builder's state from + // the outside because all members of the builder are considered + // private (we even generate random names for them to make it + // impossible to access them from the outside in the same module). + // + // We also make sure to use fully qualified paths to methods + // involved in setting the value for the required member to make + // sure no trait/function in scope can override the behavior. + ::core::option::Option::unwrap_unchecked(#member_field) + } + } + } + } + } + + pub(super) fn finish_fn(&self) -> TokenStream { + let members_vars_decls = self.members.iter().map(|member| { + let expr = self.finish_fn_member_expr(member); + let var_ident = member.orig_ident(); + + // The type hint is necessary in some cases to assist the compiler + // in type inference. + // + // For example, if the expression is passed to a function that accepts + // an impl Trait such as `impl Default`, and the expression itself looks + // like `Default::default()`. In this case nothing hints to the compiler + // the resulting type of the expression, so we add a type hint via an + // intermediate variable here. + // + // This variable can also be accessed by other member's `default` + // or `skip` expressions. + let ty = member.norm_ty(); + + quote! { + let #var_ident: #ty = #expr; + } + }); + + let state_mod = &self.state_mod.ident; + + let finish_fn_params = self + .members + .iter() + .filter_map(Member::as_finish_fn_arg) + .map(PositionalFnArgMember::fn_input_param); + + let body = &self.finish_fn.body.generate(self); + let asyncness = &self.finish_fn.asyncness; + let unsafety = &self.finish_fn.unsafety; + let must_use = &self.finish_fn.must_use; + let attrs = &self.finish_fn.attrs; + let finish_fn_vis = &self.finish_fn.vis; + let finish_fn_ident = &self.finish_fn.ident; + let output = &self.finish_fn.output; + let state_var = &self.state_var; + + quote! { + #(#attrs)* + #[inline(always)] + #[allow( + // This is intentional. We want the builder syntax to compile away + clippy::inline_always, + + // This lint flags any function that returns a possibly `!Send` future. + // However, it doesn't apply in the generic context where the future is + // `Send` if the generic parameters are `Send` as well, so we just suppress + // this lint. See the issue: https://github.com/rust-lang/rust-clippy/issues/6947 + clippy::future_not_send, + clippy::missing_const_for_fn, + )] + #must_use + #finish_fn_vis #asyncness #unsafety fn #finish_fn_ident(self, #(#finish_fn_params,)*) #output + where + #state_var: #state_mod::IsComplete + { + #(#members_vars_decls)* + #body + } + } + } +} diff --git a/bon-macros/src/builder/builder_gen/input_func.rs b/bon-macros/src/builder/builder_gen/input_fn.rs similarity index 60% rename from bon-macros/src/builder/builder_gen/input_func.rs rename to bon-macros/src/builder/builder_gen/input_fn.rs index 1bbe6e16..5ec08a71 100644 --- a/bon-macros/src/builder/builder_gen/input_func.rs +++ b/bon-macros/src/builder/builder_gen/input_fn.rs @@ -1,23 +1,23 @@ use super::builder_params::BuilderParams; +use super::models::FinishFnParams; use super::{ - generic_param_to_arg, AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFunc, - FinishFuncBody, Generics, Member, MemberOrigin, RawMember, StartFunc, + AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics, Member, + MemberOrigin, RawMember, }; -use crate::builder::builder_gen::builder_params::ItemParams; -use crate::builder::builder_gen::BuilderType; -use crate::normalization::NormalizeSelfTy; +use crate::builder::builder_gen::models::{BuilderGenCtxParams, BuilderTypeParams, StartFnParams}; +use crate::normalization::{GenericsNamespace, NormalizeSelfTy, SyntaxVariant}; +use crate::parsing::{ItemParams, SpannedKey}; use crate::util::prelude::*; use darling::util::SpannedValue; use darling::FromMeta; -use proc_macro2::Span; -use quote::quote; +use std::borrow::Cow; use std::rc::Rc; use syn::punctuated::Punctuated; use syn::visit::Visit; use syn::visit_mut::VisitMut; #[derive(Debug, FromMeta)] -pub(crate) struct FuncInputParams { +pub(crate) struct FnInputParams { expose_positional_fn: Option>, #[darling(flatten)] @@ -62,11 +62,11 @@ impl FromMeta for ExposePositionalFnParams { } } -pub(crate) struct FuncInputCtx { - pub(crate) orig_func: syn::ItemFn, - pub(crate) norm_func: syn::ItemFn, +pub(crate) struct FnInputCtx<'a> { + pub(crate) namespace: &'a GenericsNamespace, + pub(crate) fn_item: SyntaxVariant, pub(crate) impl_ctx: Option>, - pub(crate) params: FuncInputParams, + pub(crate) params: FnInputParams, } pub(crate) struct ImplCtx { @@ -79,48 +79,76 @@ pub(crate) struct ImplCtx { pub(crate) allow_attrs: Vec, } -impl FuncInputCtx { +impl FnInputCtx<'_> { fn self_ty_prefix(&self) -> Option { let prefix = self .impl_ctx .as_deref()? .self_ty - .last_path_segment_ident()? + .as_path()? + .path + .segments + .last()? + .ident .to_string(); Some(prefix) } - fn assoc_method_ctx(&self) -> Option { - Some(AssocMethodCtx { - self_ty: self.impl_ctx.as_deref()?.self_ty.clone(), - receiver: self.assoc_method_receiver_ctx(), - }) + fn assoc_method_ctx(&self) -> Result> { + let self_ty = match self.impl_ctx.as_deref() { + Some(impl_ctx) => impl_ctx.self_ty.clone(), + None => return Ok(None), + }; + + Ok(Some(AssocMethodCtx { + self_ty, + receiver: self.assoc_method_receiver_ctx()?, + })) } - fn assoc_method_receiver_ctx(&self) -> Option { - let receiver = self.norm_func.sig.receiver()?; - let self_ty = &self.impl_ctx.as_deref()?.self_ty; + fn assoc_method_receiver_ctx(&self) -> Result> { + let receiver = match self.fn_item.norm.sig.receiver() { + Some(receiver) => receiver, + None => return Ok(None), + }; + + let builder_attr_on_receiver = receiver + .attrs + .iter() + .find(|attr| attr.path().is_ident("builder")); + + if let Some(attr) = builder_attr_on_receiver { + bail!( + attr, + "#[builder] attributes on the receiver are not supported" + ); + } + + let self_ty = match self.impl_ctx.as_deref() { + Some(impl_ctx) => &impl_ctx.self_ty, + None => return Ok(None), + }; let mut without_self_keyword = receiver.ty.clone(); NormalizeSelfTy { self_ty }.visit_type_mut(&mut without_self_keyword); - Some(AssocMethodReceiverCtx { + Ok(Some(AssocMethodReceiverCtx { with_self_keyword: receiver.clone(), without_self_keyword, - }) + })) } fn generics(&self) -> Generics { let impl_ctx = self.impl_ctx.as_ref(); - let norm_func_params = &self.norm_func.sig.generics.params; + let norm_fn_params = &self.fn_item.norm.sig.generics.params; let params = impl_ctx - .map(|impl_ctx| merge_generic_params(&impl_ctx.generics.params, norm_func_params)) - .unwrap_or_else(|| norm_func_params.iter().cloned().collect()); + .map(|impl_ctx| merge_generic_params(&impl_ctx.generics.params, norm_fn_params)) + .unwrap_or_else(|| norm_fn_params.iter().cloned().collect()); let where_clauses = [ - self.norm_func.sig.generics.where_clause.clone(), + self.fn_item.norm.sig.generics.where_clause.clone(), impl_ctx.and_then(|impl_ctx| impl_ctx.generics.where_clause.clone()), ]; @@ -136,26 +164,26 @@ impl FuncInputCtx { } fn builder_ident(&self) -> syn::Ident { - let user_override = self.params.base.builder_type.name.as_ref(); + let user_override = self.params.base.builder_type.name.as_deref(); if let Some(user_override) = user_override { return user_override.clone(); } if self.is_method_new() { - return quote::format_ident!("{}Builder", self.self_ty_prefix().unwrap_or_default()); + return format_ident!("{}Builder", self.self_ty_prefix().unwrap_or_default()); } - let pascal_case_func = self.norm_func.sig.ident.snake_to_pascal_case(); + let pascal_case_fn = self.fn_item.norm.sig.ident.snake_to_pascal_case(); - quote::format_ident!( - "{}{pascal_case_func}Builder", + format_ident!( + "{}{pascal_case_fn}Builder", self.self_ty_prefix().unwrap_or_default(), ) } - pub(crate) fn adapted_func(&self) -> Result { - let mut orig = self.orig_func.clone(); + pub(crate) fn adapted_fn(&self) -> Result { + let mut orig = self.fn_item.orig.clone(); let params = self.params.expose_positional_fn.as_ref(); @@ -166,7 +194,7 @@ impl FuncInputCtx { .clone() // If exposing of positional fn is enabled without an explicit // visibility, then just use the visibility of the original function. - .unwrap_or_else(|| self.norm_func.vis.clone()) + .unwrap_or_else(|| self.fn_item.norm.vis.clone()) }) // By default we change the positional function's visibility to private // to avoid exposing it to the surrounding code. The surrounding code is @@ -207,9 +235,28 @@ impl FuncInputCtx { }) // By default we don't want to expose the positional function, so we // hide it under a generated name to avoid name conflicts. - .unwrap_or_else(|| quote::format_ident!("__orig_{}", orig_ident.raw_name())); - - strip_known_attrs_from_args(&mut orig.sig); + // + // We don't use a random name here because the name of this function + // can be used by other macros that may need a stable identifier. + // For example, if `#[tracing::instrument]` is placed on the function, + // the function name will be used as a span name. The name of the span + // may be indexed in some logs database (e.g. Grafana Loki). If the name + // of the span changes the DB index may grow and also log queries won't + // be stable. + .unwrap_or_else(|| format_ident!("__orig_{}", orig_ident.raw_name())); + + // Remove all doc comments attributes from function arguments, because they are + // not valid in that position in regular Rust code. The cool trick is that they + // are still valid syntactically when a proc macro like this one pre-processes + // them and removes them from the expanded code. We use the doc comments to put + // them on the generated setter methods. + // + // We also strip all `builder(...)` attributes because this macro processes them + // and they aren't needed in the output. + for arg in &mut orig.sig.inputs { + arg.attrs_mut() + .retain(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder")); + } // Remove all doc comments from the function itself to avoid docs duplication // which may lead to duplicating doc tests, which in turn implies repeated doc @@ -217,7 +264,7 @@ impl FuncInputCtx { // // Also remove any `#[builder]` attributes that were meant for this proc macro. orig.attrs - .retain(|attr| !attr.is_doc() && !attr.path().is_ident("builder")); + .retain(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder")); let prefix = self .self_ty_prefix() @@ -253,70 +300,71 @@ impl FuncInputCtx { } fn is_method_new(&self) -> bool { - self.impl_ctx.is_some() && self.norm_func.sig.ident == "new" + self.impl_ctx.is_some() && self.fn_item.norm.sig.ident == "new" } pub(crate) fn into_builder_gen_ctx(self) -> Result { - let receiver = self.assoc_method_ctx(); + let assoc_method_ctx = self.assoc_method_ctx()?; if self.impl_ctx.is_none() { let explanation = "\ - but #[bon] attribute \ - is absent on top of the impl block. This additional #[bon] \ - attribute on the impl block is required for the macro to see \ - the type of `Self` and properly generate the builder struct \ - definition adjacently to the impl block."; + but #[bon] attribute is absent on top of the impl block; this \ + additional #[bon] attribute on the impl block is required for \ + the macro to see the type of `Self` and properly generate + the builder struct definition adjacently to the impl block."; - if let Some(receiver) = &self.orig_func.sig.receiver() { + if let Some(receiver) = &self.fn_item.orig.sig.receiver() { bail!( &receiver.self_token, - "Function contains a `self` parameter {explanation}" + "function contains a `self` parameter {explanation}" ); } let mut ctx = FindSelfReference::default(); - ctx.visit_item_fn(&self.orig_func); + ctx.visit_item_fn(&self.fn_item.orig); if let Some(self_span) = ctx.self_span { bail!( &self_span, - "Function contains a `Self` type reference {explanation}" + "function contains a `Self` type reference {explanation}" ); } } let builder_ident = self.builder_ident(); - fn typed_args(func: &syn::ItemFn) -> impl Iterator { - func.sig.inputs.iter().filter_map(syn::FnArg::as_typed) - } - - let members = typed_args(&self.norm_func) - .zip(typed_args(&self.orig_func)) - .map(|(norm_arg, orig_arg)| { - let pat = match norm_arg.pat.as_ref() { + let members = self + .fn_item + .apply_ref(|fn_item| fn_item.sig.inputs.iter().filter_map(syn::FnArg::as_typed)) + .into_iter() + .map(|arg| { + let pat = match arg.norm.pat.as_ref() { syn::Pat::Ident(pat) => pat, _ => bail!( - &orig_arg.pat, + &arg.orig.pat, "use a simple `identifier: type` syntax for the function argument; \ destructuring patterns in arguments aren't supported by the `#[builder]`", ), }; + let ty = SyntaxVariant { + norm: arg.norm.ty.clone(), + orig: arg.orig.ty.clone(), + }; + Ok(RawMember { - attrs: &norm_arg.attrs, + attrs: &arg.norm.attrs, ident: pat.ident.clone(), - norm_ty: norm_arg.ty.clone(), - orig_ty: orig_arg.ty.clone(), + ty, }) }) .collect::>>()?; - let members = Member::from_raw(MemberOrigin::FnArg, members)?; + let members = Member::from_raw(&self.params.base.on, MemberOrigin::FnArg, members)?; let generics = self.generics(); - let finish_func_body = FnCallBody { - sig: self.adapted_func()?.sig, + let finish_fn_body = FnCallBody { + sig: self.adapted_fn()?.sig, impl_ctx: self.impl_ctx.clone(), }; @@ -324,45 +372,51 @@ impl FuncInputCtx { // Special case for `new` methods. We rename them to `builder` // since this is the name that is used in the builder pattern - let start_func_ident = if is_method_new { - quote::format_ident!("builder") + let start_fn_ident = if is_method_new { + format_ident!("builder") } else { - self.norm_func.sig.ident.clone() + self.fn_item.norm.sig.ident.clone() }; let ItemParams { - name: finish_func_ident, - vis: _, - docs: finish_func_docs, + name: finish_fn_ident, + vis: finish_fn_vis, + docs: finish_fn_docs, } = self.params.base.finish_fn; - let finish_func_ident = finish_func_ident.unwrap_or_else(|| { - // For `new` methods the `build` finisher is more conventional - if is_method_new { - quote::format_ident!("build") - } else { - quote::format_ident!("call") - } - }); - - let finish_func_docs = finish_func_docs.unwrap_or_else(|| { - vec![syn::parse_quote! { - /// Finishes building and performs the requested action. - }] - }); - - let finish_func = FinishFunc { - ident: finish_func_ident, - unsafety: self.norm_func.sig.unsafety, - asyncness: self.norm_func.sig.asyncness, - must_use: get_must_use_attribute(&self.norm_func.attrs)?, - body: Box::new(finish_func_body), - output: self.norm_func.sig.output, - attrs: finish_func_docs, + let finish_fn_ident = finish_fn_ident + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + // For `new` methods the `build` finisher is more conventional + if is_method_new { + format_ident!("build") + } else { + format_ident!("call") + } + }); + + let finish_fn_docs = finish_fn_docs + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + vec![syn::parse_quote! { + /// Finishes building and performs the requested action. + }] + }); + + let finish_fn = FinishFnParams { + ident: finish_fn_ident, + vis: finish_fn_vis.map(SpannedKey::into_value), + unsafety: self.fn_item.norm.sig.unsafety, + asyncness: self.fn_item.norm.sig.asyncness, + must_use: get_must_use_attribute(&self.fn_item.norm.attrs)?, + body: Box::new(finish_fn_body), + output: self.fn_item.norm.sig.output, + attrs: finish_fn_docs, }; let fn_allows = self - .norm_func + .fn_item + .norm .attrs .iter() .filter_map(syn::Attribute::to_allow); @@ -375,52 +429,55 @@ impl FuncInputCtx { .chain(fn_allows) .collect(); - let start_func = StartFunc { - ident: start_func_ident, + let start_fn = StartFnParams { + ident: start_fn_ident, // No override for visibility for the start fn is provided here. // It's supposed to be the same as the original function's visibility. vis: None, attrs: self - .norm_func + .fn_item + .norm .attrs .into_iter() - .filter(<_>::is_doc) + .filter(<_>::is_doc_expr) .collect(), // Override on the start fn to use the the generics from the // target function itself. We don't need to duplicate the generics // from the impl block here. generics: Some(Generics::new( - Vec::from_iter(self.norm_func.sig.generics.params), - self.norm_func.sig.generics.where_clause, + Vec::from_iter(self.fn_item.norm.sig.generics.params), + self.fn_item.norm.sig.generics.where_clause, )), }; - let builder_type = BuilderType { + let builder_type = self.params.base.builder_type; + let builder_type = BuilderTypeParams { ident: builder_ident, derives: self.params.base.derive, - docs: self.params.base.builder_type.docs, + docs: builder_type.docs.map(SpannedKey::into_value), + vis: builder_type.vis.map(SpannedKey::into_value), }; - let ctx = BuilderGenCtx { + BuilderGenCtx::new(BuilderGenCtxParams { + namespace: Cow::Borrowed(self.namespace), members, allow_attrs, on_params: self.params.base.on, - assoc_method_ctx: receiver, + assoc_method_ctx, generics, - vis: self.norm_func.vis, + orig_item_vis: self.fn_item.norm.vis, builder_type, - start_func, - finish_func, - }; - - Ok(ctx) + state_mod: self.params.base.state_mod, + start_fn, + finish_fn, + }) } } @@ -429,8 +486,8 @@ struct FnCallBody { impl_ctx: Option>, } -impl FinishFuncBody for FnCallBody { - fn generate(&self, members: &[Member]) -> TokenStream2 { +impl FinishFnBody for FnCallBody { + fn generate(&self, ctx: &BuilderGenCtx) -> TokenStream { let asyncness = &self.sig.asyncness; let maybe_await = asyncness.is_some().then(|| quote!(.await)); @@ -445,24 +502,27 @@ impl FinishFuncBody for FnCallBody { .params .iter() .filter(|arg| !matches!(arg, syn::GenericParam::Lifetime(_))) - .map(generic_param_to_arg); + .map(syn::GenericParam::to_generic_argument); let prefix = self .sig .receiver() - .map(|_| quote!(self.__private_receiver.)) + .map(|_| { + let receiver_field = &ctx.ident_pool.receiver; + quote!(self.#receiver_field.) + }) .or_else(|| { let self_ty = &self.impl_ctx.as_deref()?.self_ty; Some(quote!(<#self_ty>::)) }); - let func_ident = &self.sig.ident; + let fn_ident = &self.sig.ident; // The variables with values of members are in scope for this expression. - let member_vars = members.iter().map(Member::orig_ident); + let member_vars = ctx.members.iter().map(Member::orig_ident); quote! { - #prefix #func_ident::<#(#generic_args,)*>( + #prefix #fn_ident::<#(#generic_args,)*>( #( #member_vars ),* ) #maybe_await @@ -470,21 +530,6 @@ impl FinishFuncBody for FnCallBody { } } -/// Remove all doc comments attributes from function arguments, because they are -/// not valid in that position in regular Rust code. The cool trick is that they -/// are still valid syntactically when a proc macro like this one pre-processes -/// them and removes them from the expanded code. We use the doc comments to put -/// them on the generated setter methods. -/// -/// We also strip all `builder(...)` attributes because this macro processes them -/// and they aren't needed in the output. -fn strip_known_attrs_from_args(sig: &mut syn::Signature) { - for arg in &mut sig.inputs { - arg.attrs_mut() - .retain(|attr| !attr.is_doc() && !attr.path().is_ident("builder")); - } -} - /// To merge generic params we need to make sure lifetimes are always the first /// in the resulting list according to Rust syntax restrictions. fn merge_generic_params( @@ -543,7 +588,7 @@ fn get_must_use_attribute(attrs: &[syn::Attribute]) -> Result Result Result { ItemParamsParsing { meta, - allow_vis: true, reject_self_mentions: None, } .parse() @@ -36,16 +38,17 @@ impl StructInputParams { .map(|attr| { let meta = match &attr.meta { syn::Meta::List(meta) => meta, - _ => bail!(attr, "expected `#[builder(...)]` syntax"), + syn::Meta::Path(_) => bail!( + &attr.meta, + "this empty `#[builder]` attribute is redundant; remove it" + ), + syn::Meta::NameValue(_) => bail!( + &attr.meta, + "`#[builder = ...]` syntax is unsupported; use `#[builder(...)]` instead" + ), }; - if !matches!(meta.delimiter, syn::MacroDelimiter::Paren(_)) { - bail!( - &meta, - "wrong delimiter {:?}, expected `#[builder(...)]` syntax", - meta.delimiter - ); - } + crate::parsing::require_non_empty_paren_meta_list_or_name_value(&attr.meta)?; let meta = darling::ast::NestedMeta::parse_meta_list(meta.tokens.clone())?; @@ -60,8 +63,7 @@ impl StructInputParams { } pub(crate) struct StructInputCtx { - orig_struct: syn::ItemStruct, - norm_struct: syn::ItemStruct, + struct_item: SyntaxVariant, params: StructInputParams, struct_ty: syn::Type, } @@ -74,7 +76,7 @@ impl StructInputCtx { .generics .params .iter() - .map(super::generic_param_to_arg); + .map(syn::GenericParam::to_generic_argument); let struct_ident = &orig_struct.ident; let struct_ty = syn::parse_quote!(#struct_ident<#(#generic_args),*>); @@ -89,40 +91,30 @@ impl StructInputCtx { } .visit_item_struct_mut(&mut norm_struct); + let struct_item = SyntaxVariant { + orig: orig_struct, + norm: norm_struct, + }; + Ok(Self { - orig_struct, - norm_struct, + struct_item, params, struct_ty, }) } pub(crate) fn into_builder_gen_ctx(self) -> Result { - let builder_type = { - let ItemParams { name, vis: _, docs } = self.params.base.builder_type; - - let builder_ident = name.unwrap_or_else(|| { - quote::format_ident!("{}Builder", self.norm_struct.ident.raw_name()) - }); - - BuilderType { - derives: self.params.base.derive.clone(), - ident: builder_ident, - docs, - } - }; - - fn fields(struct_item: &syn::ItemStruct) -> Result<&syn::FieldsNamed> { - match &struct_item.fields { + let fields = self + .struct_item + .apply_ref(|struct_item| match &struct_item.fields { syn::Fields::Named(fields) => Ok(fields), _ => { bail!(&struct_item, "Only structs with named fields are supported") } - } - } + }); - let norm_fields = fields(&self.norm_struct)?; - let orig_fields = fields(&self.orig_struct)?; + let norm_fields = fields.norm?; + let orig_fields = fields.orig?; let members = norm_fields .named @@ -133,74 +125,91 @@ impl StructInputCtx { err!(norm_field, "only structs with named fields are supported") })?; + let ty = SyntaxVariant { + norm: Box::new(norm_field.ty.clone()), + orig: Box::new(orig_field.ty.clone()), + }; + Ok(RawMember { attrs: &norm_field.attrs, ident, - norm_ty: Box::new(norm_field.ty.clone()), - orig_ty: Box::new(orig_field.ty.clone()), + ty, }) }) .collect::>>()?; - let members = Member::from_raw(MemberOrigin::StructField, members)?; + let members = Member::from_raw(&self.params.base.on, MemberOrigin::StructField, members)?; let generics = Generics::new( - self.norm_struct.generics.params.iter().cloned().collect(), - self.norm_struct.generics.where_clause.clone(), + self.struct_item + .norm + .generics + .params + .iter() + .cloned() + .collect(), + self.struct_item.norm.generics.where_clause.clone(), ); - let finish_func_body = StructLiteralBody { - struct_ident: self.norm_struct.ident.clone(), + let finish_fn_body = StructLiteralBody { + struct_ident: self.struct_item.norm.ident.clone(), }; let ItemParams { - name: start_func_ident, - vis: start_func_vis, - docs: start_func_docs, + name: start_fn_ident, + vis: start_fn_vis, + docs: start_fn_docs, } = self.params.start_fn; - let start_func_ident = start_func_ident - .unwrap_or_else(|| syn::Ident::new("builder", self.norm_struct.ident.span())); + let start_fn_ident = start_fn_ident + .map(SpannedKey::into_value) + .unwrap_or_else(|| syn::Ident::new("builder", self.struct_item.norm.ident.span())); let ItemParams { - name: finish_func_ident, - vis: _, - docs: finish_func_docs, + name: finish_fn_ident, + vis: finish_fn_vis, + docs: finish_fn_docs, } = self.params.base.finish_fn; - let finish_func_ident = - finish_func_ident.unwrap_or_else(|| syn::Ident::new("build", start_func_ident.span())); + let finish_fn_ident = finish_fn_ident + .map(SpannedKey::into_value) + .unwrap_or_else(|| syn::Ident::new("build", start_fn_ident.span())); let struct_ty = &self.struct_ty; - let finish_func = FinishFunc { - ident: finish_func_ident, + let finish_fn = FinishFnParams { + ident: finish_fn_ident, + vis: finish_fn_vis.map(SpannedKey::into_value), unsafety: None, asyncness: None, must_use: Some(syn::parse_quote! { #[must_use = "building a struct without using it is likely a bug"] }), - body: Box::new(finish_func_body), + body: Box::new(finish_fn_body), output: syn::parse_quote!(-> #struct_ty), - attrs: finish_func_docs.unwrap_or_else(|| { - vec![syn::parse_quote! { - /// Finishes building and returns the requested object - }] - }), + attrs: finish_fn_docs + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + vec![syn::parse_quote! { + /// Finish building and return the requested object + }] + }), }; - let start_func_docs = start_func_docs.unwrap_or_else(|| { - let docs = format!( - "Create an instance of [`{}`] using the builder syntax", - self.norm_struct.ident - ); + let start_fn_docs = start_fn_docs + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + let docs = format!( + "Create an instance of [`{}`] using the builder syntax", + self.struct_item.norm.ident + ); - vec![syn::parse_quote!(#[doc = #docs])] - }); + vec![syn::parse_quote!(#[doc = #docs])] + }); - let start_func = StartFunc { - ident: start_func_ident, - vis: start_func_vis, - attrs: start_func_docs, + let start_fn = StartFnParams { + ident: start_fn_ident, + vis: start_fn_vis.map(SpannedKey::into_value), + attrs: start_fn_docs, generics: None, }; @@ -210,13 +219,33 @@ impl StructInputCtx { }); let allow_attrs = self - .norm_struct + .struct_item + .norm .attrs .iter() .filter_map(syn::Attribute::to_allow) .collect(); - let ctx = BuilderGenCtx { + let builder_type = { + let ItemParams { name, vis, docs } = self.params.base.builder_type; + + let builder_ident = name.map(SpannedKey::into_value).unwrap_or_else(|| { + format_ident!("{}Builder", self.struct_item.norm.ident.raw_name()) + }); + + BuilderTypeParams { + derives: self.params.base.derive, + ident: builder_ident, + docs: docs.map(SpannedKey::into_value), + vis: vis.map(SpannedKey::into_value), + } + }; + + let mut namespace = GenericsNamespace::default(); + namespace.visit_item_struct(&self.struct_item.orig); + + BuilderGenCtx::new(BuilderGenCtxParams { + namespace: Cow::Owned(namespace), members, allow_attrs, @@ -225,14 +254,13 @@ impl StructInputCtx { assoc_method_ctx, generics, - vis: self.norm_struct.vis, + orig_item_vis: self.struct_item.norm.vis, builder_type, - start_func, - finish_func, - }; - - Ok(ctx) + state_mod: self.params.base.state_mod, + start_fn, + finish_fn, + }) } } @@ -240,12 +268,12 @@ struct StructLiteralBody { struct_ident: syn::Ident, } -impl FinishFuncBody for StructLiteralBody { - fn generate(&self, member_exprs: &[Member]) -> TokenStream2 { +impl FinishFnBody for StructLiteralBody { + fn generate(&self, ctx: &BuilderGenCtx) -> TokenStream { let Self { struct_ident } = self; // The variables with values of members are in scope for this expression. - let member_vars = member_exprs.iter().map(Member::orig_ident); + let member_vars = ctx.members.iter().map(Member::orig_ident); quote! { #struct_ident { diff --git a/bon-macros/src/builder/builder_gen/member/into_conversion.rs b/bon-macros/src/builder/builder_gen/member/into_conversion.rs index 1aa00adc..bffb259f 100644 --- a/bon-macros/src/builder/builder_gen/member/into_conversion.rs +++ b/bon-macros/src/builder/builder_gen/member/into_conversion.rs @@ -1,91 +1,74 @@ -use super::{MemberOrigin, MemberParams, NamedMember, PositionalFnArgMember}; +use super::params::{BlanketParamName, EvalBlanketFlagParam}; +use super::{NamedMember, PositionalFnArgMember}; use crate::builder::builder_gen::builder_params::OnParams; use crate::util::prelude::*; -use quote::{quote, ToTokens}; impl NamedMember { - pub(crate) fn param_into(&self, on_params: &[OnParams]) -> Result { + pub(super) fn merge_param_into(&mut self, on_params: &[OnParams]) -> Result { + // `with` is mutually exclusive with `into`. So there is nothing to merge here + // if `with` is present. + if self.params.with.is_some() { + return Ok(()); + } + // For optional named members the target of the `Into` conversion is the type // inside of the `Option`, not the `Option` itself because we generate // a setter that accepts `T` itself. It also makes this logic stable regardless // if `Option` is used or the member of type `T` has `#[builder(default)]` on it. - let scrutinee = self - .as_optional_with_ty(&self.orig_ty) - .unwrap_or(&self.orig_ty); + let scrutinee = self.underlying_orig_ty(); - is_into_enabled(self.origin, &self.params, scrutinee, on_params) - } + self.params.into = EvalBlanketFlagParam { + on_params, + param_name: BlanketParamName::Into, + member_params: &self.params, + scrutinee, + origin: self.origin, + } + .eval()?; - pub(crate) fn setter_method_core_name(&self) -> &syn::Ident { - self.params.name.as_ref().unwrap_or(&self.norm_ident) + Ok(()) } } impl PositionalFnArgMember { - pub(crate) fn param_into(&self, on_params: &[OnParams]) -> Result { + pub(crate) fn merge_param_into(&mut self, on_params: &[OnParams]) -> Result { // Positional members are never optional. Users must always specify them, so there // is no need for us to look into the `Option` generic parameter, because the // `Option` itself is the target of the into conversion, not the `T` inside it. - let scrutinee = self.orig_ty.as_ref(); + let scrutinee = self.ty.orig.as_ref(); + + self.params.into = EvalBlanketFlagParam { + on_params, + param_name: BlanketParamName::Into, + member_params: &self.params, + scrutinee, + origin: self.origin, + } + .eval()?; - is_into_enabled(self.origin, &self.params, scrutinee, on_params) + Ok(()) } - pub(crate) fn fn_input_param(&self, on_params: &[OnParams]) -> Result { - let has_into = self.param_into(on_params)?; - let norm_ty = &self.norm_ty; + pub(crate) fn fn_input_param(&self) -> TokenStream { + let ty = &self.ty.norm; let ident = &self.ident; - let input = if has_into { - quote! { #ident: impl Into<#norm_ty> } + if self.params.into.is_present() { + quote! { #ident: impl Into<#ty> } } else { - quote! { #ident: #norm_ty } - }; - - Ok(input) + quote! { #ident: #ty } + } } - pub(crate) fn maybe_into_ident_expr(&self, on_params: &[OnParams]) -> Result { - let has_into = self.param_into(on_params)?; + pub(crate) fn init_expr(&self) -> TokenStream { let ident = &self.ident; - let expr = if has_into { + if self.params.into.is_present() { quote! { - ::core::convert::Into::into(#ident) + Into::into(#ident) } } else { ident.to_token_stream() - }; - - Ok(expr) + } } } - -fn is_into_enabled( - origin: MemberOrigin, - member_params: &MemberParams, - scrutinee: &syn::Type, - on_params: &[OnParams], -) -> Result { - let verdict_from_defaults = on_params - .iter() - .map(|params| Ok((params, scrutinee.matches(¶ms.type_pattern)?))) - .collect::>>()? - .into_iter() - .filter(|(_, matched)| *matched) - .any(|(params, _)| params.into.is_present()); - - let verdict_from_override = member_params.into.is_present(); - - if verdict_from_defaults && verdict_from_override { - bail!( - &member_params.into.span(), - "this `#[builder(into)]` attribute is redundant, because `into` \ - is already implied for this member via the `#[builder(on(...))]` \ - at the top of the {}", - origin.parent_construct(), - ); - } - - Ok(verdict_from_override || verdict_from_defaults) -} diff --git a/bon-macros/src/builder/builder_gen/member/mod.rs b/bon-macros/src/builder/builder_gen/member/mod.rs index 4f8b8d4f..976fc41f 100644 --- a/bon-macros/src/builder/builder_gen/member/mod.rs +++ b/bon-macros/src/builder/builder_gen/member/mod.rs @@ -1,11 +1,16 @@ mod into_conversion; +mod named; mod params; +pub(crate) use named::*; +pub(crate) use params::*; + +use super::builder_params::OnParams; +use crate::normalization::SyntaxVariant; +use crate::parsing::SpannedKey; use crate::util::prelude::*; -use darling::util::SpannedValue; use darling::FromAttributes; use params::MemberParams; -use quote::quote; use std::fmt; #[derive(Debug, Clone, Copy)] @@ -45,46 +50,6 @@ pub(crate) enum Member { Skipped(SkippedMember), } -/// Regular member for which the builder should have setter methods -#[derive(Debug, Clone)] -pub(crate) struct NamedMember { - /// Specifies what syntax the member comes from. - pub(crate) origin: MemberOrigin, - - /// Index of the member relative to other regular members. The index is 0-based. - pub(crate) index: syn::Index, - - /// Original name of the member is used as the name of the builder field and - /// in its setter methods. Struct field/fn arg names conventionally use `snake_case` - /// in Rust, but this isn't enforced, so this member isn't guaranteed to be in - /// snake case, but 99% of the time it will be. - pub(crate) orig_ident: syn::Ident, - - /// Normalized version of `orig_ident`. Here we stripped the leading `_` from the - /// member name. - pub(crate) norm_ident: syn::Ident, - - /// `PascalCase` version of the `norm_ident`. - pub(crate) norm_ident_pascal: syn::Ident, - - /// Doc comments for the setter methods are copied from the doc comments placed - /// on top of the original member - pub(crate) docs: Vec, - - /// Normalized type of the member that the builder should have setters for. - pub(crate) norm_ty: Box, - - /// Original type of the member (not normalized) - pub(crate) orig_ty: Box, - - /// The name of the type variable that can be used as the type of this - /// member in contexts where it should be generic. - pub(crate) generic_var_ident: syn::Ident, - - /// Parameters configured by the user explicitly via attributes - pub(crate) params: MemberParams, -} - /// Member that was marked with `#[builder(pos = start_fn)]` #[derive(Debug)] pub(crate) struct StartFnArgMember { @@ -102,11 +67,8 @@ pub(crate) struct PositionalFnArgMember { /// Original identifier of the member pub(crate) ident: syn::Ident, - /// Normalized type of the member - pub(crate) norm_ty: Box, - - /// Original type of the member (not normalized) - pub(crate) orig_ty: Box, + /// Type of the member + pub(crate) ty: SyntaxVariant>, /// Parameters configured by the user explicitly via attributes pub(crate) params: MemberParams, @@ -117,66 +79,17 @@ pub(crate) struct PositionalFnArgMember { pub(crate) struct SkippedMember { pub(crate) ident: syn::Ident, + /// Normalized type of the member pub(crate) norm_ty: Box, /// Value to assign to the member - pub(crate) value: SpannedValue>, -} - -impl NamedMember { - fn validate(&self) -> Result { - super::reject_self_mentions_in_docs("builder struct's impl block", &self.docs)?; - - if let Some(default) = &self.params.default { - if self.norm_ty.is_option() { - bail!( - &default.span(), - "`Option<_>` already implies a default of `None`, \ - so explicit #[builder(default)] is redundant", - ); - } - } - - Ok(()) - } - - fn as_optional_with_ty<'a>(&'a self, ty: &'a syn::Type) -> Option<&'a syn::Type> { - ty.option_type_param() - .or_else(|| (self.params.default.is_some()).then(|| ty)) - } - - pub(crate) fn as_optional_norm_ty(&self) -> Option<&syn::Type> { - Self::as_optional_with_ty(self, &self.norm_ty) - } - - pub(crate) fn is_optional(&self) -> bool { - self.as_optional_norm_ty().is_some() - } - - /// The type parameter for the `Set` type that corresponds to this member - pub(crate) fn set_state_type_param(&self) -> TokenStream2 { - let ty = &self.norm_ty; - let ty = self - .as_optional_norm_ty() - .map(|ty| quote!(Option<#ty>)) - .unwrap_or_else(|| quote!(#ty)); - - quote!(#ty) - } - - pub(crate) fn param_default(&self) -> Option> { - self.params - .default - .as_ref() - .map(|default| default.as_ref().as_ref()) - } + pub(crate) value: SpannedKey>, } pub(crate) struct RawMember<'a> { pub(crate) attrs: &'a [syn::Attribute], pub(crate) ident: syn::Ident, - pub(crate) norm_ty: Box, - pub(crate) orig_ty: Box, + pub(crate) ty: SyntaxVariant>, } impl Member { @@ -186,12 +99,21 @@ impl Member { // (there is an other lint that checks for this). #[allow(single_use_lifetimes)] pub(crate) fn from_raw<'a>( + on_params: &[OnParams], origin: MemberOrigin, members: impl IntoIterator>, ) -> Result> { let mut members = members .into_iter() .map(|member| { + for attr in member.attrs { + if attr.meta.path().is_ident("builder") { + crate::parsing::require_non_empty_paren_meta_list_or_name_value( + &attr.meta, + )?; + } + } + let params = MemberParams::from_attributes(member.attrs)?; params.validate(origin)?; Ok((member, params)) @@ -202,38 +124,35 @@ impl Member { let mut output = vec![]; - let start_fn_args = (0..).map_while(|index| { - let (member, params) = members.next_if(|(_, params)| params.start_fn.is_present())?; - let base = PositionalFnArgMember::new(origin, member, params); - Some(Self::StartFnArg(StartFnArgMember { + for index in 0.. { + let next = members.next_if(|(_, params)| params.start_fn.is_present()); + let (member, params) = match next { + Some(item) => item, + None => break, + }; + let base = PositionalFnArgMember::new(origin, member, on_params, params)?; + output.push(Self::StartFnArg(StartFnArgMember { base, index: index.into(), - })) - }); - - output.extend(start_fn_args); + })); + } while let Some((member, params)) = members.next_if(|(_, params)| params.finish_fn.is_present()) { - let member = PositionalFnArgMember::new(origin, member, params); + let member = PositionalFnArgMember::new(origin, member, on_params, params)?; output.push(Self::FinishFnArg(member)); } let mut named_count = 0; for (member, params) in members { - let RawMember { - attrs, - ident: orig_ident, - norm_ty, - orig_ty, - } = member; + let RawMember { attrs, ident, ty } = member; if let Some(value) = params.skip { output.push(Self::Skipped(SkippedMember { - ident: orig_ident, - norm_ty, + ident, + norm_ty: ty.norm, value, })); continue; @@ -247,7 +166,7 @@ impl Member { if let Some(attr) = incorrect_order { bail!( &attr.span(), - "incorrect members oredering; the order of members must be the following:\n\ + "incorrect members ordering; the order of members must be the following:\n\ (1) members annotated with #[builder(start_fn)]\n\ (2) members annotated with #[builder(finish_fn)]\n\ (3) all other members in any order", @@ -261,37 +180,25 @@ impl Member { // then these docs will just be removed from the output function. // It's probably fine since the doc comments are there in the code // itself which is also useful for people reading the source code. - let docs = attrs.iter().filter(|attr| attr.is_doc()).cloned().collect(); - - let orig_ident_str = orig_ident.to_string(); - let norm_ident = orig_ident_str - // Remove the leading underscore from the member name since it's used - // to denote unused symbols in Rust. That doesn't mean the builder - // API should expose that knowledge to the caller. - .strip_prefix('_') - .unwrap_or(&orig_ident_str); - - // Preserve the original identifier span to make IDE's "go to definition" work correctly - // and make error messages point to the correct place. - let norm_ident = syn::Ident::new_maybe_raw(norm_ident, orig_ident.span()); - let norm_ident_pascal = norm_ident.snake_to_pascal_case(); - - let me = NamedMember { + let docs = attrs + .iter() + .filter(|attr| attr.is_doc_expr()) + .cloned() + .collect(); + + let mut member = NamedMember { index: named_count.into(), origin, - generic_var_ident: quote::format_ident!("__{}", norm_ident_pascal), - norm_ident_pascal, - orig_ident, - norm_ident, - norm_ty, - orig_ty, + name: MemberName::new(ident, ¶ms), + ty, params, docs, }; - me.validate()?; + member.merge_on_params(on_params)?; + member.validate()?; - output.push(Self::Named(me)); + output.push(Self::Named(member)); named_count += 1; } @@ -302,16 +209,16 @@ impl Member { impl Member { pub(crate) fn norm_ty(&self) -> &syn::Type { match self { - Self::Named(me) => &me.norm_ty, - Self::StartFnArg(me) => &me.base.norm_ty, - Self::FinishFnArg(me) => &me.norm_ty, + Self::Named(me) => &me.ty.norm, + Self::StartFnArg(me) => &me.base.ty.norm, + Self::FinishFnArg(me) => &me.ty.norm, Self::Skipped(me) => &me.norm_ty, } } pub(crate) fn orig_ident(&self) -> &syn::Ident { match self { - Self::Named(me) => &me.orig_ident, + Self::Named(me) => &me.name.orig, Self::StartFnArg(me) => &me.base.ident, Self::FinishFnArg(me) => &me.ident, Self::Skipped(me) => &me.ident, @@ -341,20 +248,27 @@ impl Member { } impl PositionalFnArgMember { - fn new(origin: MemberOrigin, member: RawMember<'_>, params: MemberParams) -> Self { + fn new( + origin: MemberOrigin, + member: RawMember<'_>, + on_params: &[OnParams], + params: MemberParams, + ) -> Result { let RawMember { attrs: _, ident, - norm_ty, - orig_ty, + ty, } = member; - Self { + let mut me = Self { origin, ident, - norm_ty, - orig_ty, + ty, params, - } + }; + + me.merge_param_into(on_params)?; + + Ok(me) } } diff --git a/bon-macros/src/builder/builder_gen/member/named.rs b/bon-macros/src/builder/builder_gen/member/named.rs new file mode 100644 index 00000000..04e4a3c4 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/member/named.rs @@ -0,0 +1,278 @@ +use super::params::MemberParams; +use super::{params, MemberOrigin}; +use crate::builder::builder_gen::builder_params::OnParams; +use crate::builder::builder_gen::member::params::SettersFnParams; +use crate::normalization::SyntaxVariant; +use crate::parsing::{ItemParams, SpannedKey}; +use crate::util::prelude::*; + +#[derive(Debug)] +pub(crate) struct MemberName { + /// Original name of the member (unchanged). It's used in the finishing + /// function of the builder to create a variable for each member. + pub(crate) orig: syn::Ident, + + /// `snake_case` version of the member name. By default it's the `orig` name + /// itself with the `_` prefix stripped. Otherwise the user can override it + /// via `#[builder(name = custom_name)]` + pub(crate) snake: syn::Ident, + + /// `snake_case` version of the member name as a string without the `r#` prefix + /// (if there is any in the `snake` representation). It's computed and + /// stored separately to avoid recomputing it multiple times. It's used + /// to derive names for other identifiers that are based on the `snake_case` name. + pub(crate) snake_raw_str: String, + + /// `PascalCase` version of the member name. It's always computed as the + /// `snake` variant converted to `PascalCase`. The user doesn't have the + /// granular control over this name. Users can only specify the snake case + /// version of the name, and the pascal case is derived from it. + pub(crate) pascal: syn::Ident, + + /// `PascalCase` version of the member name as a string. It's computed and + /// stored separately to avoid recomputing it multiple times. It's guaranteed + /// to not have the `r#` prefix because: + /// + /// There are no pascal case keywords in Rust except for `Self`, which + /// is anyway not allowed even as a raw identifier: + /// + pub(crate) pascal_str: String, +} + +impl MemberName { + pub(crate) fn new(orig: syn::Ident, params: &MemberParams) -> Self { + let snake = params.name.clone().unwrap_or_else(|| { + let orig_str = orig.to_string(); + let norm = orig_str + // Remove the leading underscore from the member name since it's used + // to denote unused symbols in Rust. That doesn't mean the builder + // API should expose that knowledge to the caller. + .strip_prefix('_') + .unwrap_or(&orig_str); + + // Preserve the original identifier span to make IDE's "go to definition" work correctly + // and make error messages point to the correct place. + syn::Ident::new_maybe_raw(norm, orig.span()) + }); + + let pascal = snake.snake_to_pascal_case(); + + Self { + orig, + snake_raw_str: snake.raw_name(), + snake, + pascal_str: pascal.to_string(), + pascal, + } + } +} + +/// Regular member for which the builder should have setter methods +#[derive(Debug)] +pub(crate) struct NamedMember { + /// Specifies what syntax the member comes from. + pub(crate) origin: MemberOrigin, + + /// Index of the member relative to other named members. The index is 0-based. + pub(crate) index: syn::Index, + + /// Name of the member is used to generate names for the setters, names for + /// the associated types and type aliases in the builder state, etc. + pub(crate) name: MemberName, + + /// Doc comments on top of the original syntax. These are copied to the setters + /// unless there are overrides for them. + pub(crate) docs: Vec, + + /// Type of the member has to be known to generate the types for fields in + /// the builder, signatures of the setter methods, etc. + pub(crate) ty: SyntaxVariant>, + + /// Parameters configured by the user explicitly via attributes + pub(crate) params: MemberParams, +} + +impl NamedMember { + pub(super) fn validate(&self) -> Result { + if let Some(default) = &self.params.default { + if self.is_special_option_ty() { + bail!( + &default.key, + "`Option<_>` already implies a default of `None`, \ + so explicit #[builder(default)] is redundant", + ); + } + } + + let member_docs_not_copied = self + .params + .setters + .as_ref() + .map(|setters| { + if setters.docs.is_some() { + return true; + } + + let SettersFnParams { some_fn, option_fn } = &setters.fns; + matches!( + (some_fn.as_deref(), option_fn.as_deref()), + ( + Some(ItemParams { docs: Some(_), .. }), + Some(ItemParams { docs: Some(_), .. }) + ) + ) + }) + .unwrap_or(false); + + if !member_docs_not_copied { + crate::parsing::reject_self_mentions_in_docs( + "builder struct's impl block", + &self.docs, + )?; + } + + self.validate_setters_params()?; + + if self.params.transparent.is_present() && !self.ty.norm.is_option() { + bail!( + &self.params.transparent.span(), + "`#[builder(transparent)]` can only be applied to members of \ + type `Option` to disable their special handling", + ); + } + + Ok(()) + } + + fn validate_setters_params(&self) -> Result { + let setters = match &self.params.setters { + Some(setters) => setters, + None => return Ok(()), + }; + + if self.is_required() { + let SettersFnParams { some_fn, option_fn } = &setters.fns; + + let unexpected_setter = option_fn.as_ref().or(some_fn.as_ref()); + + if let Some(setter) = unexpected_setter { + bail!( + &setter.key, + "`{}` setter function applies only to members with `#[builder(default)]` \ + or members of `Option` type (if #[builder(transparent)] is not set)", + setter.key + ); + } + } + + if let SettersFnParams { + some_fn: Some(some_fn), + option_fn: Some(option_fn), + } = &setters.fns + { + let setter_fns = &[some_fn, option_fn]; + + Self::validate_unused_setters_cfg(setter_fns, &setters.name, |params| ¶ms.name)?; + Self::validate_unused_setters_cfg(setter_fns, &setters.vis, |params| ¶ms.vis)?; + Self::validate_unused_setters_cfg(setter_fns, &setters.docs, |params| ¶ms.docs)?; + } + + Ok(()) + } + + // Lint from nightly. `&Option` is used to reduce syntax at the call site + #[allow(unknown_lints, clippy::ref_option)] + fn validate_unused_setters_cfg( + overrides: &[&SpannedKey], + config: &Option>, + get_val: impl Fn(&ItemParams) -> &Option>, + ) -> Result { + let config = match config { + Some(config) => config, + None => return Ok(()), + }; + + let overrides_values = overrides + .iter() + .copied() + .map(|over| get_val(&over.value).as_ref()); + + if !overrides_values.clone().all(|over| over.is_some()) { + return Ok(()); + } + + let setters = overrides + .iter() + .map(|over| format!("`{}`", over.key)) + .join(", "); + + bail!( + &config.key, + "this `{name}` configuration is unused because all of the \ + {setters} setters contain a `{name}` override", + name = config.key, + ); + } + + /// Returns `true` if this member is of `Option<_>` type, but returns `false` + /// if `#[builder(transparent)]` is set. + pub(crate) fn is_special_option_ty(&self) -> bool { + !self.params.transparent.is_present() && self.ty.norm.is_option() + } + + /// Returns `false` if the member has a default value. It means this member + /// is required to be set before building can be finished. + pub(crate) fn is_required(&self) -> bool { + self.params.default.is_none() && !self.is_special_option_ty() + } + + /// A stateful member is the one that has a corresponding associated type in + /// the builder's type state trait. This is used to track the fact that the + /// member was set or not. This is necessary to make sure all members without + /// default values are set before building can be finished. + pub(crate) fn is_stateful(&self) -> bool { + self.is_required() || !self.params.overwritable.is_present() + } + + /// Returns the normalized type of the member stripping the `Option<_>` + /// wrapper if it's present unless `#[builder(transparent)]` is set. + pub(crate) fn underlying_norm_ty(&self) -> &syn::Type { + self.underlying_ty(&self.ty.norm) + } + + /// Returns the original type of the member stripping the `Option<_>` + /// wrapper if it's present unless `#[builder(transparent)]` is set. + pub(crate) fn underlying_orig_ty(&self) -> &syn::Type { + self.underlying_ty(&self.ty.orig) + } + + fn underlying_ty<'m>(&'m self, ty: &'m syn::Type) -> &'m syn::Type { + if self.params.transparent.is_present() || self.params.default.is_some() { + ty + } else { + ty.option_type_param().unwrap_or(ty) + } + } + + pub(crate) fn is(&self, other: &Self) -> bool { + self.index == other.index + } + + pub(crate) fn merge_on_params(&mut self, on_params: &[OnParams]) -> Result { + self.merge_param_into(on_params)?; + + // FIXME: refactor this to make it more consistent with `into` + // and allow for non-boolean flags in `OnParams`. E.g. add support + // for `with = closure` to `on` as well. + self.params.overwritable = params::EvalBlanketFlagParam { + on_params, + param_name: params::BlanketParamName::Overwritable, + member_params: &self.params, + scrutinee: self.underlying_norm_ty(), + origin: self.origin, + } + .eval()?; + + Ok(()) + } +} diff --git a/bon-macros/src/builder/builder_gen/member/params/blanket.rs b/bon-macros/src/builder/builder_gen/member/params/blanket.rs new file mode 100644 index 00000000..e0124483 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/member/params/blanket.rs @@ -0,0 +1,81 @@ +use super::MemberParams; +use crate::builder::builder_gen::builder_params::OnParams; +use crate::builder::builder_gen::member::MemberOrigin; +use crate::util::prelude::*; +use std::fmt; + +pub(crate) enum BlanketParamName { + Into, + Overwritable, +} + +impl fmt::Display for BlanketParamName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Into => fmt::Display::fmt(&super::ParamName::Into, f), + Self::Overwritable => fmt::Display::fmt(&super::ParamName::Overwritable, f), + } + } +} + +impl BlanketParamName { + fn value_in_on_params(&self, on_params: &OnParams) -> darling::util::Flag { + match self { + Self::Into => on_params.into, + Self::Overwritable => on_params.overwritable, + } + } + + fn value_in_member_params(&self, member_params: &MemberParams) -> darling::util::Flag { + match self { + Self::Into => member_params.into, + Self::Overwritable => member_params.overwritable, + } + } +} + +pub(crate) struct EvalBlanketFlagParam<'a> { + pub(crate) on_params: &'a [OnParams], + pub(crate) param_name: BlanketParamName, + pub(crate) member_params: &'a MemberParams, + pub(crate) scrutinee: &'a syn::Type, + pub(crate) origin: MemberOrigin, +} + +impl EvalBlanketFlagParam<'_> { + pub(crate) fn eval(self) -> Result { + let Self { + on_params, + param_name, + member_params, + scrutinee, + origin, + } = self; + + let verdict_from_on_params = on_params + .iter() + .map(|params| Ok((params, scrutinee.matches(¶ms.type_pattern)?))) + .collect::>>()? + .into_iter() + .filter(|(_, matched)| *matched) + .map(|(params, _)| param_name.value_in_on_params(params)) + .find(darling::util::Flag::is_present); + + let value_in_member = param_name.value_in_member_params(member_params); + let flag = match (verdict_from_on_params, value_in_member.is_present()) { + (Some(_), true) => { + bail!( + &value_in_member.span(), + "this `#[builder({param_name})]` attribute is redundant, because \ + `{param_name}` is already implied for this member via the \ + `#[builder(on(...))]` at the top of the {}", + origin.parent_construct(), + ); + } + (Some(flag), false) => flag, + (None, _) => value_in_member, + }; + + Ok(flag) + } +} diff --git a/bon-macros/src/builder/builder_gen/member/params.rs b/bon-macros/src/builder/builder_gen/member/params/mod.rs similarity index 53% rename from bon-macros/src/builder/builder_gen/member/params.rs rename to bon-macros/src/builder/builder_gen/member/params/mod.rs index 4c320991..3beb71c2 100644 --- a/bon-macros/src/builder/builder_gen/member/params.rs +++ b/bon-macros/src/builder/builder_gen/member/params/mod.rs @@ -1,10 +1,17 @@ +mod blanket; +mod setter_closure; +mod setters; + +pub(crate) use blanket::*; +pub(crate) use setter_closure::*; +pub(crate) use setters::*; + use super::MemberOrigin; +use crate::parsing::SpannedKey; use crate::util::prelude::*; -use darling::util::SpannedValue; use std::fmt; -use syn::spanned::Spanned; -#[derive(Debug, Clone, darling::FromAttributes)] +#[derive(Debug, darling::FromAttributes)] #[darling(attributes(builder))] pub(crate) struct MemberParams { /// Enables an `Into` conversion for the setter method. @@ -14,60 +21,111 @@ pub(crate) struct MemberParams { /// /// An optional expression can be provided to set the value for the member, /// otherwise its [`Default`] trait impl will be used. - #[darling(with = parse_optional_expression, map = Some)] - pub(crate) default: Option>>, + #[darling(with = parse_optional_expr, map = Some)] + pub(crate) default: Option>>, /// Skip generating a setter method for this member. /// /// An optional expression can be provided to set the value for the member, /// otherwise its [`Default`] trait impl will be used. - #[darling(with = parse_optional_expression, map = Some)] - pub(crate) skip: Option>>, + #[darling(with = parse_optional_expr, map = Some)] + pub(crate) skip: Option>>, /// Rename the name exposed in the builder API. pub(crate) name: Option, + /// Configurations for the setter methods. + #[darling(with = crate::parsing::parse_non_empty_paren_meta_list)] + pub(crate) setters: Option, + /// Where to place the member in the generated builder methods API. /// By default the member is treated like a named parameter that /// gets its own setter methods. pub(crate) start_fn: darling::util::Flag, pub(crate) finish_fn: darling::util::Flag, + + /// Allows setting the value for the member repeatedly. This reduces the + /// number of type states and thus increases the compilation performance. + /// + /// However, this also means that unintended overwrites won't be caught + /// at compile time. Measure the compilation time before and after enabling + /// this option to see if it's worth it. + pub(crate) overwritable: darling::util::Flag, + + /// Customize the setter signature and body with a custom closure. The closure + /// must return the value of the type of the member, or optionally a `Result<_>` + /// type where `_` is used to mark the type of the member. In this case the + /// generated setters will be fallible (they'll propagate the `Result`). + pub(crate) with: Option>, + + /// Disables the special handling for a member of type `Option`. The + /// member no longer has the default of `None`. It also becomes a required + /// member unless a separate `#[builder(default = ...)]` attribute is + /// also specified. + pub(crate) transparent: darling::util::Flag, } #[derive(PartialEq, Eq, Clone, Copy)] enum ParamName { Default, + FinishFn, Into, Name, + Overwritable, + Setters, Skip, StartFn, - FinishFn, + Transparent, + With, } impl fmt::Display for ParamName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { Self::Default => "default", + Self::FinishFn => "finish_fn", Self::Into => "into", Self::Name => "name", + Self::Overwritable => "overwritable", + Self::Setters => "setters", Self::Skip => "skip", Self::StartFn => "start_fn", - Self::FinishFn => "finish_fn", + Self::Transparent => "transparent", + Self::With => "with", }; f.write_str(str) } } impl MemberParams { + fn validate_mutually_exclusive( + &self, + attr_name: ParamName, + attr_span: Span, + mutually_exclusive: &[ParamName], + ) -> Result<()> { + self.validate_compat(attr_name, attr_span, mutually_exclusive, true) + } + fn validate_mutually_allowed( &self, attr_name: ParamName, attr_span: Span, - allowed: &[ParamName], + mutually_allowed: &[ParamName], + ) -> Result<()> { + self.validate_compat(attr_name, attr_span, mutually_allowed, false) + } + + fn validate_compat( + &self, + attr_name: ParamName, + attr_span: Span, + patterns: &[ParamName], + mutually_exclusive: bool, ) -> Result<()> { let conflicting: Vec<_> = self .specified_param_names() - .filter(|name| *name != attr_name && !allowed.contains(name)) + .filter(|name| *name != attr_name && patterns.contains(name) == mutually_exclusive) .collect(); if conflicting.is_empty() { @@ -87,21 +145,29 @@ impl MemberParams { fn specified_param_names(&self) -> impl Iterator { let Self { - into, default, - skip, - name, finish_fn, + into, + name, + overwritable, + setters, + skip, start_fn, + transparent, + with, } = self; let attrs = [ (default.is_some(), ParamName::Default), - (name.is_some(), ParamName::Name), + (finish_fn.is_present(), ParamName::FinishFn), (into.is_present(), ParamName::Into), + (name.is_some(), ParamName::Name), + (overwritable.is_present(), ParamName::Overwritable), + (setters.is_some(), ParamName::Setters), (skip.is_some(), ParamName::Skip), (start_fn.is_present(), ParamName::StartFn), - (finish_fn.is_present(), ParamName::FinishFn), + (transparent.is_present(), ParamName::Transparent), + (with.is_some(), ParamName::With), ]; attrs @@ -131,9 +197,9 @@ impl MemberParams { match origin { MemberOrigin::FnArg => { bail!( - &skip.span(), - "`skip` attribute is not supported on function arguments. \ - Use a local variable instead.", + &skip.key.span(), + "`skip` attribute is not supported on function arguments; \ + use a local variable instead.", ); } MemberOrigin::StructField => {} @@ -141,24 +207,28 @@ impl MemberParams { if let Some(Some(_expr)) = self.default.as_deref() { bail!( - &skip.span(), - "`skip` attribute can't be specified with `default` attribute; \ + &skip.key.span(), + "`skip` attribute can't be specified with the `default` attribute; \ if you wanted to specify a value for the member, then use \ the following syntax instead `#[builder(skip = value)]`", ); } - self.validate_mutually_allowed(ParamName::Skip, skip.span(), &[])?; + self.validate_mutually_allowed(ParamName::Skip, skip.key.span(), &[])?; + } + + if let Some(with) = &self.with { + self.validate_mutually_exclusive(ParamName::With, with.key.span(), &[ParamName::Into])?; } Ok(()) } } -fn parse_optional_expression(meta: &syn::Meta) -> Result>> { +fn parse_optional_expr(meta: &syn::Meta) -> Result>> { match meta { - syn::Meta::Path(_) => Ok(SpannedValue::new(None, meta.span())), + syn::Meta::Path(path) => SpannedKey::new(path, None), syn::Meta::List(_) => Err(Error::unsupported_format("list").with_span(meta)), - syn::Meta::NameValue(nv) => Ok(SpannedValue::new(Some(nv.value.clone()), nv.span())), + syn::Meta::NameValue(meta) => SpannedKey::new(&meta.path, Some(meta.value.clone())), } } diff --git a/bon-macros/src/builder/builder_gen/member/params/setter_closure.rs b/bon-macros/src/builder/builder_gen/member/params/setter_closure.rs new file mode 100644 index 00000000..e93e5102 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/member/params/setter_closure.rs @@ -0,0 +1,120 @@ +use crate::parsing::SimpleClosure; +use crate::util::prelude::*; +use darling::FromMeta; + +const INVALID_RETURN_TYPE_ERROR: &str = "\ +expected one of the following: + +(1) no return type annotation; + this means the closure is expected to return a value of the same type + as the member's underlying type(*); + +(2) `-> *Result<_, {{ErrorType}}>` or `-> *Result<_>` return type annotation; + this means the closure is expected to return a `Result` where the `Ok` + variant is of the same type as the member's underlying type(*); this syntax + allows you to define a fallbile setter (one that returns a `Result`); + + the `_` placeholder must be spelled literally to mark the underlying type(*) + of the member; an optional second generic parameter for the error type is allowed; + + the return type doesn't have to be named `Result` exactly, the only requirement is + that it must have the `Result` suffix; for example if you have a type alias + `ApiResult<_>`, then it'll work fine; + +(*) underlying type is the type of the member stripped from the `Option` wrapper + if this member is of `Option` type and no `#[builder(transparent)]` annotation + is present"; + +#[derive(Debug)] +pub(crate) struct SetterClosure { + pub(crate) inputs: Vec, + pub(crate) body: Box, + pub(crate) output: Option, +} + +#[derive(Debug)] +pub(crate) struct SetterClosureOutput { + pub(crate) result_path: syn::Path, + pub(crate) err_ty: Option, +} + +#[derive(Debug)] +pub(crate) struct SetterClosureInput { + pub(crate) pat: syn::PatIdent, + pub(crate) ty: Box, +} + +impl FromMeta for SetterClosure { + fn from_meta(item: &syn::Meta) -> Result { + let closure = SimpleClosure::from_meta(item)?; + + let inputs = closure + .inputs + .into_iter() + .map(|input| { + Ok(SetterClosureInput { + ty: input.ty.ok_or_else(|| { + err!(&input.pat, "expected a type for the setter input parameter") + })?, + pat: input.pat, + }) + }) + .collect::>()?; + + let return_type = match closure.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => { + let err = || err!(&ty, "{INVALID_RETURN_TYPE_ERROR}"); + + let ty = ty + .as_generic_angle_bracketed_path(|last_segment| { + // We allow for arbitrary `Result` type variations + // including custom type aliases like `ApiResult<_>` + last_segment.to_string().ends_with("Result") + }) + .ok_or_else(err)?; + + if !(1..=2).contains(&ty.args.len()) { + return Err(err()); + } + + let mut args = ty.args.iter(); + let ok_ty = args.next().ok_or_else(err)?; + + if !matches!(ok_ty, syn::GenericArgument::Type(syn::Type::Infer(_))) { + return Err(err()); + } + + let err_ty = args + .next() + .map(|arg| match arg { + syn::GenericArgument::Type(ty) => Ok(ty.clone()), + _ => Err(err()), + }) + .transpose()?; + + let mut result_path = ty.path.clone(); + + // We store the error type of the result separately. + // Strip the generic arguments, because we only care + // about the path of the `Result` in `result_path` field. + result_path + .segments + .last_mut() + .expect("BUG: segments can't be empty") + .arguments = syn::PathArguments::None; + + Some(SetterClosureOutput { + result_path, + err_ty, + }) + } + }; + + Ok(Self { + inputs, + body: closure.body, + output: return_type, + }) + } +} diff --git a/bon-macros/src/builder/builder_gen/member/params/setters.rs b/bon-macros/src/builder/builder_gen/member/params/setters.rs new file mode 100644 index 00000000..ea9a51b2 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/member/params/setters.rs @@ -0,0 +1,48 @@ +use crate::parsing::{ItemParams, ItemParamsParsing, SpannedKey}; +use crate::util::prelude::*; +use darling::FromMeta; + +const DOCS_CONTEXT: &str = "builder struct's impl block"; + +fn parse_setter_fn(meta: &syn::Meta) -> Result> { + let params = ItemParamsParsing { + meta, + reject_self_mentions: Some(DOCS_CONTEXT), + } + .parse()?; + + SpannedKey::new(meta.path(), params) +} + +fn parse_docs(meta: &syn::Meta) -> Result>> { + crate::parsing::parse_docs_without_self_mentions(DOCS_CONTEXT, meta) +} + +#[derive(Debug, FromMeta)] +pub(crate) struct SettersParams { + pub(crate) name: Option>, + pub(crate) vis: Option>, + + #[darling(rename = "doc", default, with = parse_docs, map = Some)] + pub(crate) docs: Option>>, + + #[darling(flatten)] + pub(crate) fns: SettersFnParams, +} + +#[derive(Debug, FromMeta)] +pub(crate) struct SettersFnParams { + /// Config for the setter that accepts the value of type T for a member of + /// type `Option` or with `#[builder(default)]`. + /// + /// By default, it's named `{member}` without any prefix or suffix. + #[darling(default, with = parse_setter_fn, map = Some)] + pub(crate) some_fn: Option>, + + /// The setter that accepts the value of type `Option` for a member of + /// type `Option` or with `#[builder(default)]`. + /// + /// By default, it's named `maybe_{member}`. + #[darling(default, with = parse_setter_fn, map = Some)] + pub(crate) option_fn: Option>, +} diff --git a/bon-macros/src/builder/builder_gen/mod.rs b/bon-macros/src/builder/builder_gen/mod.rs index 90bab8cc..040d2bad 100644 --- a/bon-macros/src/builder/builder_gen/mod.rs +++ b/bon-macros/src/builder/builder_gen/mod.rs @@ -1,156 +1,22 @@ mod builder_derives; mod builder_params; +mod finish_fn; mod member; -mod setter_methods; +mod models; +mod setters; +mod state_mod; -pub(crate) mod input_func; +pub(crate) mod input_fn; pub(crate) mod input_struct; use crate::util::prelude::*; -use builder_params::{BuilderDerives, OnParams}; use member::{Member, MemberOrigin, NamedMember, RawMember, StartFnArgMember}; -use quote::{quote, ToTokens}; -use setter_methods::{MemberSettersCtx, SettersReturnType}; - -struct AssocMethodReceiverCtx { - with_self_keyword: syn::Receiver, - without_self_keyword: Box, -} - -struct AssocMethodCtx { - /// The `Self` type of the impl block. It doesn't contain any nested - /// `Self` keywords in it. This is prohibited by Rust's syntax itself. - self_ty: Box, - - /// Present only if the method has a receiver, i.e. `self` or `&self` or - /// `&mut self` or `self: ExplicitType`. - receiver: Option, -} - -pub(crate) struct BuilderGenCtx { - members: Vec, - - /// Lint suppressions from the original item that will be inherited by all items - /// generated by the macro. If the original syntax used `#[expect(...)]`, - /// then it must be represented as `#[allow(...)]` here. - allow_attrs: Vec, - on_params: Vec, - - generics: Generics, - vis: syn::Visibility, - assoc_method_ctx: Option, - - builder_type: BuilderType, - start_func: StartFunc, - finish_func: FinishFunc, -} - -struct FinishFunc { - ident: syn::Ident, - - /// Additional attributes to apply to the item - attrs: Vec, - - unsafety: Option, - asyncness: Option, - /// - must_use: Option, - body: Box, - output: syn::ReturnType, -} - -struct StartFunc { - ident: syn::Ident, - - /// Additional attributes to apply to the item - attrs: Vec, - - /// Overrides the common generics - generics: Option, - - /// If present overrides the automatic visibility - vis: Option, -} - -struct BuilderType { - ident: syn::Ident, - - derives: BuilderDerives, - - /// Optional docs override - docs: Option>, -} - -pub(crate) trait FinishFuncBody { - /// Generate the `finish` function body from the ready-made variables. - /// The generated function body may assume that there are variables - /// named the same as the members in scope. - fn generate(&self, members: &[Member]) -> TokenStream2; -} - -struct Generics { - where_clause: Option, - - /// Original generics that may contain default values in them. This is only - /// suitable for use in places where default values for generic parameters - /// are allowed. - decl_with_defaults: Vec, - - /// Generic parameters without default values in them. This is suitable for - /// use as generics in function signatures or impl blocks. - decl_without_defaults: Vec, - - /// Mirrors the `decl` representing how generic params should be represented - /// when these parameters are passed through as arguments in a turbofish. - args: Vec, -} - -impl Generics { - fn new( - decl_with_defaults: Vec, - where_clause: Option, - ) -> Self { - let decl_without_defaults = decl_with_defaults - .iter() - .cloned() - .map(|mut param| { - match &mut param { - syn::GenericParam::Type(param) => { - param.default = None; - } - syn::GenericParam::Const(param) => { - param.default = None; - } - syn::GenericParam::Lifetime(_) => {} - } - param - }) - .collect(); - - let args = decl_with_defaults - .iter() - .map(generic_param_to_arg) - .collect(); - - Self { - where_clause, - decl_with_defaults, - decl_without_defaults, - args, - } - } - - fn where_clause_predicates(&self) -> impl Iterator { - self.where_clause - .as_ref() - .into_iter() - .flat_map(|clause| &clause.predicates) - } -} +use models::{AssocMethodCtx, AssocMethodReceiverCtx, BuilderGenCtx, FinishFnBody, Generics}; +use setters::SettersCtx; pub(crate) struct MacroOutput { - pub(crate) start_func: syn::ItemFn, - pub(crate) other_items: TokenStream2, + pub(crate) start_fn: syn::ItemFn, + pub(crate) other_items: TokenStream, } impl BuilderGenCtx { @@ -166,93 +32,104 @@ impl BuilderGenCtx { self.members.iter().filter_map(Member::as_start_fn_arg) } + fn stateful_members(&self) -> impl Iterator { + self.named_members().filter(|member| member.is_stateful()) + } + pub(crate) fn output(self) -> Result { - let mut start_func = self.start_func()?; + let mut start_fn = self.start_fn(); + let state_mod = state_mod::StateModGenCtx::new(&self).state_mod(); let builder_decl = self.builder_decl(); - let builder_impl = self.builder_impl()?; + let builder_impl = self.builder_impl(); let builder_derives = self.builder_derives(); + let default_allows = syn::parse_quote!(#[allow( + // We have a `deprecated` lint on all `bon::private` items which we + // use in the generated code extensively + deprecated + )]); + + let allows = self.allow_attrs.iter().cloned().chain([default_allows]); + // -- Postprocessing -- // Here we parse all items back and add the `allow` attributes to them. - let other_items: syn::File = syn::parse_quote! { + let other_items = quote! { + #state_mod #builder_decl #builder_derives #builder_impl }; + let other_items_str = other_items.to_string(); + + let other_items: syn::File = syn::parse2(other_items).map_err(|err| { + err!( + &Span::call_site(), + "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;\n\ + syntax error in generated code: {err:#?};\n\ + generated code:\n\ + ```rust + {other_items_str}\n\ + ```", + ) + })?; + let mut other_items = other_items.items; for item in &mut other_items { if let Some(attrs) = item.attrs_mut() { - attrs.extend(self.allow_attrs.iter().cloned()); + attrs.extend(allows.clone()); } } - start_func.attrs.extend(self.allow_attrs); + start_fn.attrs.extend(allows); Ok(MacroOutput { - start_func, + start_fn, other_items: quote!(#(#other_items)*), }) } - fn builder_impl(&self) -> Result { - let finish_method = self.finish_method()?; - let (setter_methods, items_for_rustdoc) = self.setter_methods()?; + fn builder_impl(&self) -> TokenStream { + let finish_fn = self.finish_fn(); + let setter_methods = self + .named_members() + .map(|member| SettersCtx::new(self, member).setter_methods()); let generics_decl = &self.generics.decl_without_defaults; let generic_args = &self.generics.args; let where_clause = &self.generics.where_clause; - let state_type_vars = self - .named_members() - .map(|member| &member.generic_var_ident) - .collect::>(); - let builder_ident = &self.builder_type.ident; + let state_mod = &self.state_mod.ident; + let state_var = &self.state_var; let allows = allow_warnings_on_member_types(); - let named_members_labels = self - .named_members() - .map(|member| self.members_label(member)); - - let vis = &self.vis; - - Ok(quote! { - #items_for_rustdoc - - #( - #[allow(non_camel_case_types)] - #[doc(hidden)] - #vis struct #named_members_labels; - )* - + quote! { #allows #[automatically_derived] impl< #(#generics_decl,)* - #(#state_type_vars,)* + #state_var: #state_mod::State > #builder_ident< #(#generic_args,)* - (#(#state_type_vars,)*) + #state_var > #where_clause { - #finish_method - #setter_methods + #finish_fn + #(#setter_methods)* } - }) - } - - fn start_func_generics(&self) -> &Generics { - self.start_func.generics.as_ref().unwrap_or(&self.generics) + } } /// Generates code that has no meaning to the compiler, but it helps /// IDEs to provide better code highlighting, completions and other /// hints. - fn ide_hints(&self) -> TokenStream2 { + fn ide_hints(&self) -> TokenStream { let type_patterns = self .on_params .iter() @@ -284,13 +161,12 @@ impl BuilderGenCtx { } } - fn start_func(&self) -> Result { + fn start_fn(&self) -> syn::ItemFn { let builder_ident = &self.builder_type.ident; + let attrs = &self.start_fn.attrs; + let vis = &self.start_fn.vis; - let docs = &self.start_func.attrs; - let vis = self.start_func.vis.as_ref().unwrap_or(&self.vis); - - let start_func_ident = &self.start_func.ident; + let start_fn_ident = &self.start_fn.ident; // TODO: we can use a shorter syntax with anonymous lifetimes to make // the generated code and function signature displayed by rust-analyzer @@ -299,18 +175,23 @@ impl BuilderGenCtx { // in the where clause. Research `darling`'s lifetime tracking API and // maybe implement this in the future - let generics = self.start_func_generics(); + let generics = self.start_fn.generics.as_ref().unwrap_or(&self.generics); let generics_decl = &generics.decl_without_defaults; let where_clause = &generics.where_clause; let generic_args = &self.generics.args; + let phantom_field = &self.ident_pool.phantom; + let receiver_field = &self.ident_pool.receiver; + let start_fn_args_field = &self.ident_pool.start_fn_args; + let named_members_field = &self.ident_pool.named_members; + let receiver = self.receiver(); let receiver_field_init = receiver.map(|receiver| { let self_token = &receiver.with_self_keyword.self_token; quote! { - __private_receiver: #self_token, + #receiver_field: #self_token, } }); @@ -319,34 +200,38 @@ impl BuilderGenCtx { quote! { #receiver, } }); - let unset_state_literals = self.named_members().map(|member| { - if member.is_optional() { - quote!(::bon::private::Unset(::bon::private::Optional)) - } else { - quote!(::bon::private::Unset(::bon::private::Required)) - } - }); - let start_fn_params = self .start_fn_args() - .map(|member| member.base.fn_input_param(&self.on_params)) - .collect::>>()?; + .map(|member| member.base.fn_input_param()); - let start_fn_arg_exprs = self + let mut start_fn_arg_exprs = self .start_fn_args() - .map(|member| member.base.maybe_into_ident_expr(&self.on_params)) - .collect::>>()?; + .map(|member| member.base.init_expr()) + .peekable(); - let start_fn_args_field_init = (!start_fn_arg_exprs.is_empty()).then(|| { + let start_fn_args_field_init = start_fn_arg_exprs.peek().is_some().then(|| { quote! { - __private_start_fn_args: (#(#start_fn_arg_exprs,)*), + #start_fn_args_field: (#(#start_fn_arg_exprs,)*), } }); let ide_hints = self.ide_hints(); - let func = quote! { - #(#docs)* + // `Default` trait implementation is provided only for tuples up to 12 + // elements in the standard library 😳: + // https://github.com/rust-lang/rust/blob/67bb749c2e1cf503fee64842963dd3e72a417a3f/library/core/src/tuple.rs#L213 + let named_members_field_init = if self.named_members().take(13).count() <= 12 { + quote!(::core::default::Default::default()) + } else { + let none = format_ident!("None"); + let nones = self.named_members().map(|_| &none); + quote! { + (#(#nones,)*) + } + }; + + syn::parse_quote! { + #(#attrs)* #[inline(always)] #[allow( // This is intentional. We want the builder syntax to compile away @@ -355,37 +240,43 @@ impl BuilderGenCtx { clippy::use_self, // Let's keep it as non-const for now to avoid restricting ourselfves to only // const operations. - clippy::missing_const_for_fn + clippy::missing_const_for_fn, )] - #vis fn #start_func_ident<#(#generics_decl),*>( + #vis fn #start_fn_ident< #(#generics_decl),* >( #receiver #(#start_fn_params,)* - ) -> #builder_ident<#(#generic_args,)*> + ) -> #builder_ident< #(#generic_args,)* > #where_clause { #ide_hints #builder_ident { - __private_phantom: ::core::marker::PhantomData, + #phantom_field: ::core::marker::PhantomData, #receiver_field_init #start_fn_args_field_init - __private_named_members: (#( #unset_state_literals, )*) + #named_members_field: #named_members_field_init, } } - }; - - Ok(syn::parse_quote!(#func)) + } } - fn phantom_data(&self) -> TokenStream2 { - let member_types = self.members.iter().map(Member::norm_ty); + fn phantom_data(&self) -> TokenStream { + let member_types = self.members.iter().filter_map(|member| { + match member { + // The types of these members already appear in the struct in the types + // of named_members and start_fn_args fields. + Member::Named(_) | Member::StartFnArg(_) => None, + Member::FinishFnArg(member) => Some(member.ty.norm.as_ref()), + Member::Skipped(member) => Some(member.norm_ty.as_ref()), + } + }); + let receiver_ty = self .assoc_method_ctx .as_ref() .map(|ctx| ctx.self_ty.as_ref()); - let generic_args = &self.generics.args; - let generic_types = generic_args.iter().filter_map(|arg| match arg { + let generic_types = self.generics.args.iter().filter_map(|arg| match arg { syn::GenericArgument::Type(ty) => Some(ty), _ => None, }); @@ -397,18 +288,34 @@ impl BuilderGenCtx { .map(|ty| { // Wrap `ty` in another phantom data because it can be `?Sized`, // and simply using it as a type of the tuple member would - // be wrong, because tuple's members must be sized - quote!(::core::marker::PhantomData<#ty>) + // be wrong, because tuple's members must be sized. + // + // We also wrap this in an `fn() -> ...` to make the compiler think + // that the builder doesn't "own" an instance of the given type. + // This removes unnecessary requirements when evaluating the + // applicability of the auto traits. + quote!(fn() -> ::core::marker::PhantomData<#ty>) }); + let state_var = &self.state_var; + quote! { ::core::marker::PhantomData<( + // We have to store the builder state in phantom data otherwise it + // would be reported as an unused type parameter. + // + // We also wrap this in an `fn() -> ...` to make the compiler think + // that the builder doesn't "own" an instance of the given type. + // This removes unnecessary requirements when evaluating the + // applicability of the auto traits. + fn() -> #state_var, + // There is an interesting quirk with lifetimes in Rust, which is the // reason why we thoughtlessly store all the function parameter types // in phantom data here. // // Suppose a function was defined with an argument of type `&'a T` - // and we then generate an impl block (simplified): + // and then we generate an impl block (simplified): // // ``` // impl<'a, T, U> for Foo @@ -425,89 +332,80 @@ impl BuilderGenCtx { // That's a weird implicit behavior in Rust, I suppose there is a reasonable // explanation for it, I just didn't care to research it yet ¯\_(ツ)_/¯. #(#types,)* - - // A special case of zero members requires storing `___State` in phantom data - // otherwise it would be reported as an unused type parameter. - ::core::marker::PhantomData<___State> )> } } - fn builder_decl(&self) -> TokenStream2 { - let vis = &self.vis; + fn builder_decl(&self) -> TokenStream { + let builder_vis = &self.builder_type.vis; let builder_ident = &self.builder_type.ident; let generics_decl = &self.generics.decl_with_defaults; let where_clause = &self.generics.where_clause; let phantom_data = self.phantom_data(); + let state_mod = &self.state_mod.ident; + let phantom_field = &self.ident_pool.phantom; + let receiver_field = &self.ident_pool.receiver; + let start_fn_args_field = &self.ident_pool.start_fn_args; + let named_members_field = &self.ident_pool.named_members; + + // The fields can't be hidden using Rust's privacy syntax. + // The details about this are described in the blog post: + // https://elastio.github.io/bon/blog/the-weird-of-function-local-types-in-rust. + // + // We could use `#[cfg(not(rust_analyzer))]` to hide the private fields in IDE. + // However, RA would then not be able to type-check the generated code, which + // may or may not be a problem, because the main thing is that the type signatures + // would still work in RA. + let private_field_attrs = { + // The message is defined separately to make it single-line in the + // generated code. This simplifies the task of removing unnecessary + // attributes from the generated code when preparing for demo purposes. + let deprecated_msg = "\ + this field should not be used directly; it's an implementation detail \ + if you found yourself needing it, then you are probably doing something wrong; \ + feel free to open an issue/discussion in our GitHub repository \ + (https://github.com/elastio/bon) or ask for help in our Discord server \ + (https://discord.gg/QcBYSamw4c)"; - let private_field_doc = "\ - Please don't touch this field. It's an implementation \ - detail that is exempt from the API stability guarantees. \ - This field couldn't be hidden using Rust's privacy syntax. \ - The details about this are described in [the blog post]\ - (https://elastio.github.io/bon/blog/the-weird-of-function-local-types-in-rust). - "; + quote! { + #[doc(hidden)] + #[deprecated = #deprecated_msg] + } + }; let receiver_field = self.receiver().map(|receiver| { let ty = &receiver.without_self_keyword; quote! { - #[doc = #private_field_doc] - __private_receiver: #ty, + #private_field_attrs + #receiver_field: #ty, } }); let must_use_message = format!( "the builder does nothing until you call `{}()` on it to finish building", - self.finish_func.ident + self.finish_fn.ident ); - let docs = self.builder_type.docs.clone().unwrap_or_else(|| { - let doc = format!( - "Use builder syntax to set the required parameters and finish \ - by calling the method [`Self::{}()`].", - self.finish_func.ident - ); - - vec![syn::parse_quote! { - #[doc = #doc] - }] - }); - let allows = allow_warnings_on_member_types(); - let initial_state_type_alias_ident = - quote::format_ident!("__{}InitialState", builder_ident.raw_name()); - - let unset_state_types = self.named_members().map(|member| { - if member.is_optional() { - quote!(::bon::private::Unset<::bon::private::Optional>) - } else { - quote!(::bon::private::Unset<::bon::private::Required>) - } - }); - let mut start_fn_arg_types = self .start_fn_args() - .map(|member| &member.base.norm_ty) + .map(|member| &member.base.ty.norm) .peekable(); - let start_fn_arg_types_field = start_fn_arg_types.peek().is_some().then(|| { + let start_fn_args_field = start_fn_arg_types.peek().is_some().then(|| { quote! { - #[doc = #private_field_doc] - __private_start_fn_args: (#(#start_fn_arg_types,)*), + #private_field_attrs + #start_fn_args_field: (#(#start_fn_arg_types,)*), } }); - quote! { - // This type alias exists just to shorten the type signature of - // the default generic argument of the builder struct. It's not - // really important for users to see what this type alias expands to. - // - // If they want to see how "bon works" they should just expand the - // macro manually where they'll see this type alias. - #[doc(hidden)] - #vis type #initial_state_type_alias_ident = (#(#unset_state_types,)*); + let named_members_types = self.named_members().map(NamedMember::underlying_norm_ty); + let docs = &self.builder_type.docs; + let state_var = &self.state_var; + + quote! { #[must_use = #must_use_message] #(#docs)* #allows @@ -521,292 +419,39 @@ impl BuilderGenCtx { // by the big PhantomData type generated by the macro clippy::type_complexity )] - #vis struct #builder_ident< + #builder_vis struct #builder_ident< #(#generics_decl,)* - ___State = #initial_state_type_alias_ident + // Having the `State` trait bound on the struct declaration is important + // for future proofing. It will allow us to use this bound in the `Drop` + // implementation of the builder if we ever add one. @Veetaha already did + // some experiments with `MaybeUninit` that requires a custom drop impl, + // so this could be useful in the future. + // + // On the flip side, if we have a custom `Drop` impl, then partially moving + // the builder will be impossible. So.. it's a trade-off, and it's probably + // not a big deal to remove this bound from here if we feel like it. + #state_var: #state_mod::State = #state_mod::Empty > #where_clause { - // We could use `#[cfg(not(rust_analyzer))]` to hide these. - // However, RA would then not be able to type-check the generated - // code, which may or may not be a problem, because the main thing - // is that the type signatures would still work in RA. - #[doc = #private_field_doc] - __private_phantom: #phantom_data, + #private_field_attrs + #phantom_field: #phantom_data, #receiver_field - #start_fn_arg_types_field - - #[doc = #private_field_doc] - __private_named_members: ___State + #start_fn_args_field + + #private_field_attrs + #named_members_field: ( + #( + ::core::option::Option<#named_members_types>, + )* + ) } } } - - fn member_expr(&self, member: &Member) -> Result { - let member = match member { - Member::Named(member) => member, - Member::Skipped(member) => { - let expr = member - .value - .as_ref() - .as_ref() - .map(|value| quote! { #value }) - .unwrap_or_else(|| quote! { ::core::default::Default::default() }); - - return Ok(expr); - } - Member::StartFnArg(member) => { - let index = &member.index; - return Ok(quote! { self.__private_start_fn_args.#index }); - } - Member::FinishFnArg(member) => { - return member.maybe_into_ident_expr(&self.on_params); - } - }; - - let maybe_default = member - .as_optional_norm_ty() - // For `Option` members we don't need any `unwrap_or_[else/default]`. - // The implementation of `From for Set>` already - // returns an `Option`. - .filter(|_| !member.norm_ty.is_option()) - .map(|_| { - member - .param_default() - .flatten() - .map(|default| { - let has_into = member.param_into(&self.on_params)?; - let default = if has_into { - quote! { ::core::convert::Into::into((|| #default)()) } - } else { - quote! { #default } - }; - - Result::<_>::Ok(quote! { .unwrap_or_else(|| #default) }) - }) - .unwrap_or_else(|| Ok(quote! { .unwrap_or_default() })) - }) - .transpose()?; - - let index = &member.index; - let set_state_type_param = member.set_state_type_param(); - let member_label = self.members_label(member); - - let expr = quote! { - ::bon::private::IntoSet::< - #set_state_type_param, - #member_label - >::into_set(self.__private_named_members.#index) - #maybe_default - }; - - Ok(expr) - } - - /// Name of the dummy struct that is generated just to give a name for - /// the member in the error message when `IntoSet` trait is not implemented. - fn members_label(&self, member: &NamedMember) -> syn::Ident { - quote::format_ident!( - "{}__{}", - self.builder_type.ident.raw_name(), - member.setter_method_core_name() - ) - } - - fn finish_method(&self) -> Result { - let members_vars_decls = self - .members - .iter() - .map(|member| { - let expr = self.member_expr(member)?; - let var_ident = member.orig_ident(); - - // The type hint is necessary in some cases to assist the compiler - // in type inference. - // - // For example, if the expression is passed to a function that accepts - // an impl Trait such as `impl Default`, and the expression itself looks - // like `Default::default()`. In this case nothing hints to the compiler - // the resulting type of the expression, so we add a type hint via an - // intermediate variable here. - let ty = member.norm_ty(); - - Ok(quote! { - let #var_ident: #ty = #expr; - }) - }) - .collect::>>()?; - - let body = &self.finish_func.body.generate(&self.members); - let asyncness = &self.finish_func.asyncness; - let unsafety = &self.finish_func.unsafety; - let must_use = &self.finish_func.must_use; - let attrs = &self.finish_func.attrs; - let vis = &self.vis; - let finish_func_ident = &self.finish_func.ident; - let output = &self.finish_func.output; - - let where_bounds = self.named_members().map(|member| { - let member_type_var = &member.generic_var_ident; - let set_state_type_param = member.set_state_type_param(); - let member_label = self.members_label(member); - quote! { - #member_type_var: ::bon::private::IntoSet< - #set_state_type_param, - #member_label - > - } - }); - - let finish_fn_params = self - .members - .iter() - .filter_map(Member::as_finish_fn_arg) - .map(|member| member.fn_input_param(&self.on_params)) - .collect::>>()?; - - Ok(quote! { - #(#attrs)* - #[inline(always)] - #[allow( - // This is intentional. We want the builder syntax to compile away - clippy::inline_always, - - // This lint flags any function that returns a possibly `!Send` future. - // However, it doesn't apply in the generic context where the future is - // `Send` if the generic parameters are `Send` as well, so we just suppress - // this lint. See the issue: https://github.com/rust-lang/rust-clippy/issues/6947 - clippy::future_not_send, - )] - #must_use - #vis #asyncness #unsafety fn #finish_func_ident( - self, - #(#finish_fn_params,)* - ) #output - where - #(#where_bounds,)* - { - #(#members_vars_decls)* - #body - } - }) - } - - fn setter_methods(&self) -> Result<(TokenStream2, TokenStream2)> { - let generics_decl = &self.generics.decl_without_defaults; - let generic_args = &self.generics.args; - let builder_ident = &self.builder_type.ident; - let where_clause = &self.generics.where_clause; - - let state_type_vars = self - .named_members() - .map(|member| &member.generic_var_ident) - .collect::>(); - - let allows = allow_warnings_on_member_types(); - - let next_state_trait_ident = - quote::format_ident!("__{}SetMember", builder_ident.raw_name()); - - let next_states_decls = self.named_members().map(|member| { - let member_pascal = &member.norm_ident_pascal; - quote! { - type #member_pascal; - } - }); - - let setters = self - .named_members() - .map(|member| { - let state_types = self.named_members().map(|other_member| { - if other_member.orig_ident == member.orig_ident { - let ty = member.set_state_type_param(); - quote!(::bon::private::Set<#ty>) - } else { - other_member.generic_var_ident.to_token_stream() - } - }); - - let member_pascal = &member.norm_ident_pascal; - - let next_state = quote! { - #builder_ident< - #(#generic_args,)* - (#(#state_types,)*) - > - }; - - let return_type = SettersReturnType { - doc_true: quote!(::#member_pascal), - doc_false: next_state.clone(), - }; - - let setter_methods = - MemberSettersCtx::new(self, member, return_type).setter_methods()?; - - let next_state = quote!(type #member_pascal = #next_state;); - - Ok((setter_methods, next_state)) - }) - .collect::>>()?; - let next_states_defs = setters.iter().map(|(_, next_state)| next_state); - - let items_for_rustdoc = quote! { - // This item is under `cfg(doc)` because it's used only to make the - // documentation less noisy (see `SettersReturnType` for more info). - #[cfg(doc)] - trait #next_state_trait_ident { - #(#next_states_decls)* - } - - // This item is under `cfg(doc)` because it's used only to make the - // documentation less noisy (see `SettersReturnType` for more info). - #[cfg(doc)] - #allows - #[automatically_derived] - impl< - #(#generics_decl,)* - #(#state_type_vars,)* - > - #next_state_trait_ident - for - #builder_ident< - #(#generic_args,)* - (#(#state_type_vars,)*) - > - #where_clause - { - #(#next_states_defs)* - } - }; - - let setter_methods = setters - .into_iter() - .map(|(setter_methods, _)| setter_methods) - .concat(); - - Ok((setter_methods, items_for_rustdoc)) - } -} - -pub(crate) fn generic_param_to_arg(param: &syn::GenericParam) -> syn::GenericArgument { - match param { - syn::GenericParam::Lifetime(param) => { - syn::GenericArgument::Lifetime(param.lifetime.clone()) - } - syn::GenericParam::Type(param) => { - let ident = ¶m.ident; - syn::GenericArgument::Type(syn::parse_quote!(#ident)) - } - syn::GenericParam::Const(param) => { - let ident = ¶m.ident; - syn::GenericArgument::Const(syn::parse_quote!(#ident)) - } - } } -fn allow_warnings_on_member_types() -> TokenStream2 { +fn allow_warnings_on_member_types() -> TokenStream { quote! { // This warning may occur when the original unnormalized syntax was // using parens around an `impl Trait` like that: @@ -822,42 +467,3 @@ fn allow_warnings_on_member_types() -> TokenStream2 { #[allow(unused_parens)] } } - -/// Validates the docs for the presence of `Self` mentions to prevent users from -/// shooting themselves in the foot where they would think that `Self` resolves -/// to the current item the docs were placed on, when in fact the docs are moved -/// to a different context where `Self` has a different meaning. -fn reject_self_mentions_in_docs(context: &'static str, attrs: &[syn::Attribute]) -> Result { - for attr in attrs { - let doc = match attr.as_doc() { - Some(doc) => doc, - _ => continue, - }; - - let doc = match &doc { - syn::Expr::Lit(doc) => doc, - _ => continue, - }; - - let doc = match &doc.lit { - syn::Lit::Str(doc) => doc, - _ => continue, - }; - - let self_references = ["[`Self`]", "[Self]"]; - - if self_references - .iter() - .any(|self_ref| doc.value().contains(self_ref)) - { - bail!( - &doc.span(), - "the documentation should not reference `Self` because it will \ - be moved to the {context} where `Self` changes meaning, which \ - may confuse the reader of this code; use explicit type names instead.", - ); - } - } - - Ok(()) -} diff --git a/bon-macros/src/builder/builder_gen/models.rs b/bon-macros/src/builder/builder_gen/models.rs new file mode 100644 index 00000000..83f54c29 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/models.rs @@ -0,0 +1,447 @@ +use super::builder_params::{BuilderDerives, OnParams}; +use super::member::Member; +use crate::normalization::GenericsNamespace; +use crate::parsing::{ItemParams, SpannedKey}; +use crate::util::prelude::*; +use std::borrow::Cow; + +pub(super) trait FinishFnBody { + /// Generate the `finish` function body from the ready-made variables. + /// The generated function body may assume that there are variables + /// named the same as the members in scope. + fn generate(&self, ctx: &BuilderGenCtx) -> TokenStream; +} + +pub(super) struct AssocMethodReceiverCtx { + pub(super) with_self_keyword: syn::Receiver, + pub(super) without_self_keyword: Box, +} + +pub(super) struct AssocMethodCtx { + /// The `Self` type of the impl block. It doesn't contain any nested + /// `Self` keywords in it. This is prohibited by Rust's syntax itself. + pub(super) self_ty: Box, + + /// Present only if the method has a receiver, i.e. `self` or `&self` or + /// `&mut self` or `self: ExplicitType`. + pub(super) receiver: Option, +} + +pub(super) struct FinishFn { + pub(super) ident: syn::Ident, + + /// Visibility override specified by the user + pub(super) vis: syn::Visibility, + + /// Additional attributes to apply to the item + pub(super) attrs: Vec, + + pub(super) unsafety: Option, + pub(super) asyncness: Option, + /// + pub(super) must_use: Option, + pub(super) body: Box, + pub(super) output: syn::ReturnType, +} + +pub(super) struct FinishFnParams { + pub(super) ident: syn::Ident, + + /// Visibility override specified by the user + pub(super) vis: Option, + + pub(super) attrs: Vec, + pub(super) unsafety: Option, + pub(super) asyncness: Option, + pub(super) must_use: Option, + pub(super) body: Box, + pub(super) output: syn::ReturnType, +} + +pub(super) struct StartFn { + pub(super) ident: syn::Ident, + pub(super) vis: syn::Visibility, + + /// Additional attributes to apply to the item + pub(super) attrs: Vec, + + /// Overrides the default generics + pub(super) generics: Option, +} + +pub(super) struct StartFnParams { + pub(super) ident: syn::Ident, + + /// If present overrides the default visibility derived from the builder's type. + pub(super) vis: Option, + + /// Additional attributes to apply to the item + pub(super) attrs: Vec, + + /// Overrides the default generics + pub(super) generics: Option, +} + +pub(super) struct BuilderType { + pub(super) ident: syn::Ident, + + /// Visibility of the builder module itself. + pub(super) vis: syn::Visibility, + + pub(super) derives: BuilderDerives, + pub(super) docs: Vec, +} + +pub(super) struct BuilderTypeParams { + pub(super) ident: syn::Ident, + pub(super) vis: Option, + pub(super) derives: BuilderDerives, + pub(super) docs: Option>, +} + +pub(super) struct StateMod { + pub(super) ident: syn::Ident, + + /// Visibility of the builder module itself. + pub(super) vis: syn::Visibility, + + /// Visibility equivalent to the [`Self::vis`], but for items + /// generated inside the builder child module. + pub(super) vis_child: syn::Visibility, + + /// Visibility equivalent to the [`Self::vis_child`], but for items + /// generated inside one more level of nesting in the builder child module. + pub(super) vis_child_child: syn::Visibility, + + pub(super) docs: Vec, +} + +pub(super) struct Generics { + pub(super) where_clause: Option, + + /// Original generics that may contain default values in them. This is only + /// suitable for use in places where default values for generic parameters + /// are allowed. + pub(super) decl_with_defaults: Vec, + + /// Generic parameters without default values in them. This is suitable for + /// use as generics in function signatures or impl blocks. + pub(super) decl_without_defaults: Vec, + + /// Mirrors the `decl` representing how generic params should be represented + /// when these parameters are passed through as arguments in a turbofish. + pub(super) args: Vec, +} + +pub(crate) struct BuilderGenCtx { + /// Private identifiers that are used in the builder implementation. + /// They are intentionally randomized to prevent users from accessing them. + pub(super) ident_pool: PrivateIdentsPool, + + /// Name of the generic variable that holds the builder's state. + pub(super) state_var: syn::Ident, + + pub(super) members: Vec, + + /// Lint suppressions from the original item that will be inherited by all items + /// generated by the macro. If the original syntax used `#[expect(...)]`, + /// then it must be represented as `#[allow(...)]` here. + pub(super) allow_attrs: Vec, + pub(super) on_params: Vec, + + pub(super) generics: Generics, + + pub(super) assoc_method_ctx: Option, + + pub(super) builder_type: BuilderType, + pub(super) state_mod: StateMod, + pub(super) start_fn: StartFn, + pub(super) finish_fn: FinishFn, +} + +/// Identifiers that are private to the builder implementation. The users should +/// not use them directly. They are randomly generated to prevent users from +/// using them. Even if they try to reference them, they won't be able to re-compile +/// their code because the names of these identifiers are regenerated on every +/// macro run. +/// +/// This is an unfortunate workaround due to the limitations of defining the +/// builder type inside of a nested module. See more details on this problem in +/// +pub(super) struct PrivateIdentsPool { + pub(super) phantom: syn::Ident, + pub(super) receiver: syn::Ident, + pub(super) start_fn_args: syn::Ident, + pub(super) named_members: syn::Ident, +} + +pub(super) struct BuilderGenCtxParams<'a> { + pub(super) namespace: Cow<'a, GenericsNamespace>, + pub(super) members: Vec, + + pub(super) allow_attrs: Vec, + pub(super) on_params: Vec, + + /// This is the visibility of the original item that the builder is generated for. + /// For example, the `struct` or `fn` item visibility that the `#[builder]` or + /// `#[derive(Builder)]` attribute is applied to. + /// + /// It is used as the default visibility for all the generated items unless + /// explicitly overridden at a more specific level. + pub(super) orig_item_vis: syn::Visibility, + + /// Generics to apply to the builder type. + pub(super) generics: Generics, + + pub(super) assoc_method_ctx: Option, + + pub(super) builder_type: BuilderTypeParams, + pub(super) state_mod: ItemParams, + pub(super) start_fn: StartFnParams, + pub(super) finish_fn: FinishFnParams, +} + +impl BuilderGenCtx { + pub(super) fn new(params: BuilderGenCtxParams<'_>) -> Result { + let BuilderGenCtxParams { + namespace, + members, + allow_attrs, + on_params, + generics, + orig_item_vis, + assoc_method_ctx, + builder_type, + state_mod, + start_fn, + finish_fn, + } = params; + + let builder_type = BuilderType { + ident: builder_type.ident, + vis: builder_type.vis.unwrap_or(orig_item_vis), + derives: builder_type.derives, + docs: builder_type.docs.unwrap_or_else(|| { + let doc = format!( + "Use builder syntax to set the inputs and finish with [`{0}()`](Self::{0}()).", + finish_fn.ident + ); + + vec![syn::parse_quote! { + #[doc = #doc] + }] + }), + }; + + let state_mod = { + let is_ident_overridden = state_mod.name.is_some(); + let ident = state_mod + .name + .map(SpannedKey::into_value) + .unwrap_or_else(|| builder_type.ident.pascal_to_snake_case()); + + if builder_type.ident == ident { + if is_ident_overridden { + bail!( + &ident, + "the builder module name must be different from the builder type name" + ) + } + + bail!( + &builder_type.ident, + "couldn't infer the builder module name that doesn't conflict with \ + the builder type name; by default, the builder module name is set \ + to a snake_case equivalent of the builder type name; the snake_case \ + conversion doesn't produce a different name for this builder type \ + name; consider using PascalCase for the builder type name or specify \ + a separate name for the builder module explicitly via \ + `#[builder(state_mod = {{new_name}})]`" + ); + } + + // The builder module is private by default, meaning all symbols under + // that module can't be accessed from outside the module where the builder + // is defined. This makes the builder type signature unnamable from outside + // the module where we output the builder. The users need to explicitly + // opt-in to make the builder module public. + let vis = state_mod + .vis + .map(SpannedKey::into_value) + .unwrap_or_else(|| syn::Visibility::Inherited); + + // The visibility for child items is based on the visibility of the + // builder type itself, because the types and traits from this module + // are part of the builder's generic type state parameter signature. + let vis_child = builder_type.vis.clone().into_equivalent_in_child_module()?; + let vis_child_child = vis_child.clone().into_equivalent_in_child_module()?; + + StateMod { + vis, + vis_child, + vis_child_child, + + ident, + + docs: state_mod + .docs + .map(SpannedKey::into_value) + .unwrap_or_else(|| { + let docs = format!( + "Tools for manipulating the type state of [`{}`].", + builder_type.ident + ); + + vec![syn::parse_quote!(#[doc = #docs])] + }), + } + }; + + let start_fn = StartFn { + ident: start_fn.ident, + vis: start_fn.vis.unwrap_or_else(|| builder_type.vis.clone()), + attrs: start_fn.attrs, + generics: start_fn.generics, + }; + + let finish_fn = FinishFn { + ident: finish_fn.ident, + vis: finish_fn.vis.unwrap_or_else(|| builder_type.vis.clone()), + attrs: finish_fn.attrs, + unsafety: finish_fn.unsafety, + asyncness: finish_fn.asyncness, + must_use: finish_fn.must_use, + body: finish_fn.body, + output: finish_fn.output, + }; + + let state_var = { + let possible_names = ["S", "State", "BuilderState"]; + possible_names + .iter() + .find(|&&candidate| !namespace.idents.contains(candidate)) + .map(|&name| syn::Ident::new(name, Span::call_site())) + .unwrap_or_else(|| namespace.unique_ident(format!("{}_", possible_names[0]))) + }; + + Ok(Self { + state_var, + ident_pool: PrivateIdentsPool::new(), + members, + allow_attrs, + on_params, + generics, + assoc_method_ctx, + builder_type, + state_mod, + start_fn, + finish_fn, + }) + } +} + +impl PrivateIdentsPool { + fn new() -> Self { + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + + // Thanks @orhun for the article https://blog.orhun.dev/zero-deps-random-in-rust/ + let random = RandomState::new().build_hasher().finish(); + + // Totally random words. All coincidences are purely accidental 😸 + let random_words = [ + "amethyst", + "applejack", + "blackjack", + "bon", + "cadance", + "celestia", + "cheerilee", + "derpy", + "fleetfoot", + "flitter", + "fluttershy", + "izzy", + "lilly", + "littlepip", + "luna", + "lyra", + "maud", + "minuette", + "octavia", + "pinkie", + "pipp", + "rainbow", + "rampage", + "rarity", + "roseluck", + "scootaloo", + "seaswirl", + "spitfire", + "starlight", + "sunset", + "sweetie", + "trixie", + "twilight", + "twinkleshine", + "twist", + "velvet", + "vinyl", + ]; + + #[allow(clippy::cast_possible_truncation)] + let random_word = random_words[(random % (random_words.len() as u64)) as usize]; + + let ident = |name: &str| format_ident!("__private_{random_word}_{name}"); + + Self { + phantom: ident("phantom"), + receiver: ident("receiver"), + start_fn_args: ident("start_fn_args"), + named_members: ident("named_members"), + } + } +} + +impl Generics { + pub(super) fn new( + decl_with_defaults: Vec, + where_clause: Option, + ) -> Self { + let decl_without_defaults = decl_with_defaults + .iter() + .cloned() + .map(|mut param| { + match &mut param { + syn::GenericParam::Type(param) => { + param.default = None; + } + syn::GenericParam::Const(param) => { + param.default = None; + } + syn::GenericParam::Lifetime(_) => {} + } + param + }) + .collect(); + + let args = decl_with_defaults + .iter() + .map(syn::GenericParam::to_generic_argument) + .collect(); + + Self { + where_clause, + decl_with_defaults, + decl_without_defaults, + args, + } + } + + pub(super) fn where_clause_predicates(&self) -> impl Iterator { + self.where_clause + .as_ref() + .into_iter() + .flat_map(|clause| &clause.predicates) + } +} diff --git a/bon-macros/src/builder/builder_gen/setter_methods.rs b/bon-macros/src/builder/builder_gen/setter_methods.rs deleted file mode 100644 index 3c48435c..00000000 --- a/bon-macros/src/builder/builder_gen/setter_methods.rs +++ /dev/null @@ -1,242 +0,0 @@ -use super::{BuilderGenCtx, NamedMember}; -use crate::util::prelude::*; -use quote::quote; - -/// Specifies the return type of the setter method. It is conditioned by the -/// `cfg(doc)`. If `cfg(doc)` is enabled, we want to generate a shorter type -/// signature that doesn't clutter the docs. -/// -/// However, such type signature uses an associated type of a trait which makes -/// it much slower to compile when the code is built outside of `rustdoc`. -/// -/// So the `doc_false` variant is used as an easier to compile alternative, -/// but still the equivalent of the same return type. -pub(crate) struct SettersReturnType { - pub(crate) doc_true: TokenStream2, - pub(crate) doc_false: TokenStream2, -} - -pub(crate) struct MemberSettersCtx<'a> { - builder_gen: &'a BuilderGenCtx, - member: &'a NamedMember, - return_type: SettersReturnType, -} - -impl<'a> MemberSettersCtx<'a> { - pub(crate) fn new( - builder_gen: &'a BuilderGenCtx, - member: &'a NamedMember, - return_type: SettersReturnType, - ) -> Self { - Self { - builder_gen, - member, - return_type, - } - } - - pub(crate) fn setter_methods(&self) -> Result { - let member_type = self.member.norm_ty.as_ref(); - - if let Some(inner_type) = self.member.as_optional_norm_ty() { - return self.setters_for_optional_member(inner_type); - } - - let has_into = self.member.param_into(&self.builder_gen.on_params)?; - - let (fn_param_type, maybe_into_call) = if has_into { - (quote!(impl Into<#member_type>), quote!(.into())) - } else { - (quote!(#member_type), quote!()) - }; - - Ok(self.setter_method(MemberSetterMethod { - method_name: self.member.setter_method_core_name().clone(), - fn_params: quote!(value: #fn_param_type), - overwrite_docs: None, - body: SetterBody::Default { - member_init: quote!(::bon::private::Set(value #maybe_into_call)), - }, - })) - } - - fn setters_for_optional_member(&self, inner_type: &syn::Type) -> Result { - let has_into = self.member.param_into(&self.builder_gen.on_params)?; - let (inner_type, maybe_map_conv_call) = if has_into { - (quote!(impl Into<#inner_type>), quote!(.map(Into::into))) - } else { - (quote!(#inner_type), quote!()) - }; - - let setter_method_name = self.member.setter_method_core_name().clone(); - - // Preserve the original identifier span to make IDE's "go to definition" work correctly - let option_method_name = syn::Ident::new( - &format!("maybe_{}", setter_method_name.raw_name()), - setter_method_name.span(), - ); - - // Option-less setter is just a shortcut for wrapping the value in `Some`. - let optionless_setter_body = quote! { - self.#option_method_name(Some(value)) - }; - - let methods = [ - MemberSetterMethod { - method_name: option_method_name, - fn_params: quote!(value: Option<#inner_type>), - overwrite_docs: Some(format!( - "Same as [`Self::{setter_method_name}`], but accepts \ - an `Option` as input. See that method's documentation for \ - more details.", - )), - body: SetterBody::Default { - member_init: quote!(::bon::private::Set(value #maybe_map_conv_call)), - }, - }, - // We intentionally keep the name and signature of the setter method - // for an optional member that accepts the value under the option the - // same as the setter method for the required member to keep the API - // of the builder compatible when a required member becomes optional. - // To be able to explicitly pass an `Option` value to the setter method - // users need to use the `maybe_{member_ident}` method. - MemberSetterMethod { - method_name: setter_method_name, - fn_params: quote!(value: #inner_type), - overwrite_docs: None, - body: SetterBody::Custom(optionless_setter_body), - }, - ]; - - Ok(methods - .into_iter() - .map(|method| self.setter_method(method)) - .collect()) - } - - fn setter_method(&self, method: MemberSetterMethod) -> TokenStream2 { - let MemberSetterMethod { - method_name, - fn_params, - overwrite_docs, - body, - } = method; - - let docs = match overwrite_docs { - Some(docs) => vec![syn::parse_quote!(#[doc = #docs])], - None if !self.member.docs.is_empty() => self.member.docs.clone(), - None => self.generate_docs_for_setter(), - }; - - let vis = &self.builder_gen.vis; - - let body = match body { - SetterBody::Custom(body) => body, - SetterBody::Default { member_init } => { - let maybe_receiver_field = self - .builder_gen - .receiver() - .map(|_| quote!(__private_receiver: self.__private_receiver,)); - - let maybe_start_fn_args_field = self - .builder_gen - .start_fn_args() - .next() - .map(|_| quote!(__private_start_fn_args: self.__private_start_fn_args,)); - - let builder_ident = &self.builder_gen.builder_type.ident; - - let member_exprs = self.builder_gen.named_members().map(|other_member| { - if other_member.norm_ident == self.member.norm_ident { - return member_init.clone(); - } - let index = &other_member.index; - quote!(self.__private_named_members.#index) - }); - - quote! { - #builder_ident { - __private_phantom: ::core::marker::PhantomData, - #maybe_receiver_field - #maybe_start_fn_args_field - __private_named_members: (#( #member_exprs, )*) - } - } - } - }; - - let member_state_type = &self.member.generic_var_ident; - let SettersReturnType { - doc_true: ret_doc_true, - doc_false: ret_doc_false, - } = &self.return_type; - - quote! { - #( #docs )* - #[allow( - // This is intentional. We want the builder syntax to compile away - clippy::inline_always, - // We don't want to avoid using `impl Trait` in the setter. This way - // the setter signature is easier to read, and anyway if you want to - // specify a type hint for the method that accepts an `impl Into`, then - // your design of this setter already went wrong. - clippy::impl_trait_in_params - )] - #[inline(always)] - // The `cfg_attr` condition is for `doc`, so we don't pay the price - // if invoking the `__return_type` macro in the usual case when the - // code is compiled outside of `rustdoc`. - #[cfg_attr(doc, bon::__return_type(#ret_doc_true))] - #vis fn #method_name(self, #fn_params) -> #ret_doc_false - where - #member_state_type: ::bon::private::IsUnset, - { - #body - } - } - } - - fn generate_docs_for_setter(&self) -> Vec { - let setter_core_name = self.member.setter_method_core_name(); - let start_fn_ident = &self.builder_gen.start_func.ident; - - let more = |start_fn_path: &std::fmt::Arguments<'_>| { - format!(" See {start_fn_path} for more info.") - }; - - let suffix = self - .builder_gen - .assoc_method_ctx - .as_ref() - .map(|assoc_ctx| { - let ty_path = match assoc_ctx.self_ty.as_path() { - Some(ty_path) => ty_path, - - // The type is quite complex. It's hard to generate a workable - // intra-doc link for it. So in order to avoid the broken - // intra-doc links lint we'll just skip adding more info. - _ => return String::new(), - }; - - let prefix = darling::util::path_to_string(&ty_path.path); - more(&format_args!("[`{prefix}::{start_fn_ident}()`]")) - }) - .unwrap_or_else(|| more(&format_args!("[`{start_fn_ident}()`]"))); - - let docs = format!("Sets the value of `{setter_core_name}`.{suffix}"); - - vec![syn::parse_quote!(#[doc = #docs])] - } -} - -enum SetterBody { - Custom(TokenStream2), - Default { member_init: TokenStream2 }, -} - -struct MemberSetterMethod { - method_name: syn::Ident, - fn_params: TokenStream2, - overwrite_docs: Option, - body: SetterBody, -} diff --git a/bon-macros/src/builder/builder_gen/setters/mod.rs b/bon-macros/src/builder/builder_gen/setters/mod.rs new file mode 100644 index 00000000..dab66e3d --- /dev/null +++ b/bon-macros/src/builder/builder_gen/setters/mod.rs @@ -0,0 +1,534 @@ +use super::member::SetterClosure; +use super::{BuilderGenCtx, NamedMember}; +use crate::parsing::ItemParams; +use crate::util::prelude::*; +use std::iter; + +pub(crate) struct SettersCtx<'a> { + base: &'a BuilderGenCtx, + member: &'a NamedMember, +} + +impl<'a> SettersCtx<'a> { + pub(crate) fn new(base: &'a BuilderGenCtx, member: &'a NamedMember) -> Self { + Self { base, member } + } + + pub(crate) fn setter_methods(&self) -> TokenStream { + match SettersItems::new(self) { + SettersItems::Required(item) => self.setter_for_required_member(item), + SettersItems::Optional(setters) => self.setters_for_optional_member(setters), + } + } + + fn setter_for_required_member(&self, item: SetterItem) -> TokenStream { + let input; + let expr; + + let member_type = self.member.ty.norm.as_ref(); + + if let Some(closure) = &self.member.params.with { + input = Self::underlying_input_from_closure(closure); + expr = self.member_expr_from_closure(closure); + } else if self.member.params.into.is_present() { + input = quote!(value: impl Into<#member_type>); + expr = quote!(Into::into(value)); + } else { + input = quote!(value: #member_type); + expr = quote!(value); + }; + + let body = SetterBody::SetMember { + expr: quote!(::core::option::Option::Some(#expr)), + }; + + self.setter_method(Setter { + item, + imp: SetterImpl { input, body }, + }) + } + + fn setters_for_optional_member(&self, items: OptionalSettersItems) -> TokenStream { + if let Some(closure) = &self.member.params.with { + return self.setters_for_optional_member_with_closure(closure, items); + } + + let underlying_ty = self.member.underlying_norm_ty(); + let underlying_ty = if self.member.params.into.is_present() { + quote!(impl Into<#underlying_ty>) + } else { + quote!(#underlying_ty) + }; + + let some_fn = Setter { + item: items.some_fn, + imp: SetterImpl { + input: quote!(value: #underlying_ty), + body: SetterBody::Forward { + body: { + let option_fn_name = &items.option_fn.name; + quote! { + self.#option_fn_name(Some(value)) + } + }, + }, + }, + }; + + let option_fn = Setter { + item: items.option_fn, + imp: SetterImpl { + input: quote!(value: Option<#underlying_ty>), + body: SetterBody::SetMember { + expr: if self.member.params.into.is_present() { + quote! { + Option::map(value, Into::into) + } + } else { + quote!(value) + }, + }, + }, + }; + + [self.setter_method(some_fn), self.setter_method(option_fn)].concat() + } + + fn setters_for_optional_member_with_closure( + &self, + closure: &SetterClosure, + items: OptionalSettersItems, + ) -> TokenStream { + let idents = closure.inputs.iter().map(|input| &input.pat.ident); + + // If the closure accepts just a single input avoid wrapping it + // in a tuple in the `option_fn` setter. + let tuple_if_many = |val: TokenStream| -> TokenStream { + if closure.inputs.len() == 1 { + val + } else { + quote!((#val)) + } + }; + + let ident_maybe_tuple = tuple_if_many(quote!( #( #idents ),* )); + + let some_fn = Setter { + item: items.some_fn, + imp: SetterImpl { + input: Self::underlying_input_from_closure(closure), + body: SetterBody::Forward { + body: { + let option_fn_name = &items.option_fn.name; + quote! { + self.#option_fn_name(Some(#ident_maybe_tuple)) + } + }, + }, + }, + }; + + let option_fn_impl = SetterImpl { + input: { + let input_types = closure.inputs.iter().map(|input| &input.ty); + let input_types = tuple_if_many(quote!(#( #input_types, )*)); + quote!(value: Option<#input_types>) + }, + body: SetterBody::SetMember { + expr: { + let expr = self.member_expr_from_closure(closure); + quote! { + // Not using `Option::map` here because the `#expr` + // can contain a `?` operator for a fallible operation. + match value { + Some(#ident_maybe_tuple) => Some(#expr), + None => None, + } + } + }, + }, + }; + + let option_fn = Setter { + item: items.option_fn, + imp: option_fn_impl, + }; + + [self.setter_method(some_fn), self.setter_method(option_fn)].concat() + } + + /// This method is reused between the setter for the required member and + /// the `some_fn` setter for the optional member. + /// + /// We intentionally keep the name and signature of the setter method + /// for an optional member that accepts the value under the option the + /// same as the setter method for the required member to keep the API + /// of the builder compatible when a required member becomes optional. + /// To be able to explicitly pass an `Option` value to the setter method + /// users need to use the `maybe_{member_ident}` method. + fn underlying_input_from_closure(closure: &SetterClosure) -> TokenStream { + let pats = closure.inputs.iter().map(|input| &input.pat); + let types = closure.inputs.iter().map(|input| &input.ty); + quote! { + #( #pats: #types ),* + } + } + + fn member_expr_from_closure(&self, closure: &SetterClosure) -> TokenStream { + let body = &closure.body; + + let ty = self.member.underlying_norm_ty().to_token_stream(); + + let output = Self::maybe_wrap_in_result(closure, ty); + + // Avoid wrapping the body in a block if it's already a block. + let body = if matches!(body.as_ref(), syn::Expr::Block(_)) { + body.to_token_stream() + } else { + quote!({ #body }) + }; + + let question_mark = closure + .output + .is_some() + .then(|| syn::Token![?](Span::call_site())); + + quote! { + (move || -> #output #body)() #question_mark + } + } + + fn maybe_wrap_in_result(closure: &SetterClosure, ty: TokenStream) -> TokenStream { + let output = match closure.output.as_ref() { + Some(output) => output, + None => return ty, + }; + let result_path = &output.result_path; + let err_ty = output.err_ty.iter(); + quote! { + #result_path< #ty #(, #err_ty )* > + } + } + + fn setter_method(&self, setter: Setter) -> TokenStream { + let Setter { item, imp } = setter; + + let maybe_mut = match imp.body { + SetterBody::Forward { .. } => None, + SetterBody::SetMember { .. } => Some(syn::Token![mut](Span::call_site())), + }; + + let body = match imp.body { + SetterBody::Forward { body } => body, + SetterBody::SetMember { expr } => { + let idents = &self.base.ident_pool; + let phantom_field = &idents.phantom; + let receiver_field = &idents.receiver; + let start_fn_args_field = &idents.start_fn_args; + let named_members_field = &idents.named_members; + + let mut output = if !self.member.is_stateful() { + quote! { + self + } + } else { + let builder_ident = &self.base.builder_type.ident; + + let maybe_receiver_field = self + .base + .receiver() + .map(|_| quote!(#receiver_field: self.#receiver_field,)); + + let maybe_start_fn_args_field = self + .base + .start_fn_args() + .next() + .map(|_| quote!(#start_fn_args_field: self.#start_fn_args_field,)); + + quote! { + #builder_ident { + #phantom_field: ::core::marker::PhantomData, + #maybe_receiver_field + #maybe_start_fn_args_field + #named_members_field: self.#named_members_field, + } + } + }; + + let result_output = self + .member + .params + .with + .as_ref() + .and_then(|closure| closure.output.as_ref()); + + if let Some(result_output) = result_output { + let result_path = &result_output.result_path; + output = quote!(#result_path::Ok(#output)); + } + + let index = &self.member.index; + quote! { + self.#named_members_field.#index = #expr; + #output + } + } + }; + + let state_mod = &self.base.state_mod.ident; + + let mut return_type = if !self.member.is_stateful() { + quote! { Self } + } else { + let state_transition = format_ident!("Set{}", self.member.name.pascal_str); + let builder_ident = &self.base.builder_type.ident; + let generic_args = &self.base.generics.args; + let state_var = &self.base.state_var; + + quote! { + #builder_ident<#(#generic_args,)* #state_mod::#state_transition<#state_var>> + } + }; + + if let Some(closure) = &self.member.params.with { + return_type = Self::maybe_wrap_in_result(closure, return_type); + } + + let where_clause = (!self.member.params.overwritable.is_present()).then(|| { + let state_var = &self.base.state_var; + let member_pascal = &self.member.name.pascal; + quote! { + where #state_var::#member_pascal: #state_mod::IsUnset, + } + }); + + let SetterItem { name, vis, docs } = item; + let input = imp.input; + + quote! { + #( #docs )* + #[allow( + // This is intentional. We want the builder syntax to compile away + clippy::inline_always, + // We don't want to avoid using `impl Trait` in the setter. This way + // the setter signature is easier to read, and anyway if you want to + // specify a type hint for the method that accepts an `impl Into`, then + // your design of this setter already went wrong. + clippy::impl_trait_in_params, + clippy::missing_const_for_fn, + )] + #[inline(always)] + #vis fn #name(#maybe_mut self, #input) -> #return_type + #where_clause + { + #body + } + } + } +} + +struct Setter { + item: SetterItem, + imp: SetterImpl, +} + +struct SetterImpl { + input: TokenStream, + body: SetterBody, +} + +enum SetterBody { + /// The setter forwards the call to another method. + Forward { body: TokenStream }, + + /// The setter sets the member as usual and transitions the builder state. + SetMember { expr: TokenStream }, +} + +enum SettersItems { + Required(SetterItem), + Optional(OptionalSettersItems), +} + +struct OptionalSettersItems { + some_fn: SetterItem, + option_fn: SetterItem, +} + +struct SetterItem { + name: syn::Ident, + vis: syn::Visibility, + docs: Vec, +} + +impl SettersItems { + fn new(ctx: &SettersCtx<'_>) -> Self { + let SettersCtx { member, base } = ctx; + let builder_type = &base.builder_type; + + let params = member.params.setters.as_ref(); + + let common_name = params.and_then(|params| params.name.as_deref()); + let common_vis = params.and_then(|params| params.vis.as_deref()); + let common_docs = params.and_then(|params| params.docs.as_deref().map(Vec::as_slice)); + + let doc = |docs: &str| iter::once(syn::parse_quote!(#[doc = #docs])); + + if member.is_required() { + let docs = common_docs.unwrap_or(&member.docs); + + let header = "\ + | **Required** |\n\ + | -- |\n\n"; + + let docs = doc(header).chain(docs.iter().cloned()).collect(); + + return Self::Required(SetterItem { + name: common_name.unwrap_or(&member.name.snake).clone(), + vis: common_vis.unwrap_or(&builder_type.vis).clone(), + docs, + }); + } + + let some_fn = params.and_then(|params| params.fns.some_fn.as_deref()); + let some_fn_name = some_fn + .and_then(ItemParams::name) + .or(common_name) + .unwrap_or(&member.name.snake) + .clone(); + + let option_fn = params.and_then(|params| params.fns.option_fn.as_deref()); + let option_fn_name = option_fn + .and_then(ItemParams::name) + .cloned() + .unwrap_or_else(|| { + let base_name = common_name.unwrap_or(&member.name.snake); + // It's important to preserve the original identifier span + // to make IDE's "go to definition" work correctly. It's so + // important that this doesn't use `format_ident!`, but rather + // `syn::Ident::new` to set the span of the `Ident` explicitly. + syn::Ident::new(&format!("maybe_{base_name}"), base_name.span()) + }); + + let default = member.params.default.as_deref().and_then(|default| { + let default = default + .clone() + .or_else(|| well_known_default(&member.ty.norm)) + .unwrap_or_else(|| { + let ty = &member.ty.norm; + syn::parse_quote!(<#ty as Default>::default()) + }); + + let file = syn::parse_quote!(const _: () = #default;); + let file = prettyplease::unparse(&file); + + let begin = file.find('=')?; + let default = file.get(begin + 1..)?.trim(); + let default = default.strip_suffix(';')?; + + Some(default.to_owned()) + }); + + let default = default.as_deref(); + + // FIXME: the docs shouldn't reference the companion setter if that + // setter has a lower visibility. + let some_fn_docs = some_fn + .and_then(ItemParams::docs) + .or(common_docs) + .unwrap_or(&member.docs); + + let some_fn_docs = { + let header = optional_setter_docs(default, &option_fn_name, "accepts an `Option`"); + + doc(&header).chain(some_fn_docs.iter().cloned()).collect() + }; + + let option_fn_docs = option_fn + .and_then(ItemParams::docs) + .or(common_docs) + .unwrap_or(&member.docs); + + let option_fn_docs = { + let header = optional_setter_docs( + default, + &some_fn_name, + "wraps the value with `Some` internally", + ); + + doc(&header).chain(option_fn_docs.iter().cloned()).collect() + }; + + let some_fn = SetterItem { + name: some_fn_name, + vis: some_fn + .and_then(ItemParams::vis) + .or(common_vis) + .unwrap_or(&builder_type.vis) + .clone(), + + docs: some_fn_docs, + }; + + let option_fn = params.and_then(|params| params.fns.option_fn.as_deref()); + let option_fn = SetterItem { + name: option_fn_name, + + vis: option_fn + .and_then(ItemParams::vis) + .or(common_vis) + .unwrap_or(&builder_type.vis) + .clone(), + + docs: option_fn_docs, + }; + + Self::Optional(OptionalSettersItems { some_fn, option_fn }) + } +} + +fn optional_setter_docs( + default: Option<&str>, + other_setter: &syn::Ident, + description: &str, +) -> String { + let default = default + .map(|default| { + if default.contains('\n') || default.len() > 80 { + format!("**Default:**\n````rust,ignore\n{default}\n````\n\n") + } else { + format!("**Default:** ```{default}```.\n\n") + } + }) + .unwrap_or_default(); + + format!( + "| **Optional** |\n\ + | -- |\n\n\ + **See also** [`{other_setter}()`](Self::{other_setter}), which is a companion setter that {description}. + \n\n{default}", + ) +} + +fn well_known_default(ty: &syn::Type) -> Option { + let path = match ty { + syn::Type::Path(syn::TypePath { path, qself: None }) => path, + _ => return None, + }; + + use syn::parse_quote as pq; + + let ident = path.get_ident()?.to_string(); + + let value = match ident.as_str() { + "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" + | "isize" => pq!(0), + "f32" | "f64" => pq!(0.0), + "bool" => pq!(false), + "char" => pq!('\0'), + "String" => pq!(""), + _ => return None, + }; + + Some(value) +} diff --git a/bon-macros/src/builder/builder_gen/state_mod.rs b/bon-macros/src/builder/builder_gen/state_mod.rs new file mode 100644 index 00000000..ca093d26 --- /dev/null +++ b/bon-macros/src/builder/builder_gen/state_mod.rs @@ -0,0 +1,280 @@ +use super::BuilderGenCtx; +use crate::util::prelude::*; + +pub(super) struct StateModGenCtx<'a> { + base: &'a BuilderGenCtx, + stateful_members_snake: Vec<&'a syn::Ident>, + stateful_members_pascal: Vec<&'a syn::Ident>, + sealed_item_decl: TokenStream, + sealed_item_impl: TokenStream, +} + +impl<'a> StateModGenCtx<'a> { + pub(super) fn new(builder_gen: &'a BuilderGenCtx) -> Self { + Self { + base: builder_gen, + + stateful_members_snake: builder_gen + .stateful_members() + .map(|member| &member.name.snake) + .collect(), + + stateful_members_pascal: builder_gen + .stateful_members() + .map(|member| &member.name.pascal) + .collect(), + + // A const item in a trait makes it non-object safe, which is convenient, + // because we want that restriction in this case. + sealed_item_decl: quote! { + #[doc(hidden)] + const SEALED: sealed::Sealed; + }, + + sealed_item_impl: quote! { + const SEALED: sealed::Sealed = sealed::Sealed; + }, + } + } + + pub(super) fn state_mod(&self) -> TokenStream { + let vis = &self.base.state_mod.vis; + let vis_child = &self.base.state_mod.vis_child; + let vis_child_child = &self.base.state_mod.vis_child_child; + + let state_mod_docs = &self.base.state_mod.docs; + let state_mod_ident = &self.base.state_mod.ident; + + let state_trait = self.state_trait(); + let is_complete_trait = self.is_complete_trait(); + let members_names_mod = self.members_names_mod(); + let state_transitions = self.state_transitions(); + + quote! { + #[allow( + // These are intentional. By default, the builder module is private + // and can't be accessed outside of the module where the builder + // type is defined. This makes the builder type "anonymous" to + // the outside modules, which is a good thing if users don't want + // to expose this API surface. + // + // Also, there are some genuinely private items like the `Sealed` + // enum and members "name" enums that we don't want to expose even + // to the module that defines the builder. These APIs are not + // public, and users instead should only reference the traits + // and state transition type aliases from here. + unnameable_types, unreachable_pub, clippy::redundant_pub_crate + )] + #( #state_mod_docs )* + #vis mod #state_mod_ident { + #[doc(inline)] + #vis_child use ::bon::private::{IsSet, IsUnset}; + use ::bon::private::{Set, Unset}; + + mod sealed { + #vis_child_child struct Sealed; + } + + #state_trait + #is_complete_trait + #members_names_mod + #state_transitions + } + } + } + + fn state_transitions(&self) -> TokenStream { + // Not using `Iterator::zip` here to make it possible to scale this in + // case if we add more vecs here. We are not using `Itertools`, so + // its `multiunzip` is not available. + let mut set_members_structs = Vec::with_capacity(self.stateful_members_snake.len()); + let mut state_impls = Vec::with_capacity(self.stateful_members_snake.len()); + + let vis_child = &self.base.state_mod.vis_child; + let sealed_item_impl = &self.sealed_item_impl; + + for member in self.base.stateful_members() { + let member_pascal = &member.name.pascal; + + let docs = format!( + "Represents a [`State`] that has [`IsSet`] implemented for [`State::{member_pascal}`].\n\n\ + The state for all other members is left the same as in the input state.", + ); + + let struct_ident = format_ident!("Set{}", member.name.pascal_str); + + set_members_structs.push(quote! { + #[doc = #docs] + #vis_child struct #struct_ident( + // We `S` in an `fn() -> ...` to make the compiler think + // that the builder doesn't "own" an instance of `S`. + // This removes unnecessary requirements when evaluating the + // applicability of the auto traits. + ::core::marker::PhantomData S> + ); + }); + + let states = self.base.stateful_members().map(|other_member| { + if other_member.is(member) { + let member_snake = &member.name.snake; + quote! { + Set + } + } else { + let member_pascal = &other_member.name.pascal; + quote! { + S::#member_pascal + } + } + }); + + let stateful_members_pascal = &self.stateful_members_pascal; + + state_impls.push(quote! { + #[doc(hidden)] + impl State for #struct_ident { + #( + type #stateful_members_pascal = #states; + )* + #sealed_item_impl + } + }); + } + + let stateful_members_snake = &self.stateful_members_snake; + let stateful_members_pascal = &self.stateful_members_pascal; + + quote! { + /// Represents a [`State`] that has [`IsUnset`] implemented for all members. + /// + /// This is the initial state of the builder before any setters are called. + #vis_child struct Empty(()); + + #( #set_members_structs )* + + #[doc(hidden)] + impl State for Empty { + #( + type #stateful_members_pascal = Unset; + )* + #sealed_item_impl + } + + #( #state_impls )* + + } + } + + fn state_trait(&self) -> TokenStream { + let assoc_types_docs = self.stateful_members_snake.iter().map(|member_snake| { + format!( + "Type state of the member `{member_snake}`.\n\ + \n\ + It can implement either [`IsSet`] or [`IsUnset`]", + ) + }); + + let vis_child = &self.base.state_mod.vis_child; + let sealed_item_decl = &self.sealed_item_decl; + let stateful_members_pascal = &self.stateful_members_pascal; + + let docs_suffix = if stateful_members_pascal.is_empty() { + "" + } else { + "\ + \n\ + \n\ + You can use the associated types of this trait to control the state of individual members \ + with the [`IsSet`] and [`IsUnset`] traits. You can change the state of the members with \ + the `Set*` structs available in this module." + }; + + let docs = format!( + "Builder's type state specifies if members are set or not (unset).{docs_suffix}" + ); + + quote! { + #[doc = #docs] + #vis_child trait State: ::core::marker::Sized { + #( + #[doc = #assoc_types_docs] + type #stateful_members_pascal; + )* + #sealed_item_decl + } + } + } + + fn is_complete_trait(&self) -> TokenStream { + let required_members_pascal = self + .base + .named_members() + .filter(|member| member.is_required()) + .map(|member| &member.name.pascal) + .collect::>(); + + // Associated types bounds syntax that provides implied bounds for them + // is available only since Rust 1.79.0. So this is an opt-in feature that + // bumps the MSRV of the crate. See more details in the comment on this + // cargo feature's declaration in `bon/Cargo.toml`. + let maybe_assoc_type_bounds = cfg!(feature = "implied-bounds").then(|| { + quote! { + < #( #required_members_pascal: IsSet, )* > + } + }); + + let vis_child = &self.base.state_mod.vis_child; + let sealed_item_decl = &self.sealed_item_decl; + let sealed_item_impl = &self.sealed_item_impl; + + let builder_ident = &self.base.builder_type.ident; + let finish_fn = &self.base.finish_fn.ident; + + let docs = format!( + "Marker trait that indicates that all required members are set.\n\n\ + In this state, you can finish the building by calling the method \ + [`{builder_ident}::{finish_fn}()`]", + ); + + quote! { + #[doc = #docs] + #vis_child trait IsComplete: State #maybe_assoc_type_bounds { + #sealed_item_decl + } + + #[doc(hidden)] + impl IsComplete for S + where + #( + S::#required_members_pascal: IsSet, + )* + { + #sealed_item_impl + } + } + } + + fn members_names_mod(&self) -> TokenStream { + let vis_child_child = &self.base.state_mod.vis_child_child; + let stateful_members_snake = &self.stateful_members_snake; + + // The message is defined separately to make it single-line in the + // generated code. This simplifies the task of removing unnecessary + // attributes from the generated code when preparing for demo purposes. + let deprecated_msg = "\ + this should not be used directly; it is an implementation detail; \ + use the Set* type aliases to control the \ + state of members instead"; + + quote! { + #[deprecated = #deprecated_msg] + #[doc(hidden)] + #[allow(non_camel_case_types)] + mod members { + #( + #vis_child_child enum #stateful_members_snake {} + )* + } + } + } +} diff --git a/bon-macros/src/builder/item_fn.rs b/bon-macros/src/builder/item_fn.rs new file mode 100644 index 00000000..7f6735aa --- /dev/null +++ b/bon-macros/src/builder/item_fn.rs @@ -0,0 +1,46 @@ +use super::builder_gen::input_fn::{FnInputCtx, FnInputParams}; +use super::builder_gen::MacroOutput; +use crate::normalization::SyntaxVariant; +use crate::util::prelude::*; +use syn::visit_mut::VisitMut; + +pub(crate) fn generate( + params: FnInputParams, + orig_fn: syn::ItemFn, + namespace: &crate::normalization::GenericsNamespace, +) -> Result { + let mut norm_fn = orig_fn.clone(); + + crate::normalization::NormalizeLifetimes::new(namespace).visit_item_fn_mut(&mut norm_fn); + crate::normalization::NormalizeImplTraits::new(namespace).visit_item_fn_mut(&mut norm_fn); + + let fn_item = SyntaxVariant { + orig: orig_fn, + norm: norm_fn, + }; + + let ctx = FnInputCtx { + namespace, + fn_item, + impl_ctx: None, + params, + }; + + let adapted_fn = ctx.adapted_fn()?; + + let MacroOutput { + start_fn, + other_items, + } = ctx.into_builder_gen_ctx()?.output()?; + + Ok(quote! { + #start_fn + #other_items + + // Keep original function at the end. It seems like rust-analyzer + // does better job of highlighting syntax when it is here. Assuming + // this is because rust-analyzer prefers the last occurrence of the + // span when highlighting. + #adapted_fn + }) +} diff --git a/bon-macros/src/builder/item_func.rs b/bon-macros/src/builder/item_func.rs deleted file mode 100644 index d96e4d70..00000000 --- a/bon-macros/src/builder/item_func.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::builder_gen::input_func::{FuncInputCtx, FuncInputParams}; -use super::builder_gen::MacroOutput; -use crate::util::prelude::*; -use quote::quote; -use syn::visit_mut::VisitMut; - -pub(crate) fn generate(params: FuncInputParams, orig_func: syn::ItemFn) -> Result { - let mut norm_func = orig_func.clone(); - - crate::normalization::NormalizeLifetimes.visit_item_fn_mut(&mut norm_func); - crate::normalization::NormalizeImplTraits.visit_item_fn_mut(&mut norm_func); - - let ctx = FuncInputCtx { - orig_func, - norm_func, - impl_ctx: None, - params, - }; - - let adapted_func = ctx.adapted_func()?; - - let MacroOutput { - start_func, - other_items, - } = ctx.into_builder_gen_ctx()?.output()?; - - Ok(quote! { - #start_func - #other_items - - // Keep original function at the end. It seems like rust-analyzer - // does better job of highlighting syntax when it is here. Assuming - // this is because rust-analyzer prefers the last occurrence of the - // span when highlighting. - #adapted_func - }) -} diff --git a/bon-macros/src/builder/item_impl.rs b/bon-macros/src/builder/item_impl.rs index 1deec50e..09dd3da5 100644 --- a/bon-macros/src/builder/item_impl.rs +++ b/bon-macros/src/builder/item_impl.rs @@ -1,17 +1,21 @@ -use super::builder_gen::input_func::{FuncInputCtx, FuncInputParams, ImplCtx}; +use super::builder_gen::input_fn::{FnInputCtx, FnInputParams, ImplCtx}; +use crate::normalization::{GenericsNamespace, SyntaxVariant}; use crate::util::prelude::*; use darling::ast::NestedMeta; use darling::FromMeta; -use quote::quote; use std::rc::Rc; +use syn::visit::Visit; use syn::visit_mut::VisitMut; -pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result { +pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result { + let mut namespace = GenericsNamespace::default(); + namespace.visit_item_impl(&orig_impl_block); + if let Some((_, trait_path, _)) = &orig_impl_block.trait_ { bail!(trait_path, "Impls of traits are not supported yet"); } - let (builder_funcs, other_items): (Vec<_>, Vec<_>) = + let (builder_fns, other_items): (Vec<_>, Vec<_>) = orig_impl_block.items.into_iter().partition(|item| { let fn_item = match item { syn::ImplItem::Fn(fn_item) => fn_item, @@ -24,7 +28,7 @@ pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result Result Result Result norm_func, + let norm_fn = match norm_item { + syn::ImplItem::Fn(norm_fn) => norm_fn, _ => unreachable!(), }; - let orig_func = match orig_item { - syn::ImplItem::Fn(orig_func) => orig_func, + let orig_fn = match orig_item { + syn::ImplItem::Fn(orig_fn) => orig_fn, _ => unreachable!(), }; - let norm_func = impl_item_fn_into_fn_item(norm_func)?; - let orig_func = impl_item_fn_into_fn_item(orig_func)?; + let norm_fn = conv_impl_item_fn_into_fn_item(norm_fn)?; + let orig_fn = conv_impl_item_fn_into_fn_item(orig_fn)?; - let meta = orig_func + let meta = orig_fn .attrs .iter() .filter(|attr| attr.path().is_ident("builder")) .map(|attr| { + if let syn::Meta::List(_) = attr.meta { + crate::parsing::require_non_empty_paren_meta_list_or_name_value( + &attr.meta, + )?; + } let meta_list = darling::util::parse_attribute_to_meta_list(attr)?; NestedMeta::parse_meta_list(meta_list.tokens).map_err(Into::into) }) @@ -102,25 +114,27 @@ pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result>(); - let params = FuncInputParams::from_list(&meta)?; + let params = FnInputParams::from_list(&meta)?; + + let fn_item = SyntaxVariant { + orig: orig_fn, + norm: norm_fn, + }; - let ctx = FuncInputCtx { - orig_func, - norm_func, + let ctx = FnInputCtx { + namespace: &namespace, + fn_item, impl_ctx: Some(impl_ctx.clone()), params, }; - Result::<_>::Ok((ctx.adapted_func()?, ctx.into_builder_gen_ctx()?.output()?)) + Result::<_>::Ok((ctx.adapted_fn()?, ctx.into_builder_gen_ctx()?.output()?)) }) .collect::>>()?; - let new_impl_items = outputs.iter().flat_map(|(adapted_func, output)| { - let start_func = &output.start_func; - [ - syn::parse_quote!(#start_func), - syn::parse_quote!(#adapted_func), - ] + let new_impl_items = outputs.iter().flat_map(|(adapted_fn, output)| { + let start_fn = &output.start_fn; + [syn::parse_quote!(#start_fn), syn::parse_quote!(#adapted_fn)] }); norm_selfful_impl_block.items = other_items; @@ -134,7 +148,7 @@ pub(crate) fn generate(mut orig_impl_block: syn::ItemImpl) -> Result Result { +fn conv_impl_item_fn_into_fn_item(func: syn::ImplItemFn) -> Result { let syn::ImplItemFn { attrs, vis, diff --git a/bon-macros/src/builder/item_struct.rs b/bon-macros/src/builder/item_struct.rs index 3e252b71..64011623 100644 --- a/bon-macros/src/builder/item_struct.rs +++ b/bon-macros/src/builder/item_struct.rs @@ -1,18 +1,17 @@ use super::builder_gen::input_struct::StructInputCtx; use super::builder_gen::MacroOutput; use crate::util::prelude::*; -use quote::quote; -pub(crate) fn generate(orig_struct: syn::ItemStruct) -> Result { +pub(crate) fn generate(orig_struct: syn::ItemStruct) -> Result { let struct_ident = orig_struct.ident.clone(); let ctx = StructInputCtx::new(orig_struct)?; let MacroOutput { - mut start_func, + mut start_fn, other_items, } = ctx.into_builder_gen_ctx()?.output()?; - let impl_generics = std::mem::take(&mut start_func.sig.generics); + let impl_generics = std::mem::take(&mut start_fn.sig.generics); let (generics_decl, generic_args, where_clause) = impl_generics.split_for_impl(); @@ -21,7 +20,7 @@ pub(crate) fn generate(orig_struct: syn::ItemStruct) -> Result { impl #generics_decl #struct_ident #generic_args #where_clause { - #start_func + #start_fn } #other_items diff --git a/bon-macros/src/builder/mod.rs b/bon-macros/src/builder/mod.rs index 88bf3710..1108fde6 100644 --- a/bon-macros/src/builder/mod.rs +++ b/bon-macros/src/builder/mod.rs @@ -2,21 +2,21 @@ mod builder_gen; pub(crate) mod item_impl; -mod item_func; +mod item_fn; mod item_struct; -use crate::normalization::{ExpandCfg, ExpansionOutput}; +use crate::normalization::{ExpandCfg, ExpansionOutput, GenericsNamespace}; use crate::util; use crate::util::prelude::*; use darling::FromMeta; -use quote::quote; use syn::parse::Parser; +use syn::visit::Visit; -pub(crate) fn generate_from_derive(item: TokenStream2) -> TokenStream2 { +pub(crate) fn generate_from_derive(item: TokenStream) -> TokenStream { try_generate_from_derive(item).unwrap_or_else(Error::write_errors) } -fn try_generate_from_derive(item: TokenStream2) -> Result { +fn try_generate_from_derive(item: TokenStream) -> Result { match syn::parse2(item)? { syn::Item::Struct(item_struct) => item_struct::generate(item_struct), _ => bail!( @@ -26,31 +26,16 @@ fn try_generate_from_derive(item: TokenStream2) -> Result { } } -pub(crate) fn generate_from_attr(params: TokenStream2, item: TokenStream2) -> TokenStream2 { - try_generate_from_attr(params.clone(), item.clone()).unwrap_or_else(|err| { - [ - generate_completion_triggers(params), - crate::error::error_into_token_stream(err, item), - ] - .concat() +pub(crate) fn generate_from_attr(params: TokenStream, item: TokenStream) -> TokenStream { + crate::error::with_fallback(item.clone(), || { + try_generate_from_attr(params.clone(), item) }) + .unwrap_or_else(|fallback| [generate_completion_triggers(params), fallback].concat()) } -fn try_generate_from_attr(params: TokenStream2, item: TokenStream2) -> Result { +fn try_generate_from_attr(params: TokenStream, item: TokenStream) -> Result { let item: syn::Item = syn::parse2(item)?; - if let syn::Item::Struct(item_struct) = item { - return Ok(quote! { - // Triggers a deprecation warning if the user is using the old attribute - // syntax on the structs instead of the derive syntax. - use ::bon::private::deprecations::builder_attribute_on_a_struct as _; - - #[derive(::bon::Builder)] - #[builder(#params)] - #item_struct - }); - } - let macro_path = syn::parse_quote!(::bon::builder); let ctx = ExpandCfg { @@ -67,7 +52,21 @@ fn try_generate_from_attr(params: TokenStream2, item: TokenStream2) -> Result item_func::generate(FromMeta::from_list(nested_meta)?, item_fn)?, + syn::Item::Fn(item_fn) => { + let mut namespace = GenericsNamespace::default(); + + namespace.visit_token_stream(params.clone()); + namespace.visit_item_fn(&item_fn); + + item_fn::generate(FromMeta::from_list(nested_meta)?, item_fn, &namespace)? + } + syn::Item::Struct(struct_item) => { + bail!( + &struct_item.struct_token, + "to generate a builder for a struct, use `#[derive(bon::Builder)]` instead; \ + `#[bon::builder]` syntax is supported only for functions starting with bon v3" + ) + } _ => bail!( &Span::call_site(), "only `fn` items are supported by the `#[bon::builder]` attribute" @@ -79,7 +78,7 @@ fn try_generate_from_attr(params: TokenStream2, item: TokenStream2) -> Result TokenStream2 { +fn generate_completion_triggers(params: TokenStream) -> TokenStream { let meta = util::ide::parse_comma_separated_meta .parse2(params) .unwrap_or_default(); diff --git a/bon-macros/src/collections/map.rs b/bon-macros/src/collections/map.rs index fbb73764..eb195a8e 100644 --- a/bon-macros/src/collections/map.rs +++ b/bon-macros/src/collections/map.rs @@ -1,5 +1,4 @@ use crate::util::prelude::*; -use quote::quote; use syn::parse::ParseStream; use syn::punctuated::Punctuated; use syn::{Expr, Token}; @@ -18,7 +17,7 @@ fn parse_map_pair(pair: ParseStream<'_>) -> Result<(Expr, Expr), syn::Error> { Ok((key, value)) } -pub(crate) fn generate(entries: Punctuated<(Expr, Expr), Token![,]>) -> TokenStream2 { +pub(crate) fn generate(entries: Punctuated<(Expr, Expr), Token![,]>) -> TokenStream { let error = super::validate_expressions_are_unique("key in the map", entries.iter().map(|(k, _)| k)); diff --git a/bon-macros/src/collections/set.rs b/bon-macros/src/collections/set.rs index e9711483..175d8edc 100644 --- a/bon-macros/src/collections/set.rs +++ b/bon-macros/src/collections/set.rs @@ -1,9 +1,8 @@ use crate::util::prelude::*; -use quote::quote; use syn::punctuated::Punctuated; use syn::{Expr, Token}; -pub(crate) fn generate(entries: Punctuated) -> TokenStream2 { +pub(crate) fn generate(entries: Punctuated) -> TokenStream { let error = super::validate_expressions_are_unique("value in the set", &entries); let entries = entries.into_iter(); let output = quote! { diff --git a/bon-macros/src/error.rs b/bon-macros/src/error.rs index 00b9605f..a66a3194 100644 --- a/bon-macros/src/error.rs +++ b/bon-macros/src/error.rs @@ -1,17 +1,55 @@ use crate::util::prelude::*; -use proc_macro2::{TokenStream as TokenStream2, TokenTree}; -use quote::{quote, ToTokens}; +use proc_macro2::{Group, TokenTree}; +use std::panic::AssertUnwindSafe; use syn::parse::Parse; -/// Handle the error returned from the macro logic. This may be either a syntax -/// error or a logic error. In either case, we want to return a [`TokenStream2`] -/// that still provides good IDE experience. See [`Fallback`] for details. -pub(crate) fn error_into_token_stream(err: Error, item: TokenStream2) -> TokenStream2 { - let compile_error = err.write_errors(); +/// Handle the error or panic returned from the macro logic. +/// +/// The error may be either a syntax error or a logic error. In either case, we +/// want to return a [`TokenStream`] that still provides good IDE experience. +/// See [`Fallback`] for details. +/// +/// This function also catches panics. Importantly, we don't use panics for error +/// handling! A panic is always a bug! However, we still handle it to provide +/// better IDE experience even if there are some bugs in the macro implementation. +/// +/// One known bug that may cause panics when using Rust Analyzer is the following one: +/// +pub(crate) fn with_fallback( + item: TokenStream, + imp: impl FnOnce() -> Result, +) -> Result { + std::panic::catch_unwind(AssertUnwindSafe(imp)) + .unwrap_or_else(|err| { + let msg = err + .downcast::<&str>() + .map(|msg| msg.to_string()) + .or_else(|err| err.downcast::().map(|msg| *msg)) + .unwrap_or_else(|_| "".to_owned()); + + let msg = if msg.contains("unsupported proc macro punctuation character") { + format!( + "known bug in rust-analyzer: `{msg}`;\n\ + Github issue: https://github.com/rust-lang/rust-analyzer/issues/18244" + ) + } else { + format!( + "bug in the crate `bon` (proc-macro panicked): `{msg}`;\n\ + please report this issue at our Github repository: \ + https://github.com/elastio/bon" + ) + }; + + Err(err!(&Span::call_site(), "{msg}")) + }) + .map_err(|err| { + let compile_error = err.write_errors(); + let item = strip_invalid_tt(item); - syn::parse2::(item) - .map(|fallback| quote!(#compile_error #fallback)) - .unwrap_or_else(|_| compile_error) + syn::parse2::(item) + .map(|fallback| quote!(#compile_error #fallback)) + .unwrap_or_else(|_| compile_error) + }) } /// This is used in error handling for better IDE experience. For example, while @@ -28,12 +66,12 @@ pub(crate) fn error_into_token_stream(err: Error, item: TokenStream2) -> TokenSt /// attributes that need to be processed by this macro to avoid the IDE from /// reporting those as well. struct Fallback { - output: TokenStream2, + output: TokenStream, } impl Parse for Fallback { fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { - let mut output = TokenStream2::new(); + let mut output = TokenStream::new(); loop { let found_attr = input.step(|cursor| { @@ -42,9 +80,7 @@ impl Parse for Fallback { match &tt { TokenTree::Group(group) => { let fallback: Self = syn::parse2(group.stream())?; - let new_group = - proc_macro2::Group::new(group.delimiter(), fallback.output); - + let new_group = Group::new(group.delimiter(), fallback.output); output.extend([TokenTree::Group(new_group)]); } TokenTree::Punct(punct) if punct.as_char() == '#' => { @@ -68,14 +104,52 @@ impl Parse for Fallback { input .call(syn::Attribute::parse_outer)? .into_iter() - .filter(|attr| !attr.is_doc() && !attr.path().is_ident("builder")) + .filter(|attr| !attr.is_doc_expr() && !attr.path().is_ident("builder")) .for_each(|attr| attr.to_tokens(&mut output)); } } } impl ToTokens for Fallback { - fn to_tokens(&self, tokens: &mut TokenStream2) { + fn to_tokens(&self, tokens: &mut TokenStream) { self.output.to_tokens(tokens); } } + +/// Workaround for the RA bug where it generates an invalid Punct token tree with +/// the character `{`. +/// +/// ## Issues +/// +/// - [Bug in RA](https://github.com/rust-lang/rust-analyzer/issues/18244) +/// - [Bug in proc-macro2](https://github.com/dtolnay/proc-macro2/issues/470) (already fixed) +fn strip_invalid_tt(tokens: TokenStream) -> TokenStream { + fn recurse(tt: TokenTree) -> TokenTree { + match &tt { + TokenTree::Group(group) => { + let mut group = Group::new(group.delimiter(), strip_invalid_tt(group.stream())); + group.set_span(group.span()); + + TokenTree::Group(group) + } + _ => tt, + } + } + + let mut tokens = tokens.into_iter(); + + std::iter::from_fn(|| { + // In newer versions of `proc-macro2` this code panics here (earlier) + loop { + // If this panics it means the next token tree is invalid. + // We can't do anything about it, and we just ignore it. + // Luckily, `proc-macro2` consumes the invalid token tree + // so this doesn't cause an infinite loop. + match std::panic::catch_unwind(AssertUnwindSafe(|| tokens.next())) { + Ok(tt) => return tt.map(recurse), + Err(_) => continue, + } + } + }) + .collect() +} diff --git a/bon-macros/src/lib.rs b/bon-macros/src/lib.rs index d3065f11..fa92e4ab 100644 --- a/bon-macros/src/lib.rs +++ b/bon-macros/src/lib.rs @@ -8,7 +8,11 @@ clippy::option_option, clippy::option_if_let_else, clippy::enum_glob_use, - clippy::too_many_lines + clippy::too_many_lines, + clippy::if_not_else, + + // We can't use the explicit captures syntax due to the MSRV + impl_trait_overcaptures, )] mod bon; @@ -16,10 +20,11 @@ mod builder; mod collections; mod error; mod normalization; +mod parsing; mod util; -use proc_macro::TokenStream; -use quote::ToTokens; +#[cfg(test)] +mod tests; /// Generates a builder for the function or method it's placed on. /// @@ -97,7 +102,10 @@ use quote::ToTokens; /// - [Guide](https://elastio.github.io/bon/guide/overview) /// - [Attributes reference](https://elastio.github.io/bon/reference/builder) #[proc_macro_attribute] -pub fn builder(params: TokenStream, item: TokenStream) -> TokenStream { +pub fn builder( + params: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { builder::generate_from_attr(params.into(), item.into()).into() } @@ -137,7 +145,7 @@ pub fn builder(params: TokenStream, item: TokenStream) -> TokenStream { /// - [Guide](https://elastio.github.io/bon/guide/overview) /// - [Attributes reference](https://elastio.github.io/bon/reference/builder) #[proc_macro_derive(Builder, attributes(builder))] -pub fn derive_builder(item: TokenStream) -> TokenStream { +pub fn derive_builder(item: proc_macro::TokenStream) -> proc_macro::TokenStream { builder::generate_from_derive(item.into()).into() } @@ -199,7 +207,10 @@ pub fn derive_builder(item: TokenStream) -> TokenStream { /// /// [`builder`]: macro@builder #[proc_macro_attribute] -pub fn bon(params: TokenStream, item: TokenStream) -> TokenStream { +pub fn bon( + params: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { bon::generate(params.into(), item.into()).into() } @@ -243,7 +254,7 @@ pub fn bon(params: TokenStream, item: TokenStream) -> TokenStream { /// [`BTreeMap`]: https://doc.rust-lang.org/stable/std/collections/struct.BTreeMap.html /// [`HashMap`]: https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html #[proc_macro] -pub fn map(input: TokenStream) -> TokenStream { +pub fn map(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let entries = syn::parse_macro_input!(input with collections::map::parse_macro_input); collections::map::generate(entries).into() @@ -289,30 +300,10 @@ pub fn map(input: TokenStream) -> TokenStream { /// [`BTreeSet`]: https://doc.rust-lang.org/stable/std/collections/struct.BTreeSet.html /// [`HashSet`]: https://doc.rust-lang.org/stable/std/collections/struct.HashSet.html #[proc_macro] -pub fn set(input: TokenStream) -> TokenStream { +pub fn set(input: proc_macro::TokenStream) -> proc_macro::TokenStream { use syn::punctuated::Punctuated; let entries = syn::parse_macro_input!(input with Punctuated::parse_terminated); collections::set::generate(entries).into() } - -/// Private proc macro! Don't use it directly, it's an implementation detail. -/// -/// This macro takes a function and overrides its return type with the provided one. -/// It's used in combination with `cfg_attr` to conditionally change the return type -/// of a function based on the `cfg(doc)` value. -#[doc(hidden)] -#[proc_macro_attribute] -pub fn __return_type(ret_ty: TokenStream, item: TokenStream) -> TokenStream { - let mut func: syn::ItemFn = match syn::parse(item.clone()) { - Ok(func) => func, - Err(err) => return error::error_into_token_stream(err.into(), item.into()).into(), - }; - - let ret_ty = proc_macro2::TokenStream::from(ret_ty); - - func.sig.output = syn::parse_quote!(-> #ret_ty); - - func.into_token_stream().into() -} diff --git a/bon-macros/src/normalization/cfg/mod.rs b/bon-macros/src/normalization/cfg/mod.rs index 4e990904..bf0e5a1b 100644 --- a/bon-macros/src/normalization/cfg/mod.rs +++ b/bon-macros/src/normalization/cfg/mod.rs @@ -3,20 +3,19 @@ mod visit; use crate::util::prelude::*; use parse::CfgSyntax; -use quote::{quote, ToTokens}; use std::collections::BTreeSet; pub(crate) enum ExpansionOutput { Expanded { - params: TokenStream2, + params: TokenStream, item: syn::Item, }, - Recurse(TokenStream2), + Recurse(TokenStream), } pub(crate) struct ExpandCfg { pub(crate) macro_path: syn::Path, - pub(crate) params: TokenStream2, + pub(crate) params: TokenStream, pub(crate) item: syn::Item, } @@ -65,7 +64,7 @@ impl ExpandCfg { /// There is no mutation happening here, but we just reuse the same /// visitor implementation that works with mutable references. - fn collect_predicates(&mut self) -> Result> { + fn collect_predicates(&mut self) -> Result> { let mut predicates = vec![]; let mut visited = BTreeSet::new(); @@ -73,7 +72,7 @@ impl ExpandCfg { for attr in attrs { let cfg_syntax = match CfgSyntax::from_meta(&attr.meta)? { Some(cfg_syntax) => cfg_syntax, - None => return Ok(true), + None => continue, }; let predicate = match cfg_syntax { @@ -95,7 +94,7 @@ impl ExpandCfg { fn into_recursion( self, recursion_counter: usize, - predicates: &[TokenStream2], + predicates: &[TokenStream], ) -> Result { let Self { params, @@ -108,7 +107,7 @@ impl ExpandCfg { let predicates = predicates.iter().enumerate().map(|(i, predicate)| { // We need to insert the recursion counter into the name so that // the name is unique on every recursive iteration of the cfg eval. - let pred_id = quote::format_ident!("{invocation_name}_{recursion_counter}_{i}"); + let pred_id = format_ident!("{invocation_name}_{recursion_counter}_{i}"); quote!(#pred_id: #predicate) }); diff --git a/bon-macros/src/normalization/cfg/parse.rs b/bon-macros/src/normalization/cfg/parse.rs index 69db5059..34bd26cd 100644 --- a/bon-macros/src/normalization/cfg/parse.rs +++ b/bon-macros/src/normalization/cfg/parse.rs @@ -6,7 +6,7 @@ mod kw { syn::custom_keyword!(__cfgs); } -pub(crate) fn parse_predicate_results(tokens: TokenStream2) -> Result> { +pub(crate) fn parse_predicate_results(tokens: TokenStream) -> Result> { let results: WrapOption = syn::parse2(tokens)?; Ok(results.0) } @@ -22,14 +22,14 @@ struct WrapOption(Option); pub(crate) struct PredicateResults { pub(crate) results: Vec, pub(crate) recursion_counter: usize, - pub(crate) rest: TokenStream2, + pub(crate) rest: TokenStream, } impl Parse for WrapOption { fn parse(input: ParseStream<'_>) -> syn::Result { if !input.peek(kw::__cfgs) { // We need to exhaust the input stream to avoid a "unexpected token" error - input.parse::()?; + input.parse::()?; return Ok(Self(None)); } @@ -61,7 +61,7 @@ impl Parse for WrapOption { } pub(crate) enum CfgSyntax { - Cfg(TokenStream2), + Cfg(TokenStream), CfgAttr(CfgAttr), } diff --git a/bon-macros/src/normalization/generics_namespace.rs b/bon-macros/src/normalization/generics_namespace.rs new file mode 100644 index 00000000..e709605c --- /dev/null +++ b/bon-macros/src/normalization/generics_namespace.rs @@ -0,0 +1,75 @@ +use crate::util::prelude::*; +use proc_macro2::TokenTree; +use std::collections::BTreeSet; +use syn::visit::Visit; + +#[derive(Debug, Default, Clone)] +pub(crate) struct GenericsNamespace { + /// Set of identifiers referenced in the syntax node. + pub(crate) idents: BTreeSet, + + /// Set of lifetimes referenced in the syntax node. + pub(crate) lifetimes: BTreeSet, +} + +impl Visit<'_> for GenericsNamespace { + fn visit_ident(&mut self, ident: &syn::Ident) { + self.idents.insert(ident.to_string()); + } + + fn visit_meta_list(&mut self, meta_list: &syn::MetaList) { + syn::visit::visit_meta_list(self, meta_list); + self.visit_token_stream(meta_list.tokens.clone()); + } + + fn visit_lifetime(&mut self, lifetime: &syn::Lifetime) { + self.lifetimes.insert(lifetime.ident.to_string()); + } + + fn visit_item(&mut self, _item: &syn::Item) { + // Don't recurse into child items. They don't inherit the parent item's generics. + } +} + +impl GenericsNamespace { + pub(crate) fn unique_ident(&self, name: String) -> syn::Ident { + let name = Self::unique_name(&self.idents, name); + syn::Ident::new(&name, Span::call_site()) + } + + pub(crate) fn unique_lifetime(&self, name: String) -> String { + Self::unique_name(&self.lifetimes, name) + } + + /// Adds `_` suffix to the name to avoid conflicts with existing identifiers. + fn unique_name(taken: &BTreeSet, mut ident: String) -> String { + while taken.contains(&ident) { + ident.push('_'); + } + ident + } + + pub(crate) fn visit_token_stream(&mut self, token_stream: TokenStream) { + let mut tokens = token_stream.into_iter().peekable(); + while let Some(tt) = tokens.next() { + match tt { + TokenTree::Group(group) => { + self.visit_token_stream(group.stream()); + } + TokenTree::Ident(ident) => { + self.visit_ident(&ident); + } + TokenTree::Punct(punct) => { + if punct.as_char() != '\'' { + continue; + } + if let Some(TokenTree::Ident(ident)) = tokens.peek() { + self.lifetimes.insert(ident.to_string()); + tokens.next(); + } + } + TokenTree::Literal(_) => {} + } + } + } +} diff --git a/bon-macros/src/normalization/impl_traits.rs b/bon-macros/src/normalization/impl_traits.rs index 7cdd4640..ee1392e0 100644 --- a/bon-macros/src/normalization/impl_traits.rs +++ b/bon-macros/src/normalization/impl_traits.rs @@ -1,9 +1,18 @@ +use super::GenericsNamespace; use crate::util::prelude::*; use syn::visit_mut::VisitMut; -pub(crate) struct NormalizeImplTraits; +pub(crate) struct NormalizeImplTraits<'a> { + namespace: &'a GenericsNamespace, +} -impl VisitMut for NormalizeImplTraits { +impl<'a> NormalizeImplTraits<'a> { + pub(crate) fn new(namespace: &'a GenericsNamespace) -> Self { + Self { namespace } + } +} + +impl VisitMut for NormalizeImplTraits<'_> { fn visit_impl_item_fn_mut(&mut self, fn_item: &mut syn::ImplItemFn) { // We are interested only in signatures of functions. Don't recurse // into the function's block. @@ -11,7 +20,7 @@ impl VisitMut for NormalizeImplTraits { } fn visit_signature_mut(&mut self, signature: &mut syn::Signature) { - let mut visitor = AssignTypeParams::new(&mut signature.generics); + let mut visitor = AssignTypeParams::new(self, &mut signature.generics); for arg in &mut signature.inputs { visitor.visit_fn_arg_mut(arg); @@ -20,15 +29,17 @@ impl VisitMut for NormalizeImplTraits { } struct AssignTypeParams<'a> { + base: &'a NormalizeImplTraits<'a>, generics: &'a mut syn::Generics, next_type_param_index: usize, } impl<'a> AssignTypeParams<'a> { - fn new(generics: &'a mut syn::Generics) -> Self { + fn new(base: &'a NormalizeImplTraits<'a>, generics: &'a mut syn::Generics) -> Self { Self { + base, generics, - next_type_param_index: 0, + next_type_param_index: 1, } } } @@ -54,7 +65,8 @@ impl VisitMut for AssignTypeParams<'_> { let index = self.next_type_param_index; self.next_type_param_index += 1; - let type_param = quote::format_ident!("__{index}"); + let type_param = self.base.namespace.unique_ident(format!("I{index}")); + let impl_trait = std::mem::replace(ty, syn::Type::Path(syn::parse_quote!(#type_param))); let impl_trait = match impl_trait { diff --git a/bon-macros/src/normalization/lifetimes.rs b/bon-macros/src/normalization/lifetimes.rs index cb4b9fe6..294d3b7b 100644 --- a/bon-macros/src/normalization/lifetimes.rs +++ b/bon-macros/src/normalization/lifetimes.rs @@ -1,25 +1,40 @@ +use super::GenericsNamespace; use crate::util::prelude::*; use syn::visit::Visit; use syn::visit_mut::VisitMut; -#[derive(Default)] -pub(crate) struct NormalizeLifetimes; +pub(crate) struct NormalizeLifetimes<'a> { + namespace: &'a GenericsNamespace, +} + +impl<'a> NormalizeLifetimes<'a> { + pub(crate) fn new(namespace: &'a GenericsNamespace) -> Self { + Self { namespace } + } +} -impl VisitMut for NormalizeLifetimes { +impl VisitMut for NormalizeLifetimes<'_> { fn visit_item_impl_mut(&mut self, impl_block: &mut syn::ItemImpl) { - syn::visit_mut::visit_item_impl_mut(self, impl_block); + for item in &mut impl_block.items { + self.visit_impl_item_mut(item); + } - AssignLifetimes::new("i", &mut impl_block.generics).visit_type_mut(&mut impl_block.self_ty); + AssignLifetimes::new(self, "i", &mut impl_block.generics) + .visit_type_mut(&mut impl_block.self_ty); } - fn visit_impl_item_fn_mut(&mut self, fn_item: &mut syn::ImplItemFn) { - // We are interested only in signatures of functions. Don't recurse - // into the function's block. + fn visit_impl_item_mut(&mut self, item: &mut syn::ImplItem) { + if let syn::ImplItem::Fn(fn_item) = item { + self.visit_signature_mut(&mut fn_item.sig); + } + } + + fn visit_item_fn_mut(&mut self, fn_item: &mut syn::ItemFn) { self.visit_signature_mut(&mut fn_item.sig); } fn visit_signature_mut(&mut self, signature: &mut syn::Signature) { - let mut visitor = AssignLifetimes::new("f", &mut signature.generics); + let mut visitor = AssignLifetimes::new(self, "f", &mut signature.generics); for arg in &mut signature.inputs { visitor.visit_fn_arg_mut(arg); } @@ -69,17 +84,27 @@ impl VisitMut for NormalizeLifetimes { } struct AssignLifetimes<'a> { + base: &'a NormalizeLifetimes<'a>, + prefix: &'static str, + + /// Generics where the assigned lifetimes should be stored. generics: &'a mut syn::Generics, + next_lifetime_index: usize, } impl<'a> AssignLifetimes<'a> { - fn new(prefix: &'static str, generics: &'a mut syn::Generics) -> Self { + fn new( + base: &'a NormalizeLifetimes<'a>, + prefix: &'static str, + generics: &'a mut syn::Generics, + ) -> Self { Self { + base, prefix, generics, - next_lifetime_index: 0, + next_lifetime_index: 1, } } } @@ -152,12 +177,20 @@ impl AssignLifetimes<'_> { let index = self.next_lifetime_index; self.next_lifetime_index += 1; - let lifetime = format!("'__{}{index}", self.prefix); + let mut lifetime = self + .base + .namespace + .unique_lifetime(format!("{}{index}", self.prefix)); + + // `syn::Lifetime::new` requires the string to start with the `'` character, + // which is just discarded in that method's impl 🗿. + lifetime.insert(0, '\''); + let lifetime = syn::Lifetime::new(&lifetime, Span::call_site()); let lifetime_param = syn::LifetimeParam::new(lifetime.clone()); let lifetime_param = syn::GenericParam::Lifetime(lifetime_param); - self.generics.params.insert(index, lifetime_param); + self.generics.params.insert(index - 1, lifetime_param); lifetime } diff --git a/bon-macros/src/normalization/mod.rs b/bon-macros/src/normalization/mod.rs index 06fabb2f..db7e8beb 100644 --- a/bon-macros/src/normalization/mod.rs +++ b/bon-macros/src/normalization/mod.rs @@ -1,9 +1,13 @@ mod cfg; +mod generics_namespace; mod impl_traits; mod lifetimes; mod self_ty; +mod syntax_variant; pub(crate) use cfg::*; -pub(crate) use impl_traits::NormalizeImplTraits; -pub(crate) use lifetimes::NormalizeLifetimes; -pub(crate) use self_ty::NormalizeSelfTy; +pub(crate) use generics_namespace::*; +pub(crate) use impl_traits::*; +pub(crate) use lifetimes::*; +pub(crate) use self_ty::*; +pub(crate) use syntax_variant::*; diff --git a/bon-macros/src/normalization/syntax_variant.rs b/bon-macros/src/normalization/syntax_variant.rs new file mode 100644 index 00000000..b12ab236 --- /dev/null +++ b/bon-macros/src/normalization/syntax_variant.rs @@ -0,0 +1,28 @@ +/// Struct, that contains both the original syntax (unprocessed) and the normalized +/// version. This is useful for code that needs access to both versions of the syntax. +#[derive(Debug)] +pub(crate) struct SyntaxVariant { + /// Original syntax that was passed to the macro without any modifications. + pub(crate) orig: T, + + /// The value that is equivalent to `orig`, but it underwent normalization. + pub(crate) norm: T, +} + +impl SyntaxVariant { + pub(crate) fn apply_ref<'a, U>(&'a self, f: impl Fn(&'a T) -> U) -> SyntaxVariant { + let orig = f(&self.orig); + let norm = f(&self.norm); + SyntaxVariant { orig, norm } + } + + pub(crate) fn into_iter(self) -> impl Iterator> + where + T: IntoIterator, + { + self.orig + .into_iter() + .zip(self.norm) + .map(|(orig, norm)| SyntaxVariant { orig, norm }) + } +} diff --git a/bon-macros/src/parsing/docs.rs b/bon-macros/src/parsing/docs.rs new file mode 100644 index 00000000..93f6a029 --- /dev/null +++ b/bon-macros/src/parsing/docs.rs @@ -0,0 +1,69 @@ +use super::SpannedKey; +use crate::util::prelude::*; + +pub(crate) fn parse_docs_without_self_mentions( + context: &'static str, + meta: &syn::Meta, +) -> Result>> { + let docs = parse_docs(meta)?; + reject_self_mentions_in_docs(context, &docs)?; + Ok(docs) +} + +pub(crate) fn parse_docs(meta: &syn::Meta) -> Result>> { + let meta = meta.require_list()?; + + meta.require_curly_braces_delim()?; + + let attrs = meta.parse_args_with(syn::Attribute::parse_outer)?; + + for attr in &attrs { + if !attr.is_doc_expr() { + bail!(attr, "expected a doc comment"); + } + } + + SpannedKey::new(&meta.path, attrs) +} + +/// Validates the docs for the presence of `Self` mentions to prevent users from +/// shooting themselves in the foot where they would think that `Self` resolves +/// to the current item the docs were placed on, when in fact the docs are moved +/// to a different context where `Self` has a different meaning. +pub(crate) fn reject_self_mentions_in_docs( + context: &'static str, + attrs: &[syn::Attribute], +) -> Result { + for attr in attrs { + let doc = match attr.as_doc_expr() { + Some(doc) => doc, + _ => continue, + }; + + let doc = match &doc { + syn::Expr::Lit(doc) => doc, + _ => continue, + }; + + let doc = match &doc.lit { + syn::Lit::Str(doc) => doc, + _ => continue, + }; + + let self_references = ["[`Self`]", "[Self]"]; + + if self_references + .iter() + .any(|self_ref| doc.value().contains(self_ref)) + { + bail!( + &doc.span(), + "the documentation should not reference `Self` because it will \ + be moved to the {context} where `Self` changes meaning, which \ + may confuse the reader of this code; use explicit type names instead.", + ); + } + } + + Ok(()) +} diff --git a/bon-macros/src/parsing/item_params.rs b/bon-macros/src/parsing/item_params.rs new file mode 100644 index 00000000..441a5bc4 --- /dev/null +++ b/bon-macros/src/parsing/item_params.rs @@ -0,0 +1,71 @@ +use super::SpannedKey; +use crate::util::prelude::*; +use darling::FromMeta; + +#[derive(Debug, Clone, Default)] +pub(crate) struct ItemParams { + pub(crate) name: Option>, + pub(crate) vis: Option>, + pub(crate) docs: Option>>, +} + +impl ItemParams { + pub(crate) fn name(&self) -> Option<&syn::Ident> { + self.name.as_ref().map(|name| &name.value) + } + + pub(crate) fn vis(&self) -> Option<&syn::Visibility> { + self.vis.as_ref().map(|vis| &vis.value) + } + + pub(crate) fn docs(&self) -> Option<&[syn::Attribute]> { + self.docs.as_ref().map(|docs| docs.value.as_slice()) + } +} + +pub(crate) struct ItemParamsParsing<'a> { + pub(crate) meta: &'a syn::Meta, + pub(crate) reject_self_mentions: Option<&'static str>, +} + +impl ItemParamsParsing<'_> { + pub(crate) fn parse(self) -> Result { + let meta = self.meta; + + if let syn::Meta::NameValue(meta) = meta { + let val = &meta.value; + let name = syn::parse2(val.to_token_stream())?; + + return Ok(ItemParams { + name: Some(SpannedKey::new(&meta.path, name)?), + vis: None, + docs: None, + }); + } + + #[derive(Debug, FromMeta)] + struct Full { + name: Option>, + vis: Option>, + + #[darling(default, with = super::parse_docs, map = Some)] + doc: Option>>, + } + + let full: Full = crate::parsing::parse_non_empty_paren_meta_list(meta)?; + + if let Some(context) = self.reject_self_mentions { + if let Some(docs) = &full.doc { + crate::parsing::reject_self_mentions_in_docs(context, docs)?; + } + } + + let params = ItemParams { + name: full.name, + vis: full.vis, + docs: full.doc, + }; + + Ok(params) + } +} diff --git a/bon-macros/src/parsing/mod.rs b/bon-macros/src/parsing/mod.rs new file mode 100644 index 00000000..6b8477e1 --- /dev/null +++ b/bon-macros/src/parsing/mod.rs @@ -0,0 +1,83 @@ +mod docs; +mod item_params; +mod simple_closure; +mod spanned_key; + +pub(crate) use docs::*; +pub(crate) use item_params::*; +pub(crate) use simple_closure::*; +pub(crate) use spanned_key::*; + +use crate::util::prelude::*; +use darling::FromMeta; +use syn::parse::Parser; +use syn::punctuated::Punctuated; + +pub(crate) fn parse_non_empty_paren_meta_list(meta: &syn::Meta) -> Result { + require_non_empty_paren_meta_list_or_name_value(meta)?; + T::from_meta(meta) +} + +pub(crate) fn require_non_empty_paren_meta_list_or_name_value(meta: &syn::Meta) -> Result { + match meta { + syn::Meta::List(meta) => { + meta.require_parens_delim()?; + + if meta.tokens.is_empty() { + bail!( + &meta.delimiter.span().join(), + "expected parameters in parentheses" + ); + } + } + syn::Meta::Path(path) => bail!( + &meta, + "this empty `#[{0}]` attribute is unexpected; \ + remove it or pass parameters in parentheses: \ + `#[{0}(...)]`", + darling::util::path_to_string(path) + ), + syn::Meta::NameValue(_) => {} + } + + Ok(()) +} + +/// Utility for parsing with `#[darling(with = ...)]` attribute that allows to +/// parse an arbitrary sequence of items inside of parentheses. For example +/// `foo(a, b, c)`, where `a`, `b`, and `c` are of type `T` and `,` is represented +/// by the token type `P`. +#[allow(dead_code)] +pub(crate) fn parse_paren_meta_list_with_terminated( + meta: &syn::Meta, +) -> Result> +where + T: syn::parse::Parse, + P: syn::parse::Parse, +{ + let item = std::any::type_name::(); + let punct = std::any::type_name::

(); + + let name = |val: &str| { + format!( + "'{}'", + val.rsplit("::").next().unwrap_or(val).to_lowercase() + ) + }; + + let meta = match meta { + syn::Meta::List(meta) => meta, + _ => bail!( + &meta, + "expected a list of {} separated by {}", + name(item), + name(punct), + ), + }; + + meta.require_parens_delim()?; + + let punctuated = Punctuated::parse_terminated.parse2(meta.tokens.clone())?; + + Ok(punctuated) +} diff --git a/bon-macros/src/parsing/simple_closure.rs b/bon-macros/src/parsing/simple_closure.rs new file mode 100644 index 00000000..69b21252 --- /dev/null +++ b/bon-macros/src/parsing/simple_closure.rs @@ -0,0 +1,97 @@ +use crate::util::prelude::*; +use darling::FromMeta; +use syn::spanned::Spanned; + +/// Utility type for parsing simple closure syntax that only allows [`syn::PatIdent`] +/// inputs and rejects any attributes and prefix keywords like `async`, `move`, `for` +/// on the closure. +#[derive(Debug)] +pub(crate) struct SimpleClosure { + pub(crate) inputs: Vec, + pub(crate) body: Box, + pub(crate) output: syn::ReturnType, +} + +#[derive(Debug)] +pub(crate) struct SimpleClosureInput { + pub(crate) pat: syn::PatIdent, + pub(crate) ty: Option>, +} + +impl FromMeta for SimpleClosure { + fn from_meta(meta: &syn::Meta) -> Result { + let err = || { + let path = darling::util::path_to_string(meta.path()); + err!( + meta, + "expected a closure e.g. `{path} = |param: T| expression`" + ) + }; + + let meta = match meta { + syn::Meta::NameValue(meta) => meta, + _ => return Err(err()), + }; + + let closure = match &meta.value { + syn::Expr::Closure(closure) => closure, + _ => return Err(err()), + }; + + reject_syntax("`for<...>` syntax", &closure.lifetimes)?; + reject_syntax("`const` keyword", &closure.constness)?; + reject_syntax("`static` keyword", &closure.movability)?; + reject_syntax("`async` keyword", &closure.asyncness)?; + reject_syntax("`move` keyword", &closure.capture)?; + reject_syntax("attribute", &closure.attrs.first())?; + + let inputs = closure + .clone() + .inputs + .into_iter() + .map(|input| match input { + syn::Pat::Ident(pat) => SimpleClosureInput::from_pat_ident(pat), + syn::Pat::Type(pat) => SimpleClosureInput::from_pat_type(pat), + _ => bail!(&input, "expected a simple identifier pattern"), + }) + .collect::>()?; + + Ok(Self { + inputs, + body: closure.body.clone(), + output: closure.output.clone(), + }) + } +} + +// Lint from nightly. `&Option` is used to reduce syntax at the callsite +#[allow(unknown_lints, clippy::ref_option)] +fn reject_syntax(name: &'static str, syntax: &Option) -> Result { + if let Some(syntax) = syntax { + bail!(syntax, "{name} is not allowed here") + } + + Ok(()) +} + +impl SimpleClosureInput { + fn from_pat_ident(pat: syn::PatIdent) -> Result { + reject_syntax("attribute", &pat.attrs.first())?; + reject_syntax("`ref` keyword", &pat.by_ref)?; + Ok(Self { pat, ty: None }) + } + + fn from_pat_type(input: syn::PatType) -> Result { + reject_syntax("attribute", &input.attrs.first())?; + + let ident = match *input.pat { + syn::Pat::Ident(pat) => Self::from_pat_ident(pat)?.pat, + _ => bail!(&input.pat, "expected a simple identifier pattern"), + }; + + Ok(Self { + pat: ident, + ty: Some(input.ty), + }) + } +} diff --git a/bon-macros/src/parsing/spanned_key.rs b/bon-macros/src/parsing/spanned_key.rs new file mode 100644 index 00000000..f90fc822 --- /dev/null +++ b/bon-macros/src/parsing/spanned_key.rs @@ -0,0 +1,47 @@ +use crate::util::prelude::*; +use darling::FromMeta; +use std::fmt; +use std::ops::Deref; + +/// A type that stores the attribute key path information along with the parsed value. +/// It is useful for error reporting. For example, if some key was unexpected, it's +/// possible to point to the key's span in the error instead of the attribute's value. +#[derive(Clone)] +pub(crate) struct SpannedKey { + pub(crate) key: syn::Ident, + pub(crate) value: T, +} + +impl SpannedKey { + pub(crate) fn new(path: &syn::Path, value: T) -> Result { + Ok(Self { + key: path.require_ident()?.clone(), + value, + }) + } + + pub(crate) fn into_value(self) -> T { + self.value + } +} + +impl FromMeta for SpannedKey { + fn from_meta(meta: &syn::Meta) -> Result { + let value = T::from_meta(meta)?; + Self::new(meta.path(), value) + } +} + +impl fmt::Debug for SpannedKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.value, f) + } +} + +impl Deref for SpannedKey { + type Target = T; + + fn deref(&self) -> &T { + &self.value + } +} diff --git a/bon-macros/src/tests/attr_setters.rs b/bon-macros/src/tests/attr_setters.rs new file mode 100644 index 00000000..e72769db --- /dev/null +++ b/bon-macros/src/tests/attr_setters.rs @@ -0,0 +1,158 @@ +use super::assert_snapshot; +use crate::util::prelude::*; + +#[test] +fn setters_docs_and_vis() { + let actual_tokens = crate::builder::generate_from_derive(quote! { + struct Sut { + /// Docs on the required field itself + #[builder(setters( + vis = "pub(in overridden)", + doc { + /// Docs on the required field setters. + /// Multiline. + } + ))] + required_field: u32, + + /// Docs on the optional field itself + #[builder(setters( + vis = "pub(in overridden)", + doc { + /// Docs on the optional field setters. + /// Multiline. + } + ))] + optional_field: Option, + + /// Docs on the default field itself + #[builder( + setters( + vis = "pub(in overridden)", + doc { + /// Docs on the default field setters. + /// Multiline. + } + ), + default = 2 + 2 * 3 + )] + default_field: u32, + + /// Docs on the field itself + #[builder( + setters( + some_fn( + vis = "pub(in some_fn_overridden)", + doc { + /// Docs on some_fn + /// Multiline. + } + ), + option_fn( + vis = "pub(in option_fn_overridden)", + doc { + /// Docs on option_fn + /// Multiline. + } + ) + ) + )] + optional_field_with_specific_overrides: Option, + + #[builder( + setters( + some_fn( + vis = "pub(in some_fn_overridden)", + doc { + /// Docs on some_fn + /// Multiline. + } + ), + option_fn( + vis = "pub(in option_fn_overridden)", + doc { + /// Docs on option_fn + /// Multiline. + } + ) + ), + default = 2 + 2 * 3 + )] + default_field_with_specific_overrides: u32, + + #[builder(setters( + doc { + /// Common docs + /// Multiline. + }, + vis = "pub(in overridden)", + option_fn( + vis = "pub(in option_fn_overridden)", + doc { + /// Docs on option_fn + /// Multiline. + } + ) + ))] + optional_field_with_inherited_overrides: Option, + + + #[builder( + setters( + doc { + /// Common docs + /// Multiline. + }, + vis = "pub(in overridden)", + option_fn( + vis = "pub(in option_fn_overridden)", + doc { + /// Docs on option_fn + /// Multiline. + } + ) + ), + default = 2 + 2 * 3 + )] + default_field_with_inherited_overrides: u32, + } + }); + + let mut actual: syn::File = syn::parse2(actual_tokens.clone()).unwrap(); + + // Sanitize the output. Keep only setters and remove their bodies. + let builder_impl = actual + .items + .iter_mut() + .find_map(|item| match item { + syn::Item::Impl(impl_item) => { + (impl_item.self_ty == syn::parse_quote!(SutBuilder)).then(|| impl_item) + } + _ => None, + }) + .unwrap_or_else(|| { + panic!("No builder impl block found. Generated toekens:\n{actual_tokens}") + }); + + builder_impl.items.retain_mut(|item| match item { + syn::ImplItem::Fn(fn_item) => { + if fn_item.sig.ident == "build" { + return false; + } + + // Remove noise attributes + fn_item.attrs.retain(|attr| { + ["allow", "inline"] + .iter() + .all(|ident| !attr.path().is_ident(ident)) + }); + + fn_item.block = syn::parse_quote!({}); + + true + } + _ => true, + }); + + assert_snapshot("setters_docs_and_vis", builder_impl); +} diff --git a/bon-macros/src/tests/mod.rs b/bon-macros/src/tests/mod.rs new file mode 100644 index 00000000..e2595f8e --- /dev/null +++ b/bon-macros/src/tests/mod.rs @@ -0,0 +1,19 @@ +mod attr_setters; +mod syntax_errors; + +use crate::util::prelude::*; +use expect_test::{expect_file, ExpectFile}; + +fn snapshot(test_name: &str) -> ExpectFile { + let snapshot_path = format!( + "{}/tests/snapshots/{test_name}.rs", + env!("CARGO_MANIFEST_DIR") + ); + expect_file![snapshot_path] +} + +#[track_caller] +fn assert_snapshot(test_name: &'static str, actual: &dyn ToTokens) { + let actual = prettyplease::unparse(&syn::parse2(actual.to_token_stream()).unwrap()); + snapshot(test_name).assert_eq(&actual); +} diff --git a/bon-macros/src/tests/syntax_errors.rs b/bon-macros/src/tests/syntax_errors.rs new file mode 100644 index 00000000..01dbda4b --- /dev/null +++ b/bon-macros/src/tests/syntax_errors.rs @@ -0,0 +1,60 @@ +use super::snapshot; +use crate::util::prelude::*; + +#[allow(non_camel_case_types)] +#[derive(Copy, Clone)] +enum MacroKind { + #[allow(dead_code)] + builder, + bon, +} + +#[track_caller] +fn assert_builder_codegen( + macro_kind: MacroKind, + test_name: &'static str, + params: TokenStream, + item: TokenStream, +) { + let sut = match macro_kind { + MacroKind::builder => crate::builder::generate_from_attr, + MacroKind::bon => crate::bon::generate, + }; + + let actual = sut(params, item); + let actual = syn::parse2(actual.clone()) + .map(|actual| prettyplease::unparse(&actual)) + // There is a syntax error, so we can't prettify it + .unwrap_or_else(|_err| actual.to_string()); + + snapshot(test_name).assert_eq(&actual); +} + +macro_rules! test_codegen { + ( + #[$test_name:ident] + #[$kind:ident$( ( $( $params:tt )* ) )?] + $( $item:tt )* + ) => { + #[test] + fn $test_name() { + assert_builder_codegen( + MacroKind::$kind, + stringify!($test_name), + quote!( $( $( $params )* )?), + quote!( $( $item )* ), + ); + } + } +} + +test_codegen! { + #[bon_incomplete_if] + #[bon] + impl Sut { + #[builder] + fn sut() { + if 1 + } + } +} diff --git a/bon-macros/src/util/attrs.rs b/bon-macros/src/util/attrs.rs index a68573d8..92503c4c 100644 --- a/bon-macros/src/util/attrs.rs +++ b/bon-macros/src/util/attrs.rs @@ -1,15 +1,15 @@ pub(crate) trait AttributeExt { - fn is_doc(&self) -> bool; - fn as_doc(&self) -> Option<&syn::Expr>; + fn is_doc_expr(&self) -> bool; + fn as_doc_expr(&self) -> Option<&syn::Expr>; fn to_allow(&self) -> Option; } impl AttributeExt for syn::Attribute { - fn is_doc(&self) -> bool { - self.as_doc().is_some() + fn is_doc_expr(&self) -> bool { + self.as_doc_expr().is_some() } - fn as_doc(&self) -> Option<&syn::Expr> { + fn as_doc_expr(&self) -> Option<&syn::Expr> { let attr = match &self.meta { syn::Meta::NameValue(attr) => attr, _ => return None, diff --git a/bon-macros/src/util/generic_param.rs b/bon-macros/src/util/generic_param.rs new file mode 100644 index 00000000..198533e0 --- /dev/null +++ b/bon-macros/src/util/generic_param.rs @@ -0,0 +1,19 @@ +pub(crate) trait GenericParamExt { + fn to_generic_argument(&self) -> syn::GenericArgument; +} + +impl GenericParamExt for syn::GenericParam { + fn to_generic_argument(&self) -> syn::GenericArgument { + match self { + Self::Lifetime(param) => syn::GenericArgument::Lifetime(param.lifetime.clone()), + Self::Type(param) => { + let ident = ¶m.ident; + syn::GenericArgument::Type(syn::parse_quote!(#ident)) + } + Self::Const(param) => { + let ident = ¶m.ident; + syn::GenericArgument::Const(syn::parse_quote!(#ident)) + } + } + } +} diff --git a/bon-macros/src/util/ide.rs b/bon-macros/src/util/ide.rs index ddb83a06..8465ac51 100644 --- a/bon-macros/src/util/ide.rs +++ b/bon-macros/src/util/ide.rs @@ -10,8 +10,7 @@ )] use crate::util::prelude::*; -use proc_macro2::{Span, TokenTree}; -use quote::quote; +use proc_macro2::TokenTree; use syn::parse::{Parse, ParseStream, Parser}; use syn::{token, Token}; @@ -100,7 +99,7 @@ impl Parse for Meta { #[derive(Clone, Debug)] pub(crate) struct MetaList { pub(crate) path: syn::Path, - pub(crate) tokens: TokenStream2, + pub(crate) tokens: TokenStream, } #[derive(Clone, Debug)] @@ -135,7 +134,7 @@ fn paths_from_meta(meta: Vec) -> Vec { /// By placing these input identifiers in the right places inside of `use` statements /// we can hint the IDEs to provide completions for the attributes based on what's /// available in the module the use statement references. -pub(crate) fn generate_completion_triggers(meta: Vec) -> TokenStream2 { +pub(crate) fn generate_completion_triggers(meta: Vec) -> TokenStream { let completions = CompletionsSchema::with_children( "builder_top_level", vec![ @@ -195,7 +194,7 @@ impl CompletionsSchema { &self, mut meta: Vec, module_prefix: &[&syn::Ident], - ) -> TokenStream2 { + ) -> TokenStream { if let Some(custom_filter) = self.custom_filter { custom_filter(&mut meta); }; diff --git a/bon-macros/src/util/ident.rs b/bon-macros/src/util/ident.rs index f9d5919a..c68d87f7 100644 --- a/bon-macros/src/util/ident.rs +++ b/bon-macros/src/util/ident.rs @@ -1,5 +1,5 @@ +use crate::util::prelude::*; use ident_case::RenameRule; -use proc_macro2::Span; pub(crate) trait IdentExt { /// Converts the ident (assumed to be in `snake_case`) to `PascalCase` without @@ -19,6 +19,9 @@ pub(crate) trait IdentExt { /// identifier. fn snake_to_pascal_case(&self) -> Self; + /// Same thing as `snake_to_pascal_case` but converts `PascalCase` to `snake_case`. + fn pascal_to_snake_case(&self) -> Self; + /// Creates a new ident with the given name and span. If the name starts with /// `r#` then automatically creates a raw ident. fn new_maybe_raw(name: &str, span: Span) -> Self; @@ -38,12 +41,33 @@ impl IdentExt for syn::Ident { Self::new(&renamed, Span::call_site()) } + fn pascal_to_snake_case(&self) -> Self { + let renamed = RenameRule::SnakeCase.apply_to_variant(self.raw_name()); + Self::new_maybe_raw(&renamed, Span::call_site()) + } + fn new_maybe_raw(name: &str, span: Span) -> Self { + // If the ident is already raw (starts with `r#`) then just create a raw ident. if let Some(name) = name.strip_prefix("r#") { - Self::new_raw(name, span) - } else { - Self::new(name, span) + return Self::new_raw(name, span); } + + // ..otherwise validate if it is a valid identifier. + // The `parse_str` method will return an error if the name is not a valid + // identifier. + if syn::parse_str::(name).is_ok() { + return Self::new(name, span); + } + + // Try to make it a raw ident by adding `r#` prefix. + // This won't work for some keywords such as `super`, `crate`, + // `Self`, which are not allowed as raw identifiers + if syn::parse_str::(&format!("r#{name}")).is_ok() { + return Self::new_raw(name, span); + } + + // As the final fallback add a trailing `_` to create a valid identifier + Self::new(&format!("{name}_"), span) } fn raw_name(&self) -> String { diff --git a/bon-macros/src/util/meta_list.rs b/bon-macros/src/util/meta_list.rs new file mode 100644 index 00000000..766085e8 --- /dev/null +++ b/bon-macros/src/util/meta_list.rs @@ -0,0 +1,66 @@ +use crate::util::prelude::*; + +pub(crate) trait MetaListExt { + fn require_parens_delim(&self) -> Result<()>; + fn require_curly_braces_delim(&self) -> Result<()>; +} + +impl MetaListExt for syn::MetaList { + fn require_parens_delim(&self) -> Result<()> { + require_delim(self, MacroDelimKind::Paren) + } + + fn require_curly_braces_delim(&self) -> Result<()> { + require_delim(self, MacroDelimKind::Brace) + } +} + +fn require_delim(meta: &syn::MetaList, expected: MacroDelimKind) -> Result<()> { + let actual = MacroDelimKind::from_syn(&meta.delimiter); + if actual == expected { + return Ok(()); + } + + let path = darling::util::path_to_string(&meta.path); + bail!( + meta, + "wrong delimiter, expected {} e.g. `{path}{}`, but got {}: `{path}{}`", + expected.name(), + expected.example(), + actual.name(), + actual.example(), + ); +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum MacroDelimKind { + Paren, + Brace, + Bracket, +} + +impl MacroDelimKind { + fn from_syn(delim: &syn::MacroDelimiter) -> Self { + match delim { + syn::MacroDelimiter::Paren(_) => Self::Paren, + syn::MacroDelimiter::Brace(_) => Self::Brace, + syn::MacroDelimiter::Bracket(_) => Self::Bracket, + } + } + + fn name(self) -> &'static str { + match self { + Self::Paren => "parentheses", + Self::Brace => "curly braces", + Self::Bracket => "square brackets", + } + } + + fn example(self) -> &'static str { + match self { + Self::Paren => "(...)", + Self::Brace => "{...}", + Self::Bracket => "[...]", + } + } +} diff --git a/bon-macros/src/util/mod.rs b/bon-macros/src/util/mod.rs index 7c3a1182..e80aaec6 100644 --- a/bon-macros/src/util/mod.rs +++ b/bon-macros/src/util/mod.rs @@ -1,20 +1,23 @@ mod attrs; mod fn_arg; +mod generic_param; mod ident; mod item; mod iterator; +mod meta_list; mod path; mod punctuated; mod ty; mod vec; +mod visibility; pub(crate) mod ide; use prelude::*; pub(crate) mod prelude { - /// A handy alias for [`proc_macro2::TokenStream`]. - pub(crate) use proc_macro2::{Span, TokenStream as TokenStream2}; + pub(crate) use proc_macro2::{Span, TokenStream}; + pub(crate) use quote::{format_ident, quote, ToTokens}; /// The `Error` type in in this crate is supposed to act like `anyhow::Error` /// providing a simple way to create and return errors from format strings. @@ -28,13 +31,16 @@ pub(crate) mod prelude { pub(crate) use super::attrs::AttributeExt; pub(crate) use super::fn_arg::FnArgExt; + pub(crate) use super::generic_param::GenericParamExt; pub(crate) use super::ident::IdentExt; pub(crate) use super::item::ItemExt; pub(crate) use super::iterator::{IntoIteratorExt, IteratorExt}; + pub(crate) use super::meta_list::MetaListExt; pub(crate) use super::path::PathExt; pub(crate) use super::punctuated::PunctuatedExt; pub(crate) use super::ty::TypeExt; pub(crate) use super::vec::VecExt; + pub(crate) use super::visibility::VisibilityExt; pub(crate) use super::{bail, err}; } diff --git a/bon-macros/src/util/ty/match_types.rs b/bon-macros/src/util/ty/match_types.rs index e170d8e9..988689fe 100644 --- a/bon-macros/src/util/ty/match_types.rs +++ b/bon-macros/src/util/ty/match_types.rs @@ -67,8 +67,8 @@ fn match_angle_bracketed_generic_args( } fn match_option( - scrutinee: &Option, - pattern: &Option, + scrutinee: Option<&T>, + pattern: Option<&T>, compare: impl Fn(&T, &T) -> Result, ) -> Result { match (scrutinee, &pattern) { @@ -119,8 +119,8 @@ fn match_generic_args( scrutinee.ident == pattern.ident && match_types(&scrutinee.ty, &pattern.ty)? && match_option( - &scrutinee.generics, - &pattern.generics, + scrutinee.generics.as_ref(), + pattern.generics.as_ref(), match_angle_bracketed_generic_args, )? } @@ -132,14 +132,14 @@ fn match_generic_args( scrutinee.ident == pattern.ident && match_option( - &scrutinee.generics, - &pattern.generics, + scrutinee.generics.as_ref(), + pattern.generics.as_ref(), match_angle_bracketed_generic_args, )? && match_exprs(&scrutinee.value, &pattern.value) } - _ => return Err(unsupported_syntax_error(&pattern, "This syntax")), + _ => return Err(unsupported_syntax_error(&pattern, "this syntax")), }; Ok(verdict) @@ -228,7 +228,7 @@ pub(crate) fn match_types(scrutinee: &syn::Type, pattern: &syn::Type) -> Result< Never(_) => matches!(scrutinee, Never(_)), - _ => return Err(unsupported_syntax_error(&pattern, "This syntax")), + _ => return Err(unsupported_syntax_error(&pattern, "this syntax")), }; Ok(verdict) diff --git a/bon-macros/src/util/ty/mod.rs b/bon-macros/src/util/ty/mod.rs index 772b643b..38c09327 100644 --- a/bon-macros/src/util/ty/mod.rs +++ b/bon-macros/src/util/ty/mod.rs @@ -1,24 +1,27 @@ mod match_types; use crate::util::prelude::*; +use syn::punctuated::Punctuated; pub(crate) trait TypeExt { /// Try downcasting the type to [`syn::Type::Path`] fn as_path(&self) -> Option<&syn::TypePath>; - /// Returns the last identifier of the path if this type is a simple path - fn last_path_segment_ident(&self) -> Option<&syn::Ident>; - - /// Returns `true` if the given type is p [`syn::Type::Path`] and its - /// final segment is equal to `needle` identifier. - fn is_last_segment(&self, needle: &str) -> bool; - - /// Detects if the type is `desired_type` and returns its generic type parameter - fn type_param(&self, desired_type: &str) -> Option<&syn::Type>; + /// Try downcasting the type to [`syn::Type::Path`]. If it has a [`syn::QSelf`] + /// then this method will return `None`. + fn as_path_no_qself(&self) -> Option<&syn::Path>; /// Detects if the type is [`Option`] and returns its generic type parameter fn option_type_param(&self) -> Option<&syn::Type>; + /// Validates that this type is a generic type (path without [`syn::QSelf`]) + /// which ends with the given last segment that passes the predicate + /// `is_desired_last_segment`. + fn as_generic_angle_bracketed_path( + &self, + is_desired_last_segment: impl FnOnce(&syn::Ident) -> bool, + ) -> Option>; + /// Heuristically detects if the type is [`Option`] fn is_option(&self) -> bool; @@ -42,41 +45,22 @@ impl TypeExt for syn::Type { } } - fn last_path_segment_ident(&self) -> Option<&syn::Ident> { - Some(&self.as_path()?.path.segments.last()?.ident) - } - - fn is_last_segment(&self, needle: &str) -> bool { - let path = match self.as_path() { - Some(path) => path, - _ => return false, - }; - - let last_segment = &path - .path - .segments - .last() - .expect("BUG: empty path is not possible") - .ident; - - last_segment == needle - } - - fn type_param(&self, desired_type: &str) -> Option<&syn::Type> { + fn as_path_no_qself(&self) -> Option<&syn::Path> { let path = self.as_path()?; + if path.qself.is_some() { + return None; + } + Some(&path.path) + } - let segment = path - .path - .segments - .iter() - .find(|&segment| segment.ident == desired_type)?; + fn option_type_param(&self) -> Option<&syn::Type> { + let ty = self.as_generic_angle_bracketed_path(|last_segment| last_segment == "Option")?; - let args = match &segment.arguments { - syn::PathArguments::AngleBracketed(args) => args, - _ => return None, - }; + if ty.args.len() != 1 { + return None; + } - let arg = args.args.first()?; + let arg = ty.args.first()?; let arg = match arg { syn::GenericArgument::Type(arg) => arg, @@ -86,12 +70,28 @@ impl TypeExt for syn::Type { Some(arg) } - fn option_type_param(&self) -> Option<&syn::Type> { - self.type_param("Option") + fn as_generic_angle_bracketed_path( + &self, + is_desired_last_segment: impl FnOnce(&syn::Ident) -> bool, + ) -> Option> { + let path = self.as_path_no_qself()?; + + let last_segment = path.segments.last()?; + + if !is_desired_last_segment(&last_segment.ident) { + return None; + } + + let args = match &last_segment.arguments { + syn::PathArguments::AngleBracketed(args) => &args.args, + _ => return None, + }; + + Some(GenericAngleBracketedPath { path, args }) } fn is_option(&self) -> bool { - self.is_last_segment("Option") + self.option_type_param().is_some() } fn peel(&self) -> &Self { @@ -106,3 +106,8 @@ impl TypeExt for syn::Type { match_types::match_types(self, pattern) } } + +pub(crate) struct GenericAngleBracketedPath<'a> { + pub(crate) path: &'a syn::Path, + pub(crate) args: &'a Punctuated, +} diff --git a/bon-macros/src/util/visibility.rs b/bon-macros/src/util/visibility.rs new file mode 100644 index 00000000..dc854da5 --- /dev/null +++ b/bon-macros/src/util/visibility.rs @@ -0,0 +1,131 @@ +use crate::util::prelude::*; + +pub(crate) trait VisibilityExt { + /// Returns [`syn::Visibility`] that is equivalent to the current visibility + /// but for an item that is inside of the child module. This basically does + /// the following conversions. + /// + /// - `pub` -> `pub` (unchanged) + /// - `pub(crate)` -> `pub(crate)` (unchanged) + /// - `pub(self)` or ` ` (default private visibility) -> `pub(super)` + /// - `pub(super)` -> `pub(in super::super)` + /// - `pub(in relative::path)` -> `pub(in super::relative::path)` + /// - `pub(in ::absolute::path)` -> `pub(in ::absolute::path)` (unchanged) + /// - `pub(in crate::path)` -> `pub(in crate::path)` (unchanged) + /// - `pub(in self::path)` -> `pub(in super::path)` + /// - `pub(in super::path)` -> `pub(in super::super::path)` + /// + /// Note that absolute paths in `pub(in ...)` are not supported with Rust 2018+, + /// according to the [Rust reference]: + /// + /// > Edition Differences: Starting with the 2018 edition, paths for pub(in path) + /// > must start with crate, self, or super. The 2015 edition may also use paths + /// > starting with :: or modules from the crate root. + /// + /// # Errors + /// + /// This function may return an error if it encounters some unexpected syntax. + /// For example, some syntax that isn't known to the latest version of Rust + /// this code was written for. + /// + /// [Rust reference]: https://doc.rust-lang.org/reference/visibility-and-privacy.html#pubin-path-pubcrate-pubsuper-and-pubself + fn into_equivalent_in_child_module(self) -> Result; +} + +impl VisibilityExt for syn::Visibility { + fn into_equivalent_in_child_module(mut self) -> Result { + match &mut self { + Self::Public(_) => Ok(self), + Self::Inherited => Ok(syn::parse_quote!(pub(super))), + Self::Restricted(syn::VisRestricted { + path, + in_token: None, + .. + }) => { + if path.is_ident("crate") { + return Ok(self); + } + + if path.is_ident("super") { + return Ok(syn::parse_quote!(pub(in super::#path))); + } + + if path.is_ident("self") { + return Ok(syn::parse_quote!(pub(super))); + } + + bail!( + &self, + "Expected either `crate` or `super` or `in some::path` inside of \ + `pub(...)` but got something else. This may be because a new \ + syntax for visibility was released in a newer Rust version, \ + but this crate doesn't support it." + ); + } + Self::Restricted(syn::VisRestricted { + path, + in_token: Some(_), + .. + }) => { + if path.leading_colon.is_some() { + return Ok(self); + } + + if path.starts_with_segment("crate") { + return Ok(self); + } + + if let Some(first_segment) = path.segments.first_mut() { + if first_segment.ident == "self" { + let span = first_segment.ident.span(); + *first_segment = syn::parse_quote_spanned!(span=>super); + return Ok(self); + } + } + + path.segments.insert(0, syn::parse_quote!(super)); + + Ok(self) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use syn::parse_quote as pq; + + #[test] + fn all_tests() { + #[track_caller] + // One less `&` character to type in assertions + #[allow(clippy::needless_pass_by_value)] + fn test(vis: syn::Visibility, expected: syn::Visibility) { + let actual = vis.into_equivalent_in_child_module().unwrap(); + assert!( + actual == expected, + "got:\nactual: {}\nexpected: {}", + actual.to_token_stream(), + expected.to_token_stream() + ); + } + + test(pq!(pub), pq!(pub)); + test(pq!(pub(crate)), pq!(pub(crate))); + test(pq!(pub(self)), pq!(pub(super))); + test(pq!(), pq!(pub(super))); + test(pq!(pub(super)), pq!(pub(in super::super))); + + test( + pq!(pub(in relative::path)), + pq!(pub(in super::relative::path)), + ); + test(pq!(pub(in crate::path)), pq!(pub(in crate::path))); + test(pq!(pub(in self::path)), pq!(pub(in super::path))); + test(pq!(pub(in super::path)), pq!(pub(in super::super::path))); + + test(pq!(pub(in ::absolute::path)), pq!(pub(in ::absolute::path))); + } +} diff --git a/bon-macros/tests/snapshots/bon_incomplete_if.rs b/bon-macros/tests/snapshots/bon_incomplete_if.rs new file mode 100644 index 00000000..e06c730a --- /dev/null +++ b/bon-macros/tests/snapshots/bon_incomplete_if.rs @@ -0,0 +1 @@ +:: core :: compile_error ! { "unexpected end of input, expected curly braces" } impl Sut { fn sut () { if 1 } } \ No newline at end of file diff --git a/bon-macros/tests/snapshots/setters_docs_and_vis.rs b/bon-macros/tests/snapshots/setters_docs_and_vis.rs new file mode 100644 index 00000000..52763184 --- /dev/null +++ b/bon-macros/tests/snapshots/setters_docs_and_vis.rs @@ -0,0 +1,222 @@ +#[allow(unused_parens)] +#[automatically_derived] +#[allow(deprecated)] +impl SutBuilder { + /**| **Required** | +| -- | + +*/ + /// Docs on the required field setters. + /// Multiline. + pub(in overridden) fn required_field( + mut self, + value: u32, + ) -> SutBuilder> + where + S::RequiredField: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_optional_field()`](Self::maybe_optional_field), which is a companion setter that accepts an `Option`. + + +*/ + /// Docs on the optional field setters. + /// Multiline. + pub(in overridden) fn optional_field( + self, + value: u32, + ) -> SutBuilder> + where + S::OptionalField: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`optional_field()`](Self::optional_field), which is a companion setter that wraps the value with `Some` internally. + + +*/ + /// Docs on the optional field setters. + /// Multiline. + pub(in overridden) fn maybe_optional_field( + mut self, + value: Option, + ) -> SutBuilder> + where + S::OptionalField: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_default_field()`](Self::maybe_default_field), which is a companion setter that accepts an `Option`. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Docs on the default field setters. + /// Multiline. + pub(in overridden) fn default_field( + self, + value: u32, + ) -> SutBuilder> + where + S::DefaultField: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`default_field()`](Self::default_field), which is a companion setter that wraps the value with `Some` internally. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Docs on the default field setters. + /// Multiline. + pub(in overridden) fn maybe_default_field( + mut self, + value: Option, + ) -> SutBuilder> + where + S::DefaultField: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_optional_field_with_specific_overrides()`](Self::maybe_optional_field_with_specific_overrides), which is a companion setter that accepts an `Option`. + + +*/ + /// Docs on some_fn + /// Multiline. + pub(in some_fn_overridden) fn optional_field_with_specific_overrides( + self, + value: u32, + ) -> SutBuilder> + where + S::OptionalFieldWithSpecificOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`optional_field_with_specific_overrides()`](Self::optional_field_with_specific_overrides), which is a companion setter that wraps the value with `Some` internally. + + +*/ + /// Docs on option_fn + /// Multiline. + pub(in option_fn_overridden) fn maybe_optional_field_with_specific_overrides( + mut self, + value: Option, + ) -> SutBuilder> + where + S::OptionalFieldWithSpecificOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_default_field_with_specific_overrides()`](Self::maybe_default_field_with_specific_overrides), which is a companion setter that accepts an `Option`. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Docs on some_fn + /// Multiline. + pub(in some_fn_overridden) fn default_field_with_specific_overrides( + self, + value: u32, + ) -> SutBuilder> + where + S::DefaultFieldWithSpecificOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`default_field_with_specific_overrides()`](Self::default_field_with_specific_overrides), which is a companion setter that wraps the value with `Some` internally. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Docs on option_fn + /// Multiline. + pub(in option_fn_overridden) fn maybe_default_field_with_specific_overrides( + mut self, + value: Option, + ) -> SutBuilder> + where + S::DefaultFieldWithSpecificOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_optional_field_with_inherited_overrides()`](Self::maybe_optional_field_with_inherited_overrides), which is a companion setter that accepts an `Option`. + + +*/ + /// Common docs + /// Multiline. + pub(in overridden) fn optional_field_with_inherited_overrides( + self, + value: u32, + ) -> SutBuilder> + where + S::OptionalFieldWithInheritedOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`optional_field_with_inherited_overrides()`](Self::optional_field_with_inherited_overrides), which is a companion setter that wraps the value with `Some` internally. + + +*/ + /// Docs on option_fn + /// Multiline. + pub(in option_fn_overridden) fn maybe_optional_field_with_inherited_overrides( + mut self, + value: Option, + ) -> SutBuilder> + where + S::OptionalFieldWithInheritedOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`maybe_default_field_with_inherited_overrides()`](Self::maybe_default_field_with_inherited_overrides), which is a companion setter that accepts an `Option`. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Common docs + /// Multiline. + pub(in overridden) fn default_field_with_inherited_overrides( + self, + value: u32, + ) -> SutBuilder> + where + S::DefaultFieldWithInheritedOverrides: sut_builder::IsUnset, + {} + /**| **Optional** | +| -- | + +**See also** [`default_field_with_inherited_overrides()`](Self::default_field_with_inherited_overrides), which is a companion setter that wraps the value with `Some` internally. + + +**Default:** ```2 + 2 * 3```. + +*/ + /// Docs on option_fn + /// Multiline. + pub(in option_fn_overridden) fn maybe_default_field_with_inherited_overrides( + mut self, + value: Option, + ) -> SutBuilder> + where + S::DefaultFieldWithInheritedOverrides: sut_builder::IsUnset, + {} +} diff --git a/bon/Cargo.toml b/bon/Cargo.toml index 822cadac..d3c73d9a 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -2,7 +2,7 @@ name = "bon" version = "2.3.0" -description = "Generate builders for everything!" +description = "Next-gen compile-time-checked builder generator, named function's arguments, and more!" categories = [ "rust-patterns", @@ -62,3 +62,43 @@ trybuild = "1.0.89" alloc = [] default = ["std"] std = ["alloc"] + +# Opts in to the higher MSRV 1.79.0. In this version, Rust stabilized the syntax +# for bounds in associated type position which can be used to make bounds on generic +# associated types implied. See the release announcement for more: +# https://blog.rust-lang.org/2024/06/13/Rust-1.79.0.htmlbounds-in-associated-type-position +# +# This feature is useful for the trait `IsComplete` generated by the builder macros. +# When this feature is enabled, the builder macros use the new syntax for bounds in +# associated type position, which enables implied `IsSet` bounds for the type state +# of required members. +# +# To understand how this can be used consider the following example: +# +# ```rust +# #[derive(bon::Builder)] +# struct Example { +# a: u32, +# b: Option, +# } +# +# use example_builder::{IsUnset, IsComplete}; +# +# impl ExampleBuilder { +# fn build_with_default_b(self) -> Example +# where +# State: IsComplete, +# State::B: IsUnset, +# { +# self.b(42).build() +# } +# } +# ``` +# +# This code wouldn't compile without this feature enabled, because `State: IsComplete` +# wouldn't automatically imply `State::A: IsSet`, so the builder type state returned +# after `self.b()` doesn't imply that the member `a` is set, and thus `build()` +# can't be called. +# +# Huge thanks to @harudagondi for suggesting the name of this cargo feature! +implied-bounds = ["bon-macros/implied-bounds"] diff --git a/bon/src/builder_state.rs b/bon/src/builder_state.rs new file mode 100644 index 00000000..4336eebb --- /dev/null +++ b/bon/src/builder_state.rs @@ -0,0 +1,38 @@ +//! The items here are intentionally defined in a private module not inside of the +//! [`crate::private`] module. This is because that module is marked with `#[deprecated]` +//! which makes all items defined in that module also deprecated. +//! +//! This is not the desired behavior for the items defined here. They are not deprecated, +//! and they are expected to be exposed to the users. However, the users must not reference +//! them through the `bon` crate. Instead, they should use the re-exports from the state +//! module generated for the builder. + +use crate::private::{Sealed, Set, Unset}; + +/// Marker trait that indicates that the member is set, i.e. at least +/// one of its setters was called. +#[rustversion::attr( + since(1.78.0), + diagnostic::on_unimplemented( + message = "the member `{Self}` was not set, but this method requires it to be set", + label = "the member `{Self}` was not set, but this method requires it to be set" + ) +)] +pub trait IsSet: Sealed {} + +/// Marker trait that indicates that the member is unset, i.e. none +/// of its setters was called. +#[rustversion::attr( + since(1.78.0), + diagnostic::on_unimplemented( + message = "the member `{Self}` was already set, but this method requires it to be unset", + label = "the member `{Self}` was already set, but this method requires it to be unset" + ) +)] +pub trait IsUnset: Sealed {} + +#[doc(hidden)] +impl IsSet for Set {} + +#[doc(hidden)] +impl IsUnset for Unset {} diff --git a/bon/src/collections.rs b/bon/src/collections.rs new file mode 100644 index 00000000..b5b0b186 --- /dev/null +++ b/bon/src/collections.rs @@ -0,0 +1,151 @@ +/// Same as [`std::vec!`] but converts each element with [`Into`]. +/// +/// **WARNING:** it's not recommended to import this macro into scope. Reference it +/// using the full path (`bon::vec![]`) to avoid confusion with the [`std::vec!`] macro. +/// +/// A good example of the use case for this macro is when you want to create a +/// [`Vec`] where part of the items are hard-coded string literals of type +/// `&str` and the other part is made of dynamic [`String`] values. +/// +/// ``` +/// fn convert_media(input_extension: &str, output_extension: &str) -> std::io::Result<()> { +/// let ffmpeg_args: Vec = bon::vec![ +/// "-i", +/// format!("input.{input_extension}"), +/// "-y", +/// format!("output.{output_extension}"), +/// ]; +/// +/// std::process::Command::new("ffmpeg").args(ffmpeg_args).output()?; +/// +/// Ok(()) +/// } +/// ``` +/// +/// This macro doesn't support `vec![expr; N]` syntax, since it's simpler to +/// just write `vec![expr.into(); N]` using [`std::vec!`] instead. +#[macro_export] +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))] +#[allow(edition_2024_expr_fragment_specifier)] +macro_rules! vec { + () => ($crate::private::alloc::vec::Vec::new()); + ($($item:expr),+ $(,)?) => ($crate::private::alloc::vec![$(::core::convert::Into::into($item)),+ ]); +} + +/// Creates a fixed-size array literal with each element converted with [`Into`]. +/// +/// You'll probably need a hint for the target type of items in the array if the +/// compiler can't infer it from its usage. +/// +/// This is similar in spirit to the [`bon::vec!`] macro, but it's for arrays. +/// See [`bon::vec!`] docs for details. +/// +/// Same example as in [`bon::vec!`], but using this macro. It works with array +/// as well because [`Command::args`] accepts any value that implements [`IntoIterator`]: +/// +/// ``` +/// fn convert_media(input_extension: &str, output_extension: &str) -> std::io::Result<()> { +/// let ffmpeg_args: [String; 4] = bon::arr![ +/// "-i", +/// format!("input.{input_extension}"), +/// "-y", +/// format!("output.{output_extension}"), +/// ]; +/// +/// std::process::Command::new("ffmpeg").args(ffmpeg_args).output()?; +/// +/// Ok(()) +/// } +/// ``` +/// +/// This macro doesn't support `[expr; N]` syntax, since it's simpler to +/// just write `[expr.into(); N]` instead. +/// +/// [`Command::args`]: std::process::Command::args +/// [`bon::vec!`]: crate::vec +#[macro_export] +#[allow(edition_2024_expr_fragment_specifier)] +macro_rules! arr { + () => ([]); + ($($item:expr),+ $(,)?) => ([$(::core::convert::Into::into($item)),+]); +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "alloc")] + use crate::private::alloc::{string::String, vec::Vec}; + use core::num::NonZeroU8; + + #[cfg(feature = "alloc")] + #[test] + fn arr_of_strings() { + let actual: [String; 3] = crate::arr!["foo", "bar", "baz"]; + assert_eq!(actual, ["foo", "bar", "baz"]); + + let actual: [String; 0] = crate::arr![]; + assert!(actual.is_empty()); + } + + #[test] + fn arr_of_numbers() { + let actual: [u8; 2] = crate::arr![NonZeroU8::new(1).unwrap(), NonZeroU8::new(2).unwrap()]; + assert_eq!(actual, [1, 2]); + + let actual: [u8; 0] = crate::arr![]; + assert!(actual.is_empty()); + } + + #[cfg(feature = "alloc")] + #[test] + fn vec_smoke() { + let actual: Vec = crate::vec!["foo", "bar", "baz"]; + assert_eq!(actual, ["foo", "bar", "baz"]); + + let actual: Vec = crate::vec![]; + assert!(actual.is_empty()); + } + + #[cfg(feature = "std")] + #[test] + fn map_smoke() { + use std::collections::{BTreeMap, HashMap}; + + let hash_strings: HashMap = crate::map! { + "Hello": "World", + "Goodbye": "Mars", + }; + + assert_eq!(hash_strings["Hello"], "World"); + assert_eq!(hash_strings["Goodbye"], "Mars"); + + let tree_strings: BTreeMap = crate::map! { + "Hello": "World", + "Goodbye": "Mars", + }; + + assert_eq!(tree_strings["Hello"], "World"); + assert_eq!(tree_strings["Goodbye"], "Mars"); + } + + #[cfg(feature = "std")] + #[test] + fn set_smoke() { + use std::collections::BTreeSet; + use std::collections::HashSet; + + let hash_strings: HashSet = crate::set!["Hello", "World", "Goodbye", "Mars"]; + + assert!(hash_strings.contains("Hello")); + assert!(hash_strings.contains("World")); + assert!(hash_strings.contains("Goodbye")); + assert!(hash_strings.contains("Mars")); + + let tree_strings: BTreeSet = crate::set!["Hello", "World", "Goodbye", "Mars"]; + + assert!(tree_strings.contains("Hello")); + assert!(tree_strings.contains("World")); + assert!(tree_strings.contains("Goodbye")); + assert!(tree_strings.contains("Mars")); + } +} diff --git a/bon/src/lib.rs b/bon/src/lib.rs index 64a3c7af..cd7349dd 100644 --- a/bon/src/lib.rs +++ b/bon/src/lib.rs @@ -1,162 +1,28 @@ +#![doc( + html_logo_url = "https://elastio.github.io/bon/bon-logo-thumb.png", + html_favicon_url = "https://elastio.github.io/bon/bon-logo-medium.png" +)] #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] +// We mark all items from the `private` module as deprecated to signal that they are +// implementation details and should not be used directly. Unfortunately, this triggers +// the deprecation warnings within this crate itself everywhere we use them, so we just +// suppress this lint for the entire crate. +#![allow(deprecated)] -pub use bon_macros::*; +// Rexport all macros from the proc-macro crate. +pub use bon_macros::{bon, builder, map, set, Builder}; + +/// Small utility declarative macros for creating colletions with [`Into`] conversions. +mod collections; -/// Symbols used by macros. They are not stable and are considered an implementation detail. -/// Don't use them! #[doc(hidden)] +#[deprecated = "the items from the `bon::private` module are an implementation detail; \ + they should not be used directly; if you found a need for this, then you are probably \ + doing something wrong; feel free to open an issue/discussion in our GitHub repository \ + (https://github.com/elastio/bon) or ask for help in our Discord server \ + (https://discord.gg/QcBYSamw4c)"] pub mod private; -/// Same as [`std::vec!`] but converts each element with [`Into`]. -/// -/// **WARNING:** it's not recommended to import this macro into scope. Reference it -/// using the full path (`bon::vec![]`) to avoid confusion with the [`std::vec!`] macro. -/// -/// A good example of the use case for this macro is when you want to create a -/// [`Vec`] where part of the items are hard-coded string literals of type -/// `&str` and the other part is made of dynamic [`String`] values. -/// -/// ``` -/// fn convert_media(input_extension: &str, output_extension: &str) -> std::io::Result<()> { -/// let ffmpeg_args: Vec = bon::vec![ -/// "-i", -/// format!("input.{input_extension}"), -/// "-y", -/// format!("output.{output_extension}"), -/// ]; -/// -/// std::process::Command::new("ffmpeg").args(ffmpeg_args).output()?; -/// -/// Ok(()) -/// } -/// ``` -/// -/// This macro doesn't support `vec![expr; N]` syntax, since it's simpler to -/// just write `vec![expr.into(); N]` using [`std::vec!`] instead. -#[macro_export] -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(any(feature = "std", feature = "alloc"))))] -#[allow(edition_2024_expr_fragment_specifier)] -macro_rules! vec { - () => ($crate::private::alloc::vec::Vec::new()); - ($($item:expr),+ $(,)?) => ($crate::private::alloc::vec![$(::core::convert::Into::into($item)),+ ]); -} - -/// Creates a fixed-size array literal with each element converted with [`Into`]. -/// -/// You'll probably need a hint for the target type of items in the array if the -/// compiler can't infer it from its usage. -/// -/// This is similar in spirit to the [`bon::vec!`] macro, but it's for arrays. -/// See [`bon::vec!`] docs for details. -/// -/// Same example as in [`bon::vec!`], but using this macro. It works with array -/// as well because [`Command::args`] accepts any value that implements [`IntoIterator`]: -/// -/// ``` -/// fn convert_media(input_extension: &str, output_extension: &str) -> std::io::Result<()> { -/// let ffmpeg_args: [String; 4] = bon::arr![ -/// "-i", -/// format!("input.{input_extension}"), -/// "-y", -/// format!("output.{output_extension}"), -/// ]; -/// -/// std::process::Command::new("ffmpeg").args(ffmpeg_args).output()?; -/// -/// Ok(()) -/// } -/// ``` -/// -/// This macro doesn't support `[expr; N]` syntax, since it's simpler to -/// just write `[expr.into(); N]` instead. -/// -/// [`Command::args`]: std::process::Command::args -/// [`bon::vec!`]: crate::vec -#[macro_export] -#[allow(edition_2024_expr_fragment_specifier)] -macro_rules! arr { - () => ([]); - ($($item:expr),+ $(,)?) => ([$(::core::convert::Into::into($item)),+]); -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "alloc")] - use crate::private::alloc::{string::String, vec::Vec}; - use core::num::NonZeroU8; - - #[cfg(feature = "alloc")] - #[test] - fn arr_of_strings() { - let actual: [String; 3] = crate::arr!["foo", "bar", "baz"]; - assert_eq!(actual, ["foo", "bar", "baz"]); - - let actual: [String; 0] = crate::arr![]; - assert!(actual.is_empty()); - } - - #[test] - fn arr_of_numbers() { - let actual: [u8; 2] = crate::arr![NonZeroU8::new(1).unwrap(), NonZeroU8::new(2).unwrap()]; - assert_eq!(actual, [1, 2]); - - let actual: [u8; 0] = crate::arr![]; - assert!(actual.is_empty()); - } - - #[cfg(feature = "alloc")] - #[test] - fn vec_smoke() { - let actual: Vec = crate::vec!["foo", "bar", "baz"]; - assert_eq!(actual, ["foo", "bar", "baz"]); - - let actual: Vec = crate::vec![]; - assert!(actual.is_empty()); - } - - #[cfg(feature = "std")] - #[test] - fn map_smoke() { - use std::collections::{BTreeMap, HashMap}; - - let hash_strings: HashMap = crate::map! { - "Hello": "World", - "Goodbye": "Mars", - }; - - assert_eq!(hash_strings["Hello"], "World"); - assert_eq!(hash_strings["Goodbye"], "Mars"); - - let tree_strings: BTreeMap = crate::map! { - "Hello": "World", - "Goodbye": "Mars", - }; - - assert_eq!(tree_strings["Hello"], "World"); - assert_eq!(tree_strings["Goodbye"], "Mars"); - } - - #[cfg(feature = "std")] - #[test] - fn set_smoke() { - use std::collections::BTreeSet; - use std::collections::HashSet; - - let hash_strings: HashSet = crate::set!["Hello", "World", "Goodbye", "Mars"]; - - assert!(hash_strings.contains("Hello")); - assert!(hash_strings.contains("World")); - assert!(hash_strings.contains("Goodbye")); - assert!(hash_strings.contains("Mars")); - - let tree_strings: BTreeSet = crate::set!["Hello", "World", "Goodbye", "Mars"]; - - assert!(tree_strings.contains("Hello")); - assert!(tree_strings.contains("World")); - assert!(tree_strings.contains("Goodbye")); - assert!(tree_strings.contains("Mars")); - } -} +mod builder_state; diff --git a/bon/src/private/cfg_eval.rs b/bon/src/private/cfg_eval.rs new file mode 100644 index 00000000..1a47d8bb --- /dev/null +++ b/bon/src/private/cfg_eval.rs @@ -0,0 +1,137 @@ +/// This is all a big embarrassing workaround, please don't oversee 😳😳😳. +/// +/// Anyway, if you are curious what the hell is going on here, then here is +/// an explanation 😸. So... where to start 🤔. Ah! The problem! +/// +/// ## The problem +/// +/// Proc macro attributes (like `#[builder]`) see all the `#[cfg(...)]` and `#[cfg_attr(...)]` +/// attributes unexpanded. For example, if you write smth like this: +/// +/// ``` +/// #[bon::builder] +/// fn func( +/// #[cfg(windows)] +/// windows_only_param: u32, +/// ) {} +/// +/// ``` +/// +/// then the `#[builder]` macro will see the full `#[cfg(...)]` attribute with +/// the `windows_only_param` it is attached to verbatim. The `#[cfg(...)]` isn't +/// removed by the time the `#[builder]`'s macro expansion is invoked. +/// +/// It is a problem because the `#[builder]` macro needs to know the exact list +/// of members it has to generate setters for. It doesn't know whether +/// the `windows` predicate evaluates to `true` or `false`, especially if this was +/// a more complex predicate. So it can't decide whether to generate a setter for +/// the `windows_only_param` or not. +/// +/// ## The solution +/// +/// This macro allows us to evaluate the `cfg` predicates by using a variation of +/// [the trick] shared by @recatek. +/// +/// When the `#[builder]` macro finds any usage of `#[cfg(...)]` or `#[cfg_attr(...)]` +/// it generates a call to this macro with all `cfg` predicates collected from the +/// item it was placed on. The `#[builder]` macro deduplicates and sorts the `cfg` +/// predicates and passes them as `$pred` to this macro. +/// +/// This macro then dispatches to `__eval_cfg_callback_true` or `__eval_cfg_callback_false` +/// by defining a conditional `use ...` statement for each predicate and collects the +/// results of the evaluation in the `$results` list. +/// +/// For the last call to this macro (when no more `$pred` are left) the macro calls back +/// to the proc macro attribute that called it with the results of the evaluation and +/// the original parameters and the item which are passed through via the `$rest` macro variable. +/// +/// [the trick]: https://users.rust-lang.org/t/supporting-or-evaluating-cfg-in-proc-macro-parameters/93240/2 +#[macro_export] +#[doc(hidden)] +macro_rules! __eval_cfg_callback { + ( + { $($results:tt)* } + ( $pred_id:ident: $($pred:tt)* ) + $($rest:tt)* + ) => { + // The `pred_id` is required to be a unique identifier for the current + // predicate evaluation so that we can use it in a `use` statement to define + // a new unique name for the macro to call. + #[cfg($($pred)*)] + #[doc(hidden)] + #[allow(deprecated)] + use $crate::__eval_cfg_callback_true as $pred_id; + + #[cfg(not($($pred)*))] + #[doc(hidden)] + #[allow(deprecated)] + use $crate::__eval_cfg_callback_false as $pred_id; + + // The trick here is that `$pred_id` now resolves either to + // `__eval_cfg_callback_true` or `__eval_cfg_callback_false` + // depending on the evaluation of the cfg predicate, so by + // invoking it as a macro, that macro internally pushes either + // `true` or `false` to the `$results` list. + $pred_id! { + { $($results)* } + $($rest)* + } + }; + + // The terminal case for the recursion when there are no more predicates left. + // We have collected all the results of the cfg evaluations and now we can + // delegate them to the proc macro attribute that called this macro. + ( + // The results of the cfg evaluation + { $($results:tt)* } + + // The proc macro attribute to invoke with the results + $final_macro:path, + + // The number of times this macro was called recursively from the proc macro + $recursion_counter:literal, + + // Parameters to pass to the proc macro attribute after the cfg results + ( $($macro_params:tt)* ) + + // The item to attach the proc macro attribute to + $($item:tt)* + ) => { + // The special `__cfgs(...)` prefix is parsed by the proc macro attribute + // to get the results of the cfg evaluations. + #[$final_macro(__cfgs($recursion_counter, $($results)*) $($macro_params)*)] + $($item)* + }; +} + +/// The `cfg` predicate evaluated to `true`, now push that information into +/// the `$results` list. +#[macro_export] +#[doc(hidden)] +macro_rules! __eval_cfg_callback_true { + ( + { $($results:tt)* } + $($tt:tt)* + ) => { + $crate::__eval_cfg_callback! { + { $($results)* true, } + $($tt)* + } + }; +} + +/// The `cfg` predicate evaluated to `false`, now push that information into +/// the `$results` list. +#[macro_export] +#[doc(hidden)] +macro_rules! __eval_cfg_callback_false { + ( + { $($results:tt)* } + $($tt:tt)* + ) => { + $crate::__eval_cfg_callback! { + { $($results)* false, } + $($tt)* + } + }; +} diff --git a/bon/src/private/deprecations.rs b/bon/src/private/deprecations.rs deleted file mode 100644 index ad9e27a4..00000000 --- a/bon/src/private/deprecations.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[doc(hidden)] -#[deprecated(note = "\ - #[bon::builder] on top of a struct is deprecated; \ - use `#[derive(bon::Builder)]` instead; \ - see more details at https://elastio.github.io/bon/blog/bon-builder-v2-2-release#derive-builder-syntax-for-structs")] -pub mod builder_attribute_on_a_struct {} diff --git a/bon/src/private/derives.rs b/bon/src/private/derives.rs new file mode 100644 index 00000000..9320ace4 --- /dev/null +++ b/bon/src/private/derives.rs @@ -0,0 +1,21 @@ +//! Utility functions for improving error messages in builder's derive implementations. +//! +//! These free functions are simple wrappers over the respective traits. They allow the +//! generated code to pass the concrete type of the member using the turbofish syntax, +//! which improves the compile errors when the member's type `T` doesn't implement +//! the target trait. +//! +//! They improve the spans of error messages because compiler knows that it needs to +//! point to the origin of the offending type (member's type T) from the turbofish +//! syntax to where the type came from (original code written by the user). +use core::fmt::Debug; + +#[inline(always)] +pub fn clone_member(member: &Option) -> Option { + member.clone() +} + +#[inline(always)] +pub fn as_dyn_debug(member: &T) -> &dyn Debug { + member +} diff --git a/bon/src/private/mod.rs b/bon/src/private/mod.rs index 015003d8..8da87ec7 100644 --- a/bon/src/private/mod.rs +++ b/bon/src/private/mod.rs @@ -6,238 +6,52 @@ // Marking every potential function as `const` is a bit too much. // Especially, this doesn't play well with our MSRV. Trait bounds // aren't allowed on const functions in older Rust versions. - clippy::missing_const_for_fn + clippy::missing_const_for_fn, )] -/// Used to trigger deprecation warnings from the macros. -pub mod deprecations; - /// Used for providing better IDE hints (completions and syntax highlighting). pub mod ide; -/// Used to implement the `alloc` feature. -#[cfg(feature = "alloc")] -pub extern crate alloc; +pub mod derives; -pub fn assert_clone() {} -pub fn assert_debug() {} +mod cfg_eval; -/// Marker trait to denote the state of the member that is not set yet. -#[rustversion::attr( - since(1.78.0), - diagnostic::on_unimplemented( - message = "can't set the same member twice", - label = "this member was already set" - ) -)] -pub trait IsUnset {} +// This reexport is a private implementation detail and should not be used +// directly! This reexport may change or be removed at any time between +// patch releases. Use the export from your generated builder's state module +// directly instead of using this reexport from `bon::__private`. +pub use crate::builder_state::{IsSet, IsUnset}; +pub use rustversion; -#[derive(Debug, Clone)] -pub struct Required; +pub(crate) mod sealed { + // The purpose of the `Sealed` trait **is** to be unnameable from outside the crate. + #[allow(unnameable_types)] + pub trait Sealed: Sized {} -#[derive(Debug, Clone)] -pub struct Optional; + impl Sealed for super::Unset {} + impl Sealed for super::Set {} +} -/// The sole implementation of the [`IsUnset`] trait. -#[derive(Debug, Clone)] -pub struct Unset(pub T); +pub(crate) use sealed::Sealed; -impl IsUnset for Unset {} +/// Used to implement the `alloc` feature. +#[cfg(feature = "alloc")] +pub extern crate alloc; + +#[derive(Debug)] +pub struct Unset(Name); + +#[derive(Debug)] +pub struct Set(Name); -/// A trait used to transition optional members to the [`Set`] state. -/// -/// It also provides a better error message when the member is not set. -/// The `Member` generic parameter isn't used by the trait implementation, -/// it's used only as a label with the name of the member to specify which one -/// was not set. #[rustversion::attr( since(1.78.0), diagnostic::on_unimplemented( - message = "can't finish building yet; the member `{Member}` was not set", - label = "the member `{Member}` was not set" + message = "expected type state for the member `{Name}`, but got `{Self}`", + label = "expected type state for the member `{Name}`, but got `{Self}`", ) )] -pub trait IntoSet { - fn into_set(self) -> T; -} - -impl IntoSet for Set { - #[inline(always)] - fn into_set(self) -> T { - self.0 - } -} - -impl IntoSet, Member> for Unset { - #[inline(always)] - fn into_set(self) -> Option { - None - } -} - -/// Implemented by `Unset` and `Set` states of members, which are basically -/// all possible states of a member. -pub trait MemberState { - fn is_set(&self) -> bool; -} - -impl MemberState for Set { - #[inline(always)] - fn is_set(&self) -> bool { - true - } -} - -impl MemberState for Unset { - #[inline(always)] - fn is_set(&self) -> bool { - false - } -} - -/// This is all a big embarrassing workaround, please don't oversee 😳😳😳. -/// -/// Anyway, if you are curious what the hell is going on here, then here is -/// an explanation 😸. So... where to start 🤔. Ah! The problem! -/// -/// ## The problem -/// -/// Proc macro attributes (like `#[builder]`) see all the `#[cfg(...)]` and `#[cfg_attr(...)]` -/// attributes unexpanded. For example, if you write smth like this: -/// -/// ``` -/// #[bon::builder] -/// fn func( -/// #[cfg(windows)] -/// windows_only_param: u32, -/// ) {} -/// -/// ``` -/// -/// then the `#[builder]` macro will see the full `#[cfg(...)]` attribute with -/// the `windows_only_param` it is attached to verbatim. The `#[cfg(...)]` isn't -/// removed by the time the `#[builder]`'s macro expansion is invoked. -/// -/// It is a problem because the `#[builder]` macro needs to know the exact list -/// of members it has to generate setters for. It doesn't know whether -/// the `windows` predicate evaluates to `true` or `false`, especially if this was -/// a more complex predicate. So it can't decide whether to generate a setter for -/// the `windows_only_param` or not. -/// -/// ## The solution -/// -/// This macro allows us to evaluate the `cfg` predicates by using a variation of -/// [the trick] shared by @recatek. -/// -/// When the `#[builder]` macro finds any usage of `#[cfg(...)]` or `#[cfg_attr(...)]` -/// it generates a call to this macro with all `cfg` predicates collected from the -/// item it was placed on. The `#[builder]` macro deduplicates and sorts the `cfg` -/// predicates and passes them as `$pred` to this macro. -/// -/// This macro then dispatches to `__eval_cfg_callback_true` or `__eval_cfg_callback_false` -/// by defining a conditional `use ...` statement for each predicate and collects the -/// results of the evaluation in the `$results` list. -/// -/// For the last call to this macro (when no more `$pred` are left) the macro calls back -/// to the proc macro attribute that called it with the results of the evaluation and -/// the original parameters and the item which are passed through via the `$rest` macro variable. -/// -/// [the trick]: https://users.rust-lang.org/t/supporting-or-evaluating-cfg-in-proc-macro-parameters/93240/2 -#[macro_export] -#[doc(hidden)] -macro_rules! __eval_cfg_callback { - ( - { $($results:tt)* } - ( $pred_id:ident: $($pred:tt)* ) - $($rest:tt)* - ) => { - // The `pred_id` is required to be a unique identifier for the current - // predicate evaluation so that we can use it in a `use` statement to define - // a new unique name for the macro to call. - #[cfg($($pred)*)] - #[doc(hidden)] - use $crate::__eval_cfg_callback_true as $pred_id; - - #[cfg(not($($pred)*))] - #[doc(hidden)] - use $crate::__eval_cfg_callback_false as $pred_id; - - // The trick here is that `$pred_id` now resolves either to - // `__eval_cfg_callback_true` or `__eval_cfg_callback_false` - // depending on the evaluation of the cfg predicate, so by - // invoking it as a macro, that macro internally pushes either - // `true` or `false` to the `$results` list. - - $pred_id! { - { $($results)* } - $($rest)* - } - }; - - // The terminal case for the recursion when there are no more predicates left. - // We have collected all the results of the cfg evaluations and now we can - // delegate them to the proc macro attribute that called this macro. - ( - // The results of the cfg evaluation - { $($results:tt)* } - - // The proc macro attribute to invoke with the results - $final_macro:path, +pub trait MemberState: Sealed {} - // The number of times this macro was called recursively from the proc macro - $recursion_counter:literal, - - // Parameters to pass to the proc macro attribute after the cfg results - ( $($macro_params:tt)* ) - - // The item to attach the proc macro attribute to - $($item:tt)* - ) => { - // The special `__cfgs(...)` prefix is parsed by the proc macro attribute - // to get the results of the cfg evaluations. - #[$final_macro(__cfgs($recursion_counter, $($results)*) $($macro_params)*)] - $($item)* - }; -} - -#[repr(transparent)] -#[derive(Clone)] -pub struct Set(pub T); - -impl core::fmt::Debug for Set { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - core::fmt::Debug::fmt(&self.0, f) - } -} - -/// The `cfg` predicate evaluated to `true`, now push that information into -/// the `$results` list. -#[macro_export] -#[doc(hidden)] -macro_rules! __eval_cfg_callback_true { - ( - { $($results:tt)* } - $($tt:tt)* - ) => { - $crate::__eval_cfg_callback! { - { $($results)* true, } - $($tt)* - } - }; -} - -/// The `cfg` predicate evaluated to `false`, now push that information into -/// the `$results` list. -#[macro_export] -#[doc(hidden)] -macro_rules! __eval_cfg_callback_false { - ( - { $($results:tt)* } - $($tt:tt)* - ) => { - $crate::__eval_cfg_callback! { - { $($results)* false, } - $($tt)* - } - }; -} +impl MemberState for Unset {} +impl MemberState for Set {} diff --git a/bon/tests/integration/builder/attr_default.rs b/bon/tests/integration/builder/attr_default.rs index 2bc72136..c3db2621 100644 --- a/bon/tests/integration/builder/attr_default.rs +++ b/bon/tests/integration/builder/attr_default.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use core::fmt; #[cfg(feature = "alloc")] #[test] @@ -8,7 +9,7 @@ fn fn_alloc() { #[builder(default = "default".to_owned())] arg1: String, #[builder(default = vec![42])] arg2: Vec, #[builder(default = "foo", into)] arg3: String, - ) -> (String, Vec, String) { + ) -> impl fmt::Debug { (arg1, arg2, arg3) } @@ -138,7 +139,7 @@ fn struct_alloc() { #[test] fn fn_no_std() { #[builder] - fn sut(#[builder(default)] arg1: u32, #[builder(default = 42)] arg2: u32) -> (u32, u32) { + fn sut(#[builder(default)] arg1: u32, #[builder(default = 42)] arg2: u32) -> impl fmt::Debug { (arg1, arg2) } @@ -212,6 +213,7 @@ fn fn_generic_default() { mod interaction_with_positional_members { use crate::prelude::*; + use core::fmt; #[test] fn test_struct() { @@ -302,7 +304,7 @@ mod interaction_with_positional_members { #[builder(default = [starter_1, starter_2, finisher_1, finisher_2])] // named_1: [u32; 4], #[builder(default = (32, named_1))] named_2: (u32, [u32; 4]), - ) -> (u32, u32, u32, u32, [u32; 4], (u32, [u32; 4])) { + ) -> impl fmt::Debug { ( starter_1, starter_2, finisher_1, finisher_2, named_1, named_2, ) @@ -335,7 +337,7 @@ mod interaction_with_positional_members { #[builder(default = [starter_1, starter_2, finisher_1, finisher_2])] // named_1: [u32; 4], #[builder(default = (32, named_1))] named_2: (u32, [u32; 4]), - ) -> (u32, u32, u32, u32, [u32; 4], (u32, [u32; 4])) { + ) -> impl fmt::Debug { ( starter_1, starter_2, finisher_1, finisher_2, named_1, named_2, ) diff --git a/bon/tests/integration/builder/attr_derive.rs b/bon/tests/integration/builder/attr_derive.rs new file mode 100644 index 00000000..072aef9d --- /dev/null +++ b/bon/tests/integration/builder/attr_derive.rs @@ -0,0 +1,350 @@ +// We intentionally exercise cloning from `#[builder(derive(Clone))]` here +// to make sure that it works. +#![allow(clippy::redundant_clone)] + +use crate::prelude::*; + +#[test] +fn smoke_fn() { + #[builder(derive(Clone, Debug))] + fn sut(_arg1: bool, _arg2: Option<()>, _arg3: Option<&str>, _arg4: Option) {} + + let actual = sut().arg1(true).arg3("value").maybe_arg4(None).clone(); + + assert_debug_eq( + actual, + expect![[r#"SutBuilder { arg1: true, arg3: "value" }"#]], + ); +} + +#[test] +fn smoke_struct() { + #[derive(Builder)] + #[builder(derive(Clone, Debug))] + struct Sut<'a> { + _arg1: bool, + _arg2: Option<()>, + _arg3: Option<&'a str>, + _arg4: Option, + } + + let actual = Sut::builder() + .arg1(true) + .arg3("value") + .maybe_arg4(None) + .clone(); + + assert_debug_eq( + actual, + expect![[r#"SutBuilder { arg1: true, arg3: "value" }"#]], + ); +} + +#[test] +fn builder_with_receiver() { + #[derive(Clone, Debug)] + struct Sut { + #[allow(dead_code)] + name: &'static str, + } + + #[bon] + impl Sut { + #[builder(derive(Clone, Debug))] + fn method(&self, other_name: &'static str, values: &[u8]) { + let _ = (self, other_name, values); + } + } + + let actual = Sut { name: "Blackjack" } + .method() + .other_name("P21") + .values(&[1, 2, 3]) + .clone(); + + assert_debug_eq( + actual, + expect![[r#" + SutMethodBuilder { + self: Sut { + name: "Blackjack", + }, + other_name: "P21", + values: [ + 1, + 2, + 3, + ], + }"#]], + ); +} + +#[test] +fn skipped_members() { + struct NoDebug; + + #[derive(Builder)] + #[builder(derive(Debug, Clone))] + struct Sut { + _arg1: bool, + + #[builder(skip = NoDebug)] + _arg2: NoDebug, + } + + let actual = Sut::builder().arg1(true).clone(); + + assert_debug_eq(actual, expect!["SutBuilder { arg1: true }"]); +} + +#[test] +fn empty_builder() { + #[derive(Builder)] + #[builder(derive(Clone, Debug))] + struct Sut {} + + #[allow(clippy::redundant_clone)] + let actual = Sut::builder().clone(); + + assert_debug_eq(actual, expect!["SutBuilder"]); +} + +mod generics { + use crate::prelude::*; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[builder(derive(Clone, Debug))] + struct Sut { + _arg1: T, + } + + let actual = Sut::builder().arg1(42).clone(); + + assert_debug_eq(actual, expect!["SutBuilder { arg1: 42 }"]); + } + + #[test] + fn test_free_fn() { + #[builder(derive(Clone, Debug))] + fn sut(_arg1: T) {} + + let actual = sut::().arg1(42).clone(); + + assert_debug_eq(actual, expect!["SutBuilder { arg1: 42 }"]); + } + + #[test] + fn test_assoc_method() { + #[derive(Clone, Debug)] + struct Sut(T); + + #[bon] + impl Sut { + #[builder(derive(Clone, Debug))] + fn sut(_arg1: U) {} + + #[builder(derive(Clone, Debug))] + fn with_self(&self, _arg1: U) { + let _ = self; + } + } + + let actual = Sut::<()>::sut::().arg1(42).clone(); + + assert_debug_eq(actual, expect!["SutSutBuilder { arg1: 42 }"]); + + let actual = Sut(true).with_self::().arg1(42).clone(); + + assert_debug_eq( + actual, + expect!["SutWithSelfBuilder { self: Sut(true), arg1: 42 }"], + ); + } +} + +mod positional_members { + use crate::prelude::*; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[builder(derive(Clone, Debug))] + #[allow(dead_code)] + struct Sut { + #[builder(start_fn)] + start_fn_arg: bool, + + #[builder(finish_fn)] + finish_fn_arg: &'static str, + + named: u32, + } + + let actual = Sut::builder(true); + + assert_debug_eq(actual.clone(), expect!["SutBuilder { start_fn_arg: true }"]); + + assert_debug_eq( + actual.named(42).clone(), + expect!["SutBuilder { start_fn_arg: true, named: 42 }"], + ); + } + + #[test] + fn test_free_fn() { + #[builder(derive(Clone, Debug))] + #[allow(unused_variables)] + fn sut( + #[builder(start_fn)] start_fn_arg: bool, + #[builder(finish_fn)] finish_fn_arg: &'static str, + named: u32, + ) { + } + + let actual = sut(true); + + assert_debug_eq(actual.clone(), expect!["SutBuilder { start_fn_arg: true }"]); + + assert_debug_eq( + actual.named(42).clone(), + expect!["SutBuilder { start_fn_arg: true, named: 42 }"], + ); + } + + #[test] + fn test_assoc_method() { + #[derive(Debug)] + struct Sut; + + #[bon] + #[allow(unused_variables)] + impl Sut { + #[builder(derive(Clone, Debug))] + fn sut( + #[builder(start_fn)] start_fn_arg: bool, + #[builder(finish_fn)] finish_fn_arg: &'static str, + named: u32, + ) { + } + + #[builder(derive(Clone, Debug))] + fn with_self( + &self, + #[builder(start_fn)] start_fn_arg: bool, + #[builder(finish_fn)] finish_fn_arg: &'static str, + named: u32, + ) { + let _ = self; + } + } + + let actual = Sut::sut(true); + + assert_debug_eq( + actual.clone(), + expect!["SutSutBuilder { start_fn_arg: true }"], + ); + assert_debug_eq( + actual.named(42).clone(), + expect!["SutSutBuilder { start_fn_arg: true, named: 42 }"], + ); + + let actual = Sut.with_self(true); + + assert_debug_eq( + actual.clone(), + expect!["SutWithSelfBuilder { self: Sut, start_fn_arg: true }"], + ); + assert_debug_eq( + actual.named(42).clone(), + expect![[r#" + SutWithSelfBuilder { + self: Sut, + start_fn_arg: true, + named: 42, + }"#]], + ); + } +} + +mod attr_bounds_empty { + use crate::prelude::*; + + struct NoTraitImpls; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[builder(derive(Clone(bounds()), Debug))] + struct Sut<'a, T> { + _arg: &'a T, + } + + let _ = Sut::builder().arg(&NoTraitImpls).clone(); + } + + #[test] + fn test_free_fn() { + #[builder(derive(Clone(bounds()), Debug))] + fn sut(_arg: &T) {} + + let _ = sut::().arg(&NoTraitImpls).clone(); + } + + #[test] + fn test_assoc_method() { + #[derive(Clone, Debug)] + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(Clone(bounds()), Debug))] + fn sut(_arg: &T) {} + } + + let _ = Sut::sut::().arg(&NoTraitImpls).clone(); + } +} + +mod attr_bounds_non_empty { + use crate::prelude::*; + + struct NoTraitImpls; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[builder(derive(Clone(bounds(&'a T: Clone, &'a &'a T: Clone)), Debug))] + struct Sut<'a, T> { + _arg: &'a T, + } + + let _ = Sut::builder().arg(&NoTraitImpls).clone(); + } + + #[test] + fn test_free_fn() { + #[builder(derive(Clone(bounds(&'a T: Clone, &'a &'a T: Clone)), Debug))] + #[allow(clippy::needless_lifetimes, single_use_lifetimes)] + fn sut<'a, T>(_arg: &'a T) {} + + let _ = sut::().arg(&NoTraitImpls).clone(); + } + + #[test] + fn test_assoc_method() { + #[derive(Clone, Debug)] + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(Clone(bounds(&'a T: Clone, &'a &'a T: Clone)), Debug))] + #[allow(clippy::needless_lifetimes, single_use_lifetimes)] + fn sut<'a, T>(_arg: &'a T) {} + } + + let _ = Sut::sut::().arg(&NoTraitImpls).clone(); + } +} diff --git a/bon/tests/integration/builder/attr_expose_positional_fn.rs b/bon/tests/integration/builder/attr_expose_positional_fn.rs index 12733810..0a1e892e 100644 --- a/bon/tests/integration/builder/attr_expose_positional_fn.rs +++ b/bon/tests/integration/builder/attr_expose_positional_fn.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use core::fmt; #[test] fn method_new_doesnt_require_a_value_for_name() { @@ -33,7 +34,7 @@ fn method_new_doesnt_require_a_value_for_name() { #[test] fn with_nested_params() { #[builder(expose_positional_fn(name = positional))] - fn sut(arg1: bool, arg2: u32) -> (bool, u32) { + fn sut(arg1: bool, arg2: u32) -> impl fmt::Debug { (arg1, arg2) } diff --git a/bon/tests/integration/builder/attr_into.rs b/bon/tests/integration/builder/attr_into.rs index 9d7b4b98..881571dd 100644 --- a/bon/tests/integration/builder/attr_into.rs +++ b/bon/tests/integration/builder/attr_into.rs @@ -1,14 +1,14 @@ use crate::prelude::*; +use core::fmt; use core::num::NonZeroU32; #[cfg(feature = "alloc")] #[test] fn into_attr_alloc() { + use core::fmt; + #[builder] - fn sut( - #[builder(into)] set: Option>, - no_into: String, - ) -> (Option>, String) { + fn sut(#[builder(into)] set: Option>, no_into: String) -> impl fmt::Debug { (set, no_into) } @@ -27,7 +27,7 @@ fn into_attr_no_std() { /// Some docs #[builder(into)] u32: u32, - ) -> (&str, u32) { + ) -> impl fmt::Debug + '_ { (str_ref, u32) } @@ -52,7 +52,7 @@ fn into_attr_no_std() { #[test] fn into_string() { #[builder(on(String, into))] - fn sut(arg1: String, arg2: Option) -> (String, Option) { + fn sut(arg1: String, arg2: Option) -> impl fmt::Debug { (arg1, arg2) } diff --git a/bon/tests/integration/builder/attr_overwritable.rs b/bon/tests/integration/builder/attr_overwritable.rs new file mode 100644 index 00000000..27dbf87f --- /dev/null +++ b/bon/tests/integration/builder/attr_overwritable.rs @@ -0,0 +1,103 @@ +mod smoke { + use crate::prelude::*; + use core::fmt; + + #[test] + fn test_struct() { + #[derive(Debug, Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(overwritable)] + a: u32, + + #[builder(overwritable)] + b: Option, + + #[builder(overwritable, default)] + c: u32, + } + + assert_debug_eq( + Sut::builder().a(1).a(2).b(3).b(4).c(5).c(6).build(), + expect!["Sut { a: 2, b: Some(4), c: 6 }"], + ); + } + + #[test] + fn test_free_fn() { + #[builder] + fn sut( + #[builder(overwritable)] a: u32, + #[builder(overwritable)] b: Option, + #[builder(overwritable, default)] c: u32, + ) -> impl fmt::Debug { + (a, b, c) + } + + assert_debug_eq( + sut().a(1).a(2).b(3).b(4).c(5).c(6).call(), + expect!["(2, Some(4), 6)"], + ); + } + + #[test] + fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut( + #[builder(overwritable)] a: u32, + #[builder(overwritable)] b: Option, + #[builder(overwritable, default)] c: u32, + ) -> impl fmt::Debug { + (a, b, c) + } + + #[builder] + fn with_self( + &self, + #[builder(overwritable)] a: u32, + #[builder(overwritable)] b: Option, + #[builder(overwritable, default)] c: u32, + ) -> impl fmt::Debug { + let _ = self; + (a, b, c) + } + } + + assert_debug_eq( + Sut::sut().a(1).a(2).b(3).b(4).c(5).c(6).call(), + expect!["(2, Some(4), 6)"], + ); + + assert_debug_eq( + Sut.with_self().a(1).a(2).b(3).b(4).c(5).c(6).call(), + expect!["(2, Some(4), 6)"], + ); + } +} + +mod on { + use crate::prelude::*; + + #[test] + fn test_struct() { + #[derive(Debug, Builder)] + #[allow(dead_code)] + #[builder(on(u32, overwritable))] + struct Sut { + a: u32, + b: Option, + + #[builder(default)] + c: u32, + } + + assert_debug_eq( + Sut::builder().a(1).a(2).b(3).b(4).c(5).c(6).build(), + expect!["Sut { a: 2, b: Some(4), c: 6 }"], + ); + } +} diff --git a/bon/tests/integration/builder/attr_setters.rs b/bon/tests/integration/builder/attr_setters.rs new file mode 100644 index 00000000..a6a6b7b8 --- /dev/null +++ b/bon/tests/integration/builder/attr_setters.rs @@ -0,0 +1,436 @@ +mod name { + use crate::prelude::*; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(setters(name = arg1_renamed))] + arg1: bool, + + #[builder(setters(name = arg2_renamed))] + arg2: Option<()>, + + #[builder(setters(name = arg3_renamed), default)] + arg3: u32, + } + + use sut_builder::*; + + #[allow(type_alias_bounds)] + type _AssocTypes = (T::Arg1, T::Arg2, T::Arg3); + + let _ = Sut::builder().arg1_renamed(true); + + let _ = Sut::builder().arg2_renamed(()); + let _ = Sut::builder().maybe_arg2_renamed(Some(())); + + let _ = Sut::builder().arg3_renamed(42); + let _ = Sut::builder().maybe_arg3_renamed(Some(42)); + + // The name in the state must remain the same + let _: SutBuilder>> = Sut::builder() + .arg1_renamed(true) + .arg2_renamed(()) + .arg3_renamed(42); + } + + #[test] + fn test_free_fn() { + #[builder] + fn sut( + #[builder(setters(name = arg1_renamed))] _arg1: bool, + #[builder(setters(name = arg2_renamed))] _arg2: Option<()>, + #[builder(setters(name = arg3_renamed), default)] _arg3: u32, + ) { + } + + use sut_builder::*; + + #[allow(type_alias_bounds)] + type _AssocTypes = (T::Arg1, T::Arg2, T::Arg3); + + let _ = sut().arg1_renamed(true); + + let _ = sut().arg2_renamed(()); + let _ = sut().maybe_arg2_renamed(Some(())); + + let _ = sut().arg3_renamed(42); + let _ = sut().maybe_arg3_renamed(Some(42)); + + // The name in the state must remain the same + let _: SutBuilder>> = + sut().arg1_renamed(true).arg2_renamed(()).arg3_renamed(42); + } + + #[test] + fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut( + #[builder(setters(name = arg1_renamed))] _arg1: bool, + #[builder(setters(name = arg2_renamed))] _arg2: Option<()>, + #[builder(setters(name = arg3_renamed), default)] _arg3: u32, + ) { + } + + #[builder] + fn with_self( + &self, + #[builder(setters(name = arg1_renamed))] _arg1: bool, + #[builder(setters(name = arg2_renamed))] _arg2: Option<()>, + #[builder(setters(name = arg3_renamed), default)] _arg3: u32, + ) { + let _ = self; + } + } + + { + use sut_sut_builder::*; + + #[allow(type_alias_bounds)] + type _AssocTypes = (T::Arg1, T::Arg2, T::Arg3); + + let _ = Sut::sut().arg1_renamed(true); + + let _ = Sut::sut().arg2_renamed(()); + let _ = Sut::sut().maybe_arg2_renamed(Some(())); + + let _ = Sut::sut().arg3_renamed(42); + let _ = Sut::sut().maybe_arg3_renamed(Some(42)); + + // The name in the state must remain the same + let _: SutSutBuilder>> = Sut::sut() + .arg1_renamed(true) + .arg2_renamed(()) + .arg3_renamed(42); + } + + { + use sut_with_self_builder::*; + + #[allow(type_alias_bounds)] + type _AssocTypes = (T::Arg1, T::Arg2, T::Arg3); + + let sut = Sut; + + let _ = sut.with_self().arg1_renamed(true); + + let _ = sut.with_self().arg2_renamed(()); + let _ = sut.with_self().maybe_arg2_renamed(Some(())); + + let _ = sut.with_self().arg3_renamed(42); + let _ = sut.with_self().maybe_arg3_renamed(Some(42)); + + // The name in the state must remain the same + let _: SutWithSelfBuilder<'_, SetArg3>> = sut + .with_self() + .arg1_renamed(true) + .arg2_renamed(()) + .arg3_renamed(42); + } + } +} + +mod option_fn_name_and_some_fn_name { + use crate::prelude::*; + + #[test] + fn test_struct() { + #[derive(Builder)] + #[builder(derive(Clone))] + #[allow(dead_code)] + struct Sut { + #[builder(setters(some_fn = arg1_some))] + arg1: Option<()>, + + #[builder(setters(option_fn = arg2_option))] + arg2: Option<()>, + + #[builder(setters(some_fn = arg3_some, option_fn = arg3_option))] + arg3: Option<()>, + + #[builder(setters(some_fn(name = arg4_some), option_fn(name = arg4_option)))] + arg4: Option<()>, + + #[builder(default, setters(some_fn = arg5_some))] + arg5: (), + + #[builder(default, setters(option_fn = arg6_option))] + arg6: (), + + #[builder(default, setters(some_fn = arg7_some, option_fn = arg7_option))] + arg7: (), + + #[builder(default, setters(some_fn(name = arg8_some), option_fn(name = arg8_option)))] + arg8: (), + } + + use sut_builder::*; + + let _ = Sut::builder().arg1_some(()); + let _ = Sut::builder().maybe_arg1(Some(())); + + let _ = Sut::builder().arg2(()); + let _ = Sut::builder().arg2_option(Some(())); + + let _ = Sut::builder().arg3_some(()); + let _ = Sut::builder().arg3_option(Some(())); + + let _ = Sut::builder().arg4_some(()); + let _ = Sut::builder().arg4_option(Some(())); + + let _ = Sut::builder().arg5_some(()); + let _ = Sut::builder().maybe_arg5(Some(())); + + let _ = Sut::builder().arg6(()); + let _ = Sut::builder().arg6_option(Some(())); + + let _ = Sut::builder().arg7_some(()); + let _ = Sut::builder().arg7_option(Some(())); + + let _ = Sut::builder().arg8_some(()); + let _ = Sut::builder().arg8_option(Some(())); + + #[allow(clippy::type_complexity)] + let _: SutBuilder< + SetArg8>>>>>>, + > = Sut::builder() + .arg1_some(()) + .arg2(()) + .arg3_some(()) + .arg4_some(()) + .arg5_some(()) + .arg6(()) + .arg7_some(()) + .arg8_some(()); + } + + #[test] + fn test_free_fn() { + #[builder(derive(Clone))] + fn sut( + #[builder(setters(some_fn = arg1_some))] _arg1: Option<()>, + #[builder(setters(option_fn = arg2_option))] _arg2: Option<()>, + #[builder(setters(some_fn = arg3_some, option_fn = arg3_option))] _arg3: Option<()>, + #[builder(setters(some_fn(name = arg4_some), option_fn(name = arg4_option)))] + _arg4: Option<()>, + + #[builder(default, setters(some_fn = arg5_some))] _arg5: (), + #[builder(default, setters(option_fn = arg6_option))] _arg6: (), + #[builder(default, setters(some_fn = arg7_some, option_fn = arg7_option))] _arg7: (), + #[builder(default, setters(some_fn(name = arg8_some), option_fn(name = arg8_option)))] + _arg8: (), + ) { + } + + use sut_builder::*; + + let _ = sut().arg1_some(()); + let _ = sut().maybe_arg1(Some(())); + + let _ = sut().arg2(()); + let _ = sut().arg2_option(Some(())); + + let _ = sut().arg3_some(()); + let _ = sut().arg3_option(Some(())); + + let _ = sut().arg4_some(()); + let _ = sut().arg4_option(Some(())); + + let _ = sut().arg5_some(()); + let _ = sut().maybe_arg5(Some(())); + + let _ = sut().arg6(()); + let _ = sut().arg6_option(Some(())); + + let _ = sut().arg7_some(()); + let _ = sut().arg7_option(Some(())); + + let _ = sut().arg8_some(()); + let _ = sut().arg8_option(Some(())); + + #[allow(clippy::type_complexity)] + let _: SutBuilder< + SetArg8>>>>>>, + > = sut() + .arg1_some(()) + .arg2(()) + .arg3_some(()) + .arg4_some(()) + .arg5_some(()) + .arg6(()) + .arg7_some(()) + .arg8_some(()); + } + + #[test] + fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(Clone))] + fn sut( + #[builder(setters(some_fn = arg1_some))] _arg1: Option<()>, + #[builder(setters(option_fn = arg2_option))] _arg2: Option<()>, + #[builder(setters(some_fn = arg3_some, option_fn = arg3_option))] _arg3: Option<()>, + #[builder(setters(some_fn(name = arg4_some), option_fn(name = arg4_option)))] + _arg4: Option<()>, + + #[builder(default, setters(some_fn = arg5_some))] _arg5: (), + #[builder(default, setters(option_fn = arg6_option))] _arg6: (), + #[builder(default, setters(some_fn = arg7_some, option_fn = arg7_option))] _arg7: ( + ), + #[builder(default, setters(some_fn(name = arg8_some), option_fn(name = arg8_option)))] + _arg8: (), + ) { + } + + #[builder(derive(Clone))] + fn with_self( + &self, + #[builder(setters(some_fn = arg1_some))] _arg1: Option<()>, + #[builder(setters(option_fn = arg2_option))] _arg2: Option<()>, + #[builder(setters(some_fn = arg3_some, option_fn = arg3_option))] _arg3: Option<()>, + #[builder(setters(some_fn(name = arg4_some), option_fn(name = arg4_option)))] + _arg4: Option<()>, + + #[builder(default, setters(some_fn = arg5_some))] _arg5: (), + #[builder(default, setters(option_fn = arg6_option))] _arg6: (), + #[builder(default, setters(some_fn = arg7_some, option_fn = arg7_option))] _arg7: ( + ), + #[builder(default, setters(some_fn(name = arg8_some), option_fn(name = arg8_option)))] + _arg8: (), + ) { + let _ = self; + } + } + + { + use sut_sut_builder::*; + + let _ = Sut::sut().arg1_some(()); + let _ = Sut::sut().maybe_arg1(Some(())); + + let _ = Sut::sut().arg2(()); + let _ = Sut::sut().arg2_option(Some(())); + + let _ = Sut::sut().arg3_some(()); + let _ = Sut::sut().arg3_option(Some(())); + + let _ = Sut::sut().arg4_some(()); + let _ = Sut::sut().arg4_option(Some(())); + + let _ = Sut::sut().arg5_some(()); + let _ = Sut::sut().maybe_arg5(Some(())); + + let _ = Sut::sut().arg6(()); + let _ = Sut::sut().arg6_option(Some(())); + + let _ = Sut::sut().arg7_some(()); + let _ = Sut::sut().arg7_option(Some(())); + + let _ = Sut::sut().arg8_some(()); + let _ = Sut::sut().arg8_option(Some(())); + + #[allow(clippy::type_complexity)] + let _: SutSutBuilder< + SetArg8>>>>>>, + > = Sut::sut() + .arg1_some(()) + .arg2(()) + .arg3_some(()) + .arg4_some(()) + .arg5_some(()) + .arg6(()) + .arg7_some(()) + .arg8_some(()); + } + + { + use sut_with_self_builder::*; + + let _ = Sut.with_self().arg1_some(()); + let _ = Sut.with_self().maybe_arg1(Some(())); + + let _ = Sut.with_self().arg2(()); + let _ = Sut.with_self().arg2_option(Some(())); + + let _ = Sut.with_self().arg3_some(()); + let _ = Sut.with_self().arg3_option(Some(())); + + let _ = Sut.with_self().arg4_some(()); + let _ = Sut.with_self().arg4_option(Some(())); + + let _ = Sut.with_self().arg5_some(()); + let _ = Sut.with_self().maybe_arg5(Some(())); + + let _ = Sut.with_self().arg6(()); + let _ = Sut.with_self().arg6_option(Some(())); + + let _ = Sut.with_self().arg7_some(()); + let _ = Sut.with_self().arg7_option(Some(())); + + let _ = Sut.with_self().arg8_some(()); + let _ = Sut.with_self().arg8_option(Some(())); + + #[allow(clippy::type_complexity)] + let _: SutWithSelfBuilder< + '_, + SetArg8>>>>>>, + > = Sut + .with_self() + .arg1_some(()) + .arg2(()) + .arg3_some(()) + .arg4_some(()) + .arg5_some(()) + .arg6(()) + .arg7_some(()) + .arg8_some(()); + } + } +} + +mod self_references_in_docs { + use crate::prelude::*; + + #[test] + fn test_struct() { + /// [`Self`] link + #[derive(Builder)] + struct Sut { + /// [`Self`] link + #[builder(setters(doc {}))] + _arg1: u32, + + /// [`Self`] link + #[builder(setters( + option_fn(doc {}), + some_fn(doc {}) + ))] + _arg2: Option, + } + + let _ = Sut::builder().arg1(42); + } + + #[test] + fn test_free_fn() { + /// [`Self`] link + #[builder] + fn sut( + /// [`Self`] link + #[builder(setters(doc {}))] + _arg1: u32, + ) { + } + + let _ = sut().arg1(42); + } +} diff --git a/bon/tests/integration/builder/attr_transparent.rs b/bon/tests/integration/builder/attr_transparent.rs new file mode 100644 index 00000000..b372003a --- /dev/null +++ b/bon/tests/integration/builder/attr_transparent.rs @@ -0,0 +1,127 @@ +use crate::prelude::*; +use core::fmt; + +#[test] +fn test_struct() { + #[derive(Debug, Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(transparent)] + regular: Option, + + #[builder(transparent)] + generic: Option, + + #[builder(transparent, into)] + with_into: Option, + + #[builder(transparent, default = Some(99))] + with_default: Option, + + #[builder(transparent, default = Some(10))] + with_default_2: Option, + } + + assert_debug_eq( + Sut::builder() + .regular(Some(1)) + .generic(Some(false)) + .with_into(2) + .maybe_with_default_2(Some(Some(3))) + .build(), + expect![[r#" + Sut { + regular: Some( + 1, + ), + generic: Some( + false, + ), + with_into: Some( + 2, + ), + with_default: Some( + 99, + ), + with_default_2: Some( + 3, + ), + }"#]], + ); +} + +#[test] +fn test_free_fn() { + #[builder] + fn sut( + #[builder(transparent)] regular: Option, + #[builder(transparent)] generic: Option, + #[builder(transparent, into)] with_into: Option, + #[builder(transparent, default = Some(99))] with_default: Option, + #[builder(transparent, default = Some(10))] with_default_2: Option, + ) -> impl fmt::Debug { + (regular, generic, with_into, with_default, with_default_2) + } + + assert_debug_eq( + sut() + .regular(Some(1)) + .generic(Some(false)) + .with_into(2) + .maybe_with_default_2(Some(Some(3))) + .call(), + expect!["(Some(1), Some(false), Some(2), Some(99), Some(3))"], + ); +} + +#[test] +fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut( + #[builder(transparent)] regular: Option, + #[builder(transparent)] generic: Option, + #[builder(transparent, into)] with_into: Option, + #[builder(transparent, default = Some(99))] with_default: Option, + #[builder(transparent, default = Some(10))] with_default_2: Option, + ) -> impl fmt::Debug { + (regular, generic, with_into, with_default, with_default_2) + } + + #[builder] + fn with_self( + &self, + #[builder(transparent)] regular: Option, + #[builder(transparent)] generic: Option, + #[builder(transparent, into)] with_into: Option, + #[builder(transparent, default = Some(99))] with_default: Option, + #[builder(transparent, default = Some(10))] with_default_2: Option, + ) -> impl fmt::Debug { + let _ = self; + (regular, generic, with_into, with_default, with_default_2) + } + } + + assert_debug_eq( + Sut::sut() + .regular(Some(1)) + .generic(Some(false)) + .with_into(2) + .maybe_with_default_2(Some(Some(3))) + .call(), + expect!["(Some(1), Some(false), Some(2), Some(99), Some(3))"], + ); + + assert_debug_eq( + Sut.with_self() + .regular(Some(1)) + .generic(Some(false)) + .with_into(2) + .maybe_with_default_2(Some(Some(3))) + .call(), + expect!["(Some(1), Some(false), Some(2), Some(99), Some(3))"], + ); +} diff --git a/bon/tests/integration/builder/attr_with/mod.rs b/bon/tests/integration/builder/attr_with/mod.rs new file mode 100644 index 00000000..c28189c6 --- /dev/null +++ b/bon/tests/integration/builder/attr_with/mod.rs @@ -0,0 +1,11 @@ +mod multi_arg; +mod overwritable; +mod single_arg; + +struct IntoStrRef<'a>(&'a str); + +impl<'a> From> for &'a str { + fn from(val: IntoStrRef<'a>) -> Self { + val.0 + } +} diff --git a/bon/tests/integration/builder/attr_with/multi_arg.rs b/bon/tests/integration/builder/attr_with/multi_arg.rs new file mode 100644 index 00000000..a70cbeea --- /dev/null +++ b/bon/tests/integration/builder/attr_with/multi_arg.rs @@ -0,0 +1,115 @@ +use super::IntoStrRef; +use crate::prelude::*; +use core::convert::Infallible; +use core::{fmt, num}; +type ParseIntResult = Result; + +#[test] +fn test_struct() { + #[derive(Debug, Builder)] + #[builder(derive(Clone))] + #[allow(dead_code)] + struct Sut { + #[builder(with = |x: u32, y: u32| x + y)] + required: u32, + + #[builder(with = |x: u32, y: u32| x + y)] + optional: Option, + + #[builder(with = |x: u32, y: u32| x + y, default)] + default: u32, + + #[builder(with = |value: &T, _value2: &T| value.clone())] + generic: T, + + #[builder(with = |value: &T, _value2: &T| value.clone())] + optional_generic: Option, + + #[builder(with = |value: &T, _value2: &T| value.clone(), default)] + default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| value.into())] + impl_trait: &'static str, + + #[builder(with = |value: &str, _value2: u32| -> Result<_, num::ParseIntError> { value.parse() })] + try_required: u32, + + #[builder(with = |value: &str, _value2: u32| -> Result<_, num::ParseIntError> { value.parse() })] + try_optional: Option, + + #[builder(with = |value: &str, _value2: u32| -> ParseIntResult<_> { value.parse() }, default)] + try_default: u32, + + #[builder(with = |value: &T, _value2: &T| -> Result<_, Infallible> { Ok(value.clone()) })] + try_optional_generic: Option, + + #[builder(with = |value: &T, _value2: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>, _value2: impl fmt::Debug| -> Result<_, Infallible> { Ok(value.into()) })] + try_impl_trait: &'static str, + } + + let builder = Sut::builder() + .required(1, 2) + .optional(3, 4) + .default(5, 6) + .generic(&"hello", &"world"); + + let _ignore = builder.clone().optional_generic(&"hello", &"you"); + let builder = builder.maybe_optional_generic(Some((&"littlepip", &"blackjack"))); + + let _ignore = builder.clone().default_generic(&"p21", &"rampage"); + let builder = builder.maybe_default_generic(Some((&"<3", &"glory"))); + + let builder = builder + .impl_trait(IntoStrRef("p21")) + .try_required("4", 99) + .unwrap(); + + let _ignore = builder.clone().try_optional("5", 99).unwrap(); + let builder = builder.maybe_try_optional(Some(("6", 99))).unwrap(); + + let _ignore = builder.clone().try_default("7", 99).unwrap(); + let builder = builder.maybe_try_default(Some(("8", 99))).unwrap(); + + let _ignore = builder.clone().try_optional_generic(&"9", &"99").unwrap(); + let builder = builder + .maybe_try_optional_generic(Some((&"10", &"99"))) + .unwrap(); + + let _ignore = builder.clone().try_default_generic(&"11", &"99").unwrap(); + let builder = builder + .maybe_try_default_generic(Some((&"12", &"99"))) + .unwrap(); + + let builder = builder.try_impl_trait(IntoStrRef("p21"), true).unwrap(); + + assert_debug_eq( + builder.build(), + expect![[r#" + Sut { + required: 3, + optional: Some( + 7, + ), + default: 11, + generic: "hello", + optional_generic: Some( + "littlepip", + ), + default_generic: "<3", + impl_trait: "p21", + try_required: 4, + try_optional: Some( + 6, + ), + try_default: 8, + try_optional_generic: Some( + "10", + ), + try_default_generic: "12", + try_impl_trait: "p21", + }"#]], + ); +} diff --git a/bon/tests/integration/builder/attr_with/overwritable.rs b/bon/tests/integration/builder/attr_with/overwritable.rs new file mode 100644 index 00000000..7ac7145b --- /dev/null +++ b/bon/tests/integration/builder/attr_with/overwritable.rs @@ -0,0 +1,119 @@ +use super::IntoStrRef; +use crate::prelude::*; +use core::convert::Infallible; +use core::num; +type ParseIntResult = Result; + +#[test] +fn test_struct() { + #[derive(Debug, Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(with = |x: u32| x + 1, overwritable)] + required: u32, + + #[builder(with = |x: u32| x + 1, overwritable)] + optional: Option, + + #[builder(with = |x: u32| x + 1, default, overwritable)] + default: u32, + + #[builder(with = |value: &T| value.clone(), overwritable)] + generic: T, + + #[builder(with = |value: &T| value.clone(), overwritable)] + optional_generic: Option, + + #[builder(with = |value: &T| value.clone(), default, overwritable)] + default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| value.into(), overwritable)] + impl_trait: &'static str, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() }, overwritable)] + try_required: u32, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() }, overwritable)] + try_optional: Option, + + #[builder(with = |value: &str| -> ParseIntResult<_> { value.parse() }, default, overwritable)] + try_default: u32, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, overwritable)] + try_optional_generic: Option, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default, overwritable)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| -> Result<_, Infallible> { Ok(value.into()) }, overwritable)] + try_impl_trait: &'static str, + } + + let builder = Sut::builder() + .required(1) + .required(2) + .optional(3) + .optional(4) + .default(5) + .default(6) + .generic(&"hello") + .generic(&"hello2") + .optional_generic(&"hello you") + .maybe_optional_generic(Some(&"littlepip")) + .default_generic(&"blackjack") + .maybe_default_generic(Some(&"<3")) + .impl_trait(IntoStrRef("p21")) + .impl_trait(IntoStrRef("rampage")) + .try_required("7") + .unwrap() + .try_required("8") + .unwrap() + .try_optional("9") + .unwrap() + .maybe_try_optional(Some("10")) + .unwrap() + .try_default("11") + .unwrap() + .maybe_try_default(Some("12")) + .unwrap() + .try_optional_generic(&"13") + .unwrap() + .maybe_try_optional_generic(Some(&"14")) + .unwrap() + .try_default_generic(&"15") + .unwrap() + .maybe_try_default_generic(Some(&"16")) + .unwrap() + .try_impl_trait(IntoStrRef("daisy")) + .unwrap() + .try_impl_trait(IntoStrRef("roseluck")) + .unwrap(); + + assert_debug_eq( + builder.build(), + expect![[r#" + Sut { + required: 3, + optional: Some( + 5, + ), + default: 7, + generic: "hello2", + optional_generic: Some( + "littlepip", + ), + default_generic: "<3", + impl_trait: "rampage", + try_required: 8, + try_optional: Some( + 10, + ), + try_default: 12, + try_optional_generic: Some( + "14", + ), + try_default_generic: "16", + try_impl_trait: "roseluck", + }"#]], + ); +} diff --git a/bon/tests/integration/builder/attr_with/single_arg.rs b/bon/tests/integration/builder/attr_with/single_arg.rs new file mode 100644 index 00000000..0381ce2e --- /dev/null +++ b/bon/tests/integration/builder/attr_with/single_arg.rs @@ -0,0 +1,459 @@ +use crate::prelude::*; +use core::convert::Infallible; +use core::{fmt, num}; +type ParseIntResult = Result; +use super::IntoStrRef; + +#[test] +fn test_struct() { + #[derive(Debug, Builder)] + #[builder(derive(Clone))] + #[allow(dead_code)] + struct Sut { + #[builder(with = |x: u32| x + 1)] + required: u32, + + #[builder(with = |x: u32| x + 1)] + optional: Option, + + #[builder(with = |x: u32| x + 1, default)] + default: u32, + + #[builder(with = |value: &T| value.clone())] + generic: T, + + #[builder(with = |value: &T| value.clone())] + optional_generic: Option, + + #[builder(with = |value: &T| value.clone(), default)] + default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| value.into())] + impl_trait: &'static str, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_required: u32, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_optional: Option, + + #[builder(with = |value: &str| -> ParseIntResult<_> { value.parse() }, default)] + try_default: u32, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) })] + try_optional_generic: Option, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| -> Result<_, Infallible> { Ok(value.into()) })] + try_impl_trait: &'static str, + } + + let builder = Sut::builder() + .required(1) + .optional(2) + .default(3) + .generic(&"hello"); + + let _ignore = builder.clone().optional_generic(&"hello you"); + let builder = builder.maybe_optional_generic(Some(&"littlepip")); + + let _ignore = builder.clone().default_generic(&"blackjack"); + let builder = builder.maybe_default_generic(Some(&"<3")); + + let builder = builder + .impl_trait(IntoStrRef("morning glory")) + .try_required("4") + .unwrap(); + + let _ignore = builder.clone().try_optional("5").unwrap(); + let builder = builder.maybe_try_optional(Some("6")).unwrap(); + + let _ignore = builder.clone().try_default("7").unwrap(); + let builder = builder.maybe_try_default(Some("8")).unwrap(); + + let _ignore = builder.clone().try_optional_generic(&"9").unwrap(); + let builder = builder.maybe_try_optional_generic(Some(&"10")).unwrap(); + + let _ignore = builder.clone().try_default_generic(&"11").unwrap(); + let builder = builder.maybe_try_default_generic(Some(&"12")).unwrap(); + + let builder = builder.try_impl_trait(IntoStrRef("morning glory")).unwrap(); + + assert_debug_eq( + builder.build(), + expect![[r#" + Sut { + required: 2, + optional: Some( + 3, + ), + default: 4, + generic: "hello", + optional_generic: Some( + "littlepip", + ), + default_generic: "<3", + impl_trait: "morning glory", + try_required: 4, + try_optional: Some( + 6, + ), + try_default: 8, + try_optional_generic: Some( + "10", + ), + try_default_generic: "12", + try_impl_trait: "morning glory", + }"#]], + ); +} + +#[test] +fn test_free_fn() { + #[builder(derive(Clone))] + fn sut( + #[builder(with = |x: u32| x + 1)] required: u32, + #[builder(with = |x: u32| x + 1)] optional: Option, + #[builder(with = |x: u32| x + 1, default)] default: u32, + #[builder(with = |value: &T| value.clone())] generic: T, + #[builder(with = |value: &T| value.clone())] optional_generic: Option, + #[builder(with = |value: &T| value.clone(), default)] default_generic: T, + #[builder(with = |value: impl Into<&'static str>| value.into())] impl_trait: &'static str, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_required: u32, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_optional: Option, + + #[builder(with = |value: &str| -> ParseIntResult<_> { value.parse() }, default)] + try_default: u32, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) })] + try_optional_generic: Option, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| -> Result<_, Infallible> { Ok(value.into()) })] + try_impl_trait: &'static str, + ) -> impl fmt::Debug { + ( + ( + required, + optional, + default, + generic, + optional_generic, + default_generic, + impl_trait, + ), + ( + try_required, + try_optional, + try_default, + try_optional_generic, + try_default_generic, + try_impl_trait, + ), + ) + } + + let builder = sut().required(1).optional(2).default(3).generic(&"hello"); + + let _ignore = builder.clone().optional_generic(&"hello you"); + let builder = builder.maybe_optional_generic(Some(&"littlepip")); + + let _ignore = builder.clone().default_generic(&"blackjack"); + let builder = builder.maybe_default_generic(Some(&"<3")); + + let builder = builder + .impl_trait(IntoStrRef("morning glory")) + .try_required("4") + .unwrap(); + + let _ignore = builder.clone().try_optional("5").unwrap(); + let builder = builder.maybe_try_optional(Some("6")).unwrap(); + + let _ignore = builder.clone().try_default("7").unwrap(); + let builder = builder.maybe_try_default(Some("8")).unwrap(); + + let _ignore = builder.clone().try_optional_generic(&"9").unwrap(); + let builder = builder.maybe_try_optional_generic(Some(&"10")).unwrap(); + + let _ignore = builder.clone().try_default_generic(&"11").unwrap(); + let builder = builder.maybe_try_default_generic(Some(&"12")).unwrap(); + + let builder = builder.try_impl_trait(IntoStrRef("morning glory")).unwrap(); + + assert_debug_eq( + builder.call(), + expect![[r#" + ( + ( + 2, + Some( + 3, + ), + 4, + "hello", + Some( + "littlepip", + ), + "<3", + "morning glory", + ), + ( + 4, + Some( + 6, + ), + 8, + Some( + "10", + ), + "12", + "morning glory", + ), + )"#]], + ); +} + +#[test] +fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(Clone))] + fn sut( + #[builder(with = |x: u32| x + 1)] required: u32, + #[builder(with = |x: u32| x + 1)] optional: Option, + #[builder(with = |x: u32| x + 1, default)] default: u32, + #[builder(with = |value: &T| value.clone())] generic: T, + #[builder(with = |value: &T| value.clone())] optional_generic: Option, + #[builder(with = |value: &T| value.clone(), default)] default_generic: T, + #[builder(with = |value: impl Into<&'static str>| value.into())] + impl_trait: &'static str, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_required: u32, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_optional: Option, + + #[builder(with = |value: &str| -> ParseIntResult<_> { value.parse() }, default)] + try_default: u32, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) })] + try_optional_generic: Option, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| -> Result<_, Infallible> { Ok(value.into()) })] + try_impl_trait: &'static str, + ) -> impl fmt::Debug { + ( + ( + required, + optional, + default, + generic, + optional_generic, + default_generic, + impl_trait, + ), + ( + try_required, + try_optional, + try_default, + try_optional_generic, + try_default_generic, + try_impl_trait, + ), + ) + } + + #[builder(derive(Clone))] + fn with_self( + &self, + #[builder(with = |x: u32| x + 1)] required: u32, + #[builder(with = |x: u32| x + 1)] optional: Option, + #[builder(with = |x: u32| x + 1, default)] default: u32, + #[builder(with = |value: &T| value.clone())] generic: T, + #[builder(with = |value: &T| value.clone())] optional_generic: Option, + #[builder(with = |value: &T| value.clone(), default)] default_generic: T, + #[builder(with = |value: impl Into<&'static str>| value.into())] + impl_trait: &'static str, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_required: u32, + + #[builder(with = |value: &str| -> Result<_, num::ParseIntError> { value.parse() })] + try_optional: Option, + + #[builder(with = |value: &str| -> ParseIntResult<_> { value.parse() }, default)] + try_default: u32, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) })] + try_optional_generic: Option, + + #[builder(with = |value: &T| -> Result<_, Infallible> { Ok(value.clone()) }, default)] + try_default_generic: T, + + #[builder(with = |value: impl Into<&'static str>| -> Result<_, Infallible> { Ok(value.into()) })] + try_impl_trait: &'static str, + ) -> impl fmt::Debug { + let _ = self; + ( + ( + required, + optional, + default, + generic, + optional_generic, + default_generic, + impl_trait, + ), + ( + try_required, + try_optional, + try_default, + try_optional_generic, + try_default_generic, + try_impl_trait, + ), + ) + } + } + + let builder = Sut::sut() + .required(1) + .optional(2) + .default(3) + .generic(&"hello"); + + let _ignore = builder.clone().optional_generic(&"hello you"); + let builder = builder.maybe_optional_generic(Some(&"littlepip")); + + let _ignore = builder.clone().default_generic(&"blackjack"); + let builder = builder.maybe_default_generic(Some(&"<3")); + + let builder = builder + .impl_trait(IntoStrRef("morning glory")) + .try_required("4") + .unwrap(); + + let _ignore = builder.clone().try_optional("5").unwrap(); + let builder = builder.maybe_try_optional(Some("6")).unwrap(); + + let _ignore = builder.clone().try_default("7").unwrap(); + let builder = builder.maybe_try_default(Some("8")).unwrap(); + + let _ignore = builder.clone().try_optional_generic(&"9").unwrap(); + let builder = builder.maybe_try_optional_generic(Some(&"10")).unwrap(); + + let _ignore = builder.clone().try_default_generic(&"11").unwrap(); + let builder = builder.maybe_try_default_generic(Some(&"12")).unwrap(); + + let builder = builder.try_impl_trait(IntoStrRef("morning glory")).unwrap(); + + assert_debug_eq( + builder.call(), + expect![[r#" + ( + ( + 2, + Some( + 3, + ), + 4, + "hello", + Some( + "littlepip", + ), + "<3", + "morning glory", + ), + ( + 4, + Some( + 6, + ), + 8, + Some( + "10", + ), + "12", + "morning glory", + ), + )"#]], + ); + + let builder = Sut + .with_self() + .required(1) + .optional(2) + .default(3) + .generic(&"hello"); + + let _ignore = builder.clone().optional_generic(&"hello you"); + let builder = builder.maybe_optional_generic(Some(&"littlepip")); + + let _ignore = builder.clone().default_generic(&"blackjack"); + let builder = builder.maybe_default_generic(Some(&"<3")); + + let builder = builder + .impl_trait(IntoStrRef("morning glory")) + .try_required("4") + .unwrap(); + + let _ignore = builder.clone().try_optional("5").unwrap(); + let builder = builder.maybe_try_optional(Some("6")).unwrap(); + + let _ignore = builder.clone().try_default("7").unwrap(); + let builder = builder.maybe_try_default(Some("8")).unwrap(); + + let _ignore = builder.clone().try_optional_generic(&"9").unwrap(); + let builder = builder.maybe_try_optional_generic(Some(&"10")).unwrap(); + + let _ignore = builder.clone().try_default_generic(&"11").unwrap(); + let builder = builder.maybe_try_default_generic(Some(&"12")).unwrap(); + + let builder = builder.try_impl_trait(IntoStrRef("morning glory")).unwrap(); + + assert_debug_eq( + builder.call(), + expect![[r#" + ( + ( + 2, + Some( + 3, + ), + 4, + "hello", + Some( + "littlepip", + ), + "<3", + "morning glory", + ), + ( + 4, + Some( + 6, + ), + 8, + Some( + "10", + ), + "12", + "morning glory", + ), + )"#]], + ); +} diff --git a/bon/tests/integration/builder/builder_derives.rs b/bon/tests/integration/builder/builder_derives.rs deleted file mode 100644 index a071bb66..00000000 --- a/bon/tests/integration/builder/builder_derives.rs +++ /dev/null @@ -1,236 +0,0 @@ -// We intentionally exercise cloning from `#[builder(derive(Clone))]` here -// to make sure that it works. -#![allow(clippy::redundant_clone)] - -use crate::prelude::*; - -#[test] -fn smoke_fn() { - #[builder(derive(Clone, Debug))] - fn sut(_arg1: bool, _arg2: Option<()>, _arg3: Option<&str>, _arg4: Option) {} - - let actual = sut().arg1(true).arg3("value").maybe_arg4(None).clone(); - - assert_debug_eq( - actual, - expect![[r#" - SutBuilder { - _arg1: true, - _arg3: Some( - "value", - ), - _arg4: None, - }"#]], - ); -} - -#[test] -fn smoke_struct() { - #[derive(Builder)] - #[builder(derive(Clone, Debug))] - struct Sut<'a> { - _arg1: bool, - _arg2: Option<()>, - _arg3: Option<&'a str>, - _arg4: Option, - } - - let actual = Sut::builder() - .arg1(true) - .arg3("value") - .maybe_arg4(None) - .clone(); - - assert_debug_eq( - actual, - expect![[r#" - SutBuilder { - _arg1: true, - _arg3: Some( - "value", - ), - _arg4: None, - }"#]], - ); -} - -#[test] -fn builder_with_receiver() { - #[derive(Clone, Debug)] - struct Sut { - #[allow(dead_code)] - name: &'static str, - } - - #[bon] - impl Sut { - #[builder(derive(Clone, Debug))] - fn method(&self, other_name: &'static str, values: &[u8]) { - let _ = (self, other_name, values); - } - } - - let actual = Sut { name: "Blackjack" } - .method() - .other_name("P21") - .values(&[1, 2, 3]) - .clone(); - - assert_debug_eq( - actual, - expect![[r#" - SutMethodBuilder { - self: Sut { - name: "Blackjack", - }, - other_name: "P21", - values: [ - 1, - 2, - 3, - ], - }"#]], - ); -} - -#[test] -fn empty_derives() { - #[derive(Builder)] - #[builder(derive())] - struct Sut { - _arg1: bool, - } - - let _ = Sut::builder().arg1(true).build(); -} - -#[test] -fn skipped_members() { - struct NoDebug; - - #[derive(Builder)] - #[builder(derive(Debug, Clone))] - struct Sut { - _arg1: bool, - - #[builder(skip = NoDebug)] - _arg2: NoDebug, - } - - let actual = Sut::builder().arg1(true).clone(); - - assert_debug_eq(actual, expect!["SutBuilder { _arg1: true }"]); -} - -#[test] -fn empty_builder() { - #[derive(Builder)] - #[builder(derive(Clone, Debug))] - struct Sut {} - - #[allow(clippy::redundant_clone)] - let actual = Sut::builder().clone(); - - assert_debug_eq(actual, expect!["SutBuilder"]); -} - -#[test] -fn positional_members_struct() { - #[derive(Builder)] - #[builder(derive(Clone, Debug))] - #[allow(dead_code)] - struct Sut { - #[builder(start_fn)] - start_fn_arg: bool, - - #[builder(finish_fn)] - finish_fn_arg: &'static str, - - named: u32, - } - - let actual = Sut::builder(true); - - assert_debug_eq(actual.clone(), expect!["SutBuilder { start_fn_arg: true }"]); - - assert_debug_eq( - actual.named(42).clone(), - expect!["SutBuilder { start_fn_arg: true, named: 42 }"], - ); -} - -#[test] -fn positional_members_fn() { - #[builder(derive(Clone, Debug))] - #[allow(unused_variables)] - fn sut( - #[builder(start_fn)] start_fn_arg: bool, - #[builder(finish_fn)] finish_fn_arg: &'static str, - named: u32, - ) { - } - - let actual = sut(true); - - assert_debug_eq(actual.clone(), expect!["SutBuilder { start_fn_arg: true }"]); - - assert_debug_eq( - actual.named(42).clone(), - expect!["SutBuilder { start_fn_arg: true, named: 42 }"], - ); -} - -#[test] -fn positional_members_impl_block() { - #[derive(Debug)] - struct Sut; - - #[bon] - #[allow(unused_variables)] - impl Sut { - #[builder(derive(Clone, Debug))] - fn sut( - #[builder(start_fn)] start_fn_arg: bool, - #[builder(finish_fn)] finish_fn_arg: &'static str, - named: u32, - ) { - } - - #[builder(derive(Clone, Debug))] - fn with_self( - &self, - #[builder(start_fn)] start_fn_arg: bool, - #[builder(finish_fn)] finish_fn_arg: &'static str, - named: u32, - ) { - let _ = self; - } - } - - let actual = Sut::sut(true); - - assert_debug_eq( - actual.clone(), - expect!["SutSutBuilder { start_fn_arg: true }"], - ); - assert_debug_eq( - actual.named(42).clone(), - expect!["SutSutBuilder { start_fn_arg: true, named: 42 }"], - ); - - let actual = Sut.with_self(true); - - assert_debug_eq( - actual.clone(), - expect!["SutWithSelfBuilder { self: Sut, start_fn_arg: true }"], - ); - assert_debug_eq( - actual.named(42).clone(), - expect![[r#" - SutWithSelfBuilder { - self: Sut, - start_fn_arg: true, - named: 42, - }"#]], - ); -} diff --git a/bon/tests/integration/builder/cfgs.rs b/bon/tests/integration/builder/cfgs.rs index 0ced1a31..3376c4a7 100644 --- a/bon/tests/integration/builder/cfgs.rs +++ b/bon/tests/integration/builder/cfgs.rs @@ -10,6 +10,7 @@ fn struct_smoke() { #[cfg_attr(all(), allow(dead_code))] arg1: bool, + /// doc comment #[cfg(not(all()))] arg1: u32, @@ -33,6 +34,7 @@ fn struct_with_params() { #[cfg(all())] arg1: bool, + /// doc comment #[cfg(not(all()))] arg1: u32, @@ -63,7 +65,9 @@ fn fn_smoke() { #[cfg_attr(all(), allow(dead_code))] arg1: bool, - #[cfg(not(all()))] arg1: u32, + /// doc comment + #[cfg(not(all()))] + arg1: u32, #[cfg(any())] arg1: String, ) -> bool { @@ -80,7 +84,9 @@ fn fn_with_params() { fn sut( #[cfg(all())] arg1: bool, - #[cfg(not(all()))] arg1: u32, + /// doc comment + #[cfg(not(all()))] + arg1: u32, #[cfg_attr(all(), builder(default))] arg2: [u8; 4], @@ -119,7 +125,9 @@ fn impl_block() { #[cfg_attr(all(), allow(dead_code))] arg1: bool, - #[cfg(not(all()))] arg1: u32, + /// doc comment + #[cfg(not(all()))] + arg1: u32, #[cfg(any())] arg1: String, ) -> bool { @@ -131,7 +139,9 @@ fn impl_block() { fn sut_with_params( #[cfg(all())] arg1: bool, - #[cfg(not(all()))] arg1: u32, + /// doc comment + #[cfg(not(all()))] + arg1: u32, #[cfg_attr(all(), builder(default))] arg2: [u8; 4], diff --git a/bon/tests/integration/builder/init_order.rs b/bon/tests/integration/builder/init_order.rs index 99e7d5ba..2dd26a6e 100644 --- a/bon/tests/integration/builder/init_order.rs +++ b/bon/tests/integration/builder/init_order.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use core::fmt; // Functions don't support `skip` attributes #[test] @@ -8,7 +9,7 @@ fn fn_init_order() { #[builder(default = 1)] arg1: u32, #[builder(default = 2)] arg2: u32, #[builder(default = [arg1, arg2, 3])] arg3: [u32; 3], - ) -> (u32, u32, [u32; 3]) { + ) -> impl fmt::Debug { (arg1, arg2, arg3) } diff --git a/bon/tests/integration/builder/legacy.rs b/bon/tests/integration/builder/legacy.rs deleted file mode 100644 index f4376d7b..00000000 --- a/bon/tests/integration/builder/legacy.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[rustversion::since(1.77.0)] -#[test] -#[expect(deprecated)] -fn builder_on_struct() { - use crate::prelude::*; - use core::net::IpAddr; - - #[builder] - #[derive(Debug)] - #[allow(dead_code)] - struct Sut { - a: u32, - - #[builder(into)] - b: IpAddr, - - #[builder(default)] - c: u32, - - d: Option, - - #[builder(name = renamed)] - e: u32, - - #[builder(skip = e + 99)] - f: u32, - } - - let actual = Sut::builder() - .a(42) - .b([127, 0, 0, 1]) - .maybe_d(None) - .renamed(1) - .build(); - - assert_debug_eq( - actual, - expect!["Sut { a: 42, b: 127.0.0.1, c: 0, d: None, e: 1, f: 100 }"], - ); -} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index f5b56ce5..bdf83a19 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -1,9 +1,13 @@ mod attr_default; +mod attr_derive; mod attr_expose_positional_fn; mod attr_into; mod attr_on; +mod attr_overwritable; +mod attr_setters; mod attr_skip; -mod builder_derives; +mod attr_transparent; +mod attr_with; mod cfgs; mod generics; mod init_order; @@ -14,10 +18,6 @@ mod positional_members; mod raw_idents; mod smoke; -/// Tests for the deprecated features that we still support, but that we'll -/// eventually remove in the future in a new major version release. -mod legacy; - use crate::prelude::*; #[test] @@ -43,7 +43,7 @@ fn lifetime_elision() { #[cfg(feature = "std")] #[tokio::test] -async fn async_func() { +async fn async_fn() { #[builder] async fn sut(arg: u32) -> u32 { std::future::ready(arg).await @@ -55,7 +55,7 @@ async fn async_func() { #[cfg(feature = "std")] #[tokio::test] -async fn async_func_with_future_arg() { +async fn async_fn_with_future_arg() { #[builder] async fn sut(fut: Fut) -> Fut::Output { fut.await @@ -73,7 +73,7 @@ async fn async_func_with_future_arg() { #[test] #[allow(unsafe_code)] -fn unsafe_func() { +fn unsafe_fn() { #[builder] unsafe fn sut(arg: bool) { let _ = arg; diff --git a/bon/tests/integration/builder/name_conflicts/builder_state.rs b/bon/tests/integration/builder/name_conflicts/builder_state.rs new file mode 100644 index 00000000..04db9b41 --- /dev/null +++ b/bon/tests/integration/builder/name_conflicts/builder_state.rs @@ -0,0 +1,335 @@ +mod conflicts_in_bodies { + use crate::prelude::*; + + #[test] + #[allow(clippy::items_after_statements)] + fn test_struct() { + #[derive(Builder, Clone, Copy)] + #[allow(dead_code)] + struct S { + field: u32, + } + + let s = S::builder().field(1).build(); + + #[derive(Builder, Clone, Copy)] + #[allow(dead_code)] + struct State { + field: S, + } + + let state = State::builder().field(s).build(); + + #[derive(Builder, Clone, Copy)] + #[allow(dead_code)] + struct BuilderState { + field1: S, + field2: State, + } + + let builder_state = BuilderState::builder().field1(s).field2(state).build(); + + #[derive(Builder, Clone, Copy)] + #[allow(dead_code)] + #[allow(non_snake_case)] + struct S_ { + field1: S, + field2: State, + field3: BuilderState, + } + + let s_ = S_::builder() + .field1(s) + .field2(state) + .field3(builder_state) + .build(); + + #[derive(Builder, Clone, Copy)] + #[allow(dead_code)] + #[allow(non_snake_case)] + struct S__ { + field1: S, + field2: State, + field3: BuilderState, + field4: S_, + } + + let _ = S__::builder() + .field1(s) + .field2(state) + .field3(builder_state) + .field4(s_) + .build(); + } + + #[test] + #[allow(clippy::items_after_statements)] + fn test_free_fn() { + struct S; + struct State; + struct BuilderState; + + { + #[builder] + fn sut(_field: S) {} + + sut().field(S).call(); + } + + { + #[builder] + fn sut(_field: S) { + let _ = State; + } + + sut().field(S).call(); + } + + { + #[builder] + fn sut(_field1: S, _field2: State) { + let _ = { + { + ((), BuilderState) + } + }; + } + + sut().field1(S).field2(State).call(); + } + + { + #[builder] + fn sut(_field1: S, _field2: State, _field3: BuilderState) {} + + sut::<()>() + .field1(S) + .field2(State) + .field3(BuilderState) + .call(); + } + + { + #[builder] + fn sut(_field1: S, _field2: State, _field3: BuilderState) {} + + sut::<(), ()>() + .field1(S) + .field2(State) + .field3(BuilderState) + .call(); + } + } + + #[test] + #[allow(clippy::items_after_statements)] + fn test_assoc_method() { + struct State; + struct BuilderState; + + { + struct S; + + #[bon] + impl S { + #[builder] + fn sut() {} + } + + S::sut().call(); + } + + { + struct S; + + #[bon] + impl S { + #[builder] + fn sut() { + let _ = State; + } + + #[builder] + fn with_self(&self) { + let _ = self; + } + } + + S::sut().call(); + S.with_self().call(); + } + + { + struct S; + + #[bon] + impl S { + #[builder] + fn sut(_field2: State) {} + + #[builder] + fn with_self(&self) { + let _ = self; + let _ = { + { + ((), BuilderState) + } + }; + } + } + + S::sut().field2(State).call(); + S.with_self().call(); + } + + { + struct S; + + #[bon] + impl S { + #[builder] + fn sut(_field2: State, _field3: BuilderState) {} + } + + S::sut::<()>().field2(State).field3(BuilderState).call(); + } + + { + struct S; + + #[bon] + impl S { + #[builder] + fn sut(_field2: State, _field3: BuilderState) {} + } + + S::sut::<(), ()>().field2(State).field3(BuilderState).call(); + } + } +} + +mod conflicts_in_attrs { + use crate::prelude::*; + + struct S; + + impl S { + fn s(&self) -> u32 { + let _ = self; + 2 + } + } + + struct State; + + impl State { + fn state(&self) -> u32 { + let _ = self; + 2 + } + } + + struct BuilderState; + + impl BuilderState { + fn builder_state(&self) -> u32 { + let _ = self; + 2 + } + } + + #[test] + fn test_struct() { + { + #[derive(Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(with = |s: S| s.s())] + field: u32, + } + + let _ = Sut::builder().field(S).build(); + } + { + #[derive(Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(with = |s: S| s.s())] + field1: u32, + + #[builder(default = State.state())] + field2: u32, + } + + let _ = Sut::builder().field1(S).maybe_field2(Some(43)).build(); + } + { + #[derive(Builder)] + #[allow(dead_code)] + struct Sut { + #[builder(with = |s: S| s.s())] + field1: u32, + + #[builder(default = State.state())] + field2: u32, + + #[builder(skip = BuilderState.builder_state())] + field3: u32, + } + + let _ = Sut::builder().field1(S).maybe_field2(Some(43)).build(); + } + } + + #[test] + fn test_free_fn() { + { + #[builder] + fn sut(#[builder(with = |s: S| s.s())] _field: u32) {} + + sut().field(S).call(); + } + { + #[builder] + fn sut( + #[builder(with = |s: S| s.s())] _field1: u32, + + #[builder(default = State.state())] _field2: u32, + ) { + } + + sut().field1(S).maybe_field2(Some(43)).call(); + } + } + + #[test] + fn test_assoc_method() { + { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut(#[builder(with = |s: S| s.s())] _field: u32) {} + } + + Sut::sut().field(S).call(); + } + { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut( + #[builder(with = |s: S| s.s())] _field1: u32, + + #[builder(default = State.state())] _field2: u32, + ) { + } + } + + Sut::sut().field1(S).maybe_field2(Some(43)).call(); + } + } +} diff --git a/bon/tests/integration/builder/name_conflicts/generics.rs b/bon/tests/integration/builder/name_conflicts/generics.rs new file mode 100644 index 00000000..80c22eb8 --- /dev/null +++ b/bon/tests/integration/builder/name_conflicts/generics.rs @@ -0,0 +1,114 @@ +mod lifetimes { + use crate::prelude::*; + + #[test] + fn test_free_fn() { + #[builder] + #[allow( + single_use_lifetimes, + clippy::needless_lifetimes, + clippy::trivially_copy_pass_by_ref + )] + fn sut<'f1, 'f1_, 'f2>( + _x1: &u32, + _x2: &'f1 u32, + _x3: &'f1_ u32, + _x4: &'f2 u32, + _x5: &u32, + ) -> u32 { + 32 + } + + sut().x1(&32).x2(&32).x3(&32).x4(&32).x5(&32).call(); + } + + #[test] + fn test_assoc_method() { + #[derive(Default)] + #[allow(dead_code)] + struct Sut<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h>( + &'a str, + &'b str, + &'c str, + &'d str, + &'e str, + &'f str, + &'g str, + &'h str, + ); + + #[bon] + impl<'i1, 'i1_, 'i2, 'f1, 'f1_, 'f2> Sut<'_, '_, 'i1, 'i1_, 'i2, 'f1, 'f1_, 'f2> { + #[builder] + #[allow(clippy::trivially_copy_pass_by_ref)] + fn sut(_val: &u32, _val2: &u32) {} + } + + Sut::sut().val(&32).val2(&32).call(); + } +} +mod impl_trait { + use crate::prelude::*; + + #[test] + fn test_free_fn() { + struct I1; + type I2 = I1; + + impl I1 { + fn get_val(&self) -> u32 { + let _ = self; + 32 + } + } + + { + #[builder] + fn sut(_arg1: impl Copy) -> u32 { + I1.get_val() + } + + sut().arg1(()).call(); + } + + { + #[builder] + fn sut(_arg1: impl Copy, _arg2: impl Sized) -> u32 { + I2 {}.get_val() + } + + sut().arg1(()).arg2(()).call(); + } + } + + #[test] + fn test_assoc_method() { + struct I1; + type I2 = I1; + + impl I1 { + fn get_val(&self) -> u32 { + let _ = self; + 32 + } + } + + #[bon] + impl I1 { + #[builder] + #[allow(clippy::use_self)] + fn sut(_arg1: impl Copy) -> u32 { + I1.get_val() + } + + #[builder] + fn with_self(&self, _arg1: impl Copy, _arg2: impl Sized) -> u32 { + let _ = self; + I2 {}.get_val() + } + } + + I1::sut().arg1(()).call(); + I1.with_self().arg1(()).arg2(()).call(); + } +} diff --git a/bon/tests/integration/builder/name_conflicts.rs b/bon/tests/integration/builder/name_conflicts/member_and_type_named_the_same.rs similarity index 99% rename from bon/tests/integration/builder/name_conflicts.rs rename to bon/tests/integration/builder/name_conflicts/member_and_type_named_the_same.rs index 5066828f..ee130ea1 100644 --- a/bon/tests/integration/builder/name_conflicts.rs +++ b/bon/tests/integration/builder/name_conflicts/member_and_type_named_the_same.rs @@ -3,7 +3,6 @@ //! type of the member, and thus leading to a name conflict. This test is //! here to catch any such simple regression. use crate::prelude::*; - struct User; #[test] diff --git a/bon/tests/integration/builder/name_conflicts/member_names_state.rs b/bon/tests/integration/builder/name_conflicts/member_names_state.rs new file mode 100644 index 00000000..1026ed38 --- /dev/null +++ b/bon/tests/integration/builder/name_conflicts/member_names_state.rs @@ -0,0 +1,45 @@ +use crate::prelude::*; + +#[test] +fn test_free_fn() { + #[builder] + fn sut(state: u32, member_state: u32, unset: u32, empty: u32) { + let _ = (state, member_state, unset, empty); + } + + sut().state(1).member_state(2).unset(3).empty(4).call(); +} + +#[test] +fn test_struct() { + #[derive(Builder)] + #[allow(dead_code)] + struct Sut { + state: u32, + member_state: u32, + unset: u32, + empty: u32, + } + + let _ = Sut::builder() + .state(1) + .member_state(2) + .unset(3) + .empty(4) + .build(); +} + +#[test] +fn test_assoc_method() { + struct Sut; + + #[bon] + impl Sut { + #[builder] + fn sut(state: u32, member_state: u32, unset: u32, empty: u32) { + let _ = (state, member_state, unset, empty); + } + } + + Sut::sut().state(1).member_state(2).unset(3).empty(4).call(); +} diff --git a/bon/tests/integration/builder/name_conflicts/mod.rs b/bon/tests/integration/builder/name_conflicts/mod.rs new file mode 100644 index 00000000..b243a0df --- /dev/null +++ b/bon/tests/integration/builder/name_conflicts/mod.rs @@ -0,0 +1,4 @@ +mod builder_state; +mod generics; +mod member_and_type_named_the_same; +mod member_names_state; diff --git a/bon/tests/integration/builder/positional_members.rs b/bon/tests/integration/builder/positional_members.rs index 0f554a1b..028bd721 100644 --- a/bon/tests/integration/builder/positional_members.rs +++ b/bon/tests/integration/builder/positional_members.rs @@ -18,6 +18,7 @@ impl From for char { mod smoke { use super::*; + use core::fmt; #[test] fn test_struct() { @@ -57,7 +58,7 @@ mod smoke { }"#]], ); - let _ = Sut::builder(true, 'c', "str"); + let _actual = Sut::builder(true, 'c', "str"); } #[test] @@ -70,14 +71,7 @@ mod smoke { #[builder(finish_fn)] finisher_1: &'static str, #[builder(finish_fn, into)] finisher_2: &'static str, named: u32, - ) -> ( - bool, - char, - Option<&'static str>, - u32, - &'static str, - &'static str, - ) { + ) -> impl fmt::Debug { ( starter_1, starter_2, starter_3, named, finisher_1, finisher_2, ) @@ -90,7 +84,7 @@ mod smoke { expect![[r#"(true, 'c', None, 99, "1", "2")"#]], ); - let _ = sut(true, 'c', "str"); + let _actual = sut(true, 'c', "str"); } #[test] @@ -107,14 +101,7 @@ mod smoke { #[builder(finish_fn)] finisher_1: &'static str, #[builder(finish_fn, into)] finisher_2: &'static str, named: u32, - ) -> ( - bool, - char, - Option<&'static str>, - u32, - &'static str, - &'static str, - ) { + ) -> impl fmt::Debug { ( starter_1, starter_2, starter_3, named, finisher_1, finisher_2, ) @@ -126,7 +113,7 @@ mod smoke { #[builder(start_fn)] starter_1: bool, #[builder(finish_fn)] finisher_1: &'static str, named: u32, - ) -> (bool, u32, &'static str) { + ) -> impl fmt::Debug { let _ = self; (starter_1, named, finisher_1) } @@ -139,7 +126,7 @@ mod smoke { expect![[r#"(true, 'c', None, 99, "1", "2")"#]], ); - let _ = Sut::sut(true, 'c', "str"); + let _actual = Sut::sut(true, 'c', "str"); assert_debug_eq( Sut.with_self(true).named(99).call("1"), @@ -150,6 +137,7 @@ mod smoke { mod attr_on { use super::*; + use core::fmt; #[test] fn test_struct() { @@ -196,7 +184,7 @@ mod attr_on { #[builder(finish_fn)] finisher_1: &'static str, named: u32, - ) -> (bool, Option<&'static str>, &'static str, u32) { + ) -> impl fmt::Debug { (starter_1, starter_3, finisher_1, named) } @@ -221,7 +209,7 @@ mod attr_on { #[builder(finish_fn)] finisher_1: &'static str, named: u32, - ) -> (bool, Option<&'static str>, &'static str, u32) { + ) -> impl fmt::Debug { (starter_1, starter_3, finisher_1, named) } @@ -231,7 +219,7 @@ mod attr_on { #[builder(start_fn)] starter_1: bool, #[builder(finish_fn)] finisher_1: &'static str, named: u32, - ) -> (bool, &'static str, u32) { + ) -> impl fmt::Debug { let _ = self; (starter_1, finisher_1, named) } diff --git a/bon/tests/integration/builder/raw_idents.rs b/bon/tests/integration/builder/raw_idents.rs index 90d71499..4cc96c51 100644 --- a/bon/tests/integration/builder/raw_idents.rs +++ b/bon/tests/integration/builder/raw_idents.rs @@ -18,11 +18,14 @@ fn struct_case() { assert_eq!(actual.other, 100); #[derive(Builder)] - #[builder(builder_type = r#type)] - #[allow(clippy::items_after_statements)] - struct Sut {} + #[builder(builder_type = r#type, state_mod = r#mod)] + #[allow(clippy::items_after_statements, dead_code)] + struct Sut { + r#while: u32, + } - let _: r#type = Sut::builder(); + let _actual: r#type = Sut::builder(); + let _actual: r#type = Sut::builder().r#while(32); } #[test] @@ -35,7 +38,7 @@ fn fn_case() { r#type().r#type(42).r#while(100).call(); - #[builder(builder_type = r#type)] + #[builder(builder_type = r#type, state_mod = r#mod)] fn sut() {} let _: r#type = sut(); diff --git a/bon/tests/integration/main.rs b/bon/tests/integration/main.rs index 0a2a323d..a58408f0 100644 --- a/bon/tests/integration/main.rs +++ b/bon/tests/integration/main.rs @@ -8,6 +8,7 @@ clippy::needless_raw_string_hashes, non_local_definitions, missing_docs, + impl_trait_overcaptures, )] #[cfg(feature = "alloc")] diff --git a/bon/tests/integration/ui/compile_fail/attr_bon.rs b/bon/tests/integration/ui/compile_fail/attr_bon.rs new file mode 100644 index 00000000..c84acc9e --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_bon.rs @@ -0,0 +1,20 @@ +use bon::bon; + +struct InvalidAttrsForBonMacro; + +#[bon(attrs)] +impl InvalidAttrsForBonMacro { + #[builder] + fn sut() {} +} + +struct BuilderAttrOnReceiver; + +#[bon] +impl BuilderAttrOnReceiver { + #[builder] + fn sut(#[builder] &self) {} +} + + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_bon.stderr b/bon/tests/integration/ui/compile_fail/attr_bon.stderr new file mode 100644 index 00000000..28613d73 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_bon.stderr @@ -0,0 +1,11 @@ +error: `#[bon]` attribute does not accept any parameters yet, but it will in future releases + --> tests/integration/ui/compile_fail/attr_bon.rs:5:7 + | +5 | #[bon(attrs)] + | ^^^^^ + +error: #[builder] attributes on the receiver are not supported + --> tests/integration/ui/compile_fail/attr_bon.rs:16:12 + | +16 | fn sut(#[builder] &self) {} + | ^ diff --git a/bon/tests/integration/ui/compile_fail/attr_builder.rs b/bon/tests/integration/ui/compile_fail/attr_builder.rs new file mode 100644 index 00000000..702af06c --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_builder.rs @@ -0,0 +1,69 @@ +use bon::{bon, builder, Builder}; + +#[derive(Builder)] +#[builder] +struct EmptyTopLevelBuilderAttr {} + +#[derive(Builder)] +#[builder()] +struct EmptyTopLevelBuilderAttrWithParens {} + +#[derive(Builder)] +struct EmptyMemberLevelBuilderAttr { + #[builder] + x: u32, +} + +#[derive(Builder)] +struct EmptyMemberLevelBuilderAttrWithParens { + #[builder()] + x: u32, +} + +#[builder] +fn fn_empty_member_level_builder_attr(#[builder] _x: u32) {} + +#[builder] +fn fn_empty_member_level_builder_attr_with_parens(#[builder()] _x: u32) {} + +struct EmptyBuilderAttr; + +#[bon] +impl EmptyBuilderAttr { + #[builder] + fn empty_member_level_builder_attr(#[builder] _x: u32) {} +} + +#[bon] +impl EmptyBuilderAttr { + #[builder] + fn empty_member_level_builder_attr_with_parens(#[builder()] _x: u32) {} +} + +#[bon] +impl EmptyBuilderAttr { + #[builder()] + fn empty_top_level_builder_attr_with_parens() {} +} + +#[builder] +struct LegacyBuilderProcMacroAttrOnStruct {} + +#[builder] +enum EnumsAreUnsupported {} + +fn main() {} + +#[builder] +#[must_use] +#[must_use] +fn double_must_use() {} + +#[builder] +fn destructuring1((x, y): (u32, u32)) { + let _ = x; + let _ = y; +} + +#[builder] +fn destructuring2((_, _): (u32, u32)) {} diff --git a/bon/tests/integration/ui/compile_fail/attr_builder.stderr b/bon/tests/integration/ui/compile_fail/attr_builder.stderr new file mode 100644 index 00000000..6996c6e6 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_builder.stderr @@ -0,0 +1,85 @@ +error: this empty `#[builder]` attribute is redundant; remove it + --> tests/integration/ui/compile_fail/attr_builder.rs:4:3 + | +4 | #[builder] + | ^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_builder.rs:8:10 + | +8 | #[builder()] + | ^^ + +error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` + --> tests/integration/ui/compile_fail/attr_builder.rs:13:7 + | +13 | #[builder] + | ^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_builder.rs:19:14 + | +19 | #[builder()] + | ^^ + +error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` + --> tests/integration/ui/compile_fail/attr_builder.rs:24:41 + | +24 | fn fn_empty_member_level_builder_attr(#[builder] _x: u32) {} + | ^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_builder.rs:27:60 + | +27 | fn fn_empty_member_level_builder_attr_with_parens(#[builder()] _x: u32) {} + | ^^ + +error: this empty `#[builder]` attribute is unexpected; remove it or pass parameters in parentheses: `#[builder(...)]` + --> tests/integration/ui/compile_fail/attr_builder.rs:34:42 + | +34 | fn empty_member_level_builder_attr(#[builder] _x: u32) {} + | ^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_builder.rs:40:61 + | +40 | fn empty_member_level_builder_attr_with_parens(#[builder()] _x: u32) {} + | ^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_builder.rs:45:14 + | +45 | #[builder()] + | ^^ + +error: to generate a builder for a struct, use `#[derive(bon::Builder)]` instead; `#[bon::builder]` syntax is supported only for functions starting with bon v3 + --> tests/integration/ui/compile_fail/attr_builder.rs:50:1 + | +50 | struct LegacyBuilderProcMacroAttrOnStruct {} + | ^^^^^^ + +error: only `fn` items are supported by the `#[bon::builder]` attribute + --> tests/integration/ui/compile_fail/attr_builder.rs:52:1 + | +52 | #[builder] + | ^^^^^^^^^^ + | + = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: found multiple #[must_use], but bon only works with exactly one or zero. + --> tests/integration/ui/compile_fail/attr_builder.rs:59:1 + | +59 | #[must_use] + | ^ + +error: use a simple `identifier: type` syntax for the function argument; destructuring patterns in arguments aren't supported by the `#[builder]` + --> tests/integration/ui/compile_fail/attr_builder.rs:63:19 + | +63 | fn destructuring1((x, y): (u32, u32)) { + | ^^^^^^ + +error: use a simple `identifier: type` syntax for the function argument; destructuring patterns in arguments aren't supported by the `#[builder]` + --> tests/integration/ui/compile_fail/attr_builder.rs:69:19 + | +69 | fn destructuring2((_, _): (u32, u32)) {} + | ^^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_derive.rs b/bon/tests/integration/ui/compile_fail/attr_derive.rs new file mode 100644 index 00000000..59ff92d7 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_derive.rs @@ -0,0 +1,69 @@ +use bon::{bon, builder, Builder}; + +struct NoTraitImpls; + +#[derive(Builder)] +#[builder(derive(Clone, Debug))] +struct StructContainsNonTrait { + #[builder(start_fn)] + no_impl_start_fn: NoTraitImpls, + + no_impls_required: NoTraitImpls, + + no_impl_optional: Option, + + #[builder(default = NoTraitImpls)] + no_impl_optional_2: NoTraitImpls, + + x: u32, +} + +#[builder(derive(Clone, Debug))] +fn fn_contains_non_trait( + #[builder(start_fn)] // + _no_impl_start_fn: NoTraitImpls, + + _no_impls_required: NoTraitImpls, + + _no_impl_optional: Option, + + #[builder(default = NoTraitImpls)] // + _no_impl_optional_2: NoTraitImpls, + + _x: u32, +) { +} + +#[bon] +impl StructContainsNonTrait { + #[builder(derive(Clone, Debug))] + fn method_contains_non_trait( + self, + + #[builder(start_fn)] _no_impl_start_fn: NoTraitImpls, + + _no_impls_required: NoTraitImpls, + + _no_impl_optional: Option, + + #[builder(default = NoTraitImpls)] // + _no_impl_optional_2: NoTraitImpls, + + _x: u32, + ) { + } +} + +#[derive(Builder)] +#[builder(derive())] +struct EmptyDerives {} + +#[derive(Builder)] +#[builder(derive(Clone()))] +struct EmptyParamsForDerive {} + +#[derive(Builder)] +#[builder(derive(Clone(bounds {})))] +struct WrongDelimInBounds {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_derive.stderr b/bon/tests/integration/ui/compile_fail/attr_derive.stderr new file mode 100644 index 00000000..fc60a841 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_derive.stderr @@ -0,0 +1,454 @@ +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_derive.rs:58:17 + | +58 | #[builder(derive())] + | ^^ + +error: Missing field `bounds` + --> tests/integration/ui/compile_fail/attr_derive.rs:62:18 + | +62 | #[builder(derive(Clone()))] + | ^^^^^ + +error: wrong delimiter, expected parentheses e.g. `bounds(...)`, but got curly braces: `bounds{...}` + --> tests/integration/ui/compile_fail/attr_derive.rs:66:24 + | +66 | #[builder(derive(Clone(bounds {})))] + | ^^^^^^ + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:9:23 + | +9 | no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:11:24 + | +11 | no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:13:30 + | +13 | no_impl_optional: Option, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:16:25 + | +16 | no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:9:23 + | +9 | no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:11:24 + | +11 | no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:13:30 + | +13 | no_impl_optional: Option, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:16:25 + | +16 | no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:24:24 + | +24 | _no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:26:25 + | +26 | _no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:28:31 + | +28 | _no_impl_optional: Option, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:31:26 + | +31 | _no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:24:24 + | +24 | _no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:26:25 + | +26 | _no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:28:31 + | +28 | _no_impl_optional: Option, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:31:26 + | +31 | _no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `StructContainsNonTrait: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:38:6 + | +38 | impl StructContainsNonTrait { + | ^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `StructContainsNonTrait` + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:43:49 + | +43 | #[builder(start_fn)] _no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:45:29 + | +45 | _no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:47:35 + | +47 | _no_impl_optional: Option, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied + --> tests/integration/ui/compile_fail/attr_derive.rs:50:30 + | +50 | _no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` + | +note: required by a bound in `clone_member` + --> src/private/derives.rs + | + | pub fn clone_member(member: &Option) -> Option { + | ^^^^^ required by this bound in `clone_member` +help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` + | +3 + #[derive(Clone)] +4 | struct NoTraitImpls; + | + +error[E0277]: `StructContainsNonTrait` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:38:6 + | +38 | impl StructContainsNonTrait { + | ^^^^^^^^^^^^^^^^^^^^^^ `StructContainsNonTrait` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `StructContainsNonTrait` + = note: add `#[derive(Debug)]` to `StructContainsNonTrait` or manually `impl Debug for StructContainsNonTrait` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:43:49 + | +43 | #[builder(start_fn)] _no_impl_start_fn: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:45:29 + | +45 | _no_impls_required: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:47:35 + | +47 | _no_impl_optional: Option, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | + +error[E0277]: `NoTraitImpls` doesn't implement `Debug` + --> tests/integration/ui/compile_fail/attr_derive.rs:50:30 + | +50 | _no_impl_optional_2: NoTraitImpls, + | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` + | + = help: the trait `Debug` is not implemented for `NoTraitImpls` + = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` +note: required by a bound in `as_dyn_debug` + --> src/private/derives.rs + | + | pub fn as_dyn_debug(member: &T) -> &dyn Debug { + | ^^^^^ required by this bound in `as_dyn_debug` +help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` + | +3 + #[derive(Debug)] +4 | struct NoTraitImpls; + | diff --git a/bon/tests/integration/ui/compile_fail/attr_into.rs b/bon/tests/integration/ui/compile_fail/attr_into.rs new file mode 100644 index 00000000..65f68a0c --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into.rs @@ -0,0 +1,6 @@ +use bon::builder; + +#[builder] +fn invalid_into_false(#[builder(into = false)] _x: u32) {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_into.stderr b/bon/tests/integration/ui/compile_fail/attr_into.stderr new file mode 100644 index 00000000..5f14a887 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into.stderr @@ -0,0 +1,5 @@ +error: Unexpected type `bool` + --> tests/integration/ui/compile_fail/attr_into.rs:4:40 + | +4 | fn invalid_into_false(#[builder(into = false)] _x: u32) {} + | ^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_on.rs b/bon/tests/integration/ui/compile_fail/attr_on.rs new file mode 100644 index 00000000..7fcafb2b --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_on.rs @@ -0,0 +1,27 @@ +use bon::builder; + +#[builder(on(String, into))] +fn unnecessary_into(#[builder(into)] _x: String) {} + +#[builder(on(String, overwritable))] +fn unnecessary_ovewritable(#[builder(overwritable)] _x: String) {} + +#[builder(on(&dyn std::fmt::Debug, into))] +fn invalid_type_pattern() {} + +#[builder(on(fn(#[attr] a: u32), into))] +fn attrs_in_on_type_pattern() {} + +#[builder(on)] +fn incomplete_on() {} + +#[builder(on())] +fn incomplete_on2() {} + +#[builder(on(_))] +fn incomplete_on3() {} + +#[builder(on(_,))] +fn incomplete_on4() {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_on.stderr b/bon/tests/integration/ui/compile_fail/attr_on.stderr new file mode 100644 index 00000000..e7ccc8c3 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_on.stderr @@ -0,0 +1,51 @@ +error: this `#[builder(into)]` attribute is redundant, because `into` is already implied for this member via the `#[builder(on(...))]` at the top of the function + --> tests/integration/ui/compile_fail/attr_on.rs:4:31 + | +4 | fn unnecessary_into(#[builder(into)] _x: String) {} + | ^^^^ + +error: this `#[builder(overwritable)]` attribute is redundant, because `overwritable` is already implied for this member via the `#[builder(on(...))]` at the top of the function + --> tests/integration/ui/compile_fail/attr_on.rs:7:38 + | +7 | fn unnecessary_ovewritable(#[builder(overwritable)] _x: String) {} + | ^^^^^^^^^^^^ + +error: this syntax is not supported in type patterns yet. If you have a use case for this, please open an issue at https://github.com/elastio/bon/issues. + --> tests/integration/ui/compile_fail/attr_on.rs:9:15 + | +9 | #[builder(on(&dyn std::fmt::Debug, into))] + | ^^^ + +error: nested attributes are not allowed in the type pattern of #[builder(on(type_pattern, ...))] + --> tests/integration/ui/compile_fail/attr_on.rs:12:17 + | +12 | #[builder(on(fn(#[attr] a: u32), into))] + | ^ + +error: this empty `#[on]` attribute is unexpected; remove it or pass parameters in parentheses: `#[on(...)]` + --> tests/integration/ui/compile_fail/attr_on.rs:15:11 + | +15 | #[builder(on)] + | ^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_on.rs:18:13 + | +18 | #[builder(on())] + | ^^ + +error: expected `,` + --> tests/integration/ui/compile_fail/attr_on.rs:21:1 + | +21 | #[builder(on(_))] + | ^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: this #[builder(on(type_pattern, ...))] contains no options to override the default behavior for the selected setters like `into`, `overwritable`, so it does nothing + --> tests/integration/ui/compile_fail/attr_on.rs:24:1 + | +24 | #[builder(on(_,))] + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/attr_setters.rs b/bon/tests/integration/ui/compile_fail/attr_setters.rs new file mode 100644 index 00000000..697f61bc --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_setters.rs @@ -0,0 +1,75 @@ +use bon::Builder; + +#[derive(Builder)] +struct UnusedNameConfig { + #[builder(setters( + name = littlepip, + some_fn = blackjack, + option_fn = roseluck, + ))] + value: Option, +} + +#[derive(Builder)] +struct UnusedNameConfigVerbose { + #[builder(setters( + name = littlepip, + some_fn(name = blackjack), + option_fn(name = roseluck), + ))] + value: Option, +} + +#[derive(Builder)] +struct UnusedVisConfig { + #[builder(setters(vis = "pub(crate)", some_fn(vis = ""), option_fn(vis = ""),))] + value: Option, +} + +#[derive(Builder)] +struct UnusedDocsConfig { + #[builder(setters( + doc { + /// Unused + }, + some_fn(doc { + /// some_fn docs + }), + option_fn(doc { + /// option_fn docs + }), + ))] + value: Option, +} + +#[derive(Builder)] +struct SomeFnSetterRequiredMember { + #[builder(setters(some_fn = foo))] + member: i32, +} + +#[derive(Builder)] +struct OptionFnSetterOnRequiredMember { + #[builder(setters(option_fn = bar))] + member: i32, +} + +#[derive(Builder)] +struct SomeFnSetterWithTransparent { + #[builder(transparent, setters(some_fn = foo))] + member: Option, +} + +#[derive(Builder)] +struct OptionFnSetterWithTransparent { + #[builder(transparent, setters(option_fn = bar))] + member: Option, +} + +#[derive(Builder)] +struct EmptySettersConfig { + #[builder(setters())] + member: i32, +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_setters.stderr b/bon/tests/integration/ui/compile_fail/attr_setters.stderr new file mode 100644 index 00000000..bf027f56 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_setters.stderr @@ -0,0 +1,53 @@ +error: this `name` configuration is unused because all of the `some_fn`, `option_fn` setters contain a `name` override + --> tests/integration/ui/compile_fail/attr_setters.rs:6:9 + | +6 | name = littlepip, + | ^^^^ + +error: this `name` configuration is unused because all of the `some_fn`, `option_fn` setters contain a `name` override + --> tests/integration/ui/compile_fail/attr_setters.rs:16:9 + | +16 | name = littlepip, + | ^^^^ + +error: this `vis` configuration is unused because all of the `some_fn`, `option_fn` setters contain a `vis` override + --> tests/integration/ui/compile_fail/attr_setters.rs:25:23 + | +25 | #[builder(setters(vis = "pub(crate)", some_fn(vis = ""), option_fn(vis = ""),))] + | ^^^ + +error: this `doc` configuration is unused because all of the `some_fn`, `option_fn` setters contain a `doc` override + --> tests/integration/ui/compile_fail/attr_setters.rs:32:9 + | +32 | doc { + | ^^^ + +error: `some_fn` setter function applies only to members with `#[builder(default)]` or members of `Option` type (if #[builder(transparent)] is not set) + --> tests/integration/ui/compile_fail/attr_setters.rs:47:23 + | +47 | #[builder(setters(some_fn = foo))] + | ^^^^^^^ + +error: `option_fn` setter function applies only to members with `#[builder(default)]` or members of `Option` type (if #[builder(transparent)] is not set) + --> tests/integration/ui/compile_fail/attr_setters.rs:53:23 + | +53 | #[builder(setters(option_fn = bar))] + | ^^^^^^^^^ + +error: `some_fn` setter function applies only to members with `#[builder(default)]` or members of `Option` type (if #[builder(transparent)] is not set) + --> tests/integration/ui/compile_fail/attr_setters.rs:59:36 + | +59 | #[builder(transparent, setters(some_fn = foo))] + | ^^^^^^^ + +error: `option_fn` setter function applies only to members with `#[builder(default)]` or members of `Option` type (if #[builder(transparent)] is not set) + --> tests/integration/ui/compile_fail/attr_setters.rs:65:36 + | +65 | #[builder(transparent, setters(option_fn = bar))] + | ^^^^^^^^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_setters.rs:71:22 + | +71 | #[builder(setters())] + | ^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_skip.rs b/bon/tests/integration/ui/compile_fail/attr_skip.rs new file mode 100644 index 00000000..451653c7 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_skip.rs @@ -0,0 +1,40 @@ +use bon::{builder, Builder}; + +#[derive(Builder)] +struct ConflictingAttrs { + #[builder(skip, into)] + x: u32, +} + +#[derive(Builder)] +struct ConflictingAttrs2 { + #[builder(skip, name = bar)] + x: u32, +} + +#[derive(Builder)] +struct ConflictingAttrs3 { + #[builder(skip, default = 42)] + z: u32, +} + +#[builder] +fn skip_on_fn_is_unsupporetd1(#[builder(skip)] _x: u32) {} +#[builder] +fn skip_on_fn_is_unsupporetd2(#[builder(skip = "skip".to_owned())] _y: String) {} +#[builder] +fn skip_on_fn_is_unsupporetd3(#[builder(skip = vec![42])] _z: Vec) {} + +fn main() { + #[derive(Builder)] + struct SkipGeneratesNoSetter { + #[builder(skip)] + x: u32, + + #[builder(skip = 4)] + y: u32, + } + + SkipGeneratesNoSetter::builder().x(42).build(); + SkipGeneratesNoSetter::builder().y(42).build(); +} diff --git a/bon/tests/integration/ui/compile_fail/attr_skip.stderr b/bon/tests/integration/ui/compile_fail/attr_skip.stderr new file mode 100644 index 00000000..bcf6526c --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_skip.stderr @@ -0,0 +1,53 @@ +error: `skip` attribute can't be specified together with `into` + --> tests/integration/ui/compile_fail/attr_skip.rs:5:15 + | +5 | #[builder(skip, into)] + | ^^^^ + +error: `skip` attribute can't be specified together with `name` + --> tests/integration/ui/compile_fail/attr_skip.rs:11:15 + | +11 | #[builder(skip, name = bar)] + | ^^^^ + +error: `skip` attribute can't be specified with the `default` attribute; if you wanted to specify a value for the member, then use the following syntax instead `#[builder(skip = value)]` + --> tests/integration/ui/compile_fail/attr_skip.rs:17:15 + | +17 | #[builder(skip, default = 42)] + | ^^^^ + +error: `skip` attribute is not supported on function arguments; use a local variable instead. + --> tests/integration/ui/compile_fail/attr_skip.rs:22:41 + | +22 | fn skip_on_fn_is_unsupporetd1(#[builder(skip)] _x: u32) {} + | ^^^^ + +error: `skip` attribute is not supported on function arguments; use a local variable instead. + --> tests/integration/ui/compile_fail/attr_skip.rs:24:41 + | +24 | fn skip_on_fn_is_unsupporetd2(#[builder(skip = "skip".to_owned())] _y: String) {} + | ^^^^ + +error: `skip` attribute is not supported on function arguments; use a local variable instead. + --> tests/integration/ui/compile_fail/attr_skip.rs:26:41 + | +26 | fn skip_on_fn_is_unsupporetd3(#[builder(skip = vec![42])] _z: Vec) {} + | ^^^^ + +error[E0599]: no method named `x` found for struct `SkipGeneratesNoSetterBuilder` in the current scope + --> tests/integration/ui/compile_fail/attr_skip.rs:38:38 + | +29 | #[derive(Builder)] + | ------- method `x` not found for this struct +... +38 | SkipGeneratesNoSetter::builder().x(42).build(); + | ^ method not found in `SkipGeneratesNoSetterBuilder` + +error[E0599]: no method named `y` found for struct `SkipGeneratesNoSetterBuilder` in the current scope + --> tests/integration/ui/compile_fail/attr_skip.rs:39:38 + | +29 | #[derive(Builder)] + | ------- method `y` not found for this struct +... +39 | SkipGeneratesNoSetter::builder().y(42).build(); + | ^ method not found in `SkipGeneratesNoSetterBuilder` diff --git a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs new file mode 100644 index 00000000..7d643da1 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.rs @@ -0,0 +1,11 @@ +use bon::Builder; + +#[derive(Builder)] +#[builder(start_fn())] +struct EmptyStartFn {} + +#[derive(Builder)] +#[builder(finish_fn())] +struct EmptyFinisFn {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr new file mode 100644 index 00000000..e8a17698 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_start_finish_fn.stderr @@ -0,0 +1,11 @@ +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_start_finish_fn.rs:4:19 + | +4 | #[builder(start_fn())] + | ^^ + +error: expected parameters in parentheses + --> tests/integration/ui/compile_fail/attr_start_finish_fn.rs:8:20 + | +8 | #[builder(finish_fn())] + | ^^ diff --git a/bon/tests/integration/ui/compile_fail/attr_transparent.rs b/bon/tests/integration/ui/compile_fail/attr_transparent.rs new file mode 100644 index 00000000..0fbc4236 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_transparent.rs @@ -0,0 +1,49 @@ +use bon::Builder; + +#[derive(Builder)] +struct InvalidOnRequiredMember { + #[builder(transparent)] + member: i32, +} + +#[derive(Builder)] +struct InvalidOnStartFnMember { + #[builder(start_fn, transparent)] + member: Option, +} + +#[derive(Builder)] +struct InvalidOnFnMember { + #[builder(finish_fn, transparent)] + member: Option, +} + +#[derive(Builder)] +struct InvalidOnSkippedMember { + #[builder(skip, transparent)] + member: Option, +} + +#[derive(Builder)] +struct Valid { + #[builder(transparent)] + member: Option, +} + +fn main() { + // Make sure there is no `maybe_` setter generated + let _ = Valid::builder().maybe_member(Some(42)); + + // Another way to get transparency + { + type OpaqueOption = Option; + + #[derive(Builder)] + struct Sut { + arg1: OpaqueOption, + } + + // Should not be allowed `OpaqueOption` is required + let _ = Sut::builder().build(); + } +} diff --git a/bon/tests/integration/ui/compile_fail/attr_transparent.stderr b/bon/tests/integration/ui/compile_fail/attr_transparent.stderr new file mode 100644 index 00000000..75366b28 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_transparent.stderr @@ -0,0 +1,59 @@ +error: `#[builder(transparent)]` can only be applied to members of type `Option` to disable their special handling + --> tests/integration/ui/compile_fail/attr_transparent.rs:5:15 + | +5 | #[builder(transparent)] + | ^^^^^^^^^^^ + +error: `start_fn` attribute can't be specified together with `transparent` + --> tests/integration/ui/compile_fail/attr_transparent.rs:11:15 + | +11 | #[builder(start_fn, transparent)] + | ^^^^^^^^ + +error: `finish_fn` attribute can't be specified together with `transparent` + --> tests/integration/ui/compile_fail/attr_transparent.rs:17:15 + | +17 | #[builder(finish_fn, transparent)] + | ^^^^^^^^^ + +error: `skip` attribute can't be specified together with `transparent` + --> tests/integration/ui/compile_fail/attr_transparent.rs:23:15 + | +23 | #[builder(skip, transparent)] + | ^^^^ + +error[E0599]: no method named `maybe_member` found for struct `ValidBuilder` in the current scope + --> tests/integration/ui/compile_fail/attr_transparent.rs:35:30 + | +27 | #[derive(Builder)] + | ------- method `maybe_member` not found for this struct +... +35 | let _ = Valid::builder().maybe_member(Some(42)); + | ^^^^^^^^^^^^ + | +help: there is a method `member` with a similar name + | +35 | let _ = Valid::builder().member(Some(42)); + | ~~~~~~ + +error[E0277]: the member `Unset` was not set, but this method requires it to be set + --> tests/integration/ui/compile_fail/attr_transparent.rs:47:32 + | +47 | let _ = Sut::builder().build(); + | ^^^^^ the member `Unset` was not set, but this method requires it to be set + | + = help: the trait `IsSet` is not implemented for `Unset`, which is required by `sut_builder::Empty: sut_builder::IsComplete` + = help: the trait `IsSet` is implemented for `Set` +note: required for `sut_builder::Empty` to implement `sut_builder::IsComplete` + --> tests/integration/ui/compile_fail/attr_transparent.rs:41:18 + | +41 | #[derive(Builder)] + | ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro +note: required by a bound in `SutBuilder::::build` + --> tests/integration/ui/compile_fail/attr_transparent.rs:41:18 + | +41 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `SutBuilder::::build` +42 | struct Sut { + | --- required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/attr_with.rs b/bon/tests/integration/ui/compile_fail/attr_with.rs new file mode 100644 index 00000000..c1a0d1c6 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_with.rs @@ -0,0 +1,84 @@ +use bon::Builder; + +#[derive(Builder)] +struct InvalidWithExpr { + #[builder(with = 42)] + x: u32, +} + +#[derive(Builder)] +struct ConflictingInto { + #[builder(into, with = |x: u32| x + 1)] + value: u32, +} + +#[derive(Builder)] +struct RejectForSyntax { + #[builder(with = for<'a> |x: &'a u32| -> u32 { x + 1 })] + value: u32, +} + +#[derive(Builder)] +struct RejectConstSyntax { + #[builder(with = const || 1)] + value: u32, +} + +#[derive(Builder)] +struct RejectStaticSyntax { + #[builder(with = static || 1)] + value: u32, +} + +#[derive(Builder)] +struct RejectAsyncSyntax { + #[builder(with = async || 1)] + value: u32, +} + +#[derive(Builder)] +struct RejectMoveSyntax { + #[builder(with = move || 1)] + value: u32, +} + +#[derive(Builder)] +struct UnexpectedReturnTypeShape { + #[builder(with = |x: u32| -> u32 { x + 1 })] + value: u32, +} + +#[derive(Builder)] +struct InvalidReturnTypeInResultClosure { + #[builder(with = |value: &str| -> ::core::result::Result<_, core::num::ParseIntError> { + Ok(value) + })] + value: u32, +} + +#[derive(Builder)] +struct InvalidReturnTypeInImplTraitClosure { + #[builder(with = |value: impl Into<::core::net::IpAddr>| value)] + value: u32, +} + +#[derive(Builder)] +struct NoGenericArgsResultReturnType1 { + #[builder(with = |value: u32| -> Result {})] + value: u32, +} + +#[derive(Builder)] +struct NoGenericArgsResultReturnType2 { + #[builder(with = |value: u32| -> Result<> {})] + value: u32, +} + +#[derive(Builder)] +struct ThreeGenericArgsResultReturnType { + #[builder(with = |value: u32| -> ::core::result::Result {})] + value: u32, +} + + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_with.stderr b/bon/tests/integration/ui/compile_fail/attr_with.stderr new file mode 100644 index 00000000..f9954c81 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_with.stderr @@ -0,0 +1,179 @@ +error: expected a closure e.g. `with = |param: T| expression` + --> tests/integration/ui/compile_fail/attr_with.rs:5:15 + | +5 | #[builder(with = 42)] + | ^^^^ + +error: `with` attribute can't be specified together with `into` + --> tests/integration/ui/compile_fail/attr_with.rs:11:21 + | +11 | #[builder(into, with = |x: u32| x + 1)] + | ^^^^ + +error: `for<...>` syntax is not allowed here + --> tests/integration/ui/compile_fail/attr_with.rs:17:22 + | +17 | #[builder(with = for<'a> |x: &'a u32| -> u32 { x + 1 })] + | ^^^ + +error: `const` keyword is not allowed here + --> tests/integration/ui/compile_fail/attr_with.rs:23:22 + | +23 | #[builder(with = const || 1)] + | ^^^^^ + +error: `static` keyword is not allowed here + --> tests/integration/ui/compile_fail/attr_with.rs:29:22 + | +29 | #[builder(with = static || 1)] + | ^^^^^^ + +error: `async` keyword is not allowed here + --> tests/integration/ui/compile_fail/attr_with.rs:35:22 + | +35 | #[builder(with = async || 1)] + | ^^^^^ + +error: `move` keyword is not allowed here + --> tests/integration/ui/compile_fail/attr_with.rs:41:22 + | +41 | #[builder(with = move || 1)] + | ^^^^ + +error: expected one of the following: + + (1) no return type annotation; + this means the closure is expected to return a value of the same type + as the member's underlying type(*); + + (2) `-> *Result<_, {{ErrorType}}>` or `-> *Result<_>` return type annotation; + this means the closure is expected to return a `Result` where the `Ok` + variant is of the same type as the member's underlying type(*); this syntax + allows you to define a fallbile setter (one that returns a `Result`); + + the `_` placeholder must be spelled literally to mark the underlying type(*) + of the member; an optional second generic parameter for the error type is allowed; + + the return type doesn't have to be named `Result` exactly, the only requirement is + that it must have the `Result` suffix; for example if you have a type alias + `ApiResult<_>`, then it'll work fine; + + (*) underlying type is the type of the member stripped from the `Option` wrapper + if this member is of `Option` type and no `#[builder(transparent)]` annotation + is present + --> tests/integration/ui/compile_fail/attr_with.rs:47:34 + | +47 | #[builder(with = |x: u32| -> u32 { x + 1 })] + | ^^^ + +error: expected one of the following: + + (1) no return type annotation; + this means the closure is expected to return a value of the same type + as the member's underlying type(*); + + (2) `-> *Result<_, {{ErrorType}}>` or `-> *Result<_>` return type annotation; + this means the closure is expected to return a `Result` where the `Ok` + variant is of the same type as the member's underlying type(*); this syntax + allows you to define a fallbile setter (one that returns a `Result`); + + the `_` placeholder must be spelled literally to mark the underlying type(*) + of the member; an optional second generic parameter for the error type is allowed; + + the return type doesn't have to be named `Result` exactly, the only requirement is + that it must have the `Result` suffix; for example if you have a type alias + `ApiResult<_>`, then it'll work fine; + + (*) underlying type is the type of the member stripped from the `Option` wrapper + if this member is of `Option` type and no `#[builder(transparent)]` annotation + is present + --> tests/integration/ui/compile_fail/attr_with.rs:67:38 + | +67 | #[builder(with = |value: u32| -> Result {})] + | ^^^^^^ + +error: expected one of the following: + + (1) no return type annotation; + this means the closure is expected to return a value of the same type + as the member's underlying type(*); + + (2) `-> *Result<_, {{ErrorType}}>` or `-> *Result<_>` return type annotation; + this means the closure is expected to return a `Result` where the `Ok` + variant is of the same type as the member's underlying type(*); this syntax + allows you to define a fallbile setter (one that returns a `Result`); + + the `_` placeholder must be spelled literally to mark the underlying type(*) + of the member; an optional second generic parameter for the error type is allowed; + + the return type doesn't have to be named `Result` exactly, the only requirement is + that it must have the `Result` suffix; for example if you have a type alias + `ApiResult<_>`, then it'll work fine; + + (*) underlying type is the type of the member stripped from the `Option` wrapper + if this member is of `Option` type and no `#[builder(transparent)]` annotation + is present + --> tests/integration/ui/compile_fail/attr_with.rs:73:38 + | +73 | #[builder(with = |value: u32| -> Result<> {})] + | ^^^^^^ + +error: expected one of the following: + + (1) no return type annotation; + this means the closure is expected to return a value of the same type + as the member's underlying type(*); + + (2) `-> *Result<_, {{ErrorType}}>` or `-> *Result<_>` return type annotation; + this means the closure is expected to return a `Result` where the `Ok` + variant is of the same type as the member's underlying type(*); this syntax + allows you to define a fallbile setter (one that returns a `Result`); + + the `_` placeholder must be spelled literally to mark the underlying type(*) + of the member; an optional second generic parameter for the error type is allowed; + + the return type doesn't have to be named `Result` exactly, the only requirement is + that it must have the `Result` suffix; for example if you have a type alias + `ApiResult<_>`, then it'll work fine; + + (*) underlying type is the type of the member stripped from the `Option` wrapper + if this member is of `Option` type and no `#[builder(transparent)]` annotation + is present + --> tests/integration/ui/compile_fail/attr_with.rs:79:38 + | +79 | #[builder(with = |value: u32| -> ::core::result::Result {})] + | ^ + +error[E0308]: mismatched types + --> tests/integration/ui/compile_fail/attr_with.rs:54:12 + | +54 | Ok(value) + | -- ^^^^^ expected `u32`, found `&str` + | | + | arguments to this enum variant are incorrect + | +help: the type constructed contains `&str` due to the type of the argument passed + --> tests/integration/ui/compile_fail/attr_with.rs:54:9 + | +54 | Ok(value) + | ^^^-----^ + | | + | this argument influences the type of `Ok` +note: tuple variant defined here + --> $RUST/core/src/result.rs + | + | Ok(#[stable(feature = "rust1", since = "1.0.0")] T), + | ^^ + +error[E0308]: mismatched types + --> tests/integration/ui/compile_fail/attr_with.rs:61:62 + | +61 | #[builder(with = |value: impl Into<::core::net::IpAddr>| value)] + | ------------------------------ ^^^^^ expected `u32`, found type parameter `impl Into<::core::net::IpAddr>` + | | + | found this type parameter +62 | value: u32, + | --- expected `u32` because of return type + | + = note: expected type `u32` + found type parameter `impl Into<::core::net::IpAddr>` diff --git a/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.rs b/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.rs new file mode 100644 index 00000000..e289df63 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.rs @@ -0,0 +1,74 @@ +use bon::{bon, builder, Builder}; + +#[builder] +fn broken_link_in_arg_docs( + /// [Self] link + _arg: u32, +) { +} + +struct BrokenLinkInArgDocs; + +#[bon] +impl BrokenLinkInArgDocs { + #[builder] + fn broken_link_in_arg_docs( + /// [Self] link + _arg: u32, + ) { + } +} + +#[derive(Builder)] +struct BrokenLinkInFieldDocs { + /// [`Self`] link + field: u32, +} + +#[derive(Builder)] +struct BrokenLinkInSettersDocs { + #[builder(setters(doc { + /// [`Self`] link + }))] + field: u32, +} + +#[derive(Builder)] +struct BrokenLinkInSomeFnDocs { + #[builder(setters( + some_fn(doc { + /// [`Self`] link + }) + ))] + field: Option, +} + +#[derive(Builder)] +struct BrokenLinkInOptionFnDocs { + #[builder(setters( + option_fn(doc { + /// [`Self`] link + }) + ))] + field: Option, +} + +#[derive(Builder)] +#[builder(builder_type(doc { + /// [Self] link +}))] +struct BrokenLinkInBuilderTypeDocs {} + +#[derive(Builder)] +#[builder(finish_fn(doc { + /// [Self] link +}))] +struct BrokenLinkInFinishFnDocs {} + +#[derive(Builder)] +#[builder(state_mod(doc { + /// [Self] link +}))] +struct BrokenLinkInStateModDocs {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.stderr b/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.stderr new file mode 100644 index 00000000..375f62ed --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/broken_intra_doc_links.stderr @@ -0,0 +1,53 @@ +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:5:5 + | +5 | /// [Self] link + | ^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:16:9 + | +16 | /// [Self] link + | ^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:24:5 + | +24 | /// [`Self`] link + | ^^^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:31:9 + | +31 | /// [`Self`] link + | ^^^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:40:13 + | +40 | /// [`Self`] link + | ^^^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:50:13 + | +50 | /// [`Self`] link + | ^^^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:58:5 + | +58 | /// [Self] link + | ^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder struct's impl block where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:64:5 + | +64 | /// [Self] link + | ^^^^^^^^^^^^^^^ + +error: the documentation should not reference `Self` because it will be moved to the builder's state module where `Self` changes meaning, which may confuse the reader of this code; use explicit type names instead. + --> tests/integration/ui/compile_fail/broken_intra_doc_links.rs:70:5 + | +70 | /// [Self] link + | ^^^^^^^^^^^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/builder_derives.rs b/bon/tests/integration/ui/compile_fail/builder_derives.rs deleted file mode 100644 index 76de8155..00000000 --- a/bon/tests/integration/ui/compile_fail/builder_derives.rs +++ /dev/null @@ -1,21 +0,0 @@ -use bon::{bon, builder, Builder}; - -struct NoTraitImpls; - -#[derive(Builder)] -#[builder(derive(Clone, Debug))] -struct StructContainsNonTrait { - non_debug: NoTraitImpls, - x: u32, -} - -#[builder(derive(Clone, Debug))] -fn fn_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} - -#[bon] -impl StructContainsNonTrait { - #[builder(derive(Clone, Debug))] - fn method_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} -} - -fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/builder_derives.stderr b/bon/tests/integration/ui/compile_fail/builder_derives.stderr deleted file mode 100644 index cd7d0b9d..00000000 --- a/bon/tests/integration/ui/compile_fail/builder_derives.stderr +++ /dev/null @@ -1,107 +0,0 @@ -error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied - --> tests/integration/ui/compile_fail/builder_derives.rs:8:16 - | -8 | non_debug: NoTraitImpls, - | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` - | -note: required by a bound in `assert_clone` - --> src/private/mod.rs - | - | pub fn assert_clone() {} - | ^^^^^ required by this bound in `assert_clone` -help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` - | -3 + #[derive(Clone)] -4 | struct NoTraitImpls; - | - -error[E0277]: `NoTraitImpls` doesn't implement `Debug` - --> tests/integration/ui/compile_fail/builder_derives.rs:8:16 - | -8 | non_debug: NoTraitImpls, - | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` - | - = help: the trait `Debug` is not implemented for `NoTraitImpls` - = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` -note: required by a bound in `assert_debug` - --> src/private/mod.rs - | - | pub fn assert_debug() {} - | ^^^^^^^^^^^^^^^^ required by this bound in `assert_debug` -help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` - | -3 + #[derive(Debug)] -4 | struct NoTraitImpls; - | - -error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied - --> tests/integration/ui/compile_fail/builder_derives.rs:13:38 - | -13 | fn fn_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} - | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` - | -note: required by a bound in `assert_clone` - --> src/private/mod.rs - | - | pub fn assert_clone() {} - | ^^^^^ required by this bound in `assert_clone` -help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` - | -3 + #[derive(Clone)] -4 | struct NoTraitImpls; - | - -error[E0277]: `NoTraitImpls` doesn't implement `Debug` - --> tests/integration/ui/compile_fail/builder_derives.rs:13:38 - | -13 | fn fn_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} - | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` - | - = help: the trait `Debug` is not implemented for `NoTraitImpls` - = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` -note: required by a bound in `assert_debug` - --> src/private/mod.rs - | - | pub fn assert_debug() {} - | ^^^^^^^^^^^^^^^^ required by this bound in `assert_debug` -help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` - | -3 + #[derive(Debug)] -4 | struct NoTraitImpls; - | - -error[E0277]: the trait bound `NoTraitImpls: Clone` is not satisfied - --> tests/integration/ui/compile_fail/builder_derives.rs:18:46 - | -18 | fn method_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} - | ^^^^^^^^^^^^ the trait `Clone` is not implemented for `NoTraitImpls` - | -note: required by a bound in `assert_clone` - --> src/private/mod.rs - | - | pub fn assert_clone() {} - | ^^^^^ required by this bound in `assert_clone` -help: consider annotating `NoTraitImpls` with `#[derive(Clone)]` - | -3 + #[derive(Clone)] -4 | struct NoTraitImpls; - | - -error[E0277]: `NoTraitImpls` doesn't implement `Debug` - --> tests/integration/ui/compile_fail/builder_derives.rs:18:46 - | -18 | fn method_contains_non_trait(_non_debug: NoTraitImpls, _x: u32) {} - | ^^^^^^^^^^^^ `NoTraitImpls` cannot be formatted using `{:?}` - | - = help: the trait `Debug` is not implemented for `NoTraitImpls` - = note: add `#[derive(Debug)]` to `NoTraitImpls` or manually `impl Debug for NoTraitImpls` -note: required by a bound in `assert_debug` - --> src/private/mod.rs - | - | pub fn assert_debug() {} - | ^^^^^^^^^^^^^^^^ required by this bound in `assert_debug` -help: consider annotating `NoTraitImpls` with `#[derive(Debug)]` - | -3 + #[derive(Debug)] -4 | struct NoTraitImpls; - | diff --git a/bon/tests/integration/ui/compile_fail/collections.rs b/bon/tests/integration/ui/compile_fail/collections.rs new file mode 100644 index 00000000..cee20925 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/collections.rs @@ -0,0 +1,10 @@ +use std::collections::{BTreeMap, BTreeSet}; + +fn main() { + let _repeated_keys_in_map: BTreeMap = bon::map! { + "Hello": "Blackjack", + "Hello": "Littlepip", + }; + + let _set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; +} diff --git a/bon/tests/integration/ui/compile_fail/collections.stderr b/bon/tests/integration/ui/compile_fail/collections.stderr new file mode 100644 index 00000000..ede02ffa --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/collections.stderr @@ -0,0 +1,23 @@ +error: duplicate key in the map + --> tests/integration/ui/compile_fail/collections.rs:5:9 + | +5 | "Hello": "Blackjack", + | ^^^^^^^ + +error: duplicate key in the map + --> tests/integration/ui/compile_fail/collections.rs:6:9 + | +6 | "Hello": "Littlepip", + | ^^^^^^^ + +error: duplicate value in the set + --> tests/integration/ui/compile_fail/collections.rs:9:44 + | +9 | let _set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; + | ^^^^^^^^^ + +error: duplicate value in the set + --> tests/integration/ui/compile_fail/collections.rs:9:63 + | +9 | let _set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; + | ^^^^^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/derive_builder.rs b/bon/tests/integration/ui/compile_fail/derive_builder.rs new file mode 100644 index 00000000..2ae1d906 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/derive_builder.rs @@ -0,0 +1,12 @@ +use bon::Builder; + +#[derive(Builder)] +struct TupleStruct(u32, u32); + +#[derive(Builder)] +struct TupleStructsAreUnsupported(u32, u32); + +#[derive(Builder)] +enum EnumsAreUnsupportedWithDerive {} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/derive_builder.stderr b/bon/tests/integration/ui/compile_fail/derive_builder.stderr new file mode 100644 index 00000000..b1c12bc4 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/derive_builder.stderr @@ -0,0 +1,19 @@ +error: Only structs with named fields are supported + --> tests/integration/ui/compile_fail/derive_builder.rs:4:1 + | +4 | struct TupleStruct(u32, u32); + | ^^^^^^ + +error: Only structs with named fields are supported + --> tests/integration/ui/compile_fail/derive_builder.rs:7:1 + | +7 | struct TupleStructsAreUnsupported(u32, u32); + | ^^^^^^ + +error: only `struct` items are supported by the `#[derive(bon::Builder)]` attribute + --> tests/integration/ui/compile_fail/derive_builder.rs:9:10 + | +9 | #[derive(Builder)] + | ^^^^^^^ + | + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs b/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs new file mode 100644 index 00000000..e13fd64d --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs @@ -0,0 +1,18 @@ +use bon::Builder; + +fn main() { + #[derive(Builder)] + struct Example { + x: u32, + y: u32, + + #[builder(name = renamed)] + z: u32, + } + + // Test error message about missing members + let _ = Example::builder().x(1).build(); + + // Test error message about repeated setter calls + let _ = Example::builder().y(1).y(2); +} diff --git a/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.stderr b/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.stderr new file mode 100644 index 00000000..e7581b13 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/diagnostic_on_unimplemented.stderr @@ -0,0 +1,61 @@ +error[E0277]: the member `Unset` was not set, but this method requires it to be set + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:14:37 + | +14 | let _ = Example::builder().x(1).build(); + | ^^^^^ the member `Unset` was not set, but this method requires it to be set + | + = help: the trait `IsSet` is not implemented for `Unset`, which is required by `SetX: IsComplete` + = help: the trait `IsSet` is implemented for `Set` +note: required for `SetX` to implement `IsComplete` + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:4:14 + | +4 | #[derive(Builder)] + | ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro +note: required by a bound in `ExampleBuilder::::build` + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:4:14 + | +4 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `ExampleBuilder::::build` +5 | struct Example { + | ------- required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the member `Unset` was not set, but this method requires it to be set + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:14:37 + | +14 | let _ = Example::builder().x(1).build(); + | ^^^^^ the member `Unset` was not set, but this method requires it to be set + | + = help: the trait `IsSet` is not implemented for `Unset`, which is required by `SetX: IsComplete` + = help: the trait `IsSet` is implemented for `Set` +note: required for `SetX` to implement `IsComplete` + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:4:14 + | +4 | #[derive(Builder)] + | ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro +note: required by a bound in `ExampleBuilder::::build` + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:4:14 + | +4 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `ExampleBuilder::::build` +5 | struct Example { + | ------- required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the member `Set` was already set, but this method requires it to be unset + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:17:37 + | +17 | let _ = Example::builder().y(1).y(2); + | ^ the member `Set` was already set, but this method requires it to be unset + | + = help: the trait `IsUnset` is not implemented for `Set` + = help: the trait `IsUnset` is implemented for `Unset` +note: required by a bound in `ExampleBuilder::::y` + --> tests/integration/ui/compile_fail/diagnostic_on_unimplemented.rs:4:14 + | +4 | #[derive(Builder)] + | ^^^^^^^ required by this bound in `ExampleBuilder::::y` +... +7 | y: u32, + | - required by a bound in this associated function + = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/errors.rs b/bon/tests/integration/ui/compile_fail/errors.rs deleted file mode 100644 index f15992f3..00000000 --- a/bon/tests/integration/ui/compile_fail/errors.rs +++ /dev/null @@ -1,132 +0,0 @@ -use bon::{builder, Builder}; -use std::collections::{BTreeMap, BTreeSet}; - -fn main() { - let map: BTreeMap = bon::map! { - "Hello": "Blackjack", - "Hello": "Littlepip", - }; - - let set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; - - #[derive(Builder)] - struct SkipGeneratesNoSetter { - #[builder(skip)] - x: u32, - - #[builder(skip = 4)] - y: u32, - } - - SkipGeneratesNoSetter::builder().x(42).build(); - SkipGeneratesNoSetter::builder().y(42).build(); - - #[derive(Builder)] - struct Example { - x: u32, - y: u32, - - #[builder(name = renamed)] - z: u32, - } - - // Test error message about missing members - let _ = Example::builder().x(1).build(); - - // Test error message about repeated setter calls - let _ = Example::builder().y(1).y(2); - - { - type OpaqueOption = Option; - - #[derive(Builder)] - struct Sut { - arg1: OpaqueOption, - } - - let _ = Sut::builder().build(); - } -} - -#[derive(Builder)] -struct TupleStruct(u32, u32); - -#[builder] -fn destructuring((x, y): (u32, u32)) { - let _ = x; - let _ = y; -} - -#[builder] -fn unnecessary_into_false(#[builder(into = false)] _x: u32) {} - -#[builder(on(String, into))] -fn unnecessary_into(#[builder(into)] _x: String) {} - -#[builder(on(&dyn std::fmt::Debug, into))] -fn invalid_type_pattern() {} - -#[builder(on(fn(#[attr] a: u32), into))] -fn attrs_in_on_type_pattern() {} - -#[builder(on)] -fn incomplete_on() {} - -#[builder(on())] -fn incomplete_on2() {} - -#[builder(on(_))] -fn incomplete_on3() {} - -#[builder(on(_,))] -fn incomplete_on4() {} - -#[derive(Builder)] -#[builder(start_fn())] -struct EmptyStartFn {} - -#[derive(Builder)] -struct ConflictingAttrs { - #[builder(skip, into)] - x: u32, -} - -#[derive(Builder)] -struct ConflictingAttrs2 { - #[builder(skip, name = bar)] - x: u32, -} - -#[derive(Builder)] -struct ConflictingAttrs3 { - #[builder(skip, default = 42)] - z: u32, -} - -#[builder] -fn skip_on_fn_is_unsupporetd( - #[builder(skip)] _x: u32, - #[builder(skip = "skip".to_owned())] _y: String, - #[builder(skip = vec![42])] _z: Vec, -) { -} - -#[derive(Builder)] -struct TupleStructsAreUnsupported(u32, u32); - -#[builder] -enum EnumsAreUnsupportedWithAttr {} - -#[derive(Builder)] -enum EnumsAreUnsupportedWithDerive {} - -#[builder] -fn destructuring_in_fn_is_unsupported((_, _): (u32, u32)) {} - -#[builder] -#[must_use] -#[must_use] -fn double_must_use() {} - -#[builder] -struct BuilderProcMacroAttrOnAStruct {} diff --git a/bon/tests/integration/ui/compile_fail/errors.stderr b/bon/tests/integration/ui/compile_fail/errors.stderr deleted file mode 100644 index ecb9a4cc..00000000 --- a/bon/tests/integration/ui/compile_fail/errors.stderr +++ /dev/null @@ -1,264 +0,0 @@ -error: duplicate key in the map - --> tests/integration/ui/compile_fail/errors.rs:6:9 - | -6 | "Hello": "Blackjack", - | ^^^^^^^ - -error: duplicate key in the map - --> tests/integration/ui/compile_fail/errors.rs:7:9 - | -7 | "Hello": "Littlepip", - | ^^^^^^^ - -error: duplicate value in the set - --> tests/integration/ui/compile_fail/errors.rs:10:43 - | -10 | let set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; - | ^^^^^^^^^ - -error: duplicate value in the set - --> tests/integration/ui/compile_fail/errors.rs:10:62 - | -10 | let set: BTreeSet = bon::set!["mintals", "guns", "mintals", "roses"]; - | ^^^^^^^^^ - -error: Only structs with named fields are supported - --> tests/integration/ui/compile_fail/errors.rs:52:1 - | -52 | struct TupleStruct(u32, u32); - | ^^^^^^ - -error: use a simple `identifier: type` syntax for the function argument; destructuring patterns in arguments aren't supported by the `#[builder]` - --> tests/integration/ui/compile_fail/errors.rs:55:18 - | -55 | fn destructuring((x, y): (u32, u32)) { - | ^^^^^^ - -error: Unexpected type `bool` - --> tests/integration/ui/compile_fail/errors.rs:61:44 - | -61 | fn unnecessary_into_false(#[builder(into = false)] _x: u32) {} - | ^^^^^ - -error: this `#[builder(into)]` attribute is redundant, because `into` is already implied for this member via the `#[builder(on(...))]` at the top of the function - --> tests/integration/ui/compile_fail/errors.rs:64:31 - | -64 | fn unnecessary_into(#[builder(into)] _x: String) {} - | ^^^^ - -error: This syntax is not supported in type patterns yet. If you have a use case for this, please open an issue at https://github.com/elastio/bon/issues. - --> tests/integration/ui/compile_fail/errors.rs:66:15 - | -66 | #[builder(on(&dyn std::fmt::Debug, into))] - | ^^^ - -error: nested attributes are not allowed in the type pattern of #[builder(on(type_pattern, ...))] - --> tests/integration/ui/compile_fail/errors.rs:69:17 - | -69 | #[builder(on(fn(#[attr] a: u32), into))] - | ^ - -error: Expected an attribute of form `on(type_pattern, ...)` - --> tests/integration/ui/compile_fail/errors.rs:72:11 - | -72 | #[builder(on)] - | ^^ - -error: unexpected end of input, expected one of: `for`, parentheses, `fn`, `unsafe`, `extern`, identifier, `::`, `<`, `dyn`, square brackets, `*`, `&`, `!`, `impl`, `_`, lifetime - --> tests/integration/ui/compile_fail/errors.rs:75:1 - | -75 | #[builder(on())] - | ^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: expected `,` - --> tests/integration/ui/compile_fail/errors.rs:78:1 - | -78 | #[builder(on(_))] - | ^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: this #[builder(on(type_pattern, ...))] contains no options to override the default behavior for the selected setters like `into`, so it does nothing - --> tests/integration/ui/compile_fail/errors.rs:81:1 - | -81 | #[builder(on(_,))] - | ^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: expected at least one parameter in parentheses - --> tests/integration/ui/compile_fail/errors.rs:85:11 - | -85 | #[builder(start_fn())] - | ^^^^^^^^ - -error: `skip` attribute can't be specified together with `into` - --> tests/integration/ui/compile_fail/errors.rs:90:15 - | -90 | #[builder(skip, into)] - | ^^^^ - -error: `skip` attribute can't be specified together with `name` - --> tests/integration/ui/compile_fail/errors.rs:96:15 - | -96 | #[builder(skip, name = bar)] - | ^^^^ - -error: `skip` attribute can't be specified with `default` attribute; if you wanted to specify a value for the member, then use the following syntax instead `#[builder(skip = value)]` - --> tests/integration/ui/compile_fail/errors.rs:102:15 - | -102 | #[builder(skip, default = 42)] - | ^^^^ - -error: `skip` attribute is not supported on function arguments. Use a local variable instead. - --> tests/integration/ui/compile_fail/errors.rs:108:15 - | -108 | #[builder(skip)] _x: u32, - | ^^^^ - -error: Only structs with named fields are supported - --> tests/integration/ui/compile_fail/errors.rs:115:1 - | -115 | struct TupleStructsAreUnsupported(u32, u32); - | ^^^^^^ - -error: only `fn` items are supported by the `#[bon::builder]` attribute - --> tests/integration/ui/compile_fail/errors.rs:117:1 - | -117 | #[builder] - | ^^^^^^^^^^ - | - = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: only `struct` items are supported by the `#[derive(bon::Builder)]` attribute - --> tests/integration/ui/compile_fail/errors.rs:120:10 - | -120 | #[derive(Builder)] - | ^^^^^^^ - | - = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: use a simple `identifier: type` syntax for the function argument; destructuring patterns in arguments aren't supported by the `#[builder]` - --> tests/integration/ui/compile_fail/errors.rs:124:39 - | -124 | fn destructuring_in_fn_is_unsupported((_, _): (u32, u32)) {} - | ^^^^^^ - -error: Found multiple #[must_use], but bon only works with exactly one (or less). - --> tests/integration/ui/compile_fail/errors.rs:128:1 - | -128 | #[must_use] - | ^ - -warning: unused attribute - --> tests/integration/ui/compile_fail/errors.rs:128:1 - | -128 | #[must_use] - | ^^^^^^^^^^^ help: remove this attribute - | -note: attribute also specified here - --> tests/integration/ui/compile_fail/errors.rs:127:1 - | -127 | #[must_use] - | ^^^^^^^^^^^ - = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! - = note: `#[warn(unused_attributes)]` on by default - -warning: use of deprecated module `bon::private::deprecations::builder_attribute_on_a_struct`: #[bon::builder] on top of a struct is deprecated; use `#[derive(bon::Builder)]` instead; see more details at https://elastio.github.io/bon/blog/bon-builder-v2-2-release#derive-builder-syntax-for-structs - --> tests/integration/ui/compile_fail/errors.rs:131:1 - | -131 | #[builder] - | ^^^^^^^^^^ - | - = note: `#[warn(deprecated)]` on by default - = note: this warning originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0599]: no method named `x` found for struct `SkipGeneratesNoSetterBuilder` in the current scope - --> tests/integration/ui/compile_fail/errors.rs:21:38 - | -12 | #[derive(Builder)] - | ------- method `x` not found for this struct -... -21 | SkipGeneratesNoSetter::builder().x(42).build(); - | ^ method not found in `SkipGeneratesNoSetterBuilder` - -error[E0599]: no method named `y` found for struct `SkipGeneratesNoSetterBuilder` in the current scope - --> tests/integration/ui/compile_fail/errors.rs:22:38 - | -12 | #[derive(Builder)] - | ------- method `y` not found for this struct -... -22 | SkipGeneratesNoSetter::builder().y(42).build(); - | ^ method not found in `SkipGeneratesNoSetterBuilder` - -error[E0277]: can't finish building yet; the member `ExampleBuilder__y` was not set - --> tests/integration/ui/compile_fail/errors.rs:34:37 - | -34 | let _ = Example::builder().x(1).build(); - | ^^^^^ the member `ExampleBuilder__y` was not set - | - = help: the trait `IntoSet` is not implemented for `Unset` - = help: the trait `IntoSet, ExampleBuilder__y>` is implemented for `Unset` -note: required by a bound in `ExampleBuilder::<(__X, __Y, __Z)>::build` - --> tests/integration/ui/compile_fail/errors.rs:24:14 - | -24 | #[derive(Builder)] - | ^^^^^^^ required by this bound in `ExampleBuilder::<(__X, __Y, __Z)>::build` -25 | struct Example { - | ------- required by a bound in this associated function - = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: can't finish building yet; the member `ExampleBuilder__renamed` was not set - --> tests/integration/ui/compile_fail/errors.rs:34:37 - | -34 | let _ = Example::builder().x(1).build(); - | ^^^^^ the member `ExampleBuilder__renamed` was not set - | - = help: the trait `IntoSet` is not implemented for `Unset` - = help: the trait `IntoSet, ExampleBuilder__renamed>` is implemented for `Unset` -note: required by a bound in `ExampleBuilder::<(__X, __Y, __Z)>::build` - --> tests/integration/ui/compile_fail/errors.rs:24:14 - | -24 | #[derive(Builder)] - | ^^^^^^^ required by this bound in `ExampleBuilder::<(__X, __Y, __Z)>::build` -25 | struct Example { - | ------- required by a bound in this associated function - = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: can't set the same member twice - --> tests/integration/ui/compile_fail/errors.rs:37:37 - | -37 | let _ = Example::builder().y(1).y(2); - | ^ this member was already set - | - = help: the trait `IsUnset` is not implemented for `Set` - = help: the trait `IsUnset` is implemented for `Unset` -note: required by a bound in `ExampleBuilder::<(__X, __Y, __Z)>::y` - --> tests/integration/ui/compile_fail/errors.rs:24:14 - | -24 | #[derive(Builder)] - | ^^^^^^^ required by this bound in `ExampleBuilder::<(__X, __Y, __Z)>::y` -... -27 | y: u32, - | - required by a bound in this associated function - = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: can't finish building yet; the member `SutBuilder__arg1` was not set - --> tests/integration/ui/compile_fail/errors.rs:47:32 - | -47 | let _ = Sut::builder().build(); - | ^^^^^ the member `SutBuilder__arg1` was not set - | - = help: the trait `IntoSet, SutBuilder__arg1>` is not implemented for `Unset` - = help: the trait `IntoSet, SutBuilder__arg1>` is implemented for `Unset` - = help: for that trait implementation, expected `Optional`, found `Required` -note: required by a bound in `SutBuilder::<(__Arg1,)>::build` - --> tests/integration/ui/compile_fail/errors.rs:42:18 - | -42 | #[derive(Builder)] - | ^^^^^^^ required by this bound in `SutBuilder::<(__Arg1,)>::build` -43 | struct Sut { - | --- required by a bound in this associated function - = note: this error originates in the derive macro `Builder` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/bon/tests/integration/ui/compile_fail/name_conflicts.rs b/bon/tests/integration/ui/compile_fail/name_conflicts.rs new file mode 100644 index 00000000..c3d671fa --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/name_conflicts.rs @@ -0,0 +1,21 @@ +use bon::builder; + +#[builder] +fn body(val: &u32) { + let _: &'fn1 u32 = val; +} + +#[builder] +fn attr_with(#[builder(with = |val: &'fn1 u32| val)] _val: &u32) {} + +#[builder] +fn attr_default( + #[builder(default = { + let val: &'fn1 u32 = &32; + val + })] + _val: &u32, +) { +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/name_conflicts.stderr b/bon/tests/integration/ui/compile_fail/name_conflicts.stderr new file mode 100644 index 00000000..d3ffe72f --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/name_conflicts.stderr @@ -0,0 +1,30 @@ +error[E0261]: use of undeclared lifetime name `'fn1` + --> tests/integration/ui/compile_fail/name_conflicts.rs:5:13 + | +3 | #[builder] + | - lifetime `'fn1` is missing in item created through this procedural macro +4 | fn body(val: &u32) { +5 | let _: &'fn1 u32 = val; + | ^^^^ undeclared lifetime + +error[E0261]: use of undeclared lifetime name `'fn1` + --> tests/integration/ui/compile_fail/name_conflicts.rs:9:38 + | +8 | #[builder] + | ---------- lifetime `'fn1` is missing in item created through this procedural macro +9 | fn attr_with(#[builder(with = |val: &'fn1 u32| val)] _val: &u32) {} + | ^^^^ - help: consider introducing lifetime `'fn1` here: `<'fn1>` + | | + | undeclared lifetime + +error[E0261]: use of undeclared lifetime name `'fn1` + --> tests/integration/ui/compile_fail/name_conflicts.rs:14:19 + | +11 | #[builder] + | - + | | + | lifetime `'fn1` is missing in item created through this procedural macro + | help: consider introducing lifetime `'fn1` here: `'fn1,` +... +14 | let val: &'fn1 u32 = &32; + | ^^^^ undeclared lifetime diff --git a/bon/tests/integration/ui/compile_fail/positional_members.stderr b/bon/tests/integration/ui/compile_fail/positional_members.stderr index 34c0a47e..cce0f0e5 100644 --- a/bon/tests/integration/ui/compile_fail/positional_members.stderr +++ b/bon/tests/integration/ui/compile_fail/positional_members.stderr @@ -1,4 +1,4 @@ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -7,7 +7,7 @@ error: incorrect members oredering; the order of members must be the following: 8 | #[builder(start_fn)] | ^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -16,7 +16,7 @@ error: incorrect members oredering; the order of members must be the following: 17 | #[builder(start_fn)] | ^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -25,7 +25,7 @@ error: incorrect members oredering; the order of members must be the following: 24 | #[builder(start_fn)] | ^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -34,7 +34,7 @@ error: incorrect members oredering; the order of members must be the following: 31 | #[builder(finish_fn)] | ^^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -43,7 +43,7 @@ error: incorrect members oredering; the order of members must be the following: 39 | #[builder(start_fn)] | ^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -52,7 +52,7 @@ error: incorrect members oredering; the order of members must be the following: 47 | #[builder(finish_fn)] | ^^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order @@ -61,7 +61,7 @@ error: incorrect members oredering; the order of members must be the following: 55 | #[builder(start_fn)] | ^^^^^^^^ -error: incorrect members oredering; the order of members must be the following: +error: incorrect members ordering; the order of members must be the following: (1) members annotated with #[builder(start_fn)] (2) members annotated with #[builder(finish_fn)] (3) all other members in any order diff --git a/bon/tests/integration/ui/compile_fail/warnings.stderr b/bon/tests/integration/ui/compile_fail/warnings.stderr index e675e446..ea2d0c22 100644 --- a/bon/tests/integration/ui/compile_fail/warnings.stderr +++ b/bon/tests/integration/ui/compile_fail/warnings.stderr @@ -52,7 +52,7 @@ help: use `let _ = ...` to ignore the resulting value 33 | let _ = Example::builder().x(1); | +++++++ -error: unused return value of `ExampleBuilder::<(__X, __Y)>::build` that must be used +error: unused return value of `ExampleBuilder::::build` that must be used --> tests/integration/ui/compile_fail/warnings.rs:34:9 | 34 | Example::builder().x(1).y(2).build(); @@ -64,7 +64,7 @@ help: use `let _ = ...` to ignore the resulting value 34 | let _ = Example::builder().x(1).y(2).build(); | +++++++ -error: unused return value of `ExampleMustUseBuilder::call` that must be used +error: unused return value of `ExampleMustUseBuilder::::call` that must be used --> tests/integration/ui/compile_fail/warnings.rs:36:9 | 36 | Example::must_use().call(); @@ -75,7 +75,7 @@ help: use `let _ = ...` to ignore the resulting value 36 | let _ = Example::must_use().call(); | +++++++ -error: unused return value of `MustUseBuilder::call` that must be used +error: unused return value of `MustUseBuilder::::call` that must be used --> tests/integration/ui/compile_fail/warnings.rs:38:9 | 38 | must_use().call(); @@ -97,7 +97,7 @@ help: use `let _ = ...` to ignore the resulting value 39 | let _ = __orig_must_use(); | +++++++ -error: unused return value of `MustUseUnderCfgBuilder::call` that must be used +error: unused return value of `MustUseUnderCfgBuilder::::call` that must be used --> tests/integration/ui/compile_fail/warnings.rs:47:9 | 47 | must_use_under_cfg().call(); diff --git a/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs b/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs new file mode 100644 index 00000000..1ba55d70 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/wrong_delimiters.rs @@ -0,0 +1,58 @@ +#[derive(bon::Builder)] +#[builder( + builder_type{}, + state_mod{}, + start_fn{}, + finish_fn{}, +)] +struct CurlyBraces {} + +#[derive(bon::Builder)] +struct CurlyBracesInField { + #[builder(setters{})] + x: u32, +} + +#[derive(bon::Builder)] +#[builder( + builder_type[doc[]], + state_mod[doc[]], + start_fn[doc[]], + finish_fn[doc[]], +)] +struct SquareBrackets { + #[builder(setters[])] + x: u32, +} + +#[derive(bon::Builder)] +struct SquareBracketsInFieldSetters { + #[builder(setters[])] + x: u32, +} + +#[derive(bon::Builder)] +#[builder( + builder_type(docs[]), + state_mod(docs[]), + start_fn(docs[]), + finish_fn(docs[]), +)] +struct SquareBracketsInFieldDoc { + #[builder(setters(doc[]))] + x: u32, +} + +#[derive(bon::Builder)] +#[builder( + builder_type(doc()), + state_mod(doc()), + start_fn(doc()), + finish_fn(doc()) +)] +struct Parentheses { + #[builder(setters(doc()))] + x: u32, +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr b/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr new file mode 100644 index 00000000..ddf1778b --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/wrong_delimiters.stderr @@ -0,0 +1,107 @@ +error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got curly braces: `start_fn{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:5:5 + | +5 | start_fn{}, + | ^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `builder_type(...)`, but got curly braces: `builder_type{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:3:5 + | +3 | builder_type{}, + | ^^^^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `state_mod(...)`, but got curly braces: `state_mod{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:4:5 + | +4 | state_mod{}, + | ^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `finish_fn(...)`, but got curly braces: `finish_fn{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:6:5 + | +6 | finish_fn{}, + | ^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `setters(...)`, but got curly braces: `setters{...}` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:12:15 + | +12 | #[builder(setters{})] + | ^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `start_fn(...)`, but got square brackets: `start_fn[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:20:5 + | +20 | start_fn[doc[]], + | ^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `builder_type(...)`, but got square brackets: `builder_type[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:18:5 + | +18 | builder_type[doc[]], + | ^^^^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `state_mod(...)`, but got square brackets: `state_mod[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:19:5 + | +19 | state_mod[doc[]], + | ^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `finish_fn(...)`, but got square brackets: `finish_fn[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:21:5 + | +21 | finish_fn[doc[]], + | ^^^^^^^^^ + +error: wrong delimiter, expected parentheses e.g. `setters(...)`, but got square brackets: `setters[...]` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:30:15 + | +30 | #[builder(setters[])] + | ^^^^^^^ + +error: Unknown field: `docs`. Did you mean `doc`? + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:38:14 + | +38 | start_fn(docs[]), + | ^^^^ + +error: Unknown field: `docs`. Did you mean `doc`? + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:36:18 + | +36 | builder_type(docs[]), + | ^^^^ + +error: Unknown field: `docs`. Did you mean `doc`? + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:37:15 + | +37 | state_mod(docs[]), + | ^^^^ + +error: Unknown field: `docs`. Did you mean `doc`? + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:39:15 + | +39 | finish_fn(docs[]), + | ^^^^ + +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:50:14 + | +50 | start_fn(doc()), + | ^^^ + +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:48:18 + | +48 | builder_type(doc()), + | ^^^ + +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:49:15 + | +49 | state_mod(doc()), + | ^^^ + +error: wrong delimiter, expected curly braces e.g. `doc{...}`, but got parentheses: `doc(...)` + --> tests/integration/ui/compile_fail/wrong_delimiters.rs:51:15 + | +51 | finish_fn(doc()) + | ^^^ diff --git a/bon/tests/integration/ui/mod.rs b/bon/tests/integration/ui/mod.rs index 5ef4f616..e5c3878d 100644 --- a/bon/tests/integration/ui/mod.rs +++ b/bon/tests/integration/ui/mod.rs @@ -1,6 +1,6 @@ +#[cfg(not(miri))] #[test] fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/integration/ui/compile_fail/*.rs"); - t.pass("tests/integration/ui/pass/*.rs"); } diff --git a/e2e-tests/src/attr_with.rs b/e2e-tests/src/attr_with.rs new file mode 100644 index 00000000..77e76bec --- /dev/null +++ b/e2e-tests/src/attr_with.rs @@ -0,0 +1,16 @@ +use bon::Builder; +use std::collections::BTreeMap; + +#[derive(Builder)] +pub struct AttrWith { + #[builder(with = |iter: impl IntoIterator| Vec::from_iter(iter))] + _vec: Vec, + + #[builder(with = |iter: impl IntoIterator, u32)>| { + iter + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect() + })] + _map: BTreeMap, +} diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 62ed539f..f5f263a1 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -2,10 +2,16 @@ //! We don't need all the aggressive lints that we use for public crates. #![allow(missing_debug_implementations, missing_docs)] +pub mod attr_with; pub mod macro_rules_wrapper_test; pub mod missing_docs_test; +pub mod state_mod_pub; -use bon::{bon, builder}; +mod reexports; + +pub use reexports::{UnexportedBuilder, UnexportedStateMod, UnexportedStateModBuilder}; + +use bon::{bon, builder, Builder}; #[cfg(doctest)] // We use a bunch of Vitepress-specific syntax in the doctests, for example to @@ -16,25 +22,32 @@ mod website_doctests { include!(concat!(env!("OUT_DIR"), "/website_doctests.rs")); } +/// Some docs on the private builder +#[derive(Builder)] +#[builder(builder_type(vis = ""))] +pub struct PrivateBuilder { + _field: String, +} + /// Docs on the [`Self`] struct -#[derive(bon::Builder)] +#[derive(Builder)] #[builder( builder_type( - docs { + doc { /// Docs on [`GreeterOverriddenBuilder`] /// the builder type }, name = GreeterOverriddenBuilder, ), start_fn( - docs { + doc { /// Docs on /// [`Self::start_fn_override`] }, name = start_fn_override, ), finish_fn( - docs { + doc { /// Docs on /// [`GreeterOverriddenBuilder::finish_fn_override()`] }, @@ -57,7 +70,6 @@ pub struct Counter { #[bon] impl Counter { - /// Creates an instance of [`Self`] with an optional provided `initial` value. #[builder] pub fn new( /// Initial value for the counter. @@ -93,13 +105,27 @@ pub fn documented( /// // Some doc tests as well /// assert_eq!(2 + 2, 4); /// ``` + #[builder(default)] _arg1: String, _arg2: &str, - _arg3: u32, + /// Optional member docs + _arg3: Option, _arg4: Vec, + + #[builder(default = + Greeter::start_fn_override() + .name( + "Some intentionally big expression to test the fallback to \ + a code fence in the default value docs" + .to_owned() + ) + .level(42) + .finish_fn_override() + )] + _arg5: Greeter, ) { eprintln!("Non-const"); } @@ -122,6 +148,9 @@ pub fn greet( format!("Hello {name} with age {age}!") } +#[builder] +pub fn fn_with_impl_trait(_arg1: impl std::fmt::Debug + Clone, _arg2: impl std::fmt::Debug) {} + #[builder] pub fn many_function_parameters( _id: Option<&str>, diff --git a/e2e-tests/src/reexports.rs b/e2e-tests/src/reexports.rs new file mode 100644 index 00000000..ec8191a8 --- /dev/null +++ b/e2e-tests/src/reexports.rs @@ -0,0 +1,13 @@ +/// Unexported builder +#[derive(bon::Builder)] +pub struct UnexportedBuilder { + _field1: u32, + _field2: u32, +} + +/// Unexported state mod +#[derive(bon::Builder)] +pub struct UnexportedStateMod { + _field1: u32, + _field2: u32, +} diff --git a/e2e-tests/src/state_mod_pub.rs b/e2e-tests/src/state_mod_pub.rs new file mode 100644 index 00000000..3b593c12 --- /dev/null +++ b/e2e-tests/src/state_mod_pub.rs @@ -0,0 +1,25 @@ +#[derive(bon::Builder)] +#[builder(state_mod(vis = "pub"))] +#[allow(dead_code)] +pub struct PubStateMod { + required_arg: u32, + optional_arg: Option, + + #[builder(default)] + default_arg: u32, + + #[builder(overwritable)] + overwritable_required_arg: u32, + + #[builder(overwritable)] + overwritable_optional_arg: Option, + + #[builder(overwritable, default = 2 * 2 + 3)] + overwritable_default_arg: u32, + + #[builder(transparent)] + transparent_arg: Option, + + #[builder(with = |x: &str| -> Result<_, std::num::ParseIntError> { x.parse() })] + with_arg: u32, +} diff --git a/rust-toolchain b/rust-toolchain index dbd41264..71fae54f 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.81.0 +1.82.0 diff --git a/scripts/install/hyperfine.sh b/scripts/install/hyperfine.sh new file mode 100755 index 00000000..4da5c6be --- /dev/null +++ b/scripts/install/hyperfine.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +: "${TOOL_VERSION:=1.18.0}" + +base_url=https://github.com/sharkdp/hyperfine/releases/download/v$TOOL_VERSION +file_stem=hyperfine-v$TOOL_VERSION-$arch_rust-unknown-linux-musl + +download_and_decompress \ + "$base_url/$file_stem.tar.gz" \ + --strip-components 1 \ + "$file_stem/hyperfine" + +move_exe_to_path hyperfine diff --git a/scripts/test-msrv.sh b/scripts/test-msrv.sh index 36555641..04d7297f 100755 --- a/scripts/test-msrv.sh +++ b/scripts/test-msrv.sh @@ -39,10 +39,9 @@ step cargo update -p windows-sys --precise 0.52.0 export RUSTFLAGS="${RUSTFLAGS:-} --allow unknown-lints" -step cargo clippy --all-features --all-targets --locked +step cargo clippy --all-targets --locked test_args=( - --all-features --locked --lib --tests diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index f017090c..da2446c1 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -152,12 +152,12 @@ export default defineConfig({ text: "Reference", items: [ { - text: "Builder macros", + text: "#[derive(Builder)] / #[builder]", link: "/reference/builder", items: [ { - text: "Top-Level Attributes", - link: "/reference/builder#top-level-attributes", + text: "Item attributes", + link: "/reference/builder#item-attributes", items: [ { text: "builder_type", @@ -186,8 +186,8 @@ export default defineConfig({ ], }, { - text: "Member-Level Attributes", - link: "/reference/builder#member-level-attributes", + text: "Member attributes", + link: "/reference/builder#member-attributes", items: [ { text: "default", diff --git a/website/.vitepress/theme/utils/versioning.ts b/website/.vitepress/theme/utils/versioning.ts index db72722a..43fe8c6d 100644 --- a/website/.vitepress/theme/utils/versioning.ts +++ b/website/.vitepress/theme/utils/versioning.ts @@ -1,4 +1,4 @@ -import { useRouter, withBase } from "vitepress"; +import { withBase } from "vitepress"; export const latestVersion = "v2"; export const versions = ["v2", "v1"]; diff --git a/website/blog/bon-builder-generator-v2-release.md b/website/blog/bon-builder-generator-v2-release.md index 5988fa27..39f26735 100644 --- a/website/blog/bon-builder-generator-v2-release.md +++ b/website/blog/bon-builder-generator-v2-release.md @@ -27,7 +27,7 @@ The way this is implemented is a big topic that deserves a separate blog post. H It's now possible to reference other members in `skip` and `default` expressions. Only the members declared higher in code are accessible to these expressions. Here is an example of how it works with `skip`: -```rust +```rust compile_fail use bon::builder; #[builder] @@ -100,7 +100,7 @@ Example::builder() Now, to preserve the same behavior, you need to do this: -```rust +```rust compile_fail use bon::builder; #[builder(on(String, into))] // Only this line needs to change @@ -128,7 +128,7 @@ Alternatively, you can add `#[builder(into)]` on top of each field that requires **Example:** -```rust +```rust compile_fail use bon::builder; #[builder] diff --git a/website/blog/bon-builder-v2-1-release.md b/website/blog/bon-builder-v2-1-release.md index 7aca068c..5f7a676e 100644 --- a/website/blog/bon-builder-v2-1-release.md +++ b/website/blog/bon-builder-v2-1-release.md @@ -105,7 +105,7 @@ It became possible after adopting the new design for the generated code (see [op We added `#[must_use]` to the `build()` method for struct builders. Now this produces a warning because the result of `#[build]` is not used: -```rust +```rust compile_fail #[bon::builder] struct Point { x: u32 diff --git a/website/changelog.md b/website/changelog.md index 1ec93e1a..4445ffe0 100644 --- a/website/changelog.md +++ b/website/changelog.md @@ -6,6 +6,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +All the breaking changes are very unlikely to actually break your code that was written against the `v2` version of `bon` unless you've been doing some crimes like using items marked as `#[doc(hidden)]` or using unconventional macro delimiters like `#[builder{}/[]]` instead of `#[builder()]`. See also the "Removed" section about removed/replaced deprecated APIs that you most likely never used. + +### Changed + +- Reject unnecessary empty attributes e.g. `#[builder()]` or `#[builder]` with no parameters on a member ([#145](https://github.com/elastio/bon/pull/145)) +- Reject non-empty `#[bon(...)]` attribute. This attribute will accept some parameters in future releases ([#145](https://github.com/elastio/bon/pull/145)) +- Reject square brackets and curly braces delimiters for `builder_type`, `finish_fn`, `start_fn` and `on` attributes syntax. Only parentheses are accepted e.g. `#[builder(finish_fn(...))]` or `#[builder(on(...))]`. This no longer works: `#[builder(finish_fn[...])]` or `#[builder(on{...})]` ([#145](https://github.com/elastio/bon/pull/145)) +- `#[builder(derive(Clone, Debug))]` now generates impl blocks that follow the behavior of standard `Clone` and `Debug` derives in that it conservatively adds `Clone/Debug` trait bounds for all the generic types declared on the original item (struct or function). See the *Added* section for details on the way to override these bounds with `#[builder(derive(Clone/Debug(bounds(...))))]`. + +### Removed +- Removed support for `#[bon::builder]` proc-macro attribute on top of a `struct`. Use `#[derive(bon::Builder)]` for that instead. This syntax has been deprecated since `2.1` and it is now removed as part of a major version cleanup ([#145](https://github.com/elastio/bon/pull/145)) + +### Added + +- Add `#[builder(builder_type(vis = "..."))]` that allows overriding the visibility of the builder struct ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(finish_fn(vis = "..."))]` that allows overriding the visibility of the finishing function ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(with = closure)]` syntax to customize setters with a custom closure. If the closure returns a `Result<_, E>` the setters become fallible ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(transparent)]` for `Option` fields to opt out from their special handling which makes `bon` treat them as regular required fields ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(state_mod)]` to configure the builder's type state API module name, visibility and docs ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(overwritable)]` and `#[builder(on(..., overwritable)]` to make it possible to call setters multiple times for the same member ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(setters)]` to fine-tune the setters names, visibility and docs ([#145](https://github.com/elastio/bon/pull/145)) +- Add `#[builder(derive(Clone/Debug(bounds(...))]` to allow overriding trait bounds on the `Clone/Debug` impl block of the builder ([#145](https://github.com/elastio/bon/pull/145)) +- Improve rustdoc output ([#145](https://github.com/elastio/bon/pull/145)) + - Add info that the member is required or optional. + - For members with defaults values show the default value in the docs. + - For optional members provide a link to a companion setter. The docs for `{member}(T)` setter mention the `maybe_{member}(Option)` setter and vice versa. + - Remove `__` prefixes for generic types and lifetimes from internal symbols. Instead, the prefixes added only if the macro detects a name collision. +- Add inheritance of `#[allow()]` and `#[expect()]` lint attributes to all generated items. This is useful to suppress any lints coming from the generated code. Although, lints coming from the generated code are generally considered defects in `bon` and should be reported via a Github issue but this provides an easy temporary workaround the problem ([#145](https://github.com/elastio/bon/pull/145)) + +### Fixed + +- Fixed `#[cfg/cfg_attr()]` not being expanded when used on function arguments with doc comments or other attributes. + + +### Other + +- Added graceful internal panic handling. If some `bon` macro panics due to an internal bug, the macro will try to still generate a fallback for IDEs to still provide intellisense ([#145](https://github.com/elastio/bon/pull/145)) + ## [2.3.0](https://github.com/elastio/bon/compare/v2.2.1...v2.3.0) - 2024-09-14 See the [blog post for this release](https://elastio.github.io/bon/blog/bon-builder-v2-3-release) that describes some of the most notable changes in detail. @@ -37,11 +75,13 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil There is a CLI to assist in migrating to the new syntax. See the [release blog post](https://elastio.github.io/bon/blog/bon-builder-v2-2-release#derive-builder-syntax-for-structs) for details about that. ### Added + - Add the top-level `#[builder(derive(...))]` attribute to be able to derive `Clone` and `Debug` for the builder type itself ([#113](https://github.com/elastio/bon/pull/113)) - Add support for conditional compilation with `cfg/cfg_attr` ([#99](https://github.com/elastio/bon/pull/99)) ### Fixed + - Fix developer experience in Rust Rover. The new `#[derive(Builder)]` syntax should now be easier for Rust Rover to analyze ([#99](https://github.com/elastio/bon/pull/99)) - Fix a bug where a member of opaque `Option` type (i.e. the `Option` type that was renamed to make the builder macro not detect it as `Option`) was still optional. ([#99](https://github.com/elastio/bon/pull/99)) - Fix code generation for structs with default values for generic parameters ([#108](https://github.com/elastio/bon/pull/108)) @@ -49,9 +89,11 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil ## [2.1.1](https://github.com/elastio/bon/compare/v2.1.0...v2.1.1) - 2024-09-03 ### Added + - Set MSRV to 1.70.0. Note that we plan to set an even lower MSRV. This is just an initial attempt to define the MSRV that should be good enough in the meantime while we work on lowering it even more ([#101](https://github.com/elastio/bon/pull/101)) ### Fixed + - Fix lints triggered by generated code such as `private_bounds`, `clippy::missing_const_for_fn` ([#101](https://github.com/elastio/bon/pull/101)) - Add more context to the messages such that it's clear what member isn't set in Rust Analyzer error messages ([#98](https://github.com/elastio/bon/pull/98)) @@ -60,6 +102,7 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil See the [blog post for this release](https://elastio.github.io/bon/blog/bon-builder-v2-1-release) that describes some of the most notable changes in detail. ### Added + - `#[must_use]` on the `build()` method for structs and `call()` for functions (if the original function has `#[must_use]`) ([#82](https://github.com/elastio/bon/pull/82)). Thanks [@EdJoPaTo](https://github.com/EdJoPaTo) for the contribution! ### Changed @@ -68,24 +111,29 @@ See the [blog post for this release](https://elastio.github.io/bon/blog/bon-buil - Improve builder() method docs ([#76](https://github.com/elastio/bon/pull/76)). Thanks [@EdJoPaTo](https://github.com/EdJoPaTo) for the contribution! ### Fixed + - Don't warn on `clippy::impl_trait_in_params` ([#80](https://github.com/elastio/bon/pull/80)). Thanks [@EdJoPaTo](https://github.com/EdJoPaTo) for the contribution! - Fix typos in messages and code comments ([#79](https://github.com/elastio/bon/pull/79)). Thanks [@EdJoPaTo](https://github.com/EdJoPaTo) for the contribution! ### Other + - Add more tests for `#[must_use]` ([#87](https://github.com/elastio/bon/pull/87)) ## [2.0.1](https://github.com/elastio/bon/compare/v2.0.0...v2.0.1) - 2024-08-28 ### Docs + - Add a new section ["`None` literals inference"](https://elastio.github.io/bon/guide/patterns/into-conversions-in-depth#none-literals-inference) to docs for "Into Conversions In-Depth" - Fix the docs about the comparison of Into conversions on the ["Alternatives"](http://elastio.github.io/bon/guide/alternatives) page that were not updated during the v2 release ### Fixed + - Fix capturing of generic params that appear only in return types ([#72](https://github.com/elastio/bon/pull/72)) - Fix support for associated types ([#72](https://github.com/elastio/bon/pull/72)) ### Internal + - Add more tests for various edge cases ([#70](https://github.com/elastio/bon/pull/70)) ## [2.0.0](https://github.com/elastio/bon/compare/v1.2.1...v2.0.0) - 2024-08-26 @@ -95,16 +143,19 @@ See the [blog post](https://elastio.github.io/bon/blog/bon-builder-generator-v2- ## [1.2.1](https://github.com/elastio/bon/compare/v1.2.0...v1.2.1) - 2024-08-12 ### Other + - Remove unnecessary const block ([#52](https://github.com/elastio/bon/pull/52)) - Small cleanup ([#51](https://github.com/elastio/bon/pull/51)) ## [1.2.0](https://github.com/elastio/bon/compare/v1.1.0...v1.2.0) - 2024-08-09 ### Added + - Add `#[builder(skip)]` attribute to skip generating setters ([#44](https://github.com/elastio/bon/pull/44)) - Add automatic docs for setters ([#45](https://github.com/elastio/bon/pull/45)) ### Other + - Remove dependencies on `easy-ext`, `heck` and `itertools` ([#42](https://github.com/elastio/bon/pull/42)) ## [1.1.0](https://github.com/elastio/bon/compare/v1.0.6...v1.1.0) - 2024-08-07 @@ -122,35 +173,42 @@ See the [blog post](https://elastio.github.io/bon/blog/bon-builder-generator-v2- ## [1.0.6](https://github.com/elastio/bon/compare/v1.0.5...v1.0.6) - 2024-08-01 ### Fixed + - Explicitly specify the minimum required version of the darling dependency ([#30](https://github.com/elastio/bon/pull/30)) ## [1.0.5](https://github.com/elastio/bon/compare/v1.0.4...v1.0.5) - 2024-07-31 ### Added + - Add `#[must_use]` to the builder and other small improvements ([#26](https://github.com/elastio/bon/pull/26)) ## [1.0.4](https://github.com/elastio/bon/compare/v1.0.3...v1.0.4) - 2024-07-30 ### Fixed + - new() method is now hidden by default and the Builder type name is the same as when `#[builder]` is on top of a `struct` ([#19](https://github.com/elastio/bon/pull/19)) ## [1.0.3](https://github.com/elastio/bon/compare/v1.0.2...v1.0.3) - 2024-07-30 ### Fixed + - Fix missing captured generics on an impl block that aren't referenced in the method ([#17](https://github.com/elastio/bon/pull/17)) ## [1.0.2](https://github.com/elastio/bon/compare/v1.0.1...v1.0.2) - 2024-07-29 ### Fixed + - Fix a bug of the `Default` trait requirement for types under an `Option` ([#13](https://github.com/elastio/bon/pull/13)) - Fix the link to docs.rs to so that it references the latest version ([#11](https://github.com/elastio/bon/pull/11)) ## [1.0.1](https://github.com/elastio/bon/compare/v1.0.0...v1.0.1) - 2024-07-29 ### Fixed + - Fix handling of raw identifiers ([#9](https://github.com/elastio/bon/pull/9)) ### Other + - Add example snippet to the docs for "adding builder to existing code" ([#7](https://github.com/elastio/bon/pull/7)) ## [1.0.0](https://github.com/elastio/bon/tree/v1.0.0) - 2024-07-28 diff --git a/website/guide/overview.md b/website/guide/overview.md index b9e6939e..b99d89bb 100644 --- a/website/guide/overview.md +++ b/website/guide/overview.md @@ -274,7 +274,7 @@ If you can't figure something out, consult the docs and maybe use that search `

diff --git a/website/guide/patterns/fallible-builders.md b/website/guide/patterns/fallible-builders.md index 20f6ebb8..bbc2940b 100644 --- a/website/guide/patterns/fallible-builders.md +++ b/website/guide/patterns/fallible-builders.md @@ -2,7 +2,7 @@ With `bon`, you can write a builder that validates its inputs and returns a `Result`. It's possible to do this only via the function or associated method syntax. -If you need to build a struct and do some validation, you won't be able to use the `#[builder]` annotation on the struct for that. You'll *have to* define your logic in the associated constructor method (e.g. `new`). +If you need to build a struct and do some validation, you won't be able to use the `#[derive(Builder)]` annotation on the struct for that. You'll *have to* define your logic in the associated constructor method (e.g. `new`). **Example:** @@ -38,4 +38,4 @@ if let Err(error) = result { } ``` -If you have a use case for some convenience attributes to do automatic validations using the `#[builder]` macro with the struct syntax, then add a 👍 reaction to [this Github issue](https://github.com/elastio/bon/issues/34). +If you have a use case for some convenience attributes to do automatic validations using the `#[derive(Builder)]` macro with the struct syntax, then add a 👍 reaction to [this Github issue](https://github.com/elastio/bon/issues/34). diff --git a/website/guide/positional-members.md b/website/guide/positional-members.md index 3cf5e657..95001b24 100644 --- a/website/guide/positional-members.md +++ b/website/guide/positional-members.md @@ -64,8 +64,10 @@ We can use a similar combination of the [top-level `#[builder(finish_fn = ...)]` use bon::Builder; #[derive(Builder)] -#[builder(start_fn = with_coordinates)] -#[builder(finish_fn = claim)] // [!code highlight] +#[builder( + start_fn = with_coordinates, + finish_fn = claim // [!code highlight] +)] struct Treasure { #[builder(start_fn)] x: u32, @@ -101,8 +103,10 @@ You may also combine these attributes with [`#[builder(into)]`](../reference/bui use bon::Builder; #[derive(Builder)] -#[builder(start_fn = with_coordinates)] -#[builder(finish_fn = claim)] +#[builder( + start_fn = with_coordinates, + finish_fn = claim // [!code focus] +)] struct Treasure { #[builder(start_fn)] x: u32, diff --git a/website/reference/builder.md b/website/reference/builder.md index bf19bf20..6784f223 100644 --- a/website/reference/builder.md +++ b/website/reference/builder.md @@ -2,59 +2,50 @@ outline: [2, 3] --- -# Builder macros +# `#[derive(Builder)]` / `#[builder]` -There are several ways to generate a builder depending on the syntax you place the builder macro on. +You can generate a builder using three different kinds of syntax (struct, free function, associated method). They all share two common groups of attributes. -**Structs:** +- [Item attributes](#item-attributes) - apply to a `struct` or `fn` declaration itself. +- [Member attributes](#member-attributes) - apply to a `struct` field or `fn` argument. -Use `#[derive(bon::Builder)]` +See examples. Make sure to click through the tabs: -```rust +:::code-group + +```rust [Struct] use bon::Builder; #[derive(Builder)] -#[builder(/* Top-Level Attributes */)] +#[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] struct Example { - #[builder(/* Member-Level Attributes */)] + #[builder(default)] // <-- this is a member attribute // [!code highlight] field: u32 } ``` -**Free functions:** - -Use `#[bon::builder]` - -```rust +```rust [Free function] use bon::builder; -#[builder(/* Top-Level Attributes */)] +#[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] fn example( - #[builder(/* Member-Level Attributes */)] + #[builder(default)] // <-- this is a member attribute // [!code highlight] arg: u32 -) { - // body -} +) { } ``` -**Associated methods:** - -Use `#[bon::bon]` + `#[builder]` on individual methods - -```rust +```rust [Associated method] use bon::bon; struct Example; #[bon] impl Example { - #[builder(/* Top-Level Attributes */)] + #[builder(finish_fn = finish)] // <-- this is an item attribute // [!code highlight] fn example( - #[builder(/* Member-Level Attributes */)] + #[builder(default)] // <-- this is a member attribute // [!code highlight] arg: u32 - ) { - // body - } + ) { } } ``` @@ -68,7 +59,7 @@ Most of the attributes apply to all kinds of syntax. However, some of them are o ::: -## Top-Level Attributes +## Item attributes ### `builder_type` @@ -649,13 +640,13 @@ struct Example { Example::builder() .name("accepts `impl Into`") .path("accepts/impl/into/PathBuf") - // This member doesn't match neither `String` nor `PathBuf`, + // This member doesn't match either `String` or `PathBuf`, // and thus #[builder(into)] was not applied to it .level(100) .build(); ``` -## Member-Level Attributes +## Member attributes ### `default` @@ -887,9 +878,9 @@ See the ["Into Conversions In-Depth"](../guide/patterns/into-conversions-in-dept ::: code-group ```rust [Struct field] -use bon::builder; +use bon::Builder; -#[builder] +#[derive(Builder)] struct Example { #[builder(into)] // [!code highlight] name: String, @@ -975,7 +966,7 @@ Example::example() **Applies to:** -Overrides the name of the setters generated for the member. This is most useful with the struct syntax where you'd like to use a different name for the field internally. For functions this attribute makes less sense since it's easy to just create a variable named differently `let new_name = param_name;`. However, this attribute is still supported for functions. +Overrides the name of the member in the builder's setters and type state. This is most useful when with struct syntax (`#[derive(Builder)]`) where you'd like to use a different name for the field internally. For functions this attribute makes less sense since it's easy to just create a variable named differently `let new_name = param_name;`. However, this attribute is still supported on function arguments. **Example:**