├── .cargo └── config.toml ├── .codecov.yml ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── DEVELOPMENT.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── coverage.sh ├── rustfmt.toml ├── scripts └── coverage.sh └── src ├── derive_input_ext.rs ├── derive_input_newtype_ext.rs ├── derive_input_struct_ext.rs ├── field_ext.rs ├── fields_ext.rs ├── fields_named_append.rs ├── fields_unnamed_append.rs ├── ident_ext.rs ├── lib.rs └── util.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | coverage_clean = ["llvm-cov", "clean", "--workspace"] 3 | coverage_0 = ["llvm-cov", "--no-report", "nextest", "--workspace", "--no-default-features"] 4 | coverage_merge = 'llvm-cov report --lcov --output-path ./target/coverage/lcov.info' 5 | coverage_open = 'llvm-cov report --open --output-dir ./target/coverage' 6 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - '**' 8 | 9 | name: CI 10 | 11 | jobs: 12 | audit: 13 | name: Audit 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | fmt: 23 | name: Rustfmt 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 10 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: dtolnay/rust-toolchain@master 29 | with: 30 | toolchain: nightly 31 | components: rustfmt 32 | 33 | - run: cargo fmt --all -- --check 34 | 35 | docs_and_spell_check: 36 | name: Docs and Spell Check 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | env: 40 | RUSTDOCFLAGS: "-Dwarnings" 41 | steps: 42 | - name: Checkout Actions Repository 43 | uses: actions/checkout@v3 44 | - uses: dtolnay/rust-toolchain@stable 45 | 46 | - name: Check spelling 47 | uses: crate-ci/typos@master 48 | 49 | - run: cargo doc --no-deps 50 | 51 | clippy: 52 | name: Clippy 53 | runs-on: ubuntu-latest 54 | timeout-minutes: 10 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: dtolnay/rust-toolchain@master 58 | with: 59 | toolchain: nightly 60 | components: clippy 61 | 62 | - name: 'Run clippy' 63 | # we cannot use `--all-features` because `envman` has features that are mutually exclusive. 64 | run: | 65 | cargo clippy --workspace --fix -- -D warnings 66 | 67 | coverage: 68 | name: Coverage 69 | runs-on: ubuntu-latest 70 | timeout-minutes: 10 71 | steps: 72 | - uses: actions/checkout@v3 73 | - uses: dtolnay/rust-toolchain@master 74 | with: 75 | toolchain: nightly 76 | components: llvm-tools-preview 77 | 78 | - uses: taiki-e/install-action@cargo-llvm-cov 79 | - uses: taiki-e/install-action@nextest 80 | 81 | - name: 'Collect coverage' 82 | run: ./coverage.sh 83 | 84 | - name: Upload to codecov.io 85 | uses: codecov/codecov-action@v3 86 | with: 87 | files: ./target/coverage/lcov.info 88 | 89 | build_and_test_linux: 90 | name: Build and Test (Linux) 91 | runs-on: ubuntu-latest 92 | timeout-minutes: 10 93 | steps: 94 | - uses: actions/checkout@v3 95 | - uses: dtolnay/rust-toolchain@stable 96 | 97 | - uses: taiki-e/install-action@nextest 98 | - name: 'Build and test' 99 | run: | 100 | cargo nextest run --workspace 101 | 102 | build_and_test_windows: 103 | name: Build and Test (Windows) 104 | runs-on: windows-latest 105 | timeout-minutes: 10 106 | steps: 107 | - name: Prepare symlink configuration 108 | run: git config --global core.symlinks true 109 | 110 | - uses: actions/checkout@v3 111 | - uses: dtolnay/rust-toolchain@stable 112 | 113 | - uses: taiki-e/install-action@nextest 114 | - name: 'Build and test' 115 | run: cargo nextest run --workspace 116 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | 6 | name: Publish 7 | 8 | jobs: 9 | audit: 10 | name: Audit 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: bp3d-actions/audit-check@9c23bd47e5e7b15b824739e0862cb878a52cc211 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | build_and_test_linux: 20 | name: Build and Test (Linux) 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 10 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: dtolnay/rust-toolchain@stable 26 | 27 | - uses: taiki-e/install-action@nextest 28 | - name: 'Build and test' 29 | run: cargo nextest run --workspace 30 | 31 | build_and_test_windows: 32 | name: Build and Test (Windows) 33 | runs-on: windows-latest 34 | timeout-minutes: 20 35 | steps: 36 | - name: Prepare symlink configuration 37 | run: git config --global core.symlinks true 38 | 39 | - uses: actions/checkout@v3 40 | - uses: dtolnay/rust-toolchain@stable 41 | 42 | - uses: taiki-e/install-action@nextest 43 | - name: 'Build and test' 44 | run: cargo nextest run --workspace 45 | 46 | crates_io_publish: 47 | name: Publish (crates.io) 48 | needs: 49 | - audit 50 | - build_and_test_linux 51 | - build_and_test_windows 52 | 53 | runs-on: ubuntu-latest 54 | timeout-minutes: 25 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: dtolnay/rust-toolchain@stable 58 | 59 | - name: cargo-release Cache 60 | id: cargo_release_cache 61 | uses: actions/cache@v3 62 | with: 63 | path: ~/.cargo/bin/cargo-release 64 | key: ${{ runner.os }}-cargo-release 65 | 66 | - run: cargo install cargo-release 67 | if: steps.cargo_release_cache.outputs.cache-hit != 'true' 68 | 69 | - name: cargo login 70 | run: cargo login ${{ secrets.CRATES_IO_API_TOKEN }} 71 | 72 | # allow-branch HEAD is because GitHub actions switches 73 | # to the tag while building, which is a detached head 74 | - name: "cargo release publish" 75 | run: |- 76 | cargo release \ 77 | publish \ 78 | --workspace \ 79 | --allow-branch HEAD \ 80 | --no-confirm \ 81 | --execute 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.8.0 (2023-06-04) 4 | 5 | syn 2 upgrade. All of the changes are **Breaking**, and so have not been highlighted as such. 6 | 7 | * `DeriveInputExt::append_derives` parameters changed from `NestedMeta` to `syn::Path`. 8 | * `DeriveInputExt::tag_parameter` return type changed from `NestedMeta` to `Meta`. 9 | * `DeriveInputExt::tag_parameters` return type changed from `NestedMeta` to `Meta`. 10 | * `util::nested_meta_to_path` is removed. 11 | * `util::meta_list_contains` is removed. 12 | * Removed `util::namespace_meta_lists` -- use `util::namespace_nested_metas_iter`. 13 | * Replaced `util::namespace_meta_lists_iter` with `util::namespace_nested_metas_iter`. 14 | * Replaced `util::tag_meta_lists_iter` with `util::tag_nested_metas_iter`. 15 | * Removed `util::tag_meta_lists_owned_iter`, there is no borrowed version because of `syn`'s new API, so use `util::tag_nested_metas_iter`. 16 | * Removed `util::ident_concat` -- users can use `quote::format_ident!` instead. 17 | * Added `util::namespace_parameter` and `util::namespace_parameters`. 18 | 19 | 20 | ## 0.7.0 (2020-01-13) 21 | 22 | * `util::namespace_parameter` returns an `Option`. 23 | * `util::namespace_parameters` returns a `Vec`. 24 | * ***Breaking:*** `util::namespace_meta_lists_iter` returns an `impl Iterator`. 25 | * ***Breaking:*** `util::tag_meta_list` renamed to `util::tag_meta_lists_iter`. 26 | * ***Breaking:*** `util::tag_meta_list_owned` renamed to `util::tag_meta_lists_owned_iter`. 27 | * ***Breaking:*** `util::tag_meta_lists_owned_iter` takes in `impl Iterator` instead of `Vec`. 28 | 29 | ## 0.6.1 (2020-01-10) 30 | 31 | * `util::contains_tag` supports checking if any list of attributes contains a `#[namespace(tag)]`. 32 | * `DeriveInputExt::contains_tag` supports checking if a type contains a `#[namespace(tag)]`. 33 | * Added `FieldsExt::is_unit/is_named/is_tuple` which returns a `bool` for the relevant `Fields` type. 34 | * `FieldsExt::construction_form` returns tokens suitable for deconstructing / constructing the relevant fields types. 35 | 36 | ## 0.6.0 (2019-10-01) 37 | 38 | * ***Breaking:*** `DeriveInputExt::tag_parameter` and `DeriveInputExt::tag_parameters` return `NestedMeta`. 39 | * ***Breaking:*** `FieldExt::tag_parameter` and `FieldExt::tag_parameters` return `NestedMeta`. 40 | * `util::tag_parameter` and `util::tag_parameters` are now `pub`. 41 | * `util::namespace_meta_list` is now `pub`. 42 | * `util::tag_meta_list` and `util::tag_meta_list_owned` are now `pub`. 43 | 44 | ## 0.5.0 (2019-08-19) 45 | 46 | * `syn`, `quote`, and `proc_macro2` are upgraded to `1.0`. 47 | * ***Breaking:*** `nested_meta_to_ident` is renamed to `nested_meta_to_path`. 48 | 49 | ## 0.4.0 (2019-08-17) 50 | 51 | * ***Breaking:*** `DeriveInputDeriveExt` is renamed to `DeriveInputExt`. 52 | * `FieldExt::tag_parameter` extracts the `Meta` param from `#[namespace(tag(param))]`. 53 | * `FieldExt::tag_parameters` extracts the `Meta` params from `#[namespace(tag(param1, param2))]`. 54 | * `DeriveInputExt::tag_parameter` extracts the `Meta` param from `#[namespace(tag(param))]`. 55 | * `DeriveInputExt::tag_parameters` extracts the `Meta` params from `#[namespace(tag(param1, param2))]`. 56 | 57 | ## 0.3.0 (2019-08-04) 58 | 59 | * `FieldExt` provides methods to work with `Field`s: 60 | 61 | - `contains_tag` 62 | - `is_phantom_data` 63 | - `type_name` 64 | 65 | ## 0.2.1 (2019-04-10) 66 | 67 | * `IdentExt::append` and `IdentExt::prepend` create new `Ident`s via concatenation. 68 | * Added the following methods to `DeriveInputStructExt`: 69 | 70 | - `is_unit` 71 | - `is_named` 72 | - `is_tuple` 73 | - `assert_fields_unit` 74 | - `assert_fields_named` 75 | - `assert_fields_unnamed` 76 | 77 | * Added `is_newtype` to `DeriveInputNewtypeExt`. 78 | 79 | ## 0.2.0 (2019-04-02) 80 | 81 | * ***Breaking:*** `FieldsNamed::append` is renamed to `FieldsNamed::append_named`. 82 | * ***Breaking:*** `FieldsUnnamed::append` is renamed to `FieldsUnnamed::append_unnamed`. 83 | 84 | ## 0.1.0 (2019-04-01) 85 | 86 | * `DeriveInputDeriveExt` provides function to append `derive`s. 87 | * `DeriveInputNewtypeExt` provides functions to get newtype inner `Field`. 88 | * `DeriveInputStructExt` provides functions to get struct `Field`s. 89 | * `FieldsNamedAppend` provides functions to append `FieldsNamed`. 90 | * `FieldsUnnamedAppend` provides functions to append `FieldsUnnamed`. 91 | * `nested_meta_to_ident` returns the `Ident` of a nested meta. 92 | * `meta_list_contains` returns whether a `MetaList` contains a specified `NestedMeta`. 93 | * `ident_concat` returns an `Ident` by concatenating `String` representations. 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Generating `README.md` 2 | 3 | Ensure `cargo-readme` is installed, then run: 4 | 5 | ```bash 6 | cargo readme > README.md 7 | ``` 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "proc-macro2" 7 | version = "1.0.59" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" 10 | dependencies = [ 11 | "unicode-ident", 12 | ] 13 | 14 | [[package]] 15 | name = "proc_macro_roids" 16 | version = "0.8.0" 17 | dependencies = [ 18 | "proc-macro2", 19 | "quote", 20 | "syn", 21 | ] 22 | 23 | [[package]] 24 | name = "quote" 25 | version = "1.0.28" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 28 | dependencies = [ 29 | "proc-macro2", 30 | ] 31 | 32 | [[package]] 33 | name = "syn" 34 | version = "2.0.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 37 | dependencies = [ 38 | "proc-macro2", 39 | "quote", 40 | "unicode-ident", 41 | ] 42 | 43 | [[package]] 44 | name = "unicode-ident" 45 | version = "1.0.9" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proc_macro_roids" 3 | version = "0.8.0" 4 | authors = ["Azriel Hoh "] 5 | edition = "2021" 6 | description = "Traits and functions to make writing proc macros more ergonomic." 7 | repository = "https://github.com/azriel91/proc_macro_roids" 8 | documentation = "https://docs.rs/proc_macro_roids/" 9 | readme = "README.md" 10 | keywords = ["proc_macro", "macros"] 11 | license = "MIT OR Apache-2.0" 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0.59" 15 | quote = "1.0.28" 16 | syn = { version = "2.0.18", features = ["extra-traits", "visit"] } 17 | 18 | [badges] 19 | appveyor = { repository = "azriel91/proc_macro_roids" } 20 | travis-ci = { repository = "azriel91/proc_macro_roids" } 21 | codecov = { repository = "azriel91/proc_macro_roids" } 22 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Dependencies 4 | 5 | ```bash 6 | rustup component add llvm-tools-preview 7 | cargo install cargo-llvm-cov 8 | cargo install cargo-nextest 9 | ``` 10 | 11 | 12 | ## Running Tests 13 | 14 | ```bash 15 | cargo nextest run --workspace --all-features 16 | 17 | # To test individual features 18 | for i in {0..0}; do cargo test_$i || break; done 19 | ``` 20 | 21 | 22 | ## Coverage 23 | 24 | Collect coverage and output as `lcov`. 25 | 26 | ```bash 27 | ./coverage.sh 28 | ``` 29 | 30 | Collect coverage and open `html` report. 31 | 32 | ```bash 33 | ./coverage.sh && cargo coverage_open 34 | ``` 35 | 36 | 37 | ## Releasing 38 | 39 | 1. Update crate versions. 40 | 41 | ```bash 42 | sd -s 'version = "0.8.0"' 'version = "0.9.0"' $(fd -tf -F toml) README.md src/lib.rs 43 | 44 | # Make sure only `proc_macro_roids` crates are updated. 45 | git --no-pager diff | rg '^[+]' | rg -v '(proc_macro_roids)|(\+\+\+)|\+version' 46 | ``` 47 | 48 | 2. Update `CHANGELOG.md` with the version and today's date. 49 | 3. Push a tag to the repository. 50 | 51 | The [`publish`] GitHub workflow will automatically publish the crates to [`crates.io`]. 52 | 53 | [`publish`]: https://github.com/azriel91/proc_macro_roids/actions/workflows/publish.yml 54 | [`crates.io`]:https://crates.io/ 55 | 56 | **Note:** 57 | 58 | An alternative to `cargo-release` is [`cargo-workspaces`], which may be used in case crates need to be published one by one -- if many new crates are being published, `cargo-release` gates the number of crates that can be published at one go. 59 | 60 | ```bash 61 | cargo workspaces \ 62 | publish \ 63 | --from-git \ 64 | --allow-branch main \ 65 | --force '*' \ 66 | --no-git-tag 67 | ``` 68 | 69 | [`cargo-workspaces`]: https://github.com/pksunkara/cargo-workspaces 70 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019 Azriel Hoh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💊 Proc Macro Roids 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/proc_macro_roids.svg)](https://crates.io/crates/proc_macro_roids) 4 | [![docs.rs](https://img.shields.io/docsrs/proc_macro_roids)](https://docs.rs/proc_macro_roids) 5 | [![CI](https://github.com/azriel91/proc_macro_roids/workflows/CI/badge.svg)](https://github.com/azriel91/proc_macro_roids/actions/workflows/ci.yml) 6 | [![Coverage Status](https://codecov.io/gh/azriel91/proc_macro_roids/branch/main/graph/badge.svg)](https://codecov.io/gh/azriel91/proc_macro_roids) 7 | 8 | Traits and functions to make writing proc macros more ergonomic. 9 | 10 | ```toml 11 | proc_macro_roids = "0.8.0" 12 | ``` 13 | 14 | Makes writing procedural macros much easier: 15 | 16 | ```rust 17 | extern crate proc_macro; 18 | 19 | use proc_macro::TokenStream; 20 | use proc_macro2::Span; 21 | use proc_macro_roids::{DeriveInputStructExt, FieldExt, IdentExt}; 22 | use quote::quote; 23 | use syn::{parse_macro_input, parse_quote, DeriveInput, Ident}; 24 | 25 | /// Derives a `Super` enum with a variant for each struct field: 26 | /// 27 | /// ```rust,edition2021 28 | /// use std::marker::PhantomData; 29 | /// use super_derive::Super; 30 | /// 31 | /// #[derive(Super)] 32 | /// pub struct Man { 33 | /// #[super_derive(skip)] 34 | /// name: String, 35 | /// power_level: u64, 36 | /// marker: PhantomData, 37 | /// } 38 | /// ``` 39 | /// 40 | /// Generates: 41 | /// 42 | /// ```rust,ignore 43 | /// pub enum SuperMan { 44 | /// U64(u64), 45 | /// } 46 | /// ``` 47 | #[proc_macro_derive(Super, attributes(super_derive))] 48 | pub fn system_desc_derive(input: TokenStream) -> TokenStream { 49 | let ast = parse_macro_input!(input as DeriveInput); 50 | let enum_name = ast.ident.prepend("Super"); 51 | let fields = ast.fields(); 52 | let relevant_fields = fields 53 | .iter() 54 | .filter(|field| !field.is_phantom_data()) 55 | .filter(|field| !field.contains_tag(&parse_quote!(super_derive), &parse_quote!(skip))); 56 | 57 | let variants = relevant_fields 58 | .map(|field| { 59 | let type_name = field.type_name(); 60 | let variant_name = type_name.to_string().to_uppercase(); 61 | let variant_name = Ident::new(&variant_name, Span::call_site()); 62 | quote! { 63 | #variant_name(#type_name) 64 | } 65 | }) 66 | .collect::>(); 67 | 68 | let token_stream2 = quote! { 69 | pub enum #enum_name { 70 | #(#variants,)* 71 | } 72 | }; 73 | 74 | token_stream2.into() 75 | } 76 | ``` 77 | 78 | ## Examples 79 | 80 |
81 | 82 | 1. Append additional `#[derive(..)]`s. 83 | 84 | This works for function-like or attribute proc macros. 85 | 86 | ```rust 87 | extern crate proc_macro; 88 | 89 | use proc_macro::TokenStream; 90 | use proc_macro_roids::DeriveInputExt; 91 | use quote::quote; 92 | use syn::{parse_macro_input, parse_quote, DeriveInput}; 93 | 94 | #[proc_macro_attribute] 95 | pub fn copy(_args: TokenStream, item: TokenStream) -> TokenStream { 96 | // Example input: 97 | // 98 | // #[derive(Debug)] 99 | // struct Struct; 100 | let mut ast = parse_macro_input!(item as DeriveInput); 101 | 102 | // Append the derives. 103 | let derives = parse_quote!(Clone, Copy); 104 | ast.append_derives(derives); 105 | 106 | // Example output: 107 | // 108 | // #[derive(Debug, Clone, Copy)] 109 | // struct Struct; 110 | TokenStream::from(quote! { #ast }) 111 | } 112 | ``` 113 | 114 |
115 | 116 |
117 | 118 | 2. Append named fields. 119 | 120 | This works for structs with named fields or unit structs. 121 | 122 | ```rust 123 | extern crate proc_macro; 124 | 125 | use proc_macro::TokenStream; 126 | use proc_macro_roids::FieldsNamedAppend; 127 | use quote::quote; 128 | use syn::{parse_macro_input, parse_quote, DeriveInput, FieldsNamed}; 129 | 130 | /// Example usage: 131 | /// 132 | /// ```rust 133 | /// use macro_crate::append_cd; 134 | /// 135 | /// #[append_cd] 136 | /// struct StructNamed { a: u32, b: i32 } 137 | /// ``` 138 | #[proc_macro_attribute] 139 | pub fn append_cd(_args: TokenStream, item: TokenStream) -> TokenStream { 140 | // Example input: 141 | // 142 | // struct StructNamed { a: u32, b: i32 } 143 | let mut ast = parse_macro_input!(item as DeriveInput); 144 | 145 | // Append the fields. 146 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 147 | ast.append_named(fields_additional); 148 | 149 | // Example output: 150 | // 151 | // struct StructNamed { a: u32, b: i32, c: i64, d: usize } 152 | TokenStream::from(quote! { #ast }) 153 | } 154 | ``` 155 | 156 |
157 | 158 |
159 | 160 | 3. Append unnamed fields (tuples). 161 | 162 | This works for structs with unnamed fields or unit structs. 163 | 164 | ```rust 165 | extern crate proc_macro; 166 | 167 | use proc_macro::TokenStream; 168 | use proc_macro_roids::FieldsUnnamedAppend; 169 | use quote::quote; 170 | use syn::{parse_macro_input, parse_quote, DeriveInput, FieldsUnnamed}; 171 | 172 | /// Example usage: 173 | /// 174 | /// ```rust 175 | /// use macro_crate::append_i64_usize; 176 | /// 177 | /// #[append_i64_usize] 178 | /// struct StructUnit; 179 | /// ``` 180 | #[proc_macro_attribute] 181 | pub fn append_i64_usize(_args: TokenStream, item: TokenStream) -> TokenStream { 182 | // Example input: 183 | // 184 | // struct StructUnit; 185 | let mut ast = parse_macro_input!(item as DeriveInput); 186 | 187 | // Append the fields. 188 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 189 | ast.append_unnamed(fields_additional); 190 | 191 | // Example output: 192 | // 193 | // struct StructUnit(i64, usize); 194 | TokenStream::from(quote! { #ast }) 195 | } 196 | ``` 197 | 198 |
199 | 200 |
201 | 202 | 4. Get newtype inner `Field`. 203 | 204 | This works for structs with unnamed fields or unit structs. 205 | 206 | ```rust 207 | extern crate proc_macro; 208 | 209 | use proc_macro::TokenStream; 210 | use proc_macro_roids::DeriveInputNewtypeExt; 211 | use quote::quote; 212 | use syn::{parse_macro_input, parse_quote, DeriveInput, Type}; 213 | 214 | #[proc_macro_derive(Deref)] 215 | pub fn derive_deref(item: TokenStream) -> TokenStream { 216 | // Example input: 217 | // 218 | // #[derive(Deref)] 219 | // struct Newtype(u32); 220 | let mut ast = parse_macro_input!(item as DeriveInput); 221 | 222 | // Get the inner field type. 223 | let inner_type = ast.inner_type(); 224 | 225 | // Implement `Deref` 226 | let type_name = &ast.ident; 227 | let token_stream_2 = quote! { 228 | #ast 229 | 230 | impl std::ops::Deref for #type_name { 231 | type Target = #inner_type; 232 | fn deref(&self) -> &Self::Target { 233 | &self.0 234 | } 235 | } 236 | } 237 | TokenStream::from(token_stream_2) 238 | } 239 | ``` 240 | 241 |
242 | 243 | 244 |
245 | 246 | 5. `Ident` concatenation. 247 | 248 | ```rust,edition2021 249 | use proc_macro_roids::IdentExt; 250 | use proc_macro2::Span; 251 | use syn::Ident; 252 | 253 | # fn main() { 254 | let one = Ident::new("One", Span::call_site()); 255 | assert_eq!(Ident::new("OneSuffix", Span::call_site()), one.append("Suffix")); 256 | assert_eq!(Ident::new("PrefixOne", Span::call_site()), one.prepend("Prefix")); 257 | 258 | let two = Ident::new("Two", Span::call_site()); 259 | assert_eq!(Ident::new("OneTwo", Span::call_site()), one.append(&two)); 260 | assert_eq!(Ident::new("TwoOne", Span::call_site()), one.prepend(&two)); 261 | # } 262 | ``` 263 | 264 |
265 | 266 |
267 | 268 | 6. Accessing struct fields. 269 | 270 | ```rust,edition2021 271 | use proc_macro_roids::DeriveInputStructExt; 272 | use syn::{parse_quote, DeriveInput, Fields}; 273 | 274 | # fn main() { 275 | let ast: DeriveInput = parse_quote! { 276 | struct Named {} 277 | }; 278 | 279 | if let Fields::Named(..) = ast.fields() { 280 | // do something 281 | } 282 | # } 283 | ``` 284 | 285 |
286 | 287 |
288 | 289 | 7. Inspecting `Field`s. 290 | 291 | ```rust,edition2021 292 | use proc_macro_roids::FieldExt; 293 | use proc_macro2::Span; 294 | use syn::{parse_quote, Fields, FieldsNamed, Lit, LitStr, Meta, MetaNameValue, NestedMeta}; 295 | 296 | let fields_named: FieldsNamed = parse_quote! {{ 297 | #[my::derive(tag::name(param = "value"))] 298 | pub name: PhantomData, 299 | }}; 300 | let fields = Fields::from(fields_named); 301 | let field = fields.iter().next().expect("Expected field to exist."); 302 | 303 | assert_eq!(field.type_name(), "PhantomData"); 304 | assert!(field.is_phantom_data()); 305 | assert!(field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name))); 306 | assert_eq!( 307 | field.tag_parameter( 308 | &parse_quote!(my::derive), 309 | &parse_quote!(tag::name), 310 | ).expect("Expected parameter to exist."), 311 | NestedMeta::Meta(Meta::NameValue(MetaNameValue { 312 | path: parse_quote!(param), 313 | eq_token: Default::default(), 314 | lit: Lit::Str(LitStr::new("value", Span::call_site())), 315 | })), 316 | ); 317 | ``` 318 | 319 |
320 | 321 |
322 | 323 | 8. (De)constructing `Fields`. 324 | 325 | ```rust,edition2021 326 | # use std::str::FromStr; 327 | # 328 | use proc_macro_roids::{DeriveInputStructExt, FieldsExt}; 329 | # use proc_macro2::{Span, TokenStream}; 330 | # use syn::{parse_quote, DeriveInput}; 331 | # use quote::quote; 332 | # 333 | // Need to generate code that instantiates `MyEnum::Struct`: 334 | // enum MyEnum { 335 | // Struct { 336 | // field_0: u32, 337 | // field_1: u32, 338 | // } 339 | // } 340 | 341 | let ast: DeriveInput = parse_quote! { 342 | struct Struct { 343 | field_0: u32, 344 | field_1: u32, 345 | } 346 | }; 347 | let fields = ast.fields(); 348 | let construction_form = fields.construction_form(); 349 | let tokens = quote! { MyEnum::Struct #construction_form }; 350 | 351 | let expected = TokenStream::from_str("MyEnum::Struct { field_0, field_1, }").unwrap(); 352 | assert_eq!(expected.to_string(), tokens.to_string()); 353 | ``` 354 | 355 |
356 | 357 | --- 358 | 359 | **Note:** The *roids* name is chosen because, although these functions make it easy to perform 360 | certain operations, they may not always be good ideas =D! 361 | 362 | ## License 363 | 364 | Licensed under either of 365 | 366 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 367 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 368 | 369 | at your option. 370 | 371 | 372 | ### Contribution 373 | 374 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 375 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/proc_macro_roids.svg)](https://crates.io/crates/proc_macro_roids) 2 | {{badges}} 3 | 4 | # Proc Macro Roids 5 | 6 | {{readme}} 7 | 8 | ## License 9 | 10 | Licensed under either of 11 | 12 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 13 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 14 | 15 | at your option. 16 | 17 | ### Contribution 18 | 19 | Unless you explicitly state otherwise, any contribution intentionally 20 | submitted for inclusion in the work by you, as defined in the Apache-2.0 21 | license, shall be dual licensed as above, without any additional terms or 22 | conditions. 23 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -euo pipefail 3 | 4 | cargo coverage_clean 5 | rm -rf ./target/coverage ./target/llvm-cov-target 6 | mkdir -p ./target/coverage 7 | 8 | # See `.config/cargo.toml` 9 | for i in {0..0} 10 | do cargo coverage_$i 11 | done 12 | 13 | cargo coverage_merge 14 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = 'crate' 2 | reorder_impl_items = true 3 | use_field_init_shorthand = true 4 | format_code_in_doc_comments = true 5 | wrap_comments = true 6 | edition = "2021" 7 | version = "Two" 8 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | # Release options 5 | profile=debug 6 | 7 | # Directories 8 | self_dir="$(dirname "$(readlink -f "${BASH_SOURCE}")")" 9 | repository_dir="$(dirname "${self_dir}")" 10 | target_dir="${repository_dir}/target" 11 | target_profile_dir="${target_dir}/${profile}" 12 | 13 | coverage_dir="${target_dir}/coverage" 14 | test -d "${coverage_dir}" || mkdir -p "${coverage_dir}" 15 | 16 | kcov_exclude_line="kcov-ignore" 17 | kcov_exclude_region="kcov-ignore-start:kcov-ignore-end" 18 | 19 | # Builds all crates including tests, but don't run them yet. 20 | # We will run the tests wrapped in `kcov`. 21 | if grep -qF 'proc-macro = true' "${repository_dir}/Cargo.toml" 22 | then 23 | test_bins_by_crate="$( 24 | cargo test --no-run --message-format=json | 25 | jq -r "select(.profile.test == true and .target.kind[0] == \"proc-macro\") | (.package_id | split(\" \"))[0] + \";\" + .filenames[]" 26 | )" 27 | else 28 | test_bins_by_crate="$( 29 | cargo test --no-run --message-format=json | 30 | jq -r "select(.profile.test == true) | (.package_id | split(\" \"))[0] + \";\" + .filenames[]" 31 | )" 32 | fi 33 | 34 | # Set `LD_LIBRARY_PATH` so that tests can link against it 35 | target_arch=$(rustup toolchain list | grep -F default | cut -d ' ' -f 1 | rev | cut -d '-' -f 1-4 | rev) 36 | export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$(rustc --print sysroot)/lib/rustlib/${target_arch}/lib/" 37 | 38 | crate_coverage_dirs=() 39 | for test_bin_by_crate in $test_bins_by_crate; do 40 | crate_name=${test_bin_by_crate%%;*} 41 | test_bin_path=${test_bin_by_crate##*;} 42 | test_bin_name=${test_bin_path##*/target/debug/} 43 | 44 | if [[ "${crate_name}" == $(basename "${repository_dir}") ]] 45 | then 46 | crate_dir="${repository_dir}" 47 | else 48 | crate_dir="${repository_dir}/${crate_name}" 49 | fi 50 | 51 | test -d "${crate_dir}" || continue; 52 | 53 | crate_coverage_dir="${coverage_dir}/${test_bin_name}" 54 | crate_coverage_dirs+=("${crate_coverage_dir}") 55 | 56 | ( 57 | echo "Running '${test_bin_path}'" 58 | 59 | export CARGO_MANIFEST_DIR="$crate_dir" 60 | kcov --include-pattern="${crate_dir}/src/,${crate_dir}/tests/" \ 61 | "--exclude-line=${kcov_exclude_line}" \ 62 | "--exclude-region=${kcov_exclude_region}" \ 63 | "${crate_coverage_dir}" "${test_bin_path}" 64 | ) 65 | 66 | done 67 | 68 | rm -rf "${coverage_dir}/merged" 69 | kcov --merge "${coverage_dir}/merged" "${crate_coverage_dirs[@]}" \ 70 | "--exclude-line=${kcov_exclude_line}" \ 71 | "--exclude-region=${kcov_exclude_region}" \ 72 | -------------------------------------------------------------------------------- /src/derive_input_ext.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, punctuated::Punctuated, Attribute, DeriveInput, Meta, Path, Token}; 2 | 3 | use crate::util; 4 | 5 | /// Functions to make it ergonomic to work with `struct` ASTs. 6 | pub trait DeriveInputExt { 7 | /// Appends derives to the list of derives. 8 | /// 9 | /// **Note:** This can only be used with [*attribute*] macros, and not 10 | /// [*derive*] macros. 11 | /// 12 | /// * If the `derive` attribute does not exist, one will be created. 13 | /// * If the `derive` attribute exists, and there are existing `derive`s 14 | /// that overlap with the derives to append, this macro will panic with 15 | /// the overlapping derives. 16 | /// * If the `derive` attribute exists, and there are no overlapping 17 | /// `derive`s, then they will be combined. 18 | /// 19 | /// # Panics 20 | /// 21 | /// Panics if there are existing `derive`s that overlap with the derives to 22 | /// append. 23 | /// 24 | /// [*attribute*]: 25 | /// [*derive*]: 26 | fn append_derives(&mut self, derives: Punctuated); 27 | 28 | /// Returns whether the type contains a given `#[namespace]` attribute. 29 | /// 30 | /// # Parameters 31 | /// 32 | /// * `namespace`: The `path()` of the first-level attribute. 33 | fn contains_namespace(&self, namespace: &Path) -> bool; 34 | 35 | /// Returns the parameter from `#[namespace(parameter)]`. 36 | /// 37 | /// # Parameters 38 | /// 39 | /// * `namespace`: The `path()` of the first-level attribute. 40 | /// 41 | /// # Panics 42 | /// 43 | /// Panics if there is more than one parameter for the tag. 44 | fn namespace_parameter(&self, namespace: &Path) -> Option; 45 | 46 | /// Returns the parameters from `#[namespace(param1, param2, ..)]`. 47 | /// 48 | /// # Parameters 49 | /// 50 | /// * `namespace`: The `path()` of the first-level attribute. 51 | fn namespace_parameters(&self, namespace: &Path) -> Vec; 52 | 53 | /// Returns whether the type contains a given `#[namespace(tag)]` attribute. 54 | /// 55 | /// # Parameters 56 | /// 57 | /// * `namespace`: The `path()` of the first-level attribute. 58 | /// * `tag`: The `path()` of the second-level attribute. 59 | fn contains_tag(&self, namespace: &Path, tag: &Path) -> bool; 60 | 61 | /// Returns the parameter from `#[namespace(tag(parameter))]`. 62 | /// 63 | /// # Parameters 64 | /// 65 | /// * `namespace`: The `path()` of the first-level attribute. 66 | /// * `tag`: The `path()` of the second-level attribute. 67 | /// 68 | /// # Panics 69 | /// 70 | /// Panics if there is more than one parameter for the tag. 71 | fn tag_parameter(&self, namespace: &Path, tag: &Path) -> Option; 72 | 73 | /// Returns the parameters from `#[namespace(tag(param1, param2, ..))]`. 74 | /// 75 | /// # Parameters 76 | /// 77 | /// * `namespace`: The `path()` of the first-level attribute. 78 | /// * `tag`: The `path()` of the second-level attribute. 79 | fn tag_parameters(&self, namespace: &Path, tag: &Path) -> Vec; 80 | } 81 | 82 | impl DeriveInputExt for DeriveInput { 83 | fn append_derives(&mut self, derives_to_append: Punctuated) { 84 | let attr_derives_existing = self 85 | .attrs 86 | .iter_mut() 87 | .filter(|attr| attr.path().is_ident("derive")) 88 | .filter_map(|attr| { 89 | match attr.parse_args_with(Punctuated::::parse_terminated) { 90 | Ok(derives_existing) => Some((attr, derives_existing)), 91 | _ => None, // kcov-ignore 92 | } 93 | }) 94 | .next(); 95 | 96 | if let Some((attr, mut derives_existing)) = attr_derives_existing { 97 | // Emit warning if the user derives any of the existing derives, as we do that 98 | // for them. 99 | let superfluous = derives_to_append 100 | .iter() 101 | .filter(|derive_to_append| { 102 | derives_existing 103 | .iter() 104 | .any(|derive_existing| derive_existing == *derive_to_append) 105 | }) 106 | .map(util::format_path) 107 | .collect::>(); 108 | if !superfluous.is_empty() { 109 | // TODO: Emit warning, pending 110 | // derives_existing 111 | // .span() 112 | // .warning( 113 | // "The following are automatically derived by this proc macro 114 | // attribute.", ) 115 | // .emit(); 116 | panic!( 117 | "The following are automatically derived when this attribute is used:\n\ 118 | {:?}", 119 | superfluous 120 | ); 121 | } else { 122 | derives_existing.extend(derives_to_append); 123 | 124 | // Replace the existing `Attribute`. 125 | // 126 | // `attr.parse_meta()` returns a `Meta`, which is not referenced by the 127 | // `DeriveInput`, so we have to replace `attr` itself. 128 | *attr = parse_quote!(#[derive(#derives_existing)]); 129 | } 130 | } else { 131 | // Add a new `#[derive(..)]` attribute with all the derives. 132 | let derive_attribute: Attribute = parse_quote!(#[derive(#derives_to_append)]); 133 | self.attrs.push(derive_attribute); 134 | } 135 | } 136 | 137 | fn contains_namespace(&self, namespace: &Path) -> bool { 138 | util::contains_namespace(&self.attrs, namespace) 139 | } 140 | 141 | fn namespace_parameter(&self, namespace: &Path) -> Option { 142 | util::namespace_parameter(&self.attrs, namespace) 143 | } 144 | 145 | fn namespace_parameters(&self, namespace: &Path) -> Vec { 146 | util::namespace_parameters(&self.attrs, namespace) 147 | } 148 | 149 | fn contains_tag(&self, namespace: &Path, tag: &Path) -> bool { 150 | util::contains_tag(&self.attrs, namespace, tag) 151 | } 152 | 153 | fn tag_parameter(&self, namespace: &Path, tag: &Path) -> Option { 154 | util::tag_parameter(&self.attrs, namespace, tag) 155 | } 156 | 157 | fn tag_parameters(&self, namespace: &Path, tag: &Path) -> Vec { 158 | util::tag_parameters(&self.attrs, namespace, tag) 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use proc_macro2::Span; 165 | use quote::quote; 166 | use syn::{parse_quote, DeriveInput, Error, Meta, MetaNameValue}; 167 | 168 | use super::DeriveInputExt; 169 | 170 | #[test] 171 | fn append_derives_creates_attr_when_attr_does_not_exist() { 172 | let mut ast: DeriveInput = parse_quote!( 173 | struct Struct; 174 | ); 175 | let derives = parse_quote!(Clone, Copy); 176 | 177 | ast.append_derives(derives); 178 | 179 | let ast_expected: DeriveInput = parse_quote! { 180 | #[derive(Clone, Copy)] 181 | struct Struct; 182 | }; 183 | assert_eq!(ast_expected, ast); 184 | } 185 | 186 | #[test] 187 | fn append_derives_appends_to_attr_when_attr_exists() { 188 | let mut ast: DeriveInput = parse_quote!( 189 | #[derive(Debug)] 190 | struct Struct; 191 | ); 192 | let derives = parse_quote!(Clone, Copy); 193 | 194 | ast.append_derives(derives); 195 | 196 | let ast_expected: DeriveInput = parse_quote! { 197 | #[derive(Debug, Clone, Copy)] 198 | struct Struct; 199 | }; 200 | assert_eq!(ast_expected, ast); 201 | } 202 | 203 | #[test] 204 | #[should_panic( 205 | expected = "The following are automatically derived when this attribute is used:\n\ 206 | [\"Clone\", \"Copy\"]" 207 | )] 208 | fn append_derives_panics_when_derives_exist() { 209 | let mut ast: DeriveInput = parse_quote!( 210 | #[derive(Clone, Copy, Debug)] 211 | struct Struct; 212 | ); 213 | let derives = parse_quote!(Clone, Copy, Default); 214 | 215 | ast.append_derives(derives); 216 | } 217 | 218 | #[test] 219 | fn contains_namespace_returns_false_when_namespace_does_not_exist() -> Result<(), Error> { 220 | let tokens_list = vec![ 221 | quote! { 222 | #[other::my::derive] 223 | struct Struct; 224 | }, 225 | quote! { 226 | #[my::derive::other(other)] 227 | struct Struct; 228 | }, 229 | quote! { 230 | #[other(tag::name)] 231 | struct Struct; 232 | }, 233 | ]; 234 | 235 | tokens_list 236 | .into_iter() 237 | .try_for_each(|tokens| -> Result<(), Error> { 238 | let message = format!("Failed to parse tokens: `{}`", &tokens); 239 | let assertion_message = format!( 240 | "Expected `contains_namespace` to return false for tokens: `{}`", 241 | &tokens 242 | ); 243 | 244 | let ast: DeriveInput = 245 | syn::parse2(tokens).map_err(|_| Error::new(Span::call_site(), &message))?; 246 | 247 | assert!( 248 | !ast.contains_namespace(&parse_quote!(my::derive)), 249 | "{}", 250 | assertion_message 251 | ); 252 | 253 | Ok(()) 254 | }) 255 | } 256 | 257 | #[test] 258 | fn namespace_parameter_returns_none_when_not_present() { 259 | let ast: DeriveInput = parse_quote!( 260 | #[my::derive] 261 | struct Struct; 262 | ); 263 | 264 | assert_eq!(ast.namespace_parameter(&parse_quote!(my::derive)), None); 265 | } 266 | 267 | #[test] 268 | fn namespace_parameter_returns_ident_when_present() { 269 | let ast: DeriveInput = parse_quote!( 270 | #[my::derive(Magic)] 271 | struct Struct; 272 | ); 273 | 274 | let tag_expected: Meta = parse_quote!(Magic); 275 | assert_eq!( 276 | Some(tag_expected), 277 | ast.namespace_parameter(&parse_quote!(my::derive)) 278 | ); 279 | } 280 | 281 | #[test] 282 | #[should_panic(expected = "Expected exactly one parameter for `#[my::derive(..)]`.")] 283 | fn namespace_parameter_panics_when_multiple_parameters_present() { 284 | let ast: DeriveInput = parse_quote!( 285 | #[my::derive(Magic::One, Magic::Two)] 286 | struct Struct; 287 | ); 288 | 289 | ast.namespace_parameter(&parse_quote!(my::derive)); 290 | } 291 | 292 | #[test] 293 | fn namespace_parameters_returns_empty_vec_when_not_present() { 294 | let ast: DeriveInput = parse_quote!( 295 | struct Struct; 296 | ); 297 | 298 | assert_eq!( 299 | Vec::::new(), 300 | ast.namespace_parameters(&parse_quote!(my::derive)) 301 | ); 302 | } 303 | 304 | #[test] 305 | fn namespace_parameters_returns_idents_when_present() { 306 | let ast: DeriveInput = parse_quote!( 307 | #[my::derive(Magic::One, second = "{ Magic::Two }")] 308 | struct Struct; 309 | ); 310 | 311 | assert_eq!( 312 | ast.namespace_parameters(&parse_quote!(my::derive)), 313 | vec![ 314 | Meta::Path(parse_quote!(Magic::One)), 315 | Meta::NameValue(MetaNameValue { 316 | path: parse_quote!(second), 317 | eq_token: Default::default(), 318 | value: parse_quote!("{ Magic::Two }") 319 | }), 320 | ] 321 | ); 322 | } 323 | 324 | #[test] 325 | fn contains_tag_returns_true_when_tag_exists() -> Result<(), Error> { 326 | let ast: DeriveInput = parse_quote!( 327 | #[my::derive(tag::name)] 328 | struct Struct; 329 | ); 330 | 331 | assert!(ast.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name))); 332 | 333 | Ok(()) 334 | } 335 | 336 | #[test] 337 | fn contains_tag_returns_false_when_tag_does_not_exist() -> Result<(), Error> { 338 | let tokens_list = vec![ 339 | quote! { 340 | #[my::derive] 341 | struct Struct; 342 | }, 343 | quote! { 344 | #[my::derive(other)] 345 | struct Struct; 346 | }, 347 | quote! { 348 | #[other(tag::name)] 349 | struct Struct; 350 | }, 351 | ]; 352 | 353 | tokens_list 354 | .into_iter() 355 | .try_for_each(|tokens| -> Result<(), Error> { 356 | let message = format!("Failed to parse tokens: `{}`", &tokens); 357 | let assertion_message = format!( 358 | "Expected `contains_tag` to return false for tokens: `{}`", 359 | &tokens 360 | ); 361 | 362 | let ast: DeriveInput = 363 | syn::parse2(tokens).map_err(|_| Error::new(Span::call_site(), &message))?; 364 | 365 | assert!( 366 | !ast.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name)), 367 | "{}", 368 | assertion_message 369 | ); 370 | 371 | Ok(()) 372 | }) 373 | } 374 | 375 | #[test] 376 | fn tag_parameter_returns_none_when_not_present() { 377 | let ast: DeriveInput = parse_quote!( 378 | #[my::derive] 379 | struct Struct; 380 | ); 381 | 382 | // kcov-ignore-start 383 | assert_eq!( 384 | // kcov-ignore-end 385 | ast.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)), 386 | None 387 | ); 388 | } 389 | 390 | #[test] 391 | fn tag_parameter_returns_ident_when_present() { 392 | let ast: DeriveInput = parse_quote!( 393 | #[my::derive(tag::name(Magic))] 394 | struct Struct; 395 | ); 396 | 397 | let tag_expected: Meta = parse_quote!(Magic); 398 | assert_eq!( 399 | Some(tag_expected), 400 | ast.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)) 401 | ); 402 | } 403 | 404 | #[test] 405 | #[should_panic(expected = "Expected exactly one parameter for `#[my::derive(tag::name(..))]`.")] 406 | fn tag_parameter_panics_when_multiple_parameters_present() { 407 | let ast: DeriveInput = parse_quote!( 408 | #[my::derive(tag::name(Magic::One, Magic::Two))] 409 | struct Struct; 410 | ); 411 | 412 | ast.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)); 413 | } 414 | 415 | #[test] 416 | fn tag_parameters_returns_empty_vec_when_not_present() { 417 | let ast: DeriveInput = parse_quote!( 418 | #[my::derive] 419 | struct Struct; 420 | ); 421 | 422 | assert_eq!( 423 | Vec::::new(), 424 | ast.tag_parameters(&parse_quote!(my::derive), &parse_quote!(tag::name)) 425 | ); 426 | } 427 | 428 | #[test] 429 | fn tag_parameters_returns_idents_when_present() { 430 | let ast: DeriveInput = parse_quote!( 431 | #[my::derive(tag::name(Magic::One, second = "{ Magic::Two }"))] 432 | struct Struct; 433 | ); 434 | 435 | assert_eq!( 436 | ast.tag_parameters(&parse_quote!(my::derive), &parse_quote!(tag::name)), 437 | vec![ 438 | Meta::Path(parse_quote!(Magic::One)), 439 | Meta::NameValue(MetaNameValue { 440 | path: parse_quote!(second), 441 | eq_token: Default::default(), 442 | value: parse_quote!("{ Magic::Two }") 443 | }), 444 | ] 445 | ); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/derive_input_newtype_ext.rs: -------------------------------------------------------------------------------- 1 | use syn::{Data, DataStruct, DeriveInput, Field, Fields}; 2 | 3 | const NEWTYPE_MUST_HAVE_ONLY_ONE_FIELD: &str = "Newtype struct must only have one field.\n\ 4 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 5 | for more information."; 6 | const MACRO_MUST_BE_USED_ON_NEWTYPE_STRUCT: &str = "This macro must be used on a newtype struct.\n\ 7 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 8 | for more information."; 9 | 10 | /// Functions to make it ergonomic to work with newtype `struct` ASTs. 11 | pub trait DeriveInputNewtypeExt { 12 | /// Returns the `Field` of the first unnamed field of this struct's AST. 13 | /// 14 | /// # Panics 15 | /// 16 | /// Panics if the AST is not for a newtype struct. 17 | fn inner_type(&self) -> &Field; 18 | 19 | /// Returns the `Field` of the first unnamed field of this struct's AST. 20 | /// 21 | /// # Panics 22 | /// 23 | /// Panics if the AST is not for a newtype struct. 24 | fn inner_type_mut(&mut self) -> &mut Field; 25 | 26 | /// Returns true if the AST is for a struct with **exactly one** unnamed 27 | /// field. 28 | fn is_newtype(&self) -> bool; 29 | } 30 | 31 | impl DeriveInputNewtypeExt for DeriveInput { 32 | fn inner_type(&self) -> &Field { 33 | if let Data::Struct(DataStruct { 34 | fields: Fields::Unnamed(fields_unnamed), 35 | .. 36 | }) = &self.data 37 | { 38 | if fields_unnamed.unnamed.len() == 1 { 39 | fields_unnamed 40 | .unnamed 41 | .first() 42 | .expect("Expected field to exist.") 43 | } else { 44 | panic!("{}", NEWTYPE_MUST_HAVE_ONLY_ONE_FIELD) 45 | } 46 | } else { 47 | panic!("{}", MACRO_MUST_BE_USED_ON_NEWTYPE_STRUCT) 48 | } 49 | } 50 | 51 | fn inner_type_mut(&mut self) -> &mut Field { 52 | if let Data::Struct(DataStruct { 53 | fields: Fields::Unnamed(fields_unnamed), 54 | .. 55 | }) = &mut self.data 56 | { 57 | if fields_unnamed.unnamed.len() == 1 { 58 | fields_unnamed 59 | .unnamed 60 | .iter_mut() 61 | .next() 62 | .expect("Expected field to exist.") 63 | // kcov-ignore-start 64 | } else { 65 | // kcov-ignore-end 66 | panic!("{}", NEWTYPE_MUST_HAVE_ONLY_ONE_FIELD) 67 | } 68 | } else { 69 | panic!("{}", MACRO_MUST_BE_USED_ON_NEWTYPE_STRUCT) 70 | } 71 | } 72 | 73 | fn is_newtype(&self) -> bool { 74 | if let Data::Struct(DataStruct { 75 | fields: Fields::Unnamed(fields_unnamed), 76 | .. 77 | }) = &self.data 78 | { 79 | fields_unnamed.unnamed.len() == 1 80 | } else { 81 | false 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use syn::{parse_quote, DeriveInput, Type}; 89 | 90 | use super::DeriveInputNewtypeExt; 91 | 92 | #[test] 93 | fn inner_type_returns_field() { 94 | let ast: DeriveInput = parse_quote! { 95 | struct Newtype(u32); 96 | }; 97 | 98 | let inner_field = ast.inner_type(); 99 | 100 | let expected_type: Type = Type::Path(parse_quote!(u32)); 101 | assert_eq!(expected_type, inner_field.ty); 102 | } 103 | 104 | #[test] 105 | #[should_panic(expected = "This macro must be used on a newtype struct.\n\ 106 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 107 | for more information.")] 108 | fn inner_type_panics_when_struct_fields_not_unnamed() { 109 | let ast: DeriveInput = parse_quote! { 110 | struct Unit; 111 | }; 112 | 113 | ast.inner_type(); 114 | } // kcov-ignore 115 | 116 | #[test] 117 | #[should_panic(expected = "Newtype struct must only have one field.\n\ 118 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 119 | for more information.")] 120 | fn inner_type_panics_when_struct_has_multiple_fields() { 121 | let ast: DeriveInput = parse_quote! { 122 | struct Newtype(u32, u32); 123 | }; 124 | 125 | ast.inner_type(); 126 | } // kcov-ignore 127 | 128 | #[test] 129 | fn inner_type_mut_returns_field() { 130 | let mut ast: DeriveInput = parse_quote! { 131 | struct Newtype(u32); 132 | }; 133 | 134 | let inner_field = ast.inner_type_mut(); 135 | 136 | let expected_type: Type = Type::Path(parse_quote!(u32)); 137 | assert_eq!(expected_type, inner_field.ty); 138 | } 139 | 140 | #[test] 141 | #[should_panic(expected = "This macro must be used on a newtype struct.\n\ 142 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 143 | for more information.")] 144 | fn inner_type_mut_panics_when_struct_fields_not_unnamed() { 145 | let mut ast: DeriveInput = parse_quote! { 146 | struct Unit; 147 | }; 148 | 149 | ast.inner_type_mut(); 150 | } // kcov-ignore 151 | 152 | #[test] 153 | #[should_panic(expected = "Newtype struct must only have one field.\n\ 154 | See https://doc.rust-lang.org/book/ch19-04-advanced-types.html#advanced-types \ 155 | for more information.")] 156 | fn inner_type_mut_panics_when_struct_has_multiple_fields() { 157 | let mut ast: DeriveInput = parse_quote! { 158 | struct Newtype(u32, u32); 159 | }; 160 | 161 | ast.inner_type_mut(); 162 | } // kcov-ignore 163 | 164 | #[test] 165 | fn is_newtype_returns_true_when_fields_unnamed_and_exactly_one() { 166 | let ast: DeriveInput = parse_quote! { 167 | struct Tuple(u32); 168 | }; 169 | 170 | assert!(ast.is_newtype()); 171 | } 172 | 173 | #[test] 174 | fn is_newtype_returns_false_when_fields_unnamed_and_zero() { 175 | let ast: DeriveInput = parse_quote! { 176 | struct Tuple(); 177 | }; 178 | 179 | assert!(!ast.is_newtype()); 180 | } 181 | 182 | #[test] 183 | fn is_newtype_returns_false_when_fields_unnamed_and_more_than_one() { 184 | let ast: DeriveInput = parse_quote! { 185 | struct Tuple(u32, u32); 186 | }; 187 | 188 | assert!(!ast.is_newtype()); 189 | } 190 | 191 | #[test] 192 | fn is_newtype_returns_false_when_fields_not_tuple() { 193 | let ast: DeriveInput = parse_quote! { 194 | struct Unit; 195 | }; 196 | 197 | assert!(!ast.is_newtype()); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/derive_input_struct_ext.rs: -------------------------------------------------------------------------------- 1 | use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed}; 2 | 3 | /// Functions to make it ergonomic to work with `struct` ASTs. 4 | pub trait DeriveInputStructExt { 5 | /// Returns a reference to the data_struct of a struct's AST. 6 | /// 7 | /// # Panics 8 | /// 9 | /// Panics if the AST is not for a struct. 10 | fn data_struct(&self) -> &DataStruct; 11 | 12 | /// Returns a mutable reference to the data_struct of a struct's AST. 13 | /// 14 | /// # Panics 15 | /// 16 | /// Panics if the AST is not for a struct. 17 | fn data_struct_mut(&mut self) -> &mut DataStruct; 18 | 19 | /// Returns a reference to the fields of a struct's AST. 20 | /// 21 | /// # Panics 22 | /// 23 | /// Panics if the AST is not for a struct. 24 | fn fields(&self) -> &Fields; 25 | 26 | /// Returns a mutable reference to the fields of a struct's AST. 27 | /// 28 | /// # Panics 29 | /// 30 | /// Panics if the AST is not for a struct. 31 | fn fields_mut(&mut self) -> &mut Fields; 32 | 33 | /// Returns a mutable reference to the named fields of a struct's AST. 34 | /// 35 | /// # Panics 36 | /// 37 | /// Panics if the AST is not for a struct with named fields. 38 | fn fields_named(&self) -> &FieldsNamed; 39 | 40 | /// Returns a mutable reference to the named fields of a struct's AST. 41 | /// 42 | /// # Panics 43 | /// 44 | /// Panics if the AST is not for a struct with named fields. 45 | fn fields_named_mut(&mut self) -> &mut FieldsNamed; 46 | 47 | /// Returns true if the AST is for a unit struct. 48 | fn is_unit(&self) -> bool; 49 | 50 | /// Returns true if the AST is for a struct with named fields. 51 | fn is_named(&self) -> bool; 52 | 53 | /// Returns true if the AST is for a struct with unnamed fields. 54 | fn is_tuple(&self) -> bool; 55 | 56 | /// Panics if the AST is not for a unit struct. 57 | fn assert_fields_unit(&self); 58 | 59 | /// Panics if the AST is not for a struct with named fields. 60 | fn assert_fields_named(&self); 61 | 62 | /// Panics if the AST is not for a struct with unnamed fields. 63 | fn assert_fields_unnamed(&self); 64 | } 65 | 66 | impl DeriveInputStructExt for DeriveInput { 67 | fn data_struct(&self) -> &DataStruct { 68 | if let Data::Struct(data_struct) = &self.data { 69 | data_struct 70 | } else { 71 | panic!("This macro must be used on a struct."); 72 | } 73 | } 74 | 75 | fn data_struct_mut(&mut self) -> &mut DataStruct { 76 | if let Data::Struct(data_struct) = &mut self.data { 77 | data_struct 78 | } else { 79 | panic!("This macro must be used on a struct."); 80 | } 81 | } 82 | 83 | fn fields(&self) -> &Fields { 84 | if let Data::Struct(DataStruct { fields, .. }) = &self.data { 85 | fields 86 | } else { 87 | panic!("This macro must be used on a struct."); 88 | } 89 | } 90 | 91 | fn fields_mut(&mut self) -> &mut Fields { 92 | if let Data::Struct(DataStruct { fields, .. }) = &mut self.data { 93 | fields 94 | } else { 95 | panic!("This macro must be used on a struct."); 96 | } 97 | } 98 | 99 | fn fields_named(&self) -> &FieldsNamed { 100 | if let Data::Struct(DataStruct { 101 | fields: Fields::Named(fields_named), 102 | .. 103 | }) = &self.data 104 | { 105 | fields_named 106 | } else { 107 | panic!("This macro must be used on a struct with named fields."); 108 | } 109 | } 110 | 111 | fn fields_named_mut(&mut self) -> &mut FieldsNamed { 112 | if let Data::Struct(DataStruct { 113 | fields: Fields::Named(fields_named), 114 | .. 115 | }) = &mut self.data 116 | { 117 | fields_named 118 | } else { 119 | panic!("This macro must be used on a struct with named fields."); 120 | } 121 | } 122 | 123 | fn is_unit(&self) -> bool { 124 | matches!( 125 | &self.data, 126 | Data::Struct(DataStruct { 127 | fields: Fields::Unit, 128 | .. 129 | }) 130 | ) 131 | } 132 | 133 | fn is_named(&self) -> bool { 134 | matches!( 135 | &self.data, 136 | Data::Struct(DataStruct { 137 | fields: Fields::Named(..), 138 | .. 139 | }) 140 | ) 141 | } 142 | 143 | fn is_tuple(&self) -> bool { 144 | matches!( 145 | &self.data, 146 | Data::Struct(DataStruct { 147 | fields: Fields::Unnamed(..), 148 | .. 149 | }) 150 | ) 151 | } 152 | 153 | fn assert_fields_unit(&self) { 154 | if !self.is_unit() { 155 | panic!("This macro must be used on a unit struct."); 156 | } 157 | } 158 | 159 | fn assert_fields_named(&self) { 160 | if !self.is_named() { 161 | panic!("This macro must be used on a struct with named fields."); 162 | } 163 | } 164 | 165 | fn assert_fields_unnamed(&self) { 166 | if !self.is_tuple() { 167 | panic!("This macro must be used on a struct with unnamed fields."); 168 | } 169 | } 170 | } 171 | 172 | #[cfg(test)] 173 | mod tests { 174 | use syn::{parse_quote, DeriveInput, Fields, FieldsNamed}; 175 | 176 | use super::DeriveInputStructExt; 177 | 178 | #[test] 179 | fn data_struct_returns_data_struct() { 180 | let ast: DeriveInput = parse_quote! { 181 | struct Unit; 182 | }; 183 | 184 | ast.data_struct(); 185 | } 186 | 187 | #[test] 188 | #[should_panic(expected = "This macro must be used on a struct.")] 189 | fn data_struct_panics_when_ast_is_not_struct() { 190 | let ast: DeriveInput = parse_quote! { 191 | enum NotStruct {} 192 | }; 193 | 194 | ast.data_struct(); 195 | } // kcov-ignore 196 | 197 | #[test] 198 | fn data_struct_mut_returns_data_struct_mut() { 199 | let mut ast: DeriveInput = parse_quote! { 200 | struct Unit; 201 | }; 202 | 203 | ast.data_struct_mut(); 204 | } 205 | 206 | #[test] 207 | #[should_panic(expected = "This macro must be used on a struct.")] 208 | fn data_struct_mut_panics_when_ast_is_not_struct() { 209 | let mut ast: DeriveInput = parse_quote! { 210 | enum NotStruct {} 211 | }; 212 | 213 | ast.data_struct_mut(); 214 | } // kcov-ignore 215 | 216 | #[test] 217 | fn fields_returns_unit_fields() { 218 | let ast: DeriveInput = parse_quote! { 219 | struct Unit; 220 | }; 221 | 222 | assert_eq!(&Fields::Unit, ast.fields()); 223 | } 224 | 225 | #[test] 226 | fn fields_returns_named_fields() { 227 | let ast: DeriveInput = parse_quote! { 228 | struct Named {} 229 | }; 230 | 231 | if let &Fields::Named(..) = ast.fields() { 232 | // pass 233 | } else { 234 | panic!("Expected `fields` to return `&Fields::Named(..)") // kcov-ignore 235 | } 236 | } 237 | 238 | #[test] 239 | fn fields_returns_unnamed_fields() { 240 | let ast: DeriveInput = parse_quote! { 241 | struct Unnamed(u32); 242 | }; 243 | 244 | if let &Fields::Unnamed(..) = ast.fields() { 245 | // pass 246 | } else { 247 | panic!("Expected `fields` to return `&Fields::Unnamed(..)") // kcov-ignore 248 | } 249 | } 250 | 251 | #[test] 252 | #[should_panic(expected = "This macro must be used on a struct.")] 253 | fn fields_panics_when_ast_is_not_struct() { 254 | let ast: DeriveInput = parse_quote! { 255 | enum NotStruct {} 256 | }; 257 | 258 | ast.fields(); 259 | } // kcov-ignore 260 | 261 | #[test] 262 | fn fields_mut_returns_unit_fields() { 263 | let mut ast: DeriveInput = parse_quote! { 264 | struct Unit; 265 | }; 266 | 267 | assert_eq!(&mut Fields::Unit, ast.fields_mut()); 268 | } 269 | 270 | #[test] 271 | fn fields_mut_returns_named_fields() { 272 | let mut ast: DeriveInput = parse_quote! { 273 | struct Named {} 274 | }; 275 | 276 | if let &mut Fields::Named(..) = ast.fields_mut() { 277 | // pass 278 | } else { 279 | panic!("Expected `fields_mut` to return `&mut Fields::Named(..)") // kcov-ignore 280 | } 281 | } 282 | 283 | #[test] 284 | fn fields_mut_returns_unnamed_fields() { 285 | let mut ast: DeriveInput = parse_quote! { 286 | struct Unnamed(u32); 287 | }; 288 | 289 | if let &mut Fields::Unnamed(..) = ast.fields_mut() { 290 | // pass 291 | } else { 292 | panic!("Expected `fields_mut` to return `&mut Fields::Unnamed(..)") // kcov-ignore 293 | } 294 | } 295 | 296 | #[test] 297 | #[should_panic(expected = "This macro must be used on a struct.")] 298 | fn fields_mut_panics_when_ast_is_not_struct() { 299 | let mut ast: DeriveInput = parse_quote! { 300 | enum NotStruct {} 301 | }; 302 | 303 | ast.fields_mut(); 304 | } // kcov-ignore 305 | 306 | #[test] 307 | fn fields_named_returns_named_fields() { 308 | let ast: DeriveInput = parse_quote! { 309 | struct Named { a: u32, b: i32 } 310 | }; 311 | 312 | let fields_named: FieldsNamed = parse_quote!({ a: u32, b: i32 }); 313 | assert_eq!(&fields_named, ast.fields_named()); 314 | } 315 | 316 | #[test] 317 | #[should_panic(expected = "This macro must be used on a struct with named fields.")] 318 | fn fields_named_panics_when_fields_unit() { 319 | let ast: DeriveInput = parse_quote! { 320 | struct Unit; 321 | }; 322 | 323 | ast.fields_named(); 324 | } // kcov-ignore 325 | 326 | #[test] 327 | #[should_panic(expected = "This macro must be used on a struct with named fields.")] 328 | fn fields_named_panics_when_ast_is_not_struct() { 329 | let ast: DeriveInput = parse_quote! { 330 | enum NotStruct {} 331 | }; 332 | 333 | ast.fields_named(); 334 | } // kcov-ignore 335 | 336 | #[test] 337 | fn fields_named_mut_returns_named_fields() { 338 | let mut ast: DeriveInput = parse_quote! { 339 | struct Named { a: u32, b: i32 } 340 | }; 341 | 342 | let mut fields_named: FieldsNamed = parse_quote!({ a: u32, b: i32 }); 343 | assert_eq!(&mut fields_named, ast.fields_named_mut()); 344 | } 345 | 346 | #[test] 347 | #[should_panic(expected = "This macro must be used on a struct with named fields.")] 348 | fn fields_named_mut_panics_when_fields_unit() { 349 | let mut ast: DeriveInput = parse_quote! { 350 | struct Unit; 351 | }; 352 | 353 | ast.fields_named_mut(); 354 | } // kcov-ignore 355 | 356 | #[test] 357 | #[should_panic(expected = "This macro must be used on a struct with named fields.")] 358 | fn fields_named_mut_panics_when_ast_is_not_struct() { 359 | let mut ast: DeriveInput = parse_quote! { 360 | enum NotStruct {} 361 | }; 362 | 363 | ast.fields_named_mut(); 364 | } // kcov-ignore 365 | 366 | #[test] 367 | fn is_unit_returns_true_when_fields_unit() { 368 | let ast: DeriveInput = parse_quote! { 369 | struct Unit; 370 | }; 371 | 372 | assert!(ast.is_unit()); 373 | } 374 | 375 | #[test] 376 | fn is_unit_returns_false_when_fields_not_unit() { 377 | let ast: DeriveInput = parse_quote! { 378 | struct Named {} 379 | }; 380 | 381 | assert!(!ast.is_unit()); 382 | } 383 | 384 | #[test] 385 | fn is_named_returns_true_when_fields_named() { 386 | let ast: DeriveInput = parse_quote! { 387 | struct Named {} 388 | }; 389 | 390 | assert!(ast.is_named()); 391 | } 392 | 393 | #[test] 394 | fn is_named_returns_false_when_fields_not_named() { 395 | let ast: DeriveInput = parse_quote! { 396 | struct Unit; 397 | }; 398 | 399 | assert!(!ast.is_named()); 400 | } 401 | 402 | #[test] 403 | fn is_tuple_returns_true_when_fields_unnamed() { 404 | let ast: DeriveInput = parse_quote! { 405 | struct Tuple(u32); 406 | }; 407 | 408 | assert!(ast.is_tuple()); 409 | } 410 | 411 | #[test] 412 | fn is_tuple_returns_false_when_fields_not_unnamed() { 413 | let ast: DeriveInput = parse_quote! { 414 | struct Unit; 415 | }; 416 | 417 | assert!(!ast.is_tuple()); 418 | } 419 | 420 | #[test] 421 | fn assert_fields_unit_does_not_panic_when_fields_unit() { 422 | let ast: DeriveInput = parse_quote! { 423 | struct Unit; 424 | }; 425 | 426 | ast.assert_fields_unit(); 427 | } 428 | 429 | #[test] 430 | #[should_panic(expected = "This macro must be used on a unit struct.")] 431 | fn assert_fields_unit_panics_when_fields_not_unit() { 432 | let ast: DeriveInput = parse_quote! { 433 | struct Named {} 434 | }; 435 | 436 | ast.assert_fields_unit(); 437 | } // kcov-ignore 438 | 439 | #[test] 440 | fn assert_fields_named_does_not_panic_when_fields_named() { 441 | let ast: DeriveInput = parse_quote! { 442 | struct Named {} 443 | }; 444 | 445 | ast.assert_fields_named(); 446 | } 447 | 448 | #[test] 449 | #[should_panic(expected = "This macro must be used on a struct with named fields.")] 450 | fn assert_fields_named_panics_when_fields_not_named() { 451 | let ast: DeriveInput = parse_quote! { 452 | struct Unit; 453 | }; 454 | 455 | ast.assert_fields_named(); 456 | } // kcov-ignore 457 | 458 | #[test] 459 | fn assert_fields_unnamed_does_not_panic_when_fields_unnamed() { 460 | let ast: DeriveInput = parse_quote! { 461 | struct Unnamed(u32); 462 | }; 463 | 464 | ast.assert_fields_unnamed(); 465 | } 466 | 467 | #[test] 468 | #[should_panic(expected = "This macro must be used on a struct with unnamed fields.")] 469 | fn assert_fields_unnamed_panics_when_fields_not_unnamed() { 470 | let ast: DeriveInput = parse_quote! { 471 | struct Named {} 472 | }; 473 | 474 | ast.assert_fields_unnamed(); 475 | } // kcov-ignore 476 | } 477 | -------------------------------------------------------------------------------- /src/field_ext.rs: -------------------------------------------------------------------------------- 1 | use syn::{Field, Ident, Meta, Path, PathSegment, Type, TypePath}; 2 | 3 | use crate::util; 4 | 5 | /// Functions to make it ergonomic to inspect `Field`s and their attributes. 6 | pub trait FieldExt { 7 | /// Returns the simple type name of a field. 8 | /// 9 | /// For example, the `PhantomData` in `std::marker::PhantomData`. 10 | fn type_name(&self) -> &Ident; 11 | 12 | /// Returns whether the field is `PhantomData`. 13 | /// 14 | /// Note that the detection is a string comparison instead of a type ID 15 | /// comparison, so is prone to inaccurate detection, for example: 16 | /// 17 | /// * `use std::marker::PhantomData as GhostData;` 18 | /// * `use other_crate::OtherType as PhantomData;` 19 | fn is_phantom_data(&self) -> bool; 20 | 21 | /// Returns whether a field contains a given `#[namespace(tag)]` attribute. 22 | /// 23 | /// # Parameters 24 | /// 25 | /// * `namespace`: The `path()` of the first-level attribute. 26 | /// * `tag`: The `path()` of the second-level attribute. 27 | fn contains_tag(&self, namespace: &Path, tag: &Path) -> bool; 28 | 29 | /// Returns the parameter from `#[namespace(parameter)]`. 30 | /// 31 | /// # Parameters 32 | /// 33 | /// * `namespace`: The `path()` of the first-level attribute. 34 | /// 35 | /// # Panics 36 | /// 37 | /// Panics if there is more than one parameter for the tag. 38 | fn namespace_parameter(&self, namespace: &Path) -> Option; 39 | 40 | /// Returns the parameters from `#[namespace(param1, param2, ..)]`. 41 | /// 42 | /// # Parameters 43 | /// 44 | /// * `namespace`: The `path()` of the first-level attribute. 45 | fn namespace_parameters(&self, namespace: &Path) -> Vec; 46 | 47 | /// Returns the parameter from `#[namespace(tag(parameter))]`. 48 | /// 49 | /// # Parameters 50 | /// 51 | /// * `namespace`: The `path()` of the first-level attribute. 52 | /// * `tag`: The `path()` of the second-level attribute. 53 | /// 54 | /// # Panics 55 | /// 56 | /// Panics if there is more than one parameter for the tag. 57 | fn tag_parameter(&self, namespace: &Path, tag: &Path) -> Option; 58 | 59 | /// Returns the parameters from `#[namespace(tag(param1, param2, ..))]`. 60 | /// 61 | /// # Parameters 62 | /// 63 | /// * `namespace`: The `path()` of the first-level attribute. 64 | /// * `tag`: The `path()` of the second-level attribute. 65 | fn tag_parameters(&self, namespace: &Path, tag: &Path) -> Vec; 66 | } 67 | 68 | impl FieldExt for Field { 69 | fn type_name(&self) -> &Ident { 70 | if let Type::Path(TypePath { path, .. }) = &self.ty { 71 | if let Some(PathSegment { ident, .. }) = path.segments.last() { 72 | return ident; 73 | } 74 | } 75 | // kcov-ignore-start 76 | panic!( 77 | "Expected {}field type to be a `Path` with a segment.", 78 | self.ident 79 | .as_ref() 80 | .map(|ident| format!("`{:?}` ", ident)) 81 | .unwrap_or_else(|| String::from("")) 82 | ); 83 | // kcov-ignore-end 84 | } 85 | 86 | fn is_phantom_data(&self) -> bool { 87 | self.type_name() == "PhantomData" 88 | } 89 | 90 | fn contains_tag(&self, namespace: &Path, tag: &Path) -> bool { 91 | util::contains_tag(&self.attrs, namespace, tag) 92 | } 93 | 94 | fn namespace_parameter(&self, namespace: &Path) -> Option { 95 | util::namespace_parameter(&self.attrs, namespace) 96 | } 97 | 98 | fn namespace_parameters(&self, namespace: &Path) -> Vec { 99 | util::namespace_parameters(&self.attrs, namespace) 100 | } 101 | 102 | fn tag_parameter(&self, namespace: &Path, tag: &Path) -> Option { 103 | util::tag_parameter(&self.attrs, namespace, tag) 104 | } 105 | 106 | fn tag_parameters(&self, namespace: &Path, tag: &Path) -> Vec { 107 | util::tag_parameters(&self.attrs, namespace, tag) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use syn::{parse_quote, Fields, FieldsNamed, Meta, MetaNameValue}; 114 | 115 | use super::FieldExt; 116 | 117 | #[test] 118 | fn type_name_returns_simple_type_name() { 119 | let fields_named: FieldsNamed = parse_quote! {{ 120 | pub name: PhantomData, 121 | }}; 122 | let fields = Fields::from(fields_named); 123 | let field = fields.iter().next().expect("Expected field to exist."); 124 | 125 | assert_eq!(field.type_name(), "PhantomData"); 126 | } 127 | 128 | #[test] 129 | fn is_phantom_data_returns_true_for_phantom_data() { 130 | let fields_named: FieldsNamed = parse_quote! {{ 131 | pub name: PhantomData, 132 | }}; 133 | let fields = Fields::from(fields_named); 134 | let field = fields.iter().next().expect("Expected field to exist."); 135 | 136 | assert!(field.is_phantom_data()); 137 | } 138 | 139 | #[test] 140 | fn is_phantom_data_returns_false_for_non_phantom_data() { 141 | let fields_named: FieldsNamed = parse_quote! {{ 142 | pub name: GhostData, 143 | }}; 144 | let fields = Fields::from(fields_named); 145 | let field = fields.iter().next().expect("Expected field to exist."); 146 | 147 | assert!(!field.is_phantom_data()); 148 | } 149 | 150 | #[test] 151 | fn namespace_parameter_returns_none_when_not_present() { 152 | let fields_named: FieldsNamed = parse_quote! {{ 153 | #[other::derive] 154 | pub name: u32, 155 | }}; 156 | let fields = Fields::from(fields_named); 157 | let field = fields.iter().next().expect("Expected field to exist."); 158 | 159 | let parameter = field.namespace_parameter(&parse_quote!(my::derive)); 160 | assert_eq!(parameter, None); 161 | } 162 | 163 | #[test] 164 | fn namespace_parameter_returns_meta_when_present() { 165 | let fields_named: FieldsNamed = parse_quote! {{ 166 | #[my::derive(Magic)] 167 | pub name: u32, 168 | }}; 169 | let fields = Fields::from(fields_named); 170 | let field = fields.iter().next().expect("Expected field to exist."); 171 | 172 | assert_eq!( 173 | field.namespace_parameter(&parse_quote!(my::derive)), 174 | Some(Meta::Path(parse_quote!(Magic))) 175 | ); 176 | } 177 | 178 | #[test] 179 | #[should_panic(expected = "Expected exactly one parameter for `#[my::derive(..)]`.")] 180 | fn namespace_parameter_panics_when_multiple_parameters_present() { 181 | let fields_named: FieldsNamed = parse_quote! {{ 182 | #[my::derive(Magic::One, Magic::Two)] 183 | pub name: u32, 184 | }}; 185 | let fields = Fields::from(fields_named); 186 | let field = fields.iter().next().expect("Expected field to exist."); 187 | 188 | field.namespace_parameter(&parse_quote!(my::derive)); 189 | } 190 | 191 | #[test] 192 | fn namespace_parameters_returns_empty_vec_when_not_present() { 193 | let fields_named: FieldsNamed = parse_quote! {{ 194 | #[my::derive] 195 | pub name: u32, 196 | }}; 197 | let fields = Fields::from(fields_named); 198 | let field = fields.iter().next().expect("Expected field to exist."); 199 | 200 | assert_eq!( 201 | field.namespace_parameters(&parse_quote!(my::derive)), 202 | Vec::::new() 203 | ); 204 | } 205 | 206 | #[test] 207 | fn namespace_parameters_returns_metas_when_present() { 208 | let fields_named: FieldsNamed = parse_quote! {{ 209 | #[my::derive(Magic::One, second = "{ Magic::Two }")] 210 | pub name: u32, 211 | }}; 212 | let fields = Fields::from(fields_named); 213 | let field = fields.iter().next().expect("Expected field to exist."); 214 | 215 | assert_eq!( 216 | field.namespace_parameters(&parse_quote!(my::derive)), 217 | vec![ 218 | Meta::Path(parse_quote!(Magic::One)), 219 | Meta::NameValue(MetaNameValue { 220 | path: parse_quote!(second), 221 | eq_token: Default::default(), 222 | value: parse_quote!("{ Magic::Two }") 223 | }), 224 | ] 225 | ); 226 | } 227 | 228 | #[test] 229 | fn tag_parameter_returns_none_when_not_present() { 230 | let fields_named: FieldsNamed = parse_quote! {{ 231 | #[my::derive] 232 | pub name: u32, 233 | }}; 234 | let fields = Fields::from(fields_named); 235 | let field = fields.iter().next().expect("Expected field to exist."); 236 | 237 | let parameter = field.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)); 238 | assert_eq!(parameter, None); 239 | } 240 | 241 | #[test] 242 | fn tag_parameter_returns_meta_when_present() { 243 | let fields_named: FieldsNamed = parse_quote! {{ 244 | #[my::derive(tag::name(Magic))] 245 | pub name: u32, 246 | }}; 247 | let fields = Fields::from(fields_named); 248 | let field = fields.iter().next().expect("Expected field to exist."); 249 | 250 | assert_eq!( 251 | field.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)), 252 | Some(Meta::Path(parse_quote!(Magic))) 253 | ); 254 | } 255 | 256 | #[test] 257 | #[should_panic(expected = "Expected exactly one parameter for `#[my::derive(tag::name(..))]`.")] 258 | fn tag_parameter_panics_when_multiple_parameters_present() { 259 | let fields_named: FieldsNamed = parse_quote! {{ 260 | #[my::derive(tag::name(Magic::One, Magic::Two))] 261 | pub name: u32, 262 | }}; 263 | let fields = Fields::from(fields_named); 264 | let field = fields.iter().next().expect("Expected field to exist."); 265 | 266 | field.tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name)); 267 | } 268 | 269 | #[test] 270 | fn tag_parameters_returns_empty_vec_when_not_present() { 271 | let fields_named: FieldsNamed = parse_quote! {{ 272 | #[my::derive] 273 | pub name: u32, 274 | }}; 275 | let fields = Fields::from(fields_named); 276 | let field = fields.iter().next().expect("Expected field to exist."); 277 | 278 | assert_eq!( 279 | field.tag_parameters(&parse_quote!(my::derive), &parse_quote!(tag::name)), 280 | Vec::::new() 281 | ); 282 | } 283 | 284 | #[test] 285 | fn tag_parameters_returns_metas_when_present() { 286 | let fields_named: FieldsNamed = parse_quote! {{ 287 | #[my::derive(tag::name(Magic::One, second = "{ Magic::Two }"))] 288 | pub name: u32, 289 | }}; 290 | let fields = Fields::from(fields_named); 291 | let field = fields.iter().next().expect("Expected field to exist."); 292 | 293 | assert_eq!( 294 | field.tag_parameters(&parse_quote!(my::derive), &parse_quote!(tag::name)), 295 | vec![ 296 | Meta::Path(parse_quote!(Magic::One)), 297 | Meta::NameValue(MetaNameValue { 298 | path: parse_quote!(second), 299 | eq_token: Default::default(), 300 | value: parse_quote!("{ Magic::Two }") 301 | }), 302 | ] 303 | ); 304 | } 305 | 306 | mod fields_named { 307 | use proc_macro2::Span; 308 | use quote::quote; 309 | use syn::{parse_quote, Error, Fields, FieldsNamed}; 310 | 311 | use super::super::FieldExt; 312 | 313 | #[test] 314 | fn contains_tag_returns_true_when_tag_exists() -> Result<(), Error> { 315 | let fields_named: FieldsNamed = parse_quote! {{ 316 | #[my::derive(tag::name)] 317 | pub name: PhantomData, 318 | }}; 319 | let fields = Fields::from(fields_named); 320 | let field = fields.iter().next().expect("Expected field to exist."); 321 | 322 | assert!(field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name))); 323 | 324 | Ok(()) 325 | } 326 | 327 | #[test] 328 | fn contains_tag_returns_false_when_tag_does_not_exist() -> Result<(), Error> { 329 | let tokens_list = vec![ 330 | quote! {{ 331 | #[my::derive] 332 | pub name: PhantomData, 333 | }}, 334 | quote! {{ 335 | #[my::derive(other)] 336 | pub name: PhantomData, 337 | }}, 338 | quote! {{ 339 | #[other(tag::name)] 340 | pub name: PhantomData, 341 | }}, 342 | ]; 343 | 344 | tokens_list 345 | .into_iter() 346 | .try_for_each(|tokens| -> Result<(), Error> { 347 | let message = format!("Failed to parse tokens: `{}`", &tokens); 348 | let assertion_message = format!( 349 | "Expected `contains_tag` to return false for tokens: `{}`", 350 | &tokens 351 | ); 352 | 353 | let fields_named: FieldsNamed = 354 | syn::parse2(tokens).map_err(|_| Error::new(Span::call_site(), &message))?; 355 | let fields = Fields::from(fields_named); 356 | let field = fields.iter().next().expect("Expected field to exist."); 357 | 358 | // kcov-ignore-start 359 | assert!( 360 | // kcov-ignore-end 361 | !field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name)), 362 | "{}", // kcov-ignore 363 | assertion_message // kcov-ignore 364 | ); 365 | 366 | Ok(()) 367 | }) 368 | } 369 | } 370 | 371 | mod fields_unnamed { 372 | use proc_macro2::Span; 373 | use quote::quote; 374 | use syn::{parse_quote, Error, Fields, FieldsUnnamed}; 375 | 376 | use super::super::FieldExt; 377 | 378 | #[test] 379 | fn contains_tag_returns_true_when_tag_exists() -> Result<(), Error> { 380 | let fields_unnamed: FieldsUnnamed = parse_quote! {( 381 | #[my::derive(tag::name)] 382 | pub PhantomData, 383 | )}; 384 | let fields = Fields::from(fields_unnamed); 385 | let field = fields.iter().next().expect("Expected field to exist."); 386 | 387 | assert!(field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name))); 388 | 389 | Ok(()) 390 | } 391 | 392 | #[test] 393 | fn contains_tag_returns_false_when_tag_does_not_exist() -> Result<(), Error> { 394 | let tokens_list = vec![ 395 | quote! {( 396 | #[my::derive] 397 | pub PhantomData, 398 | )}, 399 | quote! {( 400 | #[my::derive(other)] 401 | pub PhantomData, 402 | )}, 403 | quote! {( 404 | #[other(tag::name)] 405 | pub PhantomData, 406 | )}, 407 | ]; 408 | 409 | tokens_list 410 | .into_iter() 411 | .try_for_each(|tokens| -> Result<(), Error> { 412 | let message = format!("Failed to parse tokens: `{}`", &tokens); 413 | let assertion_message = format!( 414 | "Expected `contains_tag` to return false for tokens: `{}`", 415 | &tokens 416 | ); 417 | 418 | let fields_unnamed: FieldsUnnamed = 419 | syn::parse2(tokens).map_err(|_| Error::new(Span::call_site(), &message))?; 420 | let fields = Fields::from(fields_unnamed); 421 | let field = fields.iter().next().expect("Expected field to exist."); 422 | 423 | // kcov-ignore-start 424 | assert!( 425 | // kcov-ignore-end 426 | !field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name)), 427 | "{}", // kcov-ignore 428 | assertion_message // kcov-ignore 429 | ); 430 | 431 | Ok(()) 432 | }) 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/fields_ext.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident}; 4 | 5 | /// Functions to make it ergonomic to work with `Fields`. 6 | pub trait FieldsExt { 7 | /// Returns true if the `Fields` is for a unit struct. 8 | fn is_unit(&self) -> bool; 9 | 10 | /// Returns true if the `Fields` is for a struct with named fields. 11 | fn is_named(&self) -> bool; 12 | 13 | /// Returns true if the `Fields` is for a struct with unnamed fields. 14 | fn is_tuple(&self) -> bool; 15 | 16 | /// Returns a token stream of the construction form of the fields. 17 | /// 18 | /// For unit fields, this returns an empty token stream. 19 | /// 20 | /// * Tuple fields: `(_0, _1,)` 21 | /// * Named fields: `{ field_0, field_1 }` 22 | /// 23 | /// # Examples 24 | fn construction_form(&self) -> TokenStream; 25 | } 26 | 27 | impl FieldsExt for Fields { 28 | fn is_unit(&self) -> bool { 29 | matches!(self, Fields::Unit) 30 | } 31 | 32 | fn is_named(&self) -> bool { 33 | matches!(self, Fields::Named(..)) 34 | } 35 | 36 | fn is_tuple(&self) -> bool { 37 | matches!(self, Fields::Unnamed(..)) 38 | } 39 | 40 | fn construction_form(&self) -> TokenStream { 41 | match self { 42 | Fields::Unit => TokenStream::new(), 43 | Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { 44 | let token_stream = 45 | (0..unnamed.len()).fold(TokenStream::new(), |mut token_stream, n| { 46 | let tuple_field = Ident::new(format!("_{}", n).as_str(), Span::call_site()); 47 | token_stream.extend(quote!(#tuple_field, )); 48 | token_stream 49 | }); 50 | 51 | quote! { (#token_stream) } 52 | } 53 | Fields::Named(FieldsNamed { named, .. }) => { 54 | let token_stream = named.iter().filter_map(|field| field.ident.as_ref()).fold( 55 | TokenStream::new(), 56 | |mut token_stream, field_name| { 57 | token_stream.extend(quote!(#field_name, )); 58 | token_stream 59 | }, 60 | ); 61 | 62 | quote!({ #token_stream }) 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use quote::quote; 71 | use syn::{parse_quote, Fields, FieldsNamed, FieldsUnnamed}; 72 | 73 | use super::FieldsExt; 74 | 75 | #[test] 76 | fn is_unit_returns_true_when_fields_unit() { 77 | assert!(Fields::Unit.is_unit()); 78 | } 79 | 80 | #[test] 81 | fn is_unit_returns_false_when_fields_not_unit() { 82 | let fields_named: FieldsNamed = parse_quote! {{}}; 83 | let fields = Fields::from(fields_named); 84 | 85 | assert!(!fields.is_unit()); 86 | } 87 | 88 | #[test] 89 | fn is_named_returns_true_when_fields_named() { 90 | let fields_named: FieldsNamed = parse_quote! {{}}; 91 | let fields = Fields::from(fields_named); 92 | 93 | assert!(fields.is_named()); 94 | } 95 | 96 | #[test] 97 | fn is_named_returns_false_when_fields_not_named() { 98 | assert!(!Fields::Unit.is_named()); 99 | } 100 | 101 | #[test] 102 | fn is_tuple_returns_true_when_fields_unnamed() { 103 | let fields_unnamed: FieldsUnnamed = parse_quote! {(u32,)}; 104 | let fields = Fields::from(fields_unnamed); 105 | 106 | assert!(fields.is_tuple()); 107 | } 108 | 109 | #[test] 110 | fn is_tuple_returns_false_when_fields_not_unnamed() { 111 | assert!(!Fields::Unit.is_tuple()); 112 | } 113 | 114 | #[test] 115 | fn construction_form_fields_unit_is_empty_token_stream() { 116 | assert!(Fields::Unit.construction_form().is_empty()); 117 | } 118 | 119 | #[test] 120 | fn construction_form_fields_named_is_brace_surrounding_comma_separated_variable_names() { 121 | let fields_named: FieldsNamed = parse_quote! {{ 122 | pub field_0: u32, 123 | pub field_1: SomeType, 124 | }}; 125 | let fields = Fields::from(fields_named); 126 | let construction_tokens = fields.construction_form(); 127 | 128 | let expected_tokens = quote!({ field_0, field_1, }); 129 | assert_eq!(expected_tokens.to_string(), construction_tokens.to_string()); 130 | } 131 | 132 | #[test] 133 | fn construction_form_fields_unnamed_is_parentheses_surrounding_comma_separated_variable_ns() { 134 | let fields_unnamed: FieldsUnnamed = parse_quote! {(u32, u32)}; 135 | let fields = Fields::from(fields_unnamed); 136 | let construction_tokens = fields.construction_form(); 137 | 138 | let expected_tokens = quote!((_0, _1,)); 139 | assert_eq!(expected_tokens.to_string(), construction_tokens.to_string()); 140 | } 141 | 142 | #[test] 143 | fn construction_form_fields_unnamed_one_field_includes_trailing_comma() { 144 | let fields_unnamed: FieldsUnnamed = parse_quote! {(u32,)}; 145 | let fields = Fields::from(fields_unnamed); 146 | let construction_tokens = fields.construction_form(); 147 | 148 | let expected_tokens = quote!((_0,)); 149 | assert_eq!(expected_tokens.to_string(), construction_tokens.to_string()); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/fields_named_append.rs: -------------------------------------------------------------------------------- 1 | use syn::{DeriveInput, Fields, FieldsNamed}; 2 | 3 | use crate::DeriveInputStructExt; 4 | 5 | const ERR_MUST_BE_UNIT_OR_NAMED: &str = "Macro must be used on either a unit struct or a struct with named fields.\n\ 6 | This derive does not work on tuple structs."; 7 | 8 | /// Indicates this type may have `FieldsNamed` appended to it. 9 | pub trait FieldsNamedAppend { 10 | /// Appends the specified `fields_named` to this type. 11 | fn append_named(&mut self, fields_named: FieldsNamed); 12 | } 13 | 14 | impl FieldsNamedAppend for DeriveInput { 15 | fn append_named(&mut self, fields_named: FieldsNamed) { 16 | self.fields_mut().append_named(fields_named); 17 | self.data_struct_mut().semi_token = None; 18 | } 19 | } 20 | 21 | impl FieldsNamedAppend for Fields { 22 | fn append_named(&mut self, fields_named: FieldsNamed) { 23 | match self { 24 | Fields::Named(self_fields_named) => self_fields_named.append_named(fields_named), 25 | Fields::Unit => *self = Fields::from(fields_named), 26 | Fields::Unnamed(_) => panic!("{}", ERR_MUST_BE_UNIT_OR_NAMED), 27 | } 28 | } 29 | } 30 | 31 | impl FieldsNamedAppend for FieldsNamed { 32 | fn append_named(&mut self, fields_named: FieldsNamed) { 33 | self.named.extend(fields_named.named); 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use syn::{parse_quote, DeriveInput, Fields, FieldsNamed}; 40 | 41 | use super::FieldsNamedAppend; 42 | 43 | #[test] 44 | fn append_fields_named_to_fields_named() { 45 | let mut fields: FieldsNamed = parse_quote!({ a: u32, b: i32 }); 46 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 47 | let fields_expected: FieldsNamed = parse_quote!({ a: u32, b: i32, c: i64, d: usize }); 48 | 49 | fields.append_named(fields_additional); 50 | 51 | assert_eq!(fields_expected, fields); 52 | } 53 | 54 | #[test] 55 | fn append_fields_named_to_fields_unit() { 56 | let mut fields = Fields::Unit; 57 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 58 | let fields_expected: Fields = Fields::Named(parse_quote!({ c: i64, d: usize })); 59 | 60 | fields.append_named(fields_additional); 61 | 62 | assert_eq!(fields_expected, fields); 63 | } 64 | 65 | #[test] 66 | #[should_panic( 67 | expected = "Macro must be used on either a unit struct or a struct with named fields.\n\ 68 | This derive does not work on tuple structs." 69 | )] 70 | fn append_fields_named_to_fields_unnamed_panics() { 71 | let mut fields: Fields = Fields::Unnamed(parse_quote!((u32, i32))); 72 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 73 | 74 | fields.append_named(fields_additional); 75 | } 76 | 77 | #[test] 78 | fn append_fields_named_to_struct_named() { 79 | let mut ast: DeriveInput = parse_quote! { 80 | struct StructNamed { a: u32, b: i32 } 81 | }; 82 | 83 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 84 | ast.append_named(fields_additional); 85 | 86 | let ast_expected: DeriveInput = parse_quote! { 87 | struct StructNamed { a: u32, b: i32, c: i64, d: usize } 88 | }; 89 | assert_eq!(ast_expected, ast); 90 | } 91 | 92 | #[test] 93 | fn append_fields_named_to_struct_unit() { 94 | let mut ast: DeriveInput = parse_quote! { 95 | struct StructUnit; 96 | }; 97 | 98 | let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 99 | ast.append_named(fields_additional); 100 | 101 | let ast_expected: DeriveInput = parse_quote! { 102 | struct StructUnit { c: i64, d: usize } 103 | }; 104 | assert_eq!(ast_expected, ast); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/fields_unnamed_append.rs: -------------------------------------------------------------------------------- 1 | use syn::{DeriveInput, Fields, FieldsUnnamed}; 2 | 3 | use crate::DeriveInputStructExt; 4 | 5 | const ERR_MUST_BE_UNIT_OR_UNNAMED: &str = "Macro must be used on either a unit struct or tuple struct.\n\ 6 | This derive does not work on structs with named fields."; 7 | 8 | /// Indicates this type may have `FieldsUnnamed` appended to it. 9 | pub trait FieldsUnnamedAppend { 10 | /// Appends the specified `fields_unnamed` to this type. 11 | fn append_unnamed(&mut self, fields_unnamed: FieldsUnnamed); 12 | } 13 | 14 | impl FieldsUnnamedAppend for DeriveInput { 15 | fn append_unnamed(&mut self, fields_unnamed: FieldsUnnamed) { 16 | self.fields_mut().append_unnamed(fields_unnamed); 17 | } 18 | } 19 | 20 | impl FieldsUnnamedAppend for Fields { 21 | fn append_unnamed(&mut self, fields_unnamed: FieldsUnnamed) { 22 | match self { 23 | Fields::Named(_) => panic!("{}", ERR_MUST_BE_UNIT_OR_UNNAMED), 24 | Fields::Unit => *self = Fields::from(fields_unnamed), 25 | Fields::Unnamed(self_fields_unnamed) => { 26 | self_fields_unnamed.append_unnamed(fields_unnamed) 27 | } 28 | } 29 | } 30 | } 31 | 32 | impl FieldsUnnamedAppend for FieldsUnnamed { 33 | fn append_unnamed(&mut self, fields_unnamed: FieldsUnnamed) { 34 | self.unnamed.extend(fields_unnamed.unnamed); 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use syn::{parse_quote, DeriveInput, Fields, FieldsUnnamed}; 41 | 42 | use super::FieldsUnnamedAppend; 43 | 44 | #[test] 45 | fn append_fields_unnamed_to_fields_unnamed() { 46 | let mut fields: FieldsUnnamed = parse_quote!((u32, i32)); 47 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 48 | let fields_expected: FieldsUnnamed = parse_quote!((u32, i32, i64, usize)); 49 | 50 | fields.append_unnamed(fields_additional); 51 | 52 | assert_eq!(fields_expected, fields); 53 | } 54 | 55 | #[test] 56 | fn append_fields_unnamed_to_fields_unit() { 57 | let mut fields = Fields::Unit; 58 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 59 | let fields_expected: Fields = Fields::Unnamed(parse_quote!((i64, usize))); 60 | 61 | fields.append_unnamed(fields_additional); 62 | 63 | assert_eq!(fields_expected, fields); 64 | } 65 | 66 | #[test] 67 | #[should_panic( 68 | expected = "Macro must be used on either a unit struct or tuple struct.\n\ 69 | This derive does not work on structs with named fields." 70 | )] 71 | fn append_fields_unnamed_to_fields_unnamed_panics() { 72 | let mut fields: Fields = Fields::Named(parse_quote!({ a: u32, b: i32 })); 73 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 74 | 75 | fields.append_unnamed(fields_additional); 76 | } 77 | 78 | #[test] 79 | fn append_fields_unnamed_to_struct_unnamed() { 80 | let mut ast: DeriveInput = parse_quote! { 81 | struct StructUnnamed(u32, i32); 82 | }; 83 | 84 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 85 | ast.append_unnamed(fields_additional); 86 | 87 | let ast_expected: DeriveInput = parse_quote! { 88 | struct StructUnnamed(u32, i32, i64, usize); 89 | }; 90 | assert_eq!(ast_expected, ast); 91 | } 92 | 93 | #[test] 94 | fn append_fields_unnamed_to_struct_unit() { 95 | let mut ast: DeriveInput = parse_quote! { 96 | struct StructUnit; 97 | }; 98 | 99 | let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 100 | ast.append_unnamed(fields_additional); 101 | 102 | let ast_expected: DeriveInput = parse_quote! { 103 | struct StructUnit(i64, usize); 104 | }; 105 | assert_eq!(ast_expected, ast); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ident_ext.rs: -------------------------------------------------------------------------------- 1 | use quote::format_ident; 2 | use syn::Ident; 3 | 4 | /// Convenience methods on `Ident`s. 5 | pub trait IdentExt { 6 | /// Returns a new `Ident` by appending this Ident and the specified suffix. 7 | /// 8 | /// # Parameters 9 | /// 10 | /// * `suffix`: Suffix to append. 11 | fn append(&self, suffix: S) -> Ident 12 | where 13 | S: quote::IdentFragment; 14 | 15 | /// Returns a new `Ident` by prepending this Ident with the specified 16 | /// prefix. 17 | /// 18 | /// # Parameters 19 | /// 20 | /// * `prefix`: Prefix to prepend. 21 | fn prepend(&self, prefix: S) -> Ident 22 | where 23 | S: quote::IdentFragment; 24 | } 25 | 26 | impl IdentExt for Ident { 27 | fn append(&self, suffix: S) -> Ident 28 | where 29 | S: quote::IdentFragment, 30 | { 31 | format_ident!("{}{}", self, suffix) 32 | } 33 | 34 | fn prepend(&self, suffix: S) -> Ident 35 | where 36 | S: quote::IdentFragment, 37 | { 38 | format_ident!("{}{}", suffix, self) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use proc_macro2::Span; 45 | use syn::Ident; 46 | 47 | use super::IdentExt; 48 | 49 | #[test] 50 | fn append_str_returns_appended_ident() { 51 | let one = Ident::new("One", Span::call_site()); 52 | 53 | assert_eq!(Ident::new("OneTwo", Span::call_site()), one.append("Two")); 54 | } 55 | 56 | #[test] 57 | fn append_ident_returns_appended_ident() { 58 | let one = Ident::new("One", Span::call_site()); 59 | let two = Ident::new("Two", Span::call_site()); 60 | 61 | assert_eq!(Ident::new("OneTwo", Span::call_site()), one.append(two)); 62 | } 63 | 64 | #[test] 65 | fn append_ident_ref_returns_appended_ident() { 66 | let one = Ident::new("One", Span::call_site()); 67 | let two = Ident::new("Two", Span::call_site()); 68 | 69 | assert_eq!(Ident::new("OneTwo", Span::call_site()), one.append(two)); 70 | } 71 | 72 | #[test] 73 | fn prepend_str_returns_prepended_ident() { 74 | let one = Ident::new("One", Span::call_site()); 75 | 76 | assert_eq!(Ident::new("TwoOne", Span::call_site()), one.prepend("Two")); 77 | } 78 | 79 | #[test] 80 | fn prepend_ident_returns_prepended_ident() { 81 | let one = Ident::new("One", Span::call_site()); 82 | let two = Ident::new("Two", Span::call_site()); 83 | 84 | assert_eq!(Ident::new("TwoOne", Span::call_site()), one.prepend(two)); 85 | } 86 | 87 | #[test] 88 | fn prepend_ident_ref_returns_prepended_ident() { 89 | let one = Ident::new("One", Span::call_site()); 90 | let two = Ident::new("Two", Span::call_site()); 91 | 92 | assert_eq!(Ident::new("TwoOne", Span::call_site()), one.prepend(two)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![deny(missing_debug_implementations)] 3 | 4 | //! Traits and functions to make writing proc macros more ergonomic. 5 | //! 6 | //! ```toml 7 | //! proc_macro_roids = "0.8.0" 8 | //! ``` 9 | //! 10 | //! Makes writing procedural macros much easier: 11 | //! 12 | //! ```rust,edition2021 13 | //! extern crate proc_macro; 14 | //! 15 | //! use proc_macro::TokenStream; 16 | //! use proc_macro2::Span; 17 | //! use proc_macro_roids::{DeriveInputStructExt, FieldExt, IdentExt}; 18 | //! use quote::quote; 19 | //! use syn::{parse_macro_input, parse_quote, DeriveInput, Ident}; 20 | //! 21 | //! /// Derives a `Super` enum with a variant for each struct field: 22 | //! /// 23 | //! /// ```rust,edition2021 24 | //! /// use std::marker::PhantomData; 25 | //! /// use super_derive::Super; 26 | //! /// 27 | //! /// #[derive(Super)] 28 | //! /// pub struct Man { 29 | //! /// #[super_derive(skip)] 30 | //! /// name: String, 31 | //! /// power_level: u64, 32 | //! /// marker: PhantomData, 33 | //! /// } 34 | //! /// ``` 35 | //! /// 36 | //! /// Generates: 37 | //! /// 38 | //! /// ```rust,edition2021 39 | //! /// pub enum SuperMan { 40 | //! /// U64(u64), 41 | //! /// } 42 | //! /// ``` 43 | //! // #[proc_macro_derive(Super, attributes(super_derive))] 44 | //! pub fn system_desc_derive(input: TokenStream) -> TokenStream { 45 | //! let ast = parse_macro_input!(input as DeriveInput); 46 | //! let enum_name = ast.ident.prepend("Super"); 47 | //! let fields = ast.fields(); 48 | //! let relevant_fields = fields 49 | //! .iter() 50 | //! .filter(|field| !field.is_phantom_data()) 51 | //! .filter(|field| !field.contains_tag(&parse_quote!(super_derive), &parse_quote!(skip))); 52 | //! 53 | //! let variants = relevant_fields 54 | //! .map(|field| { 55 | //! let type_name = field.type_name(); 56 | //! let variant_name = type_name.to_string().to_uppercase(); 57 | //! let variant_name = Ident::new(&variant_name, Span::call_site()); 58 | //! quote! { 59 | //! #variant_name(#type_name) 60 | //! } 61 | //! }) 62 | //! .collect::>(); 63 | //! 64 | //! let token_stream2 = quote! { 65 | //! pub enum #enum_name { 66 | //! #(#variants,)* 67 | //! } 68 | //! }; 69 | //! 70 | //! token_stream2.into() 71 | //! } 72 | //! ``` 73 | //! 74 | //! # Examples 75 | //! 76 | //!
77 | //! 78 | //! 1. Append additional `#[derive(..)]`s. 79 | //! 80 | //! This works for function-like or attribute proc macros. 81 | //! 82 | //! ```rust,edition2021 83 | //! extern crate proc_macro; 84 | //! 85 | //! use proc_macro::TokenStream; 86 | //! use proc_macro_roids::DeriveInputExt; 87 | //! use quote::quote; 88 | //! use syn::{parse_macro_input, parse_quote, DeriveInput}; 89 | //! 90 | //! // #[proc_macro_attribute] 91 | //! pub fn copy(_args: TokenStream, item: TokenStream) -> TokenStream { 92 | //! // Example input: 93 | //! // 94 | //! // #[derive(Debug)] 95 | //! // struct Struct; 96 | //! let mut ast = parse_macro_input!(item as DeriveInput); 97 | //! 98 | //! // Append the derives. 99 | //! let derives = parse_quote!(Clone, Copy); 100 | //! ast.append_derives(derives); 101 | //! 102 | //! // Example output: 103 | //! // 104 | //! // #[derive(Debug, Clone, Copy)] 105 | //! // struct Struct; 106 | //! TokenStream::from(quote! { #ast }) 107 | //! } 108 | //! ``` 109 | //! 110 | //!
111 | //! 112 | //!
113 | //! 114 | //! 2. Append named fields. 115 | //! 116 | //! This works for structs with named fields or unit structs. 117 | //! 118 | //! ```rust,edition2021 119 | //! extern crate proc_macro; 120 | //! 121 | //! use proc_macro::TokenStream; 122 | //! use proc_macro_roids::FieldsNamedAppend; 123 | //! use quote::quote; 124 | //! use syn::{parse_macro_input, parse_quote, DeriveInput, FieldsNamed}; 125 | //! 126 | //! /// Example usage: 127 | //! /// 128 | //! /// ```rust 129 | //! /// use macro_crate::append_cd; 130 | //! /// 131 | //! /// #[append_cd] 132 | //! /// struct StructNamed { a: u32, b: i32 } 133 | //! /// ``` 134 | //! // #[proc_macro_attribute] 135 | //! pub fn append_cd(_args: TokenStream, item: TokenStream) -> TokenStream { 136 | //! // Example input: 137 | //! // 138 | //! // struct StructNamed { a: u32, b: i32 } 139 | //! let mut ast = parse_macro_input!(item as DeriveInput); 140 | //! 141 | //! // Append the fields. 142 | //! let fields_additional: FieldsNamed = parse_quote!({ c: i64, d: usize }); 143 | //! ast.append_named(fields_additional); 144 | //! 145 | //! // Example output: 146 | //! // 147 | //! // struct StructNamed { a: u32, b: i32, c: i64, d: usize } 148 | //! TokenStream::from(quote! { #ast }) 149 | //! } 150 | //! ``` 151 | //! 152 | //!
153 | //! 154 | //!
155 | //! 156 | //! 3. Append unnamed fields (tuples). 157 | //! 158 | //! This works for structs with unnamed fields or unit structs. 159 | //! 160 | //! ```rust,edition2021 161 | //! extern crate proc_macro; 162 | //! 163 | //! use proc_macro::TokenStream; 164 | //! use proc_macro_roids::FieldsUnnamedAppend; 165 | //! use quote::quote; 166 | //! use syn::{parse_macro_input, parse_quote, DeriveInput, FieldsUnnamed}; 167 | //! 168 | //! /// Example usage: 169 | //! /// 170 | //! /// ```rust 171 | //! /// use macro_crate::append_i64_usize; 172 | //! /// 173 | //! /// #[append_i64_usize] 174 | //! /// struct StructUnit; 175 | //! /// ``` 176 | //! // #[proc_macro_attribute] 177 | //! pub fn append_i64_usize(_args: TokenStream, item: TokenStream) -> TokenStream { 178 | //! // Example input: 179 | //! // 180 | //! // struct StructUnit; 181 | //! let mut ast = parse_macro_input!(item as DeriveInput); 182 | //! 183 | //! // Append the fields. 184 | //! let fields_additional: FieldsUnnamed = parse_quote!((i64, usize)); 185 | //! ast.append_unnamed(fields_additional); 186 | //! 187 | //! // Example output: 188 | //! // 189 | //! // struct StructUnit(i64, usize); 190 | //! TokenStream::from(quote! { #ast }) 191 | //! } 192 | //! ``` 193 | //! 194 | //!
195 | //! 196 | //!
197 | //! 198 | //! 4. Get newtype inner `Field`. 199 | //! 200 | //! This works for structs with unnamed fields or unit structs. 201 | //! 202 | //! ```rust,edition2021 203 | //! extern crate proc_macro; 204 | //! 205 | //! use proc_macro::TokenStream; 206 | //! use proc_macro_roids::DeriveInputNewtypeExt; 207 | //! use quote::quote; 208 | //! use syn::{parse_macro_input, parse_quote, DeriveInput, Type}; 209 | //! 210 | //! // #[proc_macro_derive(Deref)] 211 | //! pub fn derive_deref(item: TokenStream) -> TokenStream { 212 | //! // Example input: 213 | //! // 214 | //! // #[derive(Deref)] 215 | //! // struct Newtype(u32); 216 | //! let mut ast = parse_macro_input!(item as DeriveInput); 217 | //! 218 | //! // Get the inner field type. 219 | //! let inner_type = ast.inner_type(); 220 | //! 221 | //! // Implement `Deref` 222 | //! let type_name = &ast.ident; 223 | //! let token_stream_2 = quote! { 224 | //! #ast 225 | //! 226 | //! impl std::ops::Deref for #type_name { 227 | //! type Target = #inner_type; 228 | //! fn deref(&self) -> &Self::Target { 229 | //! &self.0 230 | //! } 231 | //! } 232 | //! }; 233 | //! TokenStream::from(token_stream_2) 234 | //! } 235 | //! ``` 236 | //! 237 | //!
238 | //! 239 | //! 240 | //!
241 | //! 242 | //! 5. `Ident` concatenation. 243 | //! 244 | //! ```rust,edition2021 245 | //! use proc_macro2::Span; 246 | //! use proc_macro_roids::IdentExt; 247 | //! use syn::Ident; 248 | //! 249 | //! # fn main() { 250 | //! let one = Ident::new("One", Span::call_site()); 251 | //! assert_eq!( 252 | //! Ident::new("OneSuffix", Span::call_site()), 253 | //! one.append("Suffix") 254 | //! ); 255 | //! assert_eq!( 256 | //! Ident::new("PrefixOne", Span::call_site()), 257 | //! one.prepend("Prefix") 258 | //! ); 259 | //! 260 | //! let two = Ident::new("Two", Span::call_site()); 261 | //! assert_eq!(Ident::new("OneTwo", Span::call_site()), one.append(&two)); 262 | //! assert_eq!(Ident::new("TwoOne", Span::call_site()), one.prepend(&two)); 263 | //! # } 264 | //! ``` 265 | //! 266 | //!
267 | //! 268 | //!
269 | //! 270 | //! 6. Accessing struct fields. 271 | //! 272 | //! ```rust,edition2021 273 | //! use proc_macro_roids::DeriveInputStructExt; 274 | //! use syn::{parse_quote, DeriveInput, Fields}; 275 | //! 276 | //! # fn main() { 277 | //! let ast: DeriveInput = parse_quote! { 278 | //! struct Named {} 279 | //! }; 280 | //! 281 | //! if let Fields::Named(..) = ast.fields() { 282 | //! // do something 283 | //! } 284 | //! # } 285 | //! ``` 286 | //! 287 | //!
288 | //! 289 | //!
290 | //! 291 | //! 7. Inspecting `Field`s. 292 | //! 293 | //! ```rust,edition2021 294 | //! use proc_macro2::Span; 295 | //! use proc_macro_roids::FieldExt; 296 | //! use syn::{parse_quote, Expr, ExprLit, Fields, FieldsNamed, Lit, LitStr, Meta, MetaNameValue}; 297 | //! 298 | //! let fields_named: FieldsNamed = parse_quote! {{ 299 | //! #[my::derive(tag::name(param = "value"))] 300 | //! pub name: PhantomData, 301 | //! }}; 302 | //! let fields = Fields::from(fields_named); 303 | //! let field = fields.iter().next().expect("Expected field to exist."); 304 | //! 305 | //! assert_eq!(field.type_name(), "PhantomData"); 306 | //! assert!(field.is_phantom_data()); 307 | //! assert!(field.contains_tag(&parse_quote!(my::derive), &parse_quote!(tag::name))); 308 | //! assert_eq!( 309 | //! field 310 | //! .tag_parameter(&parse_quote!(my::derive), &parse_quote!(tag::name),) 311 | //! .expect("Expected parameter to exist."), 312 | //! Meta::NameValue(MetaNameValue { 313 | //! path: parse_quote!(param), 314 | //! eq_token: Default::default(), 315 | //! value: Expr::Lit(ExprLit { 316 | //! attrs: Vec::new(), 317 | //! lit: Lit::Str(LitStr::new("value", Span::call_site())), 318 | //! }), 319 | //! }), 320 | //! ); 321 | //! ``` 322 | //! 323 | //!
324 | //! 325 | //!
326 | //! 327 | //! 8. (De)constructing `Fields`. 328 | //! 329 | //! ```rust,edition2021 330 | //! # use std::str::FromStr; 331 | //! # 332 | //! use proc_macro_roids::{DeriveInputStructExt, FieldsExt}; 333 | //! # use proc_macro2::{Span, TokenStream}; 334 | //! # use syn::{parse_quote, DeriveInput}; 335 | //! # use quote::quote; 336 | //! # 337 | //! // Need to generate code that instantiates `MyEnum::Struct`: 338 | //! // enum MyEnum { 339 | //! // Struct { 340 | //! // field_0: u32, 341 | //! // field_1: u32, 342 | //! // } 343 | //! // } 344 | //! 345 | //! let ast: DeriveInput = parse_quote! { 346 | //! struct Struct { 347 | //! field_0: u32, 348 | //! field_1: u32, 349 | //! } 350 | //! }; 351 | //! let fields = ast.fields(); 352 | //! let construction_form = fields.construction_form(); 353 | //! let tokens = quote! { MyEnum::Struct #construction_form }; 354 | //! 355 | //! let expected = TokenStream::from_str("MyEnum::Struct { field_0, field_1, }").unwrap(); 356 | //! assert_eq!(expected.to_string(), tokens.to_string()); 357 | //! ``` 358 | //! 359 | //!
360 | //! 361 | //! --- 362 | //! 363 | //! **Note:** The *roids* name is chosen because, although these functions make 364 | //! it easy to perform certain operations, they may not always be good ideas =D! 365 | 366 | #[cfg(test)] 367 | extern crate proc_macro; 368 | 369 | pub use crate::{ 370 | derive_input_ext::DeriveInputExt, 371 | derive_input_newtype_ext::DeriveInputNewtypeExt, 372 | derive_input_struct_ext::DeriveInputStructExt, 373 | field_ext::FieldExt, 374 | fields_ext::FieldsExt, 375 | fields_named_append::FieldsNamedAppend, 376 | fields_unnamed_append::FieldsUnnamedAppend, 377 | ident_ext::IdentExt, 378 | util::{ 379 | contains_namespace, contains_tag, format_path, namespace_nested_metas_iter, 380 | namespace_parameter, namespace_parameters, tag_nested_metas_iter, tag_parameter, 381 | tag_parameters, 382 | }, 383 | }; 384 | 385 | mod derive_input_ext; 386 | mod derive_input_newtype_ext; 387 | mod derive_input_struct_ext; 388 | mod field_ext; 389 | mod fields_ext; 390 | mod fields_named_append; 391 | mod fields_unnamed_append; 392 | mod ident_ext; 393 | mod util; 394 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | use syn::{punctuated::Punctuated, Attribute, Meta, Path, Token}; 3 | 4 | /// Returns whether an item's attributes contains a given `#[namespace]` 5 | /// attribute. 6 | /// 7 | /// # Parameters 8 | /// 9 | /// * `attrs`: The attributes on the item. 10 | /// * `namespace`: The `path()` of the first-level attribute. 11 | pub fn contains_namespace(attrs: &[Attribute], namespace: &Path) -> bool { 12 | attrs.iter().any(|attr| attr.path() == namespace) 13 | } 14 | 15 | /// Returns whether an item's attributes contains a given `#[namespace(tag)]` 16 | /// attribute. 17 | /// 18 | /// # Parameters 19 | /// 20 | /// * `attrs`: The attributes on the item. 21 | /// * `namespace`: The `path()` of the first-level attribute. 22 | /// * `tag`: The `path()` of the second-level attribute. 23 | pub fn contains_tag(attrs: &[Attribute], namespace: &Path, tag: &Path) -> bool { 24 | attrs 25 | .iter() 26 | .filter(|attr| attr.path() == namespace) 27 | .any(|attr| { 28 | attr.parse_args_with(Punctuated::::parse_terminated) 29 | .map(|tags| tags.iter().any(|tag_meta| tag_meta.path() == tag)) 30 | .unwrap_or(false) 31 | }) 32 | } 33 | 34 | /// Returns the parameter from `#[namespace(parameter)]`. 35 | /// 36 | /// # Parameters 37 | /// 38 | /// * `attrs`: Attributes of the item to inspect. 39 | /// * `namespace`: The `path()` of the first-level attribute. 40 | /// 41 | /// # Examples 42 | /// 43 | /// ```rust,edition2021 44 | /// use proc_macro_roids::namespace_parameter; 45 | /// use syn::{parse_quote, DeriveInput, Meta, Path}; 46 | /// 47 | /// let ast: DeriveInput = parse_quote! { 48 | /// #[namespace(One)] 49 | /// pub struct MyEnum; 50 | /// }; 51 | /// 52 | /// let ns: Path = parse_quote!(namespace); 53 | /// let namespace_param = namespace_parameter(&ast.attrs, &ns); 54 | /// 55 | /// let meta_one: Path = parse_quote!(One); 56 | /// let param_one = Meta::Path(meta_one); 57 | /// assert_eq!(Some(param_one), namespace_param); 58 | /// 59 | /// let ns_other: Path = parse_quote!(namespace_other); 60 | /// let namespace_param_other = namespace_parameter(&ast.attrs, &ns_other); 61 | /// assert_eq!(None, namespace_param_other); 62 | /// ``` 63 | /// 64 | /// # Panics 65 | /// 66 | /// Panics if the number of parameters for the tag is not exactly one. 67 | #[allow(clippy::let_and_return)] // Needed due to bug in clippy. 68 | pub fn namespace_parameter(attrs: &[Attribute], namespace: &Path) -> Option { 69 | let mut namespace_nested_metas_iter = namespace_nested_metas_iter(attrs, namespace); 70 | let namespace_parameter = namespace_nested_metas_iter.next(); 71 | let namespace_parameter_second = namespace_nested_metas_iter.next(); 72 | 73 | if namespace_parameter_second.is_some() { 74 | panic!( 75 | "Expected exactly one parameter for `#[{}(..)]`.", 76 | format_path(namespace), 77 | ); 78 | } 79 | 80 | namespace_parameter 81 | } 82 | 83 | /// Returns the parameters from `#[namespace(param1, param2, ..)]`. 84 | /// 85 | /// # Parameters 86 | /// 87 | /// * `attrs`: Attributes of the item to inspect. 88 | /// * `namespace`: The `path()` of the first-level attribute. 89 | /// 90 | /// # Examples 91 | /// 92 | /// ```rust,edition2021 93 | /// use proc_macro_roids::namespace_parameters; 94 | /// use syn::{parse_quote, DeriveInput, Lit, LitStr, Meta, MetaNameValue, Path}; 95 | /// 96 | /// let ast: DeriveInput = parse_quote! { 97 | /// #[namespace(One, two = "")] 98 | /// #[namespace(three(Value))] 99 | /// pub struct MyEnum; 100 | /// }; 101 | /// 102 | /// let ns: Path = parse_quote!(namespace); 103 | /// let namespace_parameters = namespace_parameters(&ast.attrs, &ns); 104 | /// 105 | /// let meta_one: Path = parse_quote!(One); 106 | /// let param_one = Meta::Path(meta_one); 107 | /// let meta_two: MetaNameValue = parse_quote!(two = ""); 108 | /// let param_two = Meta::NameValue(meta_two); 109 | /// let meta_three: LitStr = parse_quote!("three"); 110 | /// let param_three = Meta::List(parse_quote!(three(Value))); 111 | /// assert_eq!( 112 | /// vec![param_one, param_two, param_three], 113 | /// namespace_parameters 114 | /// ); 115 | /// ``` 116 | pub fn namespace_parameters(attrs: &[Attribute], namespace: &Path) -> Vec { 117 | let namespace_nested_metas_iter = namespace_nested_metas_iter(attrs, namespace); 118 | 119 | namespace_nested_metas_iter.collect::>() 120 | } 121 | 122 | /// Returns the parameter from `#[namespace(tag(parameter))]`. 123 | /// 124 | /// # Parameters 125 | /// 126 | /// * `attrs`: Attributes of the item to inspect. 127 | /// * `namespace`: The `path()` of the first-level attribute. 128 | /// * `tag`: The `path()` of the second-level attribute. 129 | /// 130 | /// # Examples 131 | /// 132 | /// ```rust,edition2021 133 | /// use proc_macro_roids::tag_parameter; 134 | /// use syn::{parse_quote, DeriveInput, Meta, Path}; 135 | /// 136 | /// let ast: DeriveInput = parse_quote! { 137 | /// #[namespace(tag(One))] 138 | /// pub struct MyEnum; 139 | /// }; 140 | /// 141 | /// let ns: Path = parse_quote!(namespace); 142 | /// let tag: Path = parse_quote!(tag); 143 | /// let tag_param = tag_parameter(&ast.attrs, &ns, &tag); 144 | /// 145 | /// let meta_one: Path = parse_quote!(One); 146 | /// let param_one = Meta::Path(meta_one); 147 | /// assert_eq!(Some(param_one), tag_param); 148 | /// 149 | /// let tag_other: Path = parse_quote!(tag_other); 150 | /// let tag_param_other = tag_parameter(&ast.attrs, &ns, &tag_other); 151 | /// assert_eq!(None, tag_param_other); 152 | /// ``` 153 | /// 154 | /// # Panics 155 | /// 156 | /// Panics if the number of parameters for the tag is not exactly one. 157 | #[allow(clippy::let_and_return)] // Needed due to bug in clippy. 158 | pub fn tag_parameter(attrs: &[Attribute], namespace: &Path, tag: &Path) -> Option { 159 | let namespace_nested_metas_iter = namespace_nested_metas_iter(attrs, namespace); 160 | let mut tag_nested_metas_iter = tag_nested_metas_iter(namespace_nested_metas_iter, tag); 161 | let tag_param = tag_nested_metas_iter.next(); 162 | let tag_param_second = tag_nested_metas_iter.next(); 163 | 164 | if tag_param_second.is_some() { 165 | panic!( 166 | "Expected exactly one parameter for `#[{}({}(..))]`.", 167 | format_path(namespace), 168 | format_path(tag), 169 | ); 170 | } 171 | 172 | tag_param 173 | } 174 | 175 | /// Returns the parameters from `#[namespace(tag(param1, param2, ..))]`. 176 | /// 177 | /// # Parameters 178 | /// 179 | /// * `attrs`: Attributes of the item to inspect. 180 | /// * `namespace`: The `path()` of the first-level attribute. 181 | /// * `tag`: The `path()` of the second-level attribute. 182 | /// 183 | /// # Examples 184 | /// 185 | /// ```rust,edition2021 186 | /// use proc_macro_roids::tag_parameters; 187 | /// use syn::{parse_quote, DeriveInput, Meta, MetaNameValue, Path}; 188 | /// 189 | /// let ast: DeriveInput = parse_quote! { 190 | /// #[namespace(tag(One))] 191 | /// #[namespace(tag(two = ""))] 192 | /// pub struct MyEnum; 193 | /// }; 194 | /// 195 | /// let ns: Path = parse_quote!(namespace); 196 | /// let tag: Path = parse_quote!(tag); 197 | /// let tag_parameters = tag_parameters(&ast.attrs, &ns, &tag); 198 | /// 199 | /// let meta_one: Path = parse_quote!(One); 200 | /// let param_one = Meta::Path(meta_one); 201 | /// let meta_two: MetaNameValue = parse_quote!(two = ""); 202 | /// let param_two = Meta::NameValue(meta_two); 203 | /// assert_eq!(vec![param_one, param_two], tag_parameters); 204 | /// ``` 205 | pub fn tag_parameters(attrs: &[Attribute], namespace: &Path, tag: &Path) -> Vec { 206 | let namespace_nested_metas_iter = namespace_nested_metas_iter(attrs, namespace); 207 | let parameters = tag_nested_metas_iter(namespace_nested_metas_iter, tag).collect::>(); 208 | 209 | parameters 210 | } 211 | 212 | /// Returns the meta lists of the form: `#[namespace(..)]`. 213 | /// 214 | /// Each `meta_list` is a `namespace(..)` meta item. 215 | /// 216 | /// # Parameters 217 | /// 218 | /// * `attrs`: Attributes of the item to inspect. 219 | /// * `namespace`: The `path()` of the first-level attribute. 220 | /// 221 | /// # Examples 222 | /// 223 | /// ```rust,edition2021 224 | /// use proc_macro_roids::namespace_nested_metas_iter; 225 | /// use syn::{parse_quote, DeriveInput, Meta, Path}; 226 | /// 227 | /// let ast: DeriveInput = parse_quote! { 228 | /// #[namespace(One)] 229 | /// #[namespace(two = "")] 230 | /// pub struct MyEnum; 231 | /// }; 232 | /// 233 | /// let ns: Path = parse_quote!(namespace); 234 | /// let nested_metas = namespace_nested_metas_iter(&ast.attrs, &ns).collect::>(); 235 | /// 236 | /// let meta_one: Meta = Meta::Path(parse_quote!(One)); 237 | /// let meta_two: Meta = Meta::NameValue(parse_quote!(two = "")); 238 | /// assert_eq!(vec![meta_one, meta_two], nested_metas); 239 | /// ``` 240 | pub fn namespace_nested_metas_iter<'f>( 241 | attrs: &'f [Attribute], 242 | namespace: &'f Path, 243 | ) -> impl Iterator + 'f { 244 | attrs 245 | .iter() 246 | .filter_map(move |attr| { 247 | if attr.path() == namespace { 248 | attr.parse_args_with(Punctuated::::parse_terminated) 249 | .ok() 250 | } else { 251 | None 252 | } 253 | }) 254 | .flat_map(|nested_metas| nested_metas.into_iter()) 255 | } 256 | 257 | /// Returns an iterator over nested metas from `#[namespace(tag(..))]`. 258 | /// 259 | /// # Parameters 260 | /// 261 | /// * `namespace_nested_metas_iter`: The `#[namespace(..)]` meta lists. 262 | /// * `tag`: The `path()` of the second-level attribute. 263 | /// 264 | /// # Examples 265 | /// 266 | /// ```rust,edition2021 267 | /// use proc_macro_roids::{namespace_nested_metas_iter, tag_nested_metas_iter}; 268 | /// use syn::{parse_quote, DeriveInput, Meta, Path}; 269 | /// 270 | /// let ast: DeriveInput = parse_quote! { 271 | /// #[namespace(tag(One))] 272 | /// #[namespace(tag(two = ""))] 273 | /// pub struct MyEnum; 274 | /// }; 275 | /// 276 | /// let ns: Path = parse_quote!(namespace); 277 | /// let tag: Path = parse_quote!(tag); 278 | /// let ns_lists = namespace_nested_metas_iter(&ast.attrs, &ns); 279 | /// let nested_metas = tag_nested_metas_iter(ns_lists, &tag).collect::>(); 280 | /// 281 | /// let meta_one: Meta = Meta::Path(parse_quote!(One)); 282 | /// let meta_two: Meta = Meta::NameValue(parse_quote!(two = "")); 283 | /// assert_eq!(vec![meta_one, meta_two], nested_metas); 284 | pub fn tag_nested_metas_iter<'f>( 285 | namespace_nested_metas_iter: impl Iterator + 'f, 286 | tag: &'f Path, 287 | ) -> impl Iterator + 'f { 288 | namespace_nested_metas_iter 289 | .filter_map(move |meta| { 290 | if meta.path() == tag { 291 | meta.require_list() 292 | .and_then(|meta_list| { 293 | meta_list.parse_args_with(Punctuated::::parse_terminated) 294 | }) 295 | .ok() 296 | } else { 297 | None 298 | } 299 | }) 300 | .flatten() 301 | } 302 | 303 | /// Returns a `Path` as a String without whitespace between tokens. 304 | pub fn format_path(path: &Path) -> String { 305 | quote!(#path) 306 | .to_string() 307 | .chars() 308 | .filter(|c| !c.is_whitespace()) 309 | .collect::() 310 | } 311 | --------------------------------------------------------------------------------