From 8b6e102a3f001b81872659c2df1f3ddddc6259c6 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Tue, 21 Nov 2023 13:51:54 +0000 Subject: Import displaydoc crate. Request Document: go/android-rust-importing-crates For CL Reviewers: go/android3p#cl-review For Build Team: go/ab-third-party-imports Bug: 312432303 Test: Treehugger Change-Id: I77f25d4c9c365b618d24e5b0c041e7d857ff6435 --- .cargo_vcs_info.json | 6 + .github/workflows/ci.yml | 120 ++++++++++ .gitignore | 3 + Android.bp | 23 ++ CHANGELOG.md | 50 ++++ Cargo.toml | 114 +++++++++ Cargo.toml.orig | 80 +++++++ LICENSE | 1 + LICENSE-APACHE | 201 ++++++++++++++++ LICENSE-MIT | 23 ++ METADATA | 20 ++ MODULE_LICENSE_APACHE2 | 0 OWNERS | 1 + README.md | 115 +++++++++ README.tpl | 23 ++ cargo_embargo.json | 3 + examples/simple.rs | 36 +++ src/attr.rs | 137 +++++++++++ src/expand.rs | 410 ++++++++++++++++++++++++++++++++ src/fmt.rs | 159 +++++++++++++ src/lib.rs | 187 +++++++++++++++ tests/compile_tests.rs | 29 +++ tests/happy.rs | 152 ++++++++++++ tests/no_std/enum_prefix.rs | 36 +++ tests/no_std/enum_prefix_missing.rs | 35 +++ tests/no_std/enum_prefix_missing.stderr | 22 ++ tests/no_std/multi_line.rs | 37 +++ tests/no_std/multi_line.stderr | 22 ++ tests/no_std/multi_line_allow.rs | 38 +++ tests/no_std/with.rs | 32 +++ tests/no_std/without.rs | 28 +++ tests/no_std/without.stderr | 22 ++ tests/num_in_field.rs | 22 ++ tests/std/enum_prefix.rs | 36 +++ tests/std/enum_prefix_missing.rs | 35 +++ tests/std/enum_prefix_missing.stderr | 22 ++ tests/std/multi_line.rs | 1 + tests/std/multi_line.stderr | 22 ++ tests/std/multi_line_allow.rs | 38 +++ tests/std/multiple.rs | 38 +++ tests/std/without.rs | 1 + tests/std/without.stderr | 22 ++ tests/variantless.rs | 6 + update-readme.sh | 5 + 44 files changed, 2413 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Android.bp create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 120000 LICENSE create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 METADATA create mode 100644 MODULE_LICENSE_APACHE2 create mode 100644 OWNERS create mode 100644 README.md create mode 100644 README.tpl create mode 100644 cargo_embargo.json create mode 100644 examples/simple.rs create mode 100644 src/attr.rs create mode 100644 src/expand.rs create mode 100644 src/fmt.rs create mode 100644 src/lib.rs create mode 100644 tests/compile_tests.rs create mode 100644 tests/happy.rs create mode 100644 tests/no_std/enum_prefix.rs create mode 100644 tests/no_std/enum_prefix_missing.rs create mode 100644 tests/no_std/enum_prefix_missing.stderr create mode 100644 tests/no_std/multi_line.rs create mode 100644 tests/no_std/multi_line.stderr create mode 100644 tests/no_std/multi_line_allow.rs create mode 100644 tests/no_std/with.rs create mode 100644 tests/no_std/without.rs create mode 100644 tests/no_std/without.stderr create mode 100644 tests/num_in_field.rs create mode 100644 tests/std/enum_prefix.rs create mode 100644 tests/std/enum_prefix_missing.rs create mode 100644 tests/std/enum_prefix_missing.stderr create mode 100644 tests/std/multi_line.rs create mode 100644 tests/std/multi_line.stderr create mode 100644 tests/std/multi_line_allow.rs create mode 100644 tests/std/multiple.rs create mode 100644 tests/std/without.rs create mode 100644 tests/std/without.stderr create mode 100644 tests/variantless.rs create mode 100644 update-readme.sh diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..de41e72 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "f0b62a55ec2495b1a60b18f1d93f8b27e53123a7" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7bf4d5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +name: Continuous integration + +jobs: + check: + name: Check + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - 1.56.0 + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + test: + name: Test Suite + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - nightly + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - uses: Swatinem/rust-cache@v1 + - name: Install cargo-nextest + uses: baptiste0928/cargo-install@v1 + with: + crate: cargo-nextest + version: 0.9 + - uses: actions-rs/cargo@v1 + with: + command: nextest + args: run + - uses: actions-rs/cargo@v1 + with: + command: nextest + args: run --no-default-features + - uses: actions-rs/cargo@v1 + with: + command: test + args: --doc + + test-msrv: + name: msrv Test Suite + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.56.0 + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - uses: Swatinem/rust-cache@v1 + - uses: actions-rs/cargo@v1 + with: + command: test + - uses: actions-rs/cargo@v1 + with: + command: test + args: --no-default-features + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - 1.56.0 + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2411493 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..e4c7c8a --- /dev/null +++ b/Android.bp @@ -0,0 +1,23 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +// TODO: Add license. +rust_proc_macro { + name: "libdisplaydoc", + crate_name: "displaydoc", + cargo_env_compat: true, + cargo_pkg_version: "0.2.4", + srcs: ["src/lib.rs"], + edition: "2018", + features: [ + "default", + "std", + ], + rustlibs: [ + "libproc_macro2", + "libquote", + "libsyn", + ], + product_available: true, + vendor_available: true, +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4513b4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [Unreleased] - ReleaseDate + +# [0.2.4] - 2022-05-02 + +## Added +- Updated `syn` dependency to 2.0 +- Support for empty enums +- Implicitly require fmt::Display on all type parameters unless overridden + +## Changed +- Bumped MSRV to 1.56 + +# [0.2.3] - 2021-07-16 +## Added +- Added `#[displaydoc("..")]` attribute for overriding a doc comment + +# [0.2.2] - 2021-07-01 +## Added +- Added prefix feature to use the doc comment from an enum and prepend it + before the error message from each variant. + +# [0.2.1] - 2021-03-26 +## Added +- Added opt in support for ignoring extra doc attributes + +# [0.2.0] - 2021-03-16 +## Changed + +- (BREAKING) disallow multiple `doc` attributes in display impl + [https://github.com/yaahc/displaydoc/pull/22]. Allowing and ignoring extra + doc attributes made it too easy to accidentally create a broken display + implementation with missing context without realizing it, this change turns + that into a hard error and directs users towards block comments if multiple + lines are needed. + + +[Unreleased]: https://github.com/yaahc/displaydoc/compare/v0.2.4...HEAD +[0.2.4]: https://github.com/yaahc/displaydoc/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/yaahc/displaydoc/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/yaahc/displaydoc/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/yaahc/displaydoc/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/yaahc/displaydoc/releases/tag/v0.2.0 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1affcca --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,114 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "displaydoc" +version = "0.2.4" +authors = ["Jane Lusby "] +description = """ +A derive macro for implementing the display Trait via a doc comment and string interpolation +""" +homepage = "https://github.com/yaahc/displaydoc" +documentation = "https://docs.rs/displaydoc" +readme = "README.md" +keywords = [ + "display", + "derive", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/yaahc/displaydoc" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.release] +no-dev-version = true +pre-release-hook = ["./update-readme.sh"] + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace = "{{version}}" + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = '#!\[doc\(html_root_url.*' +replace = "#![doc(html_root_url = \"https://docs.rs/{{crate_name}}/{{version}}\")]" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace = "{{date}}" + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "" +replace = """ + + +# [Unreleased] - ReleaseDate""" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = '\.\.\.HEAD' +replace = "...{{tag_name}}" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "" +replace = """ + +[Unreleased]: https://github.com/yaahc/{{crate_name}}/compare/{{tag_name}}...HEAD""" +exactly = 1 + +[lib] +path = "src/lib.rs" +proc-macro = true + +[dependencies.proc-macro2] +version = "1.0" + +[dependencies.quote] +version = "1.0" + +[dependencies.syn] +version = "2.0" + +[dev-dependencies.libc] +version = "0.2" +default-features = false + +[dev-dependencies.pretty_assertions] +version = "0.6.1" + +[dev-dependencies.rustversion] +version = "1.0.0" + +[dev-dependencies.static_assertions] +version = "1.1" + +[dev-dependencies.thiserror] +version = "1.0.24" + +[dev-dependencies.trybuild] +version = "1.0" + +[features] +default = ["std"] +std = [] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..5528713 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,80 @@ +[package] +name = "displaydoc" +version = "0.2.4" +authors = ["Jane Lusby "] +edition = "2018" +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/yaahc/displaydoc" +homepage = "https://github.com/yaahc/displaydoc" +documentation = "https://docs.rs/displaydoc" +keywords = ["display", "derive"] +description = """ +A derive macro for implementing the display Trait via a doc comment and string interpolation +""" + +[lib] +proc-macro = true +path = "src/lib.rs" + +[features] +default = ["std"] +std = [] + +[dependencies] +syn = "2.0" +quote = "1.0" +proc-macro2 = "1.0" + +[dev-dependencies] +trybuild = "1.0" +static_assertions = "1.1" +libc = { version = "0.2", default-features = false } +rustversion = "1.0.0" +pretty_assertions = "0.6.1" +thiserror = "1.0.24" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.release] +no-dev-version = true +pre-release-hook = ["./update-readme.sh"] + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace="{{version}}" + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = "#!\\[doc\\(html_root_url.*" +replace = "#![doc(html_root_url = \"https://docs.rs/{{crate_name}}/{{version}}\")]" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace="{{date}}" + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n\n# [Unreleased] - ReleaseDate" +exactly=1 + +# Disable this replacement on the very first release +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "\\.\\.\\.HEAD" +replace="...{{tag_name}}" +exactly = 1 +# END SECTION, do not comment out the replacement below this, and do not reorder them + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n[Unreleased]: https://github.com/yaahc/{{crate_name}}/compare/{{tag_name}}...HEAD" +exactly=1 + diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..6b579aa --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +LICENSE-APACHE \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f47c941 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..458723b --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +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: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +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. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..7fda2bb --- /dev/null +++ b/METADATA @@ -0,0 +1,20 @@ +name: "displaydoc" +description: "()" +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/displaydoc" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/displaydoc/displaydoc-0.2.4.crate" + } + version: "0.2.4" + # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same. + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 11 + day: 21 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..5a2b844 --- /dev/null +++ b/OWNERS @@ -0,0 +1 @@ +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8fa091 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +derive(Display) /// `From` +=============== + +[![Latest Version](https://img.shields.io/crates/v/displaydoc.svg)](https://crates.io/crates/displaydoc) +[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/displaydoc) + +This library provides a convenient derive macro for the standard library's +[`core::fmt::Display`] trait. + +[`core::fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html + +```toml +[dependencies] +displaydoc = "0.2" +``` + +*Compiler support: requires rustc 1.56+* + +
+ +### Example + +*Demonstration alongside the [`Error`][std::error::Error] derive macro from [`thiserror`](https://docs.rs/thiserror/1.0.25/thiserror/index.html), +to propagate source locations from [`io::Error`][std::io::Error] with the `#[source]` attribute:* +```rust +use std::io; +use displaydoc::Display; +use thiserror::Error; + +#[derive(Display, Error, Debug)] +pub enum DataStoreError { + /// data store disconnected + Disconnect(#[source] io::Error), + /// the data for key `{0}` is not available + Redaction(String), + /// invalid header (expected {expected:?}, found {found:?}) + InvalidHeader { + expected: String, + found: String, + }, + /// unknown data store error + Unknown, +} + +let error = DataStoreError::Redaction("CLASSIFIED CONTENT".to_string()); +assert!("the data for key `CLASSIFIED CONTENT` is not available" == &format!("{}", error)); +``` +*Note that although [`io::Error`][std::io::Error] implements `Display`, we do not add it to the +generated message for `DataStoreError::Disconnect`, since it is already made available via +`#[source]`. See further context on avoiding duplication in error reports at the rust blog +[here](https://github.com/yaahc/blog.rust-lang.org/blob/master/posts/inside-rust/2021-05-15-What-the-error-handling-project-group-is-working-towards.md#duplicate-information-issue).* + +
+ +### Details + +- A `fmt::Display` impl is generated for your enum if you provide + a docstring comment on each variant as shown above in the example. The + `Display` derive macro supports a shorthand for interpolating fields from + the error: + - `/// {var}` ⟶ `write!("{}", self.var)` + - `/// {0}` ⟶ `write!("{}", self.0)` + - `/// {var:?}` ⟶ `write!("{:?}", self.var)` + - `/// {0:?}` ⟶ `write!("{:?}", self.0)` +- This also works with structs and [generic types][crate::Display#generic-type-parameters]: +```rust +/// oh no, an error: {0} +#[derive(Display)] +pub struct Error(pub E); + +let error: Error<&str> = Error("muahaha i am an error"); +assert!("oh no, an error: muahaha i am an error" == &format!("{}", error)); +``` + +- Two optional attributes can be added to your types next to the derive: + + - `#[ignore_extra_doc_attributes]` makes the macro ignore any doc + comment attributes (or `///` lines) after the first. Multi-line + comments using `///` are otherwise treated as an error, so use this + attribute or consider switching to block doc comments (`/** */`). + + - `#[prefix_enum_doc_attributes]` combines the doc comment message on + your enum itself with the messages for each variant, in the format + “enum: variant”. When added to an enum, the doc comment on the enum + becomes mandatory. When added to any other type, it has no effect. + +- In case you want to have an independent doc comment, the + `#[displaydoc("...")` atrribute may be used on the variant or struct to + override it. + +
+ +### FAQ + +1. **Is this crate `no_std` compatible?** + * Yes! This crate implements the [`core::fmt::Display`] trait, not the [`std::fmt::Display`] trait, so it should work in `std` and `no_std` environments. Just add `default-features = false`. + +2. **Does this crate work with `Path` and `PathBuf` via the `Display` trait?** + * Yuuup. This crate uses @dtolnay's [autoref specialization technique](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) to add a special trait for types to get the display impl. It then specializes for `Path` and `PathBuf`, and when either of these types are found, it calls `self.display()` to get a `std::path::Display<'_>` type which can be used with the `Display` format specifier! + + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..b131fb8 --- /dev/null +++ b/README.tpl @@ -0,0 +1,23 @@ +derive(Display) /// `From` +=============== + +[![Latest Version](https://img.shields.io/crates/v/displaydoc.svg)](https://crates.io/crates/displaydoc) +[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/displaydoc) + +{{readme}} + + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..fca634f --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1,3 @@ +{ + "run_cargo": false +} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..1f9fd11 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,36 @@ +use displaydoc::Display; + +#[derive(Debug, Display)] +pub enum DataStoreError { + /// data store disconnected + Disconnect, + /// the data for key `{0}` is not available + Redaction(String), + /// invalid header (expected {expected:?}, found {found:?}) + InvalidHeader { expected: String, found: String }, + /// unknown data store error + Unknown, +} + +fn main() { + let disconnect = DataStoreError::Disconnect; + println!( + "Enum value `Disconnect` should be printed as:\n\t{}", + disconnect + ); + + let redaction = DataStoreError::Redaction(String::from("Dummy")); + println!( + "Enum value `Redaction` should be printed as:\n\t{}", + redaction + ); + + let invalid_header = DataStoreError::InvalidHeader { + expected: String::from("https"), + found: String::from("http"), + }; + println!( + "Enum value `InvalidHeader` should be printed as:\n\t{}", + invalid_header + ); +} diff --git a/src/attr.rs b/src/attr.rs new file mode 100644 index 0000000..a965d04 --- /dev/null +++ b/src/attr.rs @@ -0,0 +1,137 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{Attribute, LitStr, Meta, Result}; + +#[derive(Clone)] +pub(crate) struct Display { + pub(crate) fmt: LitStr, + pub(crate) args: TokenStream, +} + +pub(crate) struct VariantDisplay { + pub(crate) r#enum: Option, + pub(crate) variant: Display, +} + +impl ToTokens for Display { + fn to_tokens(&self, tokens: &mut TokenStream) { + let fmt = &self.fmt; + let args = &self.args; + tokens.extend(quote! { + write!(formatter, #fmt #args) + }); + } +} + +impl ToTokens for VariantDisplay { + fn to_tokens(&self, tokens: &mut TokenStream) { + if let Some(ref r#enum) = self.r#enum { + r#enum.to_tokens(tokens); + tokens.extend(quote! { ?; write!(formatter, ": ")?; }); + } + self.variant.to_tokens(tokens); + } +} + +pub(crate) struct AttrsHelper { + ignore_extra_doc_attributes: bool, + prefix_enum_doc_attributes: bool, +} + +impl AttrsHelper { + pub(crate) fn new(attrs: &[Attribute]) -> Self { + let ignore_extra_doc_attributes = attrs + .iter() + .any(|attr| attr.path().is_ident("ignore_extra_doc_attributes")); + let prefix_enum_doc_attributes = attrs + .iter() + .any(|attr| attr.path().is_ident("prefix_enum_doc_attributes")); + + Self { + ignore_extra_doc_attributes, + prefix_enum_doc_attributes, + } + } + + pub(crate) fn display(&self, attrs: &[Attribute]) -> Result> { + let displaydoc_attr = attrs.iter().find(|attr| attr.path().is_ident("displaydoc")); + + if let Some(displaydoc_attr) = displaydoc_attr { + let lit = displaydoc_attr + .parse_args() + .expect("#[displaydoc(\"foo\")] must contain string arguments"); + let mut display = Display { + fmt: lit, + args: TokenStream::new(), + }; + + display.expand_shorthand(); + return Ok(Some(display)); + } + + let num_doc_attrs = attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .count(); + + if !self.ignore_extra_doc_attributes && num_doc_attrs > 1 { + panic!("Multi-line comments are disabled by default by displaydoc. Please consider using block doc comments (/** */) or adding the #[ignore_extra_doc_attributes] attribute to your type next to the derive."); + } + + for attr in attrs { + if attr.path().is_ident("doc") { + let lit = match &attr.meta { + Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }), + .. + }) => lit, + _ => unimplemented!(), + }; + + // Make an attempt at cleaning up multiline doc comments. + let doc_str = lit + .value() + .lines() + .map(|line| line.trim().trim_start_matches('*').trim()) + .collect::>() + .join("\n"); + + let lit = LitStr::new(doc_str.trim(), lit.span()); + + let mut display = Display { + fmt: lit, + args: TokenStream::new(), + }; + + display.expand_shorthand(); + return Ok(Some(display)); + } + } + + Ok(None) + } + + pub(crate) fn display_with_input( + &self, + r#enum: &[Attribute], + variant: &[Attribute], + ) -> Result> { + let r#enum = if self.prefix_enum_doc_attributes { + let result = self + .display(r#enum)? + .expect("Missing doc comment on enum with #[prefix_enum_doc_attributes]. Please remove the attribute or add a doc comment to the enum itself."); + + Some(result) + } else { + None + }; + + Ok(self + .display(variant)? + .map(|variant| VariantDisplay { r#enum, variant })) + } +} diff --git a/src/expand.rs b/src/expand.rs new file mode 100644 index 0000000..8a84390 --- /dev/null +++ b/src/expand.rs @@ -0,0 +1,410 @@ +use super::attr::AttrsHelper; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{ + punctuated::Punctuated, + token::{Colon, Comma, PathSep, Plus, Where}, + Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Generics, Ident, Path, PathArguments, + PathSegment, PredicateType, Result, TraitBound, TraitBoundModifier, Type, TypeParam, + TypeParamBound, TypePath, WhereClause, WherePredicate, +}; + +use std::collections::HashMap; + +pub(crate) fn derive(input: &DeriveInput) -> Result { + let impls = match &input.data { + Data::Struct(data) => impl_struct(input, data), + Data::Enum(data) => impl_enum(input, data), + Data::Union(_) => Err(Error::new_spanned(input, "Unions are not supported")), + }?; + + let helpers = specialization(); + let dummy_const = format_ident!("_DERIVE_Display_FOR_{}", input.ident); + Ok(quote! { + #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] + const #dummy_const: () = { + #helpers + #impls + }; + }) +} + +#[cfg(feature = "std")] +fn specialization() -> TokenStream { + quote! { + trait DisplayToDisplayDoc { + fn __displaydoc_display(&self) -> Self; + } + + impl DisplayToDisplayDoc for &T { + fn __displaydoc_display(&self) -> Self { + self + } + } + + // If the `std` feature gets enabled we want to ensure that any crate + // using displaydoc can still reference the std crate, which is already + // being compiled in by whoever enabled the `std` feature in + // `displaydoc`, even if the crates using displaydoc are no_std. + extern crate std; + + trait PathToDisplayDoc { + fn __displaydoc_display(&self) -> std::path::Display<'_>; + } + + impl PathToDisplayDoc for std::path::Path { + fn __displaydoc_display(&self) -> std::path::Display<'_> { + self.display() + } + } + + impl PathToDisplayDoc for std::path::PathBuf { + fn __displaydoc_display(&self) -> std::path::Display<'_> { + self.display() + } + } + } +} + +#[cfg(not(feature = "std"))] +fn specialization() -> TokenStream { + quote! {} +} + +fn impl_struct(input: &DeriveInput, data: &DataStruct) -> Result { + let ty = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let where_clause = generate_where_clause(&input.generics, where_clause); + + let helper = AttrsHelper::new(&input.attrs); + + let display = helper.display(&input.attrs)?.map(|display| { + let pat = match &data.fields { + Fields::Named(fields) => { + let var = fields.named.iter().map(|field| &field.ident); + quote!(Self { #(#var),* }) + } + Fields::Unnamed(fields) => { + let var = (0..fields.unnamed.len()).map(|i| format_ident!("_{}", i)); + quote!(Self(#(#var),*)) + } + Fields::Unit => quote!(_), + }; + quote! { + impl #impl_generics core::fmt::Display for #ty #ty_generics #where_clause { + fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + // NB: This destructures the fields of `self` into named variables (for unnamed + // fields, it uses _0, _1, etc as above). The `#[allow(unused_variables)]` + // section means it doesn't have to parse the individual field references out of + // the docstring. + #[allow(unused_variables)] + let #pat = self; + #display + } + } + } + }); + + Ok(quote! { #display }) +} + +/// Create a `where` predicate for `ident`, without any [bound][TypeParamBound]s yet. +fn new_empty_where_type_predicate(ident: Ident) -> PredicateType { + let mut path_segments = Punctuated::::new(); + path_segments.push_value(PathSegment { + ident, + arguments: PathArguments::None, + }); + PredicateType { + lifetimes: None, + bounded_ty: Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments: path_segments, + }, + }), + colon_token: Colon { + spans: [Span::call_site()], + }, + bounds: Punctuated::::new(), + } +} + +/// Create a `where` clause that we can add [WherePredicate]s to. +fn new_empty_where_clause() -> WhereClause { + WhereClause { + where_token: Where { + span: Span::call_site(), + }, + predicates: Punctuated::::new(), + } +} + +enum UseGlobalPrefix { + LeadingColon, + #[allow(dead_code)] + NoLeadingColon, +} + +/// Create a path with segments composed of [Idents] *without* any [PathArguments]. +fn join_paths(name_segments: &[&str], use_global_prefix: UseGlobalPrefix) -> Path { + let mut segments = Punctuated::::new(); + assert!(!name_segments.is_empty()); + segments.push_value(PathSegment { + ident: Ident::new(name_segments[0], Span::call_site()), + arguments: PathArguments::None, + }); + for name in name_segments[1..].iter() { + segments.push_punct(PathSep { + spans: [Span::call_site(), Span::mixed_site()], + }); + segments.push_value(PathSegment { + ident: Ident::new(name, Span::call_site()), + arguments: PathArguments::None, + }); + } + Path { + leading_colon: match use_global_prefix { + UseGlobalPrefix::LeadingColon => Some(PathSep { + spans: [Span::call_site(), Span::mixed_site()], + }), + UseGlobalPrefix::NoLeadingColon => None, + }, + segments, + } +} + +/// Push `new_type_predicate` onto the end of `where_clause`. +fn append_where_clause_type_predicate( + where_clause: &mut WhereClause, + new_type_predicate: PredicateType, +) { + // Push a comma at the end if there are already any `where` predicates. + if !where_clause.predicates.is_empty() { + where_clause.predicates.push_punct(Comma { + spans: [Span::call_site()], + }); + } + where_clause + .predicates + .push_value(WherePredicate::Type(new_type_predicate)); +} + +/// Add a requirement for [core::fmt::Display] to a `where` predicate for some type. +fn add_display_constraint_to_type_predicate( + predicate_that_needs_a_display_impl: &mut PredicateType, +) { + // Create a `Path` of `::core::fmt::Display`. + let display_path = join_paths(&["core", "fmt", "Display"], UseGlobalPrefix::LeadingColon); + + let display_bound = TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: display_path, + }); + if !predicate_that_needs_a_display_impl.bounds.is_empty() { + predicate_that_needs_a_display_impl.bounds.push_punct(Plus { + spans: [Span::call_site()], + }); + } + + predicate_that_needs_a_display_impl + .bounds + .push_value(display_bound); +} + +/// Map each declared generic type parameter to the set of all trait boundaries declared on it. +/// +/// These boundaries may come from the declaration site: +/// pub enum E { ... } +/// or a `where` clause after the parameter declarations: +/// pub enum E where T: MyTrait { ... } +/// This method will return the boundaries from both of those cases. +fn extract_trait_constraints_from_source( + where_clause: &WhereClause, + type_params: &[&TypeParam], +) -> HashMap> { + // Add trait bounds provided at the declaration site of type parameters for the struct/enum. + let mut param_constraint_mapping: HashMap> = type_params + .iter() + .map(|type_param| { + let trait_bounds: Vec = type_param + .bounds + .iter() + .flat_map(|bound| match bound { + TypeParamBound::Trait(trait_bound) => Some(trait_bound), + _ => None, + }) + .cloned() + .collect(); + (type_param.ident.clone(), trait_bounds) + }) + .collect(); + + // Add trait bounds from `where` clauses, which may be type parameters or types containing + // those parameters. + for predicate in where_clause.predicates.iter() { + // We only care about type and not lifetime constraints here. + if let WherePredicate::Type(ref pred_ty) = predicate { + let ident = match &pred_ty.bounded_ty { + Type::Path(TypePath { path, qself: None }) => match path.get_ident() { + None => continue, + Some(ident) => ident, + }, + _ => continue, + }; + // We ignore any type constraints that aren't direct references to type + // parameters of the current enum of struct definition. No types can be + // constrained in a `where` clause unless they are a type parameter or a generic + // type instantiated with one of the type parameters, so by only allowing single + // identifiers, we can be sure that the constrained type is a type parameter + // that is contained in `param_constraint_mapping`. + if let Some((_, ref mut known_bounds)) = param_constraint_mapping + .iter_mut() + .find(|(id, _)| *id == ident) + { + for bound in pred_ty.bounds.iter() { + // We only care about trait bounds here. + if let TypeParamBound::Trait(ref bound) = bound { + known_bounds.push(bound.clone()); + } + } + } + } + } + + param_constraint_mapping +} + +/// Hygienically add `where _: Display` to the set of [TypeParamBound]s for `ident`, creating such +/// a set if necessary. +fn ensure_display_in_where_clause_for_type(where_clause: &mut WhereClause, ident: Ident) { + for pred_ty in where_clause + .predicates + .iter_mut() + // Find the `where` predicate constraining the current type param, if it exists. + .flat_map(|predicate| match predicate { + WherePredicate::Type(pred_ty) => Some(pred_ty), + // We're looking through type constraints, not lifetime constraints. + _ => None, + }) + { + // Do a complicated destructuring in order to check if the type being constrained in this + // `where` clause is the type we're looking for, so we can use the mutable reference to + // `pred_ty` if so. + let matches_desired_type = matches!( + &pred_ty.bounded_ty, + Type::Path(TypePath { path, .. }) if Some(&ident) == path.get_ident()); + if matches_desired_type { + add_display_constraint_to_type_predicate(pred_ty); + return; + } + } + + // If there is no `where` predicate for the current type param, we will construct one. + let mut new_type_predicate = new_empty_where_type_predicate(ident); + add_display_constraint_to_type_predicate(&mut new_type_predicate); + append_where_clause_type_predicate(where_clause, new_type_predicate); +} + +/// For all declared type parameters, add a [core::fmt::Display] constraint, unless the type +/// parameter already has any type constraint. +fn ensure_where_clause_has_display_for_all_unconstrained_members( + where_clause: &mut WhereClause, + type_params: &[&TypeParam], +) { + let param_constraint_mapping = extract_trait_constraints_from_source(where_clause, type_params); + + for (ident, known_bounds) in param_constraint_mapping.into_iter() { + // If the type parameter has any constraints already, we don't want to touch it, to avoid + // breaking use cases where a type parameter only needs to impl `Debug`, for example. + if known_bounds.is_empty() { + ensure_display_in_where_clause_for_type(where_clause, ident); + } + } +} + +/// Generate a `where` clause that ensures all generic type parameters `impl` +/// [core::fmt::Display] unless already constrained. +/// +/// This approach allows struct/enum definitions deriving [crate::Display] to avoid hardcoding +/// a [core::fmt::Display] constraint into every type parameter. +/// +/// If the type parameter isn't already constrained, we add a `where _: Display` clause to our +/// display implementation to expect to be able to format every enum case or struct member. +/// +/// In fact, we would preferably only require `where _: Display` or `where _: Debug` where the +/// format string actually requires it. However, while [`std::fmt` defines a formal syntax for +/// `format!()`][format syntax], it *doesn't* expose the actual logic to parse the format string, +/// which appears to live in [`rustc_parse_format`]. While we use the [`syn`] crate to parse rust +/// syntax, it also doesn't currently provide any method to introspect a `format!()` string. It +/// would be nice to contribute this upstream in [`syn`]. +/// +/// [format syntax]: std::fmt#syntax +/// [`rustc_parse_format`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse_format/index.html +fn generate_where_clause(generics: &Generics, where_clause: Option<&WhereClause>) -> WhereClause { + let mut where_clause = where_clause.cloned().unwrap_or_else(new_empty_where_clause); + let type_params: Vec<&TypeParam> = generics.type_params().collect(); + ensure_where_clause_has_display_for_all_unconstrained_members(&mut where_clause, &type_params); + where_clause +} + +fn impl_enum(input: &DeriveInput, data: &DataEnum) -> Result { + let ty = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let where_clause = generate_where_clause(&input.generics, where_clause); + + let helper = AttrsHelper::new(&input.attrs); + + let displays = data + .variants + .iter() + .map(|variant| helper.display_with_input(&input.attrs, &variant.attrs)) + .collect::>>()?; + + if data.variants.is_empty() { + Ok(quote! { + impl #impl_generics core::fmt::Display for #ty #ty_generics #where_clause { + fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + unreachable!("empty enums cannot be instantiated and thus cannot be printed") + } + } + }) + } else if displays.iter().any(Option::is_some) { + let arms = data + .variants + .iter() + .zip(displays) + .map(|(variant, display)| { + let display = + display.ok_or_else(|| Error::new_spanned(variant, "missing doc comment"))?; + let ident = &variant.ident; + Ok(match &variant.fields { + Fields::Named(fields) => { + let var = fields.named.iter().map(|field| &field.ident); + quote!(Self::#ident { #(#var),* } => { #display }) + } + Fields::Unnamed(fields) => { + let var = (0..fields.unnamed.len()).map(|i| format_ident!("_{}", i)); + quote!(Self::#ident(#(#var),*) => { #display }) + } + Fields::Unit => quote!(Self::#ident => { #display }), + }) + }) + .collect::>>()?; + Ok(quote! { + impl #impl_generics core::fmt::Display for #ty #ty_generics #where_clause { + fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + #[allow(unused_variables)] + match self { + #(#arms,)* + } + } + } + }) + } else { + Err(Error::new_spanned(input, "Missing doc comments")) + } +} diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..3334a82 --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,159 @@ +use crate::attr::Display; +use proc_macro2::TokenStream; +use quote::quote_spanned; +use syn::{Ident, LitStr}; + +macro_rules! peek_next { + ($read:ident) => { + match $read.chars().next() { + Some(next) => next, + None => return, + } + }; +} + +impl Display { + // Transform `"error {var}"` to `"error {}", var`. + pub(crate) fn expand_shorthand(&mut self) { + let span = self.fmt.span(); + let fmt = self.fmt.value(); + let mut read = fmt.as_str(); + let mut out = String::new(); + let mut args = TokenStream::new(); + + while let Some(brace) = read.find('{') { + out += &read[..=brace]; + read = &read[brace + 1..]; + + // skip cases where we find a {{ + if read.starts_with('{') { + out.push('{'); + read = &read[1..]; + continue; + } + + let next = peek_next!(read); + + let var = match next { + '0'..='9' => take_int(&mut read), + 'a'..='z' | 'A'..='Z' | '_' => take_ident(&mut read), + _ => return, + }; + + let ident = Ident::new(&var, span); + + let next = peek_next!(read); + + let arg = if cfg!(feature = "std") && next == '}' { + quote_spanned!(span=> , #ident.__displaydoc_display()) + } else { + quote_spanned!(span=> , #ident) + }; + + args.extend(arg); + } + + out += read; + self.fmt = LitStr::new(&out, self.fmt.span()); + self.args = args; + } +} + +fn take_int(read: &mut &str) -> String { + let mut int = String::new(); + int.push('_'); + for (i, ch) in read.char_indices() { + match ch { + '0'..='9' => int.push(ch), + _ => { + *read = &read[i..]; + break; + } + } + } + int +} + +fn take_ident(read: &mut &str) -> String { + let mut ident = String::new(); + for (i, ch) in read.char_indices() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch), + _ => { + *read = &read[i..]; + break; + } + } + } + ident +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use proc_macro2::Span; + + fn assert(input: &str, fmt: &str, args: &str) { + let mut display = Display { + fmt: LitStr::new(input, Span::call_site()), + args: TokenStream::new(), + }; + display.expand_shorthand(); + assert_eq!(fmt, display.fmt.value()); + assert_eq!(args, display.args.to_string()); + } + + #[test] + fn test_expand() { + assert("fn main() {{ }}", "fn main() {{ }}", ""); + } + + #[test] + #[cfg_attr(not(feature = "std"), ignore)] + fn test_std_expand() { + assert( + "{v} {v:?} {0} {0:?}", + "{} {:?} {} {:?}", + ", v . __displaydoc_display () , v , _0 . __displaydoc_display () , _0", + ); + assert("error {var}", "error {}", ", var . __displaydoc_display ()"); + + assert( + "error {var1}", + "error {}", + ", var1 . __displaydoc_display ()", + ); + + assert( + "error {var1var}", + "error {}", + ", var1var . __displaydoc_display ()", + ); + + assert( + "The path {0}", + "The path {}", + ", _0 . __displaydoc_display ()", + ); + assert("The path {0:?}", "The path {:?}", ", _0"); + } + + #[test] + #[cfg_attr(feature = "std", ignore)] + fn test_nostd_expand() { + assert( + "{v} {v:?} {0} {0:?}", + "{} {:?} {} {:?}", + ", v , v , _0 , _0", + ); + assert("error {var}", "error {}", ", var"); + + assert("The path {0}", "The path {}", ", _0"); + assert("The path {0:?}", "The path {:?}", ", _0"); + + assert("error {var1}", "error {}", ", var1"); + + assert("error {var1var}", "error {}", ", var1var"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e4653d6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,187 @@ +//! This library provides a convenient derive macro for the standard library's +//! [`core::fmt::Display`] trait. +//! +//! [`core::fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html +//! +//! ```toml +//! [dependencies] +//! displaydoc = "0.2" +//! ``` +//! +//! *Compiler support: requires rustc 1.56+* +//! +//!
+//! +//! ## Example +//! +//! *Demonstration alongside the [`Error`][std::error::Error] derive macro from [`thiserror`](https://docs.rs/thiserror/1.0.25/thiserror/index.html), +//! to propagate source locations from [`io::Error`][std::io::Error] with the `#[source]` attribute:* +//! ```rust +//! use std::io; +//! use displaydoc::Display; +//! use thiserror::Error; +//! +//! #[derive(Display, Error, Debug)] +//! pub enum DataStoreError { +//! /// data store disconnected +//! Disconnect(#[source] io::Error), +//! /// the data for key `{0}` is not available +//! Redaction(String), +//! /// invalid header (expected {expected:?}, found {found:?}) +//! InvalidHeader { +//! expected: String, +//! found: String, +//! }, +//! /// unknown data store error +//! Unknown, +//! } +//! +//! let error = DataStoreError::Redaction("CLASSIFIED CONTENT".to_string()); +//! assert!("the data for key `CLASSIFIED CONTENT` is not available" == &format!("{}", error)); +//! ``` +//! *Note that although [`io::Error`][std::io::Error] implements `Display`, we do not add it to the +//! generated message for `DataStoreError::Disconnect`, since it is already made available via +//! `#[source]`. See further context on avoiding duplication in error reports at the rust blog +//! [here](https://github.com/yaahc/blog.rust-lang.org/blob/master/posts/inside-rust/2021-05-15-What-the-error-handling-project-group-is-working-towards.md#duplicate-information-issue).* +//! +//!
+//! +//! ## Details +//! +//! - A `fmt::Display` impl is generated for your enum if you provide +//! a docstring comment on each variant as shown above in the example. The +//! `Display` derive macro supports a shorthand for interpolating fields from +//! the error: +//! - `/// {var}` ⟶ `write!("{}", self.var)` +//! - `/// {0}` ⟶ `write!("{}", self.0)` +//! - `/// {var:?}` ⟶ `write!("{:?}", self.var)` +//! - `/// {0:?}` ⟶ `write!("{:?}", self.0)` +//! - This also works with structs and [generic types][crate::Display#generic-type-parameters]: +//! ```rust +//! # use displaydoc::Display; +//! /// oh no, an error: {0} +//! #[derive(Display)] +//! pub struct Error(pub E); +//! +//! let error: Error<&str> = Error("muahaha i am an error"); +//! assert!("oh no, an error: muahaha i am an error" == &format!("{}", error)); +//! ``` +//! +//! - Two optional attributes can be added to your types next to the derive: +//! +//! - `#[ignore_extra_doc_attributes]` makes the macro ignore any doc +//! comment attributes (or `///` lines) after the first. Multi-line +//! comments using `///` are otherwise treated as an error, so use this +//! attribute or consider switching to block doc comments (`/** */`). +//! +//! - `#[prefix_enum_doc_attributes]` combines the doc comment message on +//! your enum itself with the messages for each variant, in the format +//! “enum: variant”. When added to an enum, the doc comment on the enum +//! becomes mandatory. When added to any other type, it has no effect. +//! +//! - In case you want to have an independent doc comment, the +//! `#[displaydoc("...")` atrribute may be used on the variant or struct to +//! override it. +//! +//!
+//! +//! ## FAQ +//! +//! 1. **Is this crate `no_std` compatible?** +//! * Yes! This crate implements the [`core::fmt::Display`] trait, not the [`std::fmt::Display`] trait, so it should work in `std` and `no_std` environments. Just add `default-features = false`. +//! +//! 2. **Does this crate work with `Path` and `PathBuf` via the `Display` trait?** +//! * Yuuup. This crate uses @dtolnay's [autoref specialization technique](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) to add a special trait for types to get the display impl. It then specializes for `Path` and `PathBuf`, and when either of these types are found, it calls `self.display()` to get a `std::path::Display<'_>` type which can be used with the `Display` format specifier! +#![doc(html_root_url = "https://docs.rs/displaydoc/0.2.3")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn( + rust_2018_idioms, + unreachable_pub, + bad_style, + dead_code, + improper_ctypes, + non_shorthand_field_patterns, + no_mangle_generic_items, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + private_in_public, + unconditional_recursion, + unused, + unused_allocation, + unused_comparisons, + unused_parens, + while_true +)] +#![allow(clippy::try_err)] + +#[allow(unused_extern_crates)] +extern crate proc_macro; + +mod attr; +mod expand; +mod fmt; + +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +/// [Custom `#[derive(...)]` macro](https://doc.rust-lang.org/edition-guide/rust-2018/macros/custom-derive.html) +/// for implementing [`fmt::Display`][core::fmt::Display] via doc comment attributes. +/// +/// ### Generic Type Parameters +/// +/// Type parameters to an enum or struct using this macro should *not* need to +/// have an explicit `Display` constraint at the struct or enum definition +/// site. A `Display` implementation for the `derive`d struct or enum is +/// generated assuming each type parameter implements `Display`, but that should +/// be possible without adding the constraint to the struct definition itself: +/// ```rust +/// use displaydoc::Display; +/// +/// /// oh no, an error: {0} +/// #[derive(Display)] +/// pub struct Error(pub E); +/// +/// // No need to require `E: Display`, since `displaydoc::Display` adds that implicitly. +/// fn generate_error(e: E) -> Error { Error(e) } +/// +/// assert!("oh no, an error: muahaha" == &format!("{}", generate_error("muahaha"))); +/// ``` +/// +/// ### Using [`Debug`][core::fmt::Debug] Implementations with Type Parameters +/// However, if a type parameter must instead be constrained with the +/// [`Debug`][core::fmt::Debug] trait so that some field may be printed with +/// `{:?}`, that constraint must currently still also be specified redundantly +/// at the struct or enum definition site. If a struct or enum field is being +/// formatted with `{:?}` via [`displaydoc`][crate], and a generic type +/// parameter must implement `Debug` to do that, then that struct or enum +/// definition will need to propagate the `Debug` constraint to every type +/// parameter it's instantiated with: +/// ```rust +/// use core::fmt::Debug; +/// use displaydoc::Display; +/// +/// /// oh no, an error: {0:?} +/// #[derive(Display)] +/// pub struct Error(pub E); +/// +/// // `E: Debug` now has to propagate to callers. +/// fn generate_error(e: E) -> Error { Error(e) } +/// +/// assert!("oh no, an error: \"cool\"" == &format!("{}", generate_error("cool"))); +/// +/// // Try this with a struct that doesn't impl `Display` at all, unlike `str`. +/// #[derive(Debug)] +/// pub struct Oh; +/// assert!("oh no, an error: Oh" == &format!("{}", generate_error(Oh))); +/// ``` +#[proc_macro_derive( + Display, + attributes(ignore_extra_doc_attributes, prefix_enum_doc_attributes, displaydoc) +)] +pub fn derive_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand::derive(&input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/tests/compile_tests.rs b/tests/compile_tests.rs new file mode 100644 index 0000000..ac7427a --- /dev/null +++ b/tests/compile_tests.rs @@ -0,0 +1,29 @@ +#[allow(unused_attributes)] +#[rustversion::attr(not(nightly), ignore)] +#[test] +fn no_std() { + let t = trybuild::TestCases::new(); + #[cfg(not(feature = "std"))] + t.compile_fail("tests/no_std/without.rs"); + #[cfg(not(feature = "std"))] + t.compile_fail("tests/no_std/multi_line.rs"); + #[cfg(not(feature = "std"))] + t.pass("tests/no_std/multi_line_allow.rs"); + #[cfg(not(feature = "std"))] + t.compile_fail("tests/no_std/enum_prefix_missing.rs"); + #[cfg(not(feature = "std"))] + t.pass("tests/no_std/enum_prefix.rs"); + #[cfg(feature = "std")] + t.compile_fail("tests/std/without.rs"); + #[cfg(feature = "std")] + t.compile_fail("tests/std/multi_line.rs"); + #[cfg(feature = "std")] + t.pass("tests/std/multi_line_allow.rs"); + #[cfg(feature = "std")] + t.compile_fail("tests/std/enum_prefix_missing.rs"); + #[cfg(feature = "std")] + t.pass("tests/std/enum_prefix.rs"); + #[cfg(feature = "std")] + t.pass("tests/std/multiple.rs"); + t.pass("tests/no_std/with.rs"); +} diff --git a/tests/happy.rs b/tests/happy.rs new file mode 100644 index 0000000..f8fde9c --- /dev/null +++ b/tests/happy.rs @@ -0,0 +1,152 @@ +use displaydoc::Display; + +#[cfg(feature = "std")] +use std::path::PathBuf; + +#[derive(Display)] +/// Just a basic struct {thing} +struct HappyStruct { + thing: &'static str, +} + +#[derive(Display)] +#[ignore_extra_doc_attributes] +/// Just a basic struct {thing} +/// and this line should get ignored +struct HappyStruct2 { + thing: &'static str, +} + +#[derive(Display)] +enum Happy { + /// I really like Variant1 + Variant1, + /// Variant2 is pretty swell 2 + Variant2, + /// Variant3 is okay {sometimes} + Variant3 { sometimes: &'static str }, + /** + * Variant4 wants to have a lot of lines + * + * Lets see how this works out for it + */ + Variant4, + /// Variant5 has a parameter {0} and some regular comments + // A regular comment that won't get picked + Variant5(u32), + + /// The path {0} + #[cfg(feature = "std")] + Variant6(PathBuf), + + /// These docs are ignored + #[displaydoc("Variant7 has a parameter {0} and uses #[displaydoc]")] + /// These docs are also ignored + Variant7(u32), +} + +// Used for testing indented doc comments +mod inner_mod { + use super::Display; + + #[derive(Display)] + pub enum InnerHappy { + /// I really like Variant1 + Variant1, + /// Variant2 is pretty swell 2 + Variant2, + /// Variant3 is okay {sometimes} + Variant3 { sometimes: &'static str }, + /** + * Variant4 wants to have a lot of lines + * + * Lets see how this works out for it + */ + Variant4, + /// Variant5 has a parameter {0} and some regular comments + // A regular comment that won't get picked + Variant5(u32), + + /** what happens if we + * put text on the first line? + */ + Variant6, + + /** + what happens if we don't use *? + */ + Variant7, + + /** + * + * what about extra new lines? + */ + Variant8, + } +} + +fn assert_display(input: T, expected: &'static str) { + let out = format!("{}", input); + assert_eq!(expected, out); +} + +#[test] +fn does_it_print() { + assert_display(Happy::Variant1, "I really like Variant1"); + assert_display(Happy::Variant2, "Variant2 is pretty swell 2"); + assert_display(Happy::Variant3 { sometimes: "hi" }, "Variant3 is okay hi"); + assert_display( + Happy::Variant4, + "Variant4 wants to have a lot of lines\n\nLets see how this works out for it", + ); + assert_display( + Happy::Variant5(2), + "Variant5 has a parameter 2 and some regular comments", + ); + assert_display( + Happy::Variant7(2), + "Variant7 has a parameter 2 and uses #[displaydoc]", + ); + assert_display(HappyStruct { thing: "hi" }, "Just a basic struct hi"); + + assert_display(HappyStruct2 { thing: "hi2" }, "Just a basic struct hi2"); + + assert_display(inner_mod::InnerHappy::Variant1, "I really like Variant1"); + assert_display( + inner_mod::InnerHappy::Variant2, + "Variant2 is pretty swell 2", + ); + assert_display( + inner_mod::InnerHappy::Variant3 { sometimes: "hi" }, + "Variant3 is okay hi", + ); + assert_display( + inner_mod::InnerHappy::Variant4, + "Variant4 wants to have a lot of lines\n\nLets see how this works out for it", + ); + assert_display( + inner_mod::InnerHappy::Variant5(2), + "Variant5 has a parameter 2 and some regular comments", + ); + assert_display( + inner_mod::InnerHappy::Variant6, + "what happens if we\nput text on the first line?", + ); + assert_display( + inner_mod::InnerHappy::Variant7, + "what happens if we don\'t use *?", + ); + assert_display( + inner_mod::InnerHappy::Variant8, + "what about extra new lines?", + ); +} + +#[test] +#[cfg(feature = "std")] +fn does_it_print_path() { + assert_display( + Happy::Variant6(PathBuf::from("/var/log/happy")), + "The path /var/log/happy", + ); +} diff --git a/tests/no_std/enum_prefix.rs b/tests/no_std/enum_prefix.rs new file mode 100644 index 0000000..b8482ca --- /dev/null +++ b/tests/no_std/enum_prefix.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +#[prefix_enum_doc_attributes] +enum TestType { + /// this variant is too + Variant1, + + /// this variant is two + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/no_std/enum_prefix_missing.rs b/tests/no_std/enum_prefix_missing.rs new file mode 100644 index 0000000..474073e --- /dev/null +++ b/tests/no_std/enum_prefix_missing.rs @@ -0,0 +1,35 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +#[derive(Display)] +#[prefix_enum_doc_attributes] +enum TestType { + /// this variant is too + Variant1, + + /// this variant is two + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/no_std/enum_prefix_missing.stderr b/tests/no_std/enum_prefix_missing.stderr new file mode 100644 index 0000000..8d40232 --- /dev/null +++ b/tests/no_std/enum_prefix_missing.stderr @@ -0,0 +1,22 @@ +error: proc-macro derive panicked + --> $DIR/enum_prefix_missing.rs:22:10 + | +22 | #[derive(Display)] + | ^^^^^^^ + | + = help: message: Missing doc comment on enum with #[prefix_enum_doc_attributes]. Please remove the attribute or add a doc comment to the enum itself. + +error[E0277]: `TestType` doesn't implement `Display` + --> $DIR/enum_prefix_missing.rs:32:37 + | +32 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^ `TestType` cannot be formatted with the default formatter + | + = help: the trait `Display` is not implemented for `TestType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/enum_prefix_missing.rs:32:1 + | +32 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/no_std/multi_line.rs b/tests/no_std/multi_line.rs new file mode 100644 index 0000000..351f6a4 --- /dev/null +++ b/tests/no_std/multi_line.rs @@ -0,0 +1,37 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +enum TestType { + /// This one is okay + Variant1, + + /// Multi + /// line + /// doc. + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/no_std/multi_line.stderr b/tests/no_std/multi_line.stderr new file mode 100644 index 0000000..ae4956e --- /dev/null +++ b/tests/no_std/multi_line.stderr @@ -0,0 +1,22 @@ +error: proc-macro derive panicked + --> $DIR/multi_line.rs:23:10 + | +23 | #[derive(Display)] + | ^^^^^^^ + | + = help: message: Multi-line comments are disabled by default by displaydoc. Please consider using block doc comments (/** */) or adding the #[ignore_extra_doc_attributes] attribute to your type next to the derive. + +error[E0277]: `TestType` doesn't implement `Display` + --> $DIR/multi_line.rs:34:37 + | +34 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^ `TestType` cannot be formatted with the default formatter + | + = help: the trait `Display` is not implemented for `TestType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/multi_line.rs:34:1 + | +34 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/no_std/multi_line_allow.rs b/tests/no_std/multi_line_allow.rs new file mode 100644 index 0000000..22511d9 --- /dev/null +++ b/tests/no_std/multi_line_allow.rs @@ -0,0 +1,38 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +#[ignore_extra_doc_attributes] +enum TestType { + /// This one is okay + Variant1, + + /// Multi + /// line + /// doc. + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/no_std/with.rs b/tests/no_std/with.rs new file mode 100644 index 0000000..eba7460 --- /dev/null +++ b/tests/no_std/with.rs @@ -0,0 +1,32 @@ +#![feature(lang_items, start)] +#![no_std] + +#[start] +#[cfg(not(feature = "std"))] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} + +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} + +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +#[cfg(feature = "std")] +fn main() {} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +struct FakeType; + +static_assertions::assert_impl_all!(FakeType: core::fmt::Display); diff --git a/tests/no_std/without.rs b/tests/no_std/without.rs new file mode 100644 index 0000000..aab3164 --- /dev/null +++ b/tests/no_std/without.rs @@ -0,0 +1,28 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +struct FakeType; + +static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/no_std/without.stderr b/tests/no_std/without.stderr new file mode 100644 index 0000000..afcea63 --- /dev/null +++ b/tests/no_std/without.stderr @@ -0,0 +1,22 @@ +warning: unused import: `displaydoc::Display` + --> $DIR/without.rs:20:5 + | +20 | use displaydoc::Display; + | ^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +error[E0277]: `FakeType` doesn't implement `Display` + --> $DIR/without.rs:25:37 + | +25 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + | ^^^^^^^^ `FakeType` cannot be formatted with the default formatter + | + = help: the trait `Display` is not implemented for `FakeType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/without.rs:25:1 + | +25 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/num_in_field.rs b/tests/num_in_field.rs new file mode 100644 index 0000000..ab90f0d --- /dev/null +++ b/tests/num_in_field.rs @@ -0,0 +1,22 @@ +/// {foo1} {foo2} +#[derive(displaydoc::Display)] +pub struct Test { + foo1: String, + foo2: String, +} + +fn assert_display(input: T, expected: &'static str) { + let out = format!("{}", input); + assert_eq!(expected, out); +} + +#[test] +fn does_it_print() { + assert_display( + Test { + foo1: "hi".into(), + foo2: "hello".into(), + }, + "hi hello", + ); +} diff --git a/tests/std/enum_prefix.rs b/tests/std/enum_prefix.rs new file mode 100644 index 0000000..b8482ca --- /dev/null +++ b/tests/std/enum_prefix.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +#[prefix_enum_doc_attributes] +enum TestType { + /// this variant is too + Variant1, + + /// this variant is two + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/std/enum_prefix_missing.rs b/tests/std/enum_prefix_missing.rs new file mode 100644 index 0000000..474073e --- /dev/null +++ b/tests/std/enum_prefix_missing.rs @@ -0,0 +1,35 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +#[derive(Display)] +#[prefix_enum_doc_attributes] +enum TestType { + /// this variant is too + Variant1, + + /// this variant is two + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/std/enum_prefix_missing.stderr b/tests/std/enum_prefix_missing.stderr new file mode 100644 index 0000000..fe4b5ef --- /dev/null +++ b/tests/std/enum_prefix_missing.stderr @@ -0,0 +1,22 @@ +error: proc-macro derive panicked + --> $DIR/enum_prefix_missing.rs:22:10 + | +22 | #[derive(Display)] + | ^^^^^^^ + | + = help: message: Missing doc comment on enum with #[prefix_enum_doc_attributes]. Please remove the attribute or add a doc comment to the enum itself. + +error[E0277]: `TestType` doesn't implement `std::fmt::Display` + --> $DIR/enum_prefix_missing.rs:32:37 + | +32 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^ `TestType` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `TestType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/enum_prefix_missing.rs:32:1 + | +32 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/std/multi_line.rs b/tests/std/multi_line.rs new file mode 100644 index 0000000..5b0f2cf --- /dev/null +++ b/tests/std/multi_line.rs @@ -0,0 +1 @@ +../no_std/multi_line.rs \ No newline at end of file diff --git a/tests/std/multi_line.stderr b/tests/std/multi_line.stderr new file mode 100644 index 0000000..a8b6602 --- /dev/null +++ b/tests/std/multi_line.stderr @@ -0,0 +1,22 @@ +error: proc-macro derive panicked + --> $DIR/multi_line.rs:23:10 + | +23 | #[derive(Display)] + | ^^^^^^^ + | + = help: message: Multi-line comments are disabled by default by displaydoc. Please consider using block doc comments (/** */) or adding the #[ignore_extra_doc_attributes] attribute to your type next to the derive. + +error[E0277]: `TestType` doesn't implement `std::fmt::Display` + --> $DIR/multi_line.rs:34:37 + | +34 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^ `TestType` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `TestType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/multi_line.rs:34:1 + | +34 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/std/multi_line_allow.rs b/tests/std/multi_line_allow.rs new file mode 100644 index 0000000..22511d9 --- /dev/null +++ b/tests/std/multi_line_allow.rs @@ -0,0 +1,38 @@ +#![cfg_attr(not(feature = "std"), feature(lang_items, start))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg_attr(not(feature = "std"), start)] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +#[ignore_extra_doc_attributes] +enum TestType { + /// This one is okay + Variant1, + + /// Multi + /// line + /// doc. + Variant2, +} + +static_assertions::assert_impl_all!(TestType: core::fmt::Display); + +#[cfg(feature = "std")] +fn main() {} diff --git a/tests/std/multiple.rs b/tests/std/multiple.rs new file mode 100644 index 0000000..b0a4de0 --- /dev/null +++ b/tests/std/multiple.rs @@ -0,0 +1,38 @@ +#![feature(lang_items, start)] +#![no_std] + +#[start] +#[cfg(not(feature = "std"))] +fn start(_argc: isize, _argv: *const *const u8) -> isize { + 0 +} + +#[lang = "eh_personality"] +#[no_mangle] +#[cfg(not(feature = "std"))] +pub extern "C" fn rust_eh_personality() {} + +#[panic_handler] +#[cfg(not(feature = "std"))] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + libc::abort(); + } +} + +#[cfg(feature = "std")] +fn main() {} + +use displaydoc::Display; + +/// this type is pretty swell +#[derive(Display)] +struct FakeType; + +static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + +/// this type is pretty swell2 +#[derive(Display)] +struct FakeType2; + +static_assertions::assert_impl_all!(FakeType2: core::fmt::Display); diff --git a/tests/std/without.rs b/tests/std/without.rs new file mode 100644 index 0000000..6b5b714 --- /dev/null +++ b/tests/std/without.rs @@ -0,0 +1 @@ +../no_std/without.rs \ No newline at end of file diff --git a/tests/std/without.stderr b/tests/std/without.stderr new file mode 100644 index 0000000..552ae82 --- /dev/null +++ b/tests/std/without.stderr @@ -0,0 +1,22 @@ +warning: unused import: `displaydoc::Display` + --> $DIR/without.rs:20:5 + | +20 | use displaydoc::Display; + | ^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +error[E0277]: `FakeType` doesn't implement `std::fmt::Display` + --> $DIR/without.rs:25:37 + | +25 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + | ^^^^^^^^ `FakeType` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `FakeType` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `assert_impl_all` + --> $DIR/without.rs:25:1 + | +25 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` + = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/variantless.rs b/tests/variantless.rs new file mode 100644 index 0000000..bc23ed4 --- /dev/null +++ b/tests/variantless.rs @@ -0,0 +1,6 @@ +use displaydoc::Display; + +#[derive(Display)] +enum EmptyInside {} + +static_assertions::assert_impl_all!(EmptyInside: core::fmt::Display); diff --git a/update-readme.sh b/update-readme.sh new file mode 100644 index 0000000..90db1a8 --- /dev/null +++ b/update-readme.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env bash + +cargo readme > ./README.md +git add ./README.md +git commit -m "Update readme" || true -- cgit v1.2.3