├── .gitignore ├── update-readme.sh ├── tests ├── variantless.rs ├── ui │ ├── without.rs │ ├── enum_prefix_missing.rs │ ├── multi_line.rs │ ├── enum_prefix.rs │ ├── multiple.rs │ ├── multi_line_allow.rs │ ├── without.stderr │ ├── enum_prefix_missing.stderr │ └── multi_line.stderr ├── compile_tests.rs ├── num_in_field.rs ├── no_std.rs └── happy.rs ├── README.tpl ├── LICENSE-MIT ├── examples └── simple.rs ├── CHANGELOG.md ├── Cargo.toml ├── .github └── workflows │ └── ci.yml ├── src ├── fmt.rs ├── attr.rs ├── lib.rs └── expand.rs ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /update-readme.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | cargo readme > ./README.md 4 | git add ./README.md 5 | git commit -m "Update readme" || true 6 | -------------------------------------------------------------------------------- /tests/variantless.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | #[derive(Display)] 4 | enum EmptyInside {} 5 | 6 | static_assertions::assert_impl_all!(EmptyInside: core::fmt::Display); 7 | -------------------------------------------------------------------------------- /tests/ui/without.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | /// this type is pretty swell 4 | struct FakeType; 5 | 6 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /tests/ui/enum_prefix_missing.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | #[derive(Display)] 4 | #[prefix_enum_doc_attributes] 5 | enum TestType { 6 | /// this variant is too 7 | Variant1, 8 | 9 | /// this variant is two 10 | Variant2, 11 | } 12 | 13 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 14 | 15 | fn main() {} -------------------------------------------------------------------------------- /tests/ui/multi_line.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | /// this type is pretty swell 4 | #[derive(Display)] 5 | enum TestType { 6 | /// This one is okay 7 | Variant1, 8 | 9 | /// Multi 10 | /// line 11 | /// doc. 12 | Variant2, 13 | } 14 | 15 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /tests/ui/enum_prefix.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | /// this type is pretty swell 4 | #[derive(Display)] 5 | #[prefix_enum_doc_attributes] 6 | enum TestType { 7 | /// this variant is too 8 | Variant1, 9 | 10 | /// this variant is two 11 | Variant2, 12 | } 13 | 14 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 15 | 16 | fn main() {} -------------------------------------------------------------------------------- /tests/ui/multiple.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | /// this type is pretty swell 4 | #[derive(Display)] 5 | struct FakeType; 6 | 7 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); 8 | 9 | /// this type is pretty swell2 10 | #[derive(Display)] 11 | struct FakeType2; 12 | 13 | static_assertions::assert_impl_all!(FakeType2: core::fmt::Display); 14 | 15 | fn main() {} -------------------------------------------------------------------------------- /tests/ui/multi_line_allow.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | /// this type is pretty swell 4 | #[derive(Display)] 5 | #[ignore_extra_doc_attributes] 6 | enum TestType { 7 | /// This one is okay 8 | Variant1, 9 | 10 | /// Multi 11 | /// line 12 | /// doc. 13 | Variant2, 14 | } 15 | 16 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 17 | 18 | fn main() {} -------------------------------------------------------------------------------- /tests/compile_tests.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_attributes)] 2 | #[rustversion::attr(not(nightly), ignore)] 3 | #[test] 4 | fn no_std() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/ui/without.rs"); 7 | t.compile_fail("tests/ui/multi_line.rs"); 8 | t.pass("tests/ui/multi_line_allow.rs"); 9 | t.compile_fail("tests/ui/enum_prefix_missing.rs"); 10 | t.pass("tests/ui/enum_prefix.rs"); 11 | t.pass("tests/ui/multiple.rs"); 12 | } 13 | -------------------------------------------------------------------------------- /tests/num_in_field.rs: -------------------------------------------------------------------------------- 1 | /// {foo1} {foo2} 2 | #[derive(displaydoc::Display)] 3 | pub struct Test { 4 | foo1: String, 5 | foo2: String, 6 | } 7 | 8 | fn assert_display(input: T, expected: &'static str) { 9 | let out = format!("{}", input); 10 | assert_eq!(expected, out); 11 | } 12 | 13 | #[test] 14 | fn does_it_print() { 15 | assert_display( 16 | Test { 17 | foo1: "hi".into(), 18 | foo2: "hello".into(), 19 | }, 20 | "hi hello", 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | derive(Display) /// `From` 2 | =============== 3 | 4 | [![Latest Version](https://img.shields.io/crates/v/displaydoc.svg)](https://crates.io/crates/displaydoc) 5 | [![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/displaydoc) 6 | 7 | {{readme}} 8 | 9 | 10 | #### License 11 | 12 | 13 | Licensed under either of Apache License, Version 14 | 2.0 or MIT license at your option. 15 | 16 | 17 |
18 | 19 | 20 | Unless you explicitly state otherwise, any contribution intentionally submitted 21 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 22 | be dual licensed as above, without any additional terms or conditions. 23 | 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | #[derive(Debug, Display)] 4 | pub enum DataStoreError { 5 | /// data store disconnected 6 | Disconnect, 7 | /// the data for key `{0}` is not available 8 | Redaction(String), 9 | /// invalid header (expected {expected:?}, found {found:?}) 10 | InvalidHeader { expected: String, found: String }, 11 | /// unknown data store error 12 | Unknown, 13 | } 14 | 15 | fn main() { 16 | let disconnect = DataStoreError::Disconnect; 17 | println!( 18 | "Enum value `Disconnect` should be printed as:\n\t{}", 19 | disconnect 20 | ); 21 | 22 | let redaction = DataStoreError::Redaction(String::from("Dummy")); 23 | println!( 24 | "Enum value `Redaction` should be printed as:\n\t{}", 25 | redaction 26 | ); 27 | 28 | let invalid_header = DataStoreError::InvalidHeader { 29 | expected: String::from("https"), 30 | found: String::from("http"), 31 | }; 32 | println!( 33 | "Enum value `InvalidHeader` should be printed as:\n\t{}", 34 | invalid_header 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /tests/ui/without.stderr: -------------------------------------------------------------------------------- 1 | warning: unused import: `displaydoc::Display` 2 | --> tests/ui/without.rs:1:5 3 | | 4 | 1 | use displaydoc::Display; 5 | | ^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default 8 | 9 | error[E0277]: `FakeType` doesn't implement `std::fmt::Display` 10 | --> tests/ui/without.rs:6:37 11 | | 12 | 6 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); 13 | | ^^^^^^^^ unsatisfied trait bound 14 | | 15 | help: the trait `std::fmt::Display` is not implemented for `FakeType` 16 | --> tests/ui/without.rs:4:1 17 | | 18 | 4 | struct FakeType; 19 | | ^^^^^^^^^^^^^^^ 20 | note: required by a bound in `assert_impl_all` 21 | --> tests/ui/without.rs:6:1 22 | | 23 | 6 | static_assertions::assert_impl_all!(FakeType: core::fmt::Display); 24 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` 25 | = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) 26 | -------------------------------------------------------------------------------- /tests/no_std.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![allow(unused)] 3 | 4 | // This test ensures that the generated code doesn't reference any stdlib items. 5 | 6 | use displaydoc::Display; 7 | 8 | #[derive(Display)] 9 | /// Just a basic struct {thing} 10 | struct HappyStruct { 11 | thing: &'static str, 12 | } 13 | 14 | #[derive(Display)] 15 | #[ignore_extra_doc_attributes] 16 | /// Just a basic struct {thing} 17 | /// and this line should get ignored 18 | struct HappyStruct2 { 19 | thing: &'static str, 20 | } 21 | 22 | #[derive(Display)] 23 | enum Happy { 24 | /// I really like Variant1 25 | Variant1, 26 | /// Variant2 is pretty swell 2 27 | Variant2, 28 | /// Variant3 is okay {sometimes} 29 | Variant3 { sometimes: &'static str }, 30 | /** 31 | * Variant4 wants to have a lot of lines 32 | * 33 | * Lets see how this works out for it 34 | */ 35 | Variant4, 36 | /// Variant5 has a parameter {0} and some regular comments 37 | // A regular comment that won't get picked 38 | Variant5(u32), 39 | 40 | /// These docs are ignored 41 | #[displaydoc("Variant7 has a parameter {0} and uses #[displaydoc]")] 42 | /// These docs are also ignored 43 | Variant7(u32), 44 | } 45 | -------------------------------------------------------------------------------- /tests/ui/enum_prefix_missing.stderr: -------------------------------------------------------------------------------- 1 | error: proc-macro derive panicked 2 | --> tests/ui/enum_prefix_missing.rs:3:10 3 | | 4 | 3 | #[derive(Display)] 5 | | ^^^^^^^ 6 | | 7 | = 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. 8 | 9 | error[E0277]: `TestType` doesn't implement `std::fmt::Display` 10 | --> tests/ui/enum_prefix_missing.rs:13:37 11 | | 12 | 13 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 13 | | ^^^^^^^^ unsatisfied trait bound 14 | | 15 | help: the trait `std::fmt::Display` is not implemented for `TestType` 16 | --> tests/ui/enum_prefix_missing.rs:5:1 17 | | 18 | 5 | enum TestType { 19 | | ^^^^^^^^^^^^^ 20 | note: required by a bound in `assert_impl_all` 21 | --> tests/ui/enum_prefix_missing.rs:13:1 22 | | 23 | 13 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 24 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` 25 | = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) 26 | -------------------------------------------------------------------------------- /tests/ui/multi_line.stderr: -------------------------------------------------------------------------------- 1 | error: proc-macro derive panicked 2 | --> tests/ui/multi_line.rs:4:10 3 | | 4 | 4 | #[derive(Display)] 5 | | ^^^^^^^ 6 | | 7 | = 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. 8 | 9 | error[E0277]: `TestType` doesn't implement `std::fmt::Display` 10 | --> tests/ui/multi_line.rs:15:37 11 | | 12 | 15 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 13 | | ^^^^^^^^ unsatisfied trait bound 14 | | 15 | help: the trait `std::fmt::Display` is not implemented for `TestType` 16 | --> tests/ui/multi_line.rs:5:1 17 | | 18 | 5 | enum TestType { 19 | | ^^^^^^^^^^^^^ 20 | note: required by a bound in `assert_impl_all` 21 | --> tests/ui/multi_line.rs:15:1 22 | | 23 | 15 | static_assertions::assert_impl_all!(TestType: core::fmt::Display); 24 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `assert_impl_all` 25 | = note: this error originates in the macro `static_assertions::assert_impl_all` (in Nightly builds, run with -Z macro-backtrace for more info) 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | 9 | ## [Unreleased] - ReleaseDate 10 | 11 | # [0.2.5] - 2024-06-20 12 | 13 | # Changed 14 | - Don't name the output of the const block to work around `non_local_definitions` error (#47) 15 | - Reference the `core` crate correctly to avoid clashes with modules named `core` (#45) 16 | - Explicitly list MSRV in Cargo.toml (#51) 17 | - Bump edition to 2021 (#51) 18 | 19 | # [0.2.4] - 2022-05-02 20 | 21 | ## Added 22 | - Updated `syn` dependency to 2.0 23 | - Support for empty enums 24 | - Implicitly require fmt::Display on all type parameters unless overridden 25 | 26 | ## Changed 27 | - Bumped MSRV to 1.56 28 | 29 | # [0.2.3] - 2021-07-16 30 | ## Added 31 | - Added `#[displaydoc("..")]` attribute for overriding a doc comment 32 | 33 | # [0.2.2] - 2021-07-01 34 | ## Added 35 | - Added prefix feature to use the doc comment from an enum and prepend it 36 | before the error message from each variant. 37 | 38 | # [0.2.1] - 2021-03-26 39 | ## Added 40 | - Added opt in support for ignoring extra doc attributes 41 | 42 | # [0.2.0] - 2021-03-16 43 | ## Changed 44 | 45 | - (BREAKING) disallow multiple `doc` attributes in display impl 46 | [https://github.com/yaahc/displaydoc/pull/22]. Allowing and ignoring extra 47 | doc attributes made it too easy to accidentally create a broken display 48 | implementation with missing context without realizing it, this change turns 49 | that into a hard error and directs users towards block comments if multiple 50 | lines are needed. 51 | 52 | 53 | [Unreleased]: https://github.com/yaahc/displaydoc/compare/v0.2.4...HEAD 54 | [0.2.4]: https://github.com/yaahc/displaydoc/compare/v0.2.3...v0.2.4 55 | [0.2.3]: https://github.com/yaahc/displaydoc/compare/v0.2.2...v0.2.3 56 | [0.2.2]: https://github.com/yaahc/displaydoc/compare/v0.2.1...v0.2.2 57 | [0.2.1]: https://github.com/yaahc/displaydoc/compare/v0.2.0...v0.2.1 58 | [0.2.0]: https://github.com/yaahc/displaydoc/releases/tag/v0.2.0 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "displaydoc" 3 | version = "0.2.5" 4 | rust-version = "1.56.0" 5 | authors = ["Jane Lusby "] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | repository = "https://github.com/yaahc/displaydoc" 10 | homepage = "https://github.com/yaahc/displaydoc" 11 | documentation = "https://docs.rs/displaydoc" 12 | keywords = ["display", "derive"] 13 | description = """ 14 | A derive macro for implementing the display Trait via a doc comment and string interpolation 15 | """ 16 | include = ["Cargo.toml", "README.md", "LICENSE-MIT", "LICENSE-APACHE", "CHANGELOG.md", "src/**/*.rs", "tests/**/*.rs"] 17 | 18 | [lib] 19 | proc-macro = true 20 | path = "src/lib.rs" 21 | 22 | [features] 23 | default = ["std"] 24 | std = [] 25 | 26 | [dependencies] 27 | syn = "2.0" 28 | quote = "1.0" 29 | proc-macro2 = "1.0" 30 | 31 | [dev-dependencies] 32 | trybuild = "1.0" 33 | static_assertions = "1.1" 34 | libc = { version = "0.2", default-features = false } 35 | rustversion = "1.0.0" 36 | pretty_assertions = "1.4.0" 37 | thiserror = "1.0.24" 38 | 39 | [package.metadata.docs.rs] 40 | all-features = true 41 | rustdoc-args = ["--cfg", "docsrs"] 42 | 43 | [package.metadata.release] 44 | no-dev-version = true 45 | pre-release-hook = ["./update-readme.sh"] 46 | 47 | [[package.metadata.release.pre-release-replacements]] 48 | file = "CHANGELOG.md" 49 | search = "Unreleased" 50 | replace="{{version}}" 51 | 52 | [[package.metadata.release.pre-release-replacements]] 53 | file = "src/lib.rs" 54 | search = "#!\\[doc\\(html_root_url.*" 55 | replace = "#![doc(html_root_url = \"https://docs.rs/{{crate_name}}/{{version}}\")]" 56 | exactly = 1 57 | 58 | [[package.metadata.release.pre-release-replacements]] 59 | file = "CHANGELOG.md" 60 | search = "ReleaseDate" 61 | replace="{{date}}" 62 | 63 | [[package.metadata.release.pre-release-replacements]] 64 | file="CHANGELOG.md" 65 | search="" 66 | replace="\n\n# [Unreleased] - ReleaseDate" 67 | exactly=1 68 | 69 | # Disable this replacement on the very first release 70 | [[package.metadata.release.pre-release-replacements]] 71 | file = "CHANGELOG.md" 72 | search = "\\.\\.\\.HEAD" 73 | replace="...{{tag_name}}" 74 | exactly = 1 75 | # END SECTION, do not comment out the replacement below this, and do not reorder them 76 | 77 | [[package.metadata.release.pre-release-replacements]] 78 | file="CHANGELOG.md" 79 | search="" 80 | replace="\n[Unreleased]: https://github.com/yaahc/{{crate_name}}/compare/{{tag_name}}...HEAD" 81 | exactly=1 82 | 83 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | name: Continuous integration 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | rust: 16 | - stable 17 | - 1.56.0 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: ${{ matrix.rust }} 23 | override: true 24 | - name: Downgrade deps for MSRV 25 | if: ${{ matrix.rust == '1.56.0' }} 26 | run: | 27 | cargo update -p libc --precise 0.2.160 28 | cargo update -p pretty_assertions --precise 1.4.0 29 | cargo update -p proc-macro2 --precise 1.0.101 30 | cargo update -p quote --precise 1.0.40 31 | cargo update -p glob --precise 0.3.2 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: check 35 | 36 | test: 37 | name: Test Suite 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | rust: 42 | - stable 43 | - nightly 44 | steps: 45 | - uses: actions/checkout@v1 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: ${{ matrix.rust }} 49 | override: true 50 | - uses: Swatinem/rust-cache@v1 51 | - name: Install cargo-nextest 52 | uses: baptiste0928/cargo-install@v1 53 | with: 54 | crate: cargo-nextest 55 | version: 0.9 56 | - uses: actions-rs/cargo@v1 57 | with: 58 | command: nextest 59 | args: run 60 | - uses: actions-rs/cargo@v1 61 | with: 62 | command: nextest 63 | args: run --no-default-features 64 | - uses: actions-rs/cargo@v1 65 | with: 66 | command: test 67 | args: --doc 68 | 69 | test-msrv: 70 | name: msrv Test Suite 71 | runs-on: ubuntu-latest 72 | strategy: 73 | matrix: 74 | rust: 75 | - 1.56.0 76 | steps: 77 | - uses: actions/checkout@v1 78 | - uses: actions-rs/toolchain@v1 79 | with: 80 | toolchain: ${{ matrix.rust }} 81 | override: true 82 | - uses: Swatinem/rust-cache@v1 83 | 84 | - name: Downgrade deps for MSRV 85 | run: | 86 | cargo update -p libc --precise 0.2.160 87 | cargo update -p pretty_assertions --precise 1.4.0 88 | cargo update -p proc-macro2 --precise 1.0.101 89 | cargo update -p quote --precise 1.0.40 90 | cargo update -p glob --precise 0.3.2 91 | - uses: actions-rs/cargo@v1 92 | with: 93 | command: test 94 | - uses: actions-rs/cargo@v1 95 | with: 96 | command: test 97 | args: --no-default-features 98 | 99 | fmt: 100 | name: Rustfmt 101 | runs-on: ubuntu-latest 102 | strategy: 103 | matrix: 104 | rust: 105 | - stable 106 | - 1.56.0 107 | steps: 108 | - uses: actions/checkout@v1 109 | - uses: actions-rs/toolchain@v1 110 | with: 111 | toolchain: ${{ matrix.rust }} 112 | override: true 113 | - run: rustup component add rustfmt 114 | - uses: actions-rs/cargo@v1 115 | with: 116 | command: fmt 117 | args: --all -- --check 118 | 119 | clippy: 120 | name: Clippy 121 | runs-on: ubuntu-latest 122 | strategy: 123 | matrix: 124 | rust: 125 | - stable 126 | steps: 127 | - uses: actions/checkout@v1 128 | - uses: actions-rs/toolchain@v1 129 | with: 130 | toolchain: ${{ matrix.rust }} 131 | override: true 132 | - run: rustup component add clippy 133 | - uses: actions-rs/cargo@v1 134 | with: 135 | command: clippy 136 | args: -- -D warnings 137 | -------------------------------------------------------------------------------- /tests/happy.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | 3 | #[cfg(feature = "std")] 4 | use std::path::PathBuf; 5 | 6 | #[derive(Display)] 7 | /// Just a basic struct {thing} 8 | struct HappyStruct { 9 | thing: &'static str, 10 | } 11 | 12 | #[derive(Display)] 13 | #[ignore_extra_doc_attributes] 14 | /// Just a basic struct {thing} 15 | /// and this line should get ignored 16 | struct HappyStruct2 { 17 | thing: &'static str, 18 | } 19 | 20 | #[derive(Display)] 21 | enum Happy { 22 | /// I really like Variant1 23 | Variant1, 24 | /// Variant2 is pretty swell 2 25 | Variant2, 26 | /// Variant3 is okay {sometimes} 27 | Variant3 { sometimes: &'static str }, 28 | /** 29 | * Variant4 wants to have a lot of lines 30 | * 31 | * Lets see how this works out for it 32 | */ 33 | Variant4, 34 | /// Variant5 has a parameter {0} and some regular comments 35 | // A regular comment that won't get picked 36 | Variant5(u32), 37 | 38 | /// The path {0} 39 | #[cfg(feature = "std")] 40 | Variant6(PathBuf), 41 | 42 | /// These docs are ignored 43 | #[displaydoc("Variant7 has a parameter {0} and uses #[displaydoc]")] 44 | /// These docs are also ignored 45 | Variant7(u32), 46 | } 47 | 48 | // Used for testing indented doc comments 49 | mod inner_mod { 50 | use super::Display; 51 | 52 | #[derive(Display)] 53 | pub enum InnerHappy { 54 | /// I really like Variant1 55 | Variant1, 56 | /// Variant2 is pretty swell 2 57 | Variant2, 58 | /// Variant3 is okay {sometimes} 59 | Variant3 { sometimes: &'static str }, 60 | /** 61 | * Variant4 wants to have a lot of lines 62 | * 63 | * Lets see how this works out for it 64 | */ 65 | Variant4, 66 | /// Variant5 has a parameter {0} and some regular comments 67 | // A regular comment that won't get picked 68 | Variant5(u32), 69 | 70 | /** what happens if we 71 | * put text on the first line? 72 | */ 73 | Variant6, 74 | 75 | /** 76 | what happens if we don't use *? 77 | */ 78 | Variant7, 79 | 80 | /** 81 | * 82 | * what about extra new lines? 83 | */ 84 | Variant8, 85 | } 86 | } 87 | 88 | fn assert_display(input: T, expected: &'static str) { 89 | let out = format!("{}", input); 90 | assert_eq!(expected, out); 91 | } 92 | 93 | #[test] 94 | fn does_it_print() { 95 | assert_display(Happy::Variant1, "I really like Variant1"); 96 | assert_display(Happy::Variant2, "Variant2 is pretty swell 2"); 97 | assert_display(Happy::Variant3 { sometimes: "hi" }, "Variant3 is okay hi"); 98 | assert_display( 99 | Happy::Variant4, 100 | "Variant4 wants to have a lot of lines\n\nLets see how this works out for it", 101 | ); 102 | assert_display( 103 | Happy::Variant5(2), 104 | "Variant5 has a parameter 2 and some regular comments", 105 | ); 106 | assert_display( 107 | Happy::Variant7(2), 108 | "Variant7 has a parameter 2 and uses #[displaydoc]", 109 | ); 110 | assert_display(HappyStruct { thing: "hi" }, "Just a basic struct hi"); 111 | 112 | assert_display(HappyStruct2 { thing: "hi2" }, "Just a basic struct hi2"); 113 | 114 | assert_display(inner_mod::InnerHappy::Variant1, "I really like Variant1"); 115 | assert_display( 116 | inner_mod::InnerHappy::Variant2, 117 | "Variant2 is pretty swell 2", 118 | ); 119 | assert_display( 120 | inner_mod::InnerHappy::Variant3 { sometimes: "hi" }, 121 | "Variant3 is okay hi", 122 | ); 123 | assert_display( 124 | inner_mod::InnerHappy::Variant4, 125 | "Variant4 wants to have a lot of lines\n\nLets see how this works out for it", 126 | ); 127 | assert_display( 128 | inner_mod::InnerHappy::Variant5(2), 129 | "Variant5 has a parameter 2 and some regular comments", 130 | ); 131 | assert_display( 132 | inner_mod::InnerHappy::Variant6, 133 | "what happens if we\nput text on the first line?", 134 | ); 135 | assert_display( 136 | inner_mod::InnerHappy::Variant7, 137 | "what happens if we don\'t use *?", 138 | ); 139 | assert_display( 140 | inner_mod::InnerHappy::Variant8, 141 | "what about extra new lines?", 142 | ); 143 | } 144 | 145 | #[test] 146 | #[cfg(feature = "std")] 147 | fn does_it_print_path() { 148 | assert_display( 149 | Happy::Variant6(PathBuf::from("/var/log/happy")), 150 | "The path /var/log/happy", 151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | use crate::attr::Display; 2 | use proc_macro2::TokenStream; 3 | use quote::quote_spanned; 4 | use syn::{Ident, LitStr}; 5 | 6 | macro_rules! peek_next { 7 | ($read:ident) => { 8 | match $read.chars().next() { 9 | Some(next) => next, 10 | None => return, 11 | } 12 | }; 13 | } 14 | 15 | impl Display { 16 | // Transform `"error {var}"` to `"error {}", var`. 17 | pub(crate) fn expand_shorthand(&mut self) { 18 | let span = self.fmt.span(); 19 | let fmt = self.fmt.value(); 20 | let mut read = fmt.as_str(); 21 | let mut out = String::new(); 22 | let mut args = TokenStream::new(); 23 | 24 | while let Some(brace) = read.find('{') { 25 | out += &read[..=brace]; 26 | read = &read[brace + 1..]; 27 | 28 | // skip cases where we find a {{ 29 | if read.starts_with('{') { 30 | out.push('{'); 31 | read = &read[1..]; 32 | continue; 33 | } 34 | 35 | let next = peek_next!(read); 36 | 37 | let var = match next { 38 | '0'..='9' => take_int(&mut read), 39 | 'a'..='z' | 'A'..='Z' | '_' => take_ident(&mut read), 40 | _ => return, 41 | }; 42 | 43 | let ident = Ident::new(&var, span); 44 | 45 | let next = peek_next!(read); 46 | 47 | let arg = if cfg!(feature = "std") && next == '}' { 48 | quote_spanned!(span=> , #ident.__displaydoc_display()) 49 | } else { 50 | quote_spanned!(span=> , #ident) 51 | }; 52 | 53 | args.extend(arg); 54 | } 55 | 56 | out += read; 57 | self.fmt = LitStr::new(&out, self.fmt.span()); 58 | self.args = args; 59 | } 60 | } 61 | 62 | fn take_int(read: &mut &str) -> String { 63 | let mut int = String::new(); 64 | int.push('_'); 65 | for (i, ch) in read.char_indices() { 66 | match ch { 67 | '0'..='9' => int.push(ch), 68 | _ => { 69 | *read = &read[i..]; 70 | break; 71 | } 72 | } 73 | } 74 | int 75 | } 76 | 77 | fn take_ident(read: &mut &str) -> String { 78 | let mut ident = String::new(); 79 | for (i, ch) in read.char_indices() { 80 | match ch { 81 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch), 82 | _ => { 83 | *read = &read[i..]; 84 | break; 85 | } 86 | } 87 | } 88 | ident 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | use pretty_assertions::assert_eq; 95 | use proc_macro2::Span; 96 | 97 | fn assert(input: &str, fmt: &str, args: &str) { 98 | let mut display = Display { 99 | fmt: LitStr::new(input, Span::call_site()), 100 | args: TokenStream::new(), 101 | }; 102 | display.expand_shorthand(); 103 | assert_eq!(fmt, display.fmt.value()); 104 | assert_eq!(args, display.args.to_string()); 105 | } 106 | 107 | #[test] 108 | fn test_expand() { 109 | assert("fn main() {{ }}", "fn main() {{ }}", ""); 110 | } 111 | 112 | #[test] 113 | #[cfg_attr(not(feature = "std"), ignore)] 114 | fn test_std_expand() { 115 | assert( 116 | "{v} {v:?} {0} {0:?}", 117 | "{} {:?} {} {:?}", 118 | ", v . __displaydoc_display () , v , _0 . __displaydoc_display () , _0", 119 | ); 120 | assert("error {var}", "error {}", ", var . __displaydoc_display ()"); 121 | 122 | assert( 123 | "error {var1}", 124 | "error {}", 125 | ", var1 . __displaydoc_display ()", 126 | ); 127 | 128 | assert( 129 | "error {var1var}", 130 | "error {}", 131 | ", var1var . __displaydoc_display ()", 132 | ); 133 | 134 | assert( 135 | "The path {0}", 136 | "The path {}", 137 | ", _0 . __displaydoc_display ()", 138 | ); 139 | assert("The path {0:?}", "The path {:?}", ", _0"); 140 | } 141 | 142 | #[test] 143 | #[cfg_attr(feature = "std", ignore)] 144 | fn test_nostd_expand() { 145 | assert( 146 | "{v} {v:?} {0} {0:?}", 147 | "{} {:?} {} {:?}", 148 | ", v , v , _0 , _0", 149 | ); 150 | assert("error {var}", "error {}", ", var"); 151 | 152 | assert("The path {0}", "The path {}", ", _0"); 153 | assert("The path {0:?}", "The path {:?}", ", _0"); 154 | 155 | assert("error {var1}", "error {}", ", var1"); 156 | 157 | assert("error {var1var}", "error {}", ", var1var"); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/attr.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::{Attribute, LitStr, Meta, Result}; 4 | 5 | #[derive(Clone)] 6 | pub(crate) struct Display { 7 | pub(crate) fmt: LitStr, 8 | pub(crate) args: TokenStream, 9 | } 10 | 11 | pub(crate) struct VariantDisplay { 12 | pub(crate) r#enum: Option, 13 | pub(crate) variant: Display, 14 | } 15 | 16 | impl ToTokens for Display { 17 | fn to_tokens(&self, tokens: &mut TokenStream) { 18 | let fmt = &self.fmt; 19 | let args = &self.args; 20 | tokens.extend(quote! { 21 | write!(formatter, #fmt #args) 22 | }); 23 | } 24 | } 25 | 26 | impl ToTokens for VariantDisplay { 27 | fn to_tokens(&self, tokens: &mut TokenStream) { 28 | if let Some(ref r#enum) = self.r#enum { 29 | r#enum.to_tokens(tokens); 30 | tokens.extend(quote! { ?; write!(formatter, ": ")?; }); 31 | } 32 | self.variant.to_tokens(tokens); 33 | } 34 | } 35 | 36 | pub(crate) struct AttrsHelper { 37 | ignore_extra_doc_attributes: bool, 38 | prefix_enum_doc_attributes: bool, 39 | } 40 | 41 | impl AttrsHelper { 42 | pub(crate) fn new(attrs: &[Attribute]) -> Self { 43 | let ignore_extra_doc_attributes = attrs 44 | .iter() 45 | .any(|attr| attr.path().is_ident("ignore_extra_doc_attributes")); 46 | let prefix_enum_doc_attributes = attrs 47 | .iter() 48 | .any(|attr| attr.path().is_ident("prefix_enum_doc_attributes")); 49 | 50 | Self { 51 | ignore_extra_doc_attributes, 52 | prefix_enum_doc_attributes, 53 | } 54 | } 55 | 56 | pub(crate) fn display(&self, attrs: &[Attribute]) -> Result> { 57 | let displaydoc_attr = attrs.iter().find(|attr| attr.path().is_ident("displaydoc")); 58 | 59 | if let Some(displaydoc_attr) = displaydoc_attr { 60 | let lit = displaydoc_attr 61 | .parse_args() 62 | .expect("#[displaydoc(\"foo\")] must contain string arguments"); 63 | let mut display = Display { 64 | fmt: lit, 65 | args: TokenStream::new(), 66 | }; 67 | 68 | display.expand_shorthand(); 69 | return Ok(Some(display)); 70 | } 71 | 72 | let num_doc_attrs = attrs 73 | .iter() 74 | .filter(|attr| attr.path().is_ident("doc")) 75 | .count(); 76 | 77 | if !self.ignore_extra_doc_attributes && num_doc_attrs > 1 { 78 | 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."); 79 | } 80 | 81 | for attr in attrs { 82 | if attr.path().is_ident("doc") { 83 | let lit = match &attr.meta { 84 | Meta::NameValue(syn::MetaNameValue { 85 | value: 86 | syn::Expr::Lit(syn::ExprLit { 87 | lit: syn::Lit::Str(lit), 88 | .. 89 | }), 90 | .. 91 | }) => lit, 92 | _ => unimplemented!(), 93 | }; 94 | 95 | // Make an attempt at cleaning up multiline doc comments. 96 | let doc_str = lit 97 | .value() 98 | .lines() 99 | .map(|line| line.trim().trim_start_matches('*').trim()) 100 | .collect::>() 101 | .join("\n"); 102 | 103 | let lit = LitStr::new(doc_str.trim(), lit.span()); 104 | 105 | let mut display = Display { 106 | fmt: lit, 107 | args: TokenStream::new(), 108 | }; 109 | 110 | display.expand_shorthand(); 111 | return Ok(Some(display)); 112 | } 113 | } 114 | 115 | Ok(None) 116 | } 117 | 118 | pub(crate) fn display_with_input( 119 | &self, 120 | r#enum: &[Attribute], 121 | variant: &[Attribute], 122 | ) -> Result> { 123 | let r#enum = if self.prefix_enum_doc_attributes { 124 | let result = self 125 | .display(r#enum)? 126 | .expect("Missing doc comment on enum with #[prefix_enum_doc_attributes]. Please remove the attribute or add a doc comment to the enum itself."); 127 | 128 | Some(result) 129 | } else { 130 | None 131 | }; 132 | 133 | Ok(self 134 | .display(variant)? 135 | .map(|variant| VariantDisplay { r#enum, variant })) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | derive(Display) /// `From` 2 | =============== 3 | 4 | [![Latest Version](https://img.shields.io/crates/v/displaydoc.svg)](https://crates.io/crates/displaydoc) 5 | [![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/displaydoc) 6 | 7 | This library provides a convenient derive macro for the standard library's 8 | [`core::fmt::Display`] trait. 9 | 10 | [`core::fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html 11 | 12 | ```toml 13 | [dependencies] 14 | displaydoc = "0.2" 15 | ``` 16 | 17 | *Compiler support: requires rustc 1.56+* 18 | 19 |
20 | 21 | ### Example 22 | 23 | *Demonstration alongside the [`Error`][std::error::Error] derive macro from [`thiserror`](https://docs.rs/thiserror/1.0.25/thiserror/index.html), 24 | to propagate source locations from [`io::Error`][std::io::Error] with the `#[source]` attribute:* 25 | ```rust 26 | use std::io; 27 | use displaydoc::Display; 28 | use thiserror::Error; 29 | 30 | #[derive(Display, Error, Debug)] 31 | pub enum DataStoreError { 32 | /// data store disconnected 33 | Disconnect(#[source] io::Error), 34 | /// the data for key `{0}` is not available 35 | Redaction(String), 36 | /// invalid header (expected {expected:?}, found {found:?}) 37 | InvalidHeader { 38 | expected: String, 39 | found: String, 40 | }, 41 | /// unknown data store error 42 | Unknown, 43 | } 44 | 45 | let error = DataStoreError::Redaction("CLASSIFIED CONTENT".to_string()); 46 | assert!("the data for key `CLASSIFIED CONTENT` is not available" == &format!("{}", error)); 47 | ``` 48 | *Note that although [`io::Error`][std::io::Error] implements `Display`, we do not add it to the 49 | generated message for `DataStoreError::Disconnect`, since it is already made available via 50 | `#[source]`. See further context on avoiding duplication in error reports at the rust blog 51 | [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).* 52 | 53 |
54 | 55 | ### Details 56 | 57 | - A `fmt::Display` impl is generated for your enum if you provide 58 | a docstring comment on each variant as shown above in the example. The 59 | `Display` derive macro supports a shorthand for interpolating fields from 60 | the error: 61 | - `/// {var}` ⟶ `write!("{}", self.var)` 62 | - `/// {0}` ⟶ `write!("{}", self.0)` 63 | - `/// {var:?}` ⟶ `write!("{:?}", self.var)` 64 | - `/// {0:?}` ⟶ `write!("{:?}", self.0)` 65 | - This also works with structs and [generic types][crate::Display#generic-type-parameters]: 66 | ```rust 67 | /// oh no, an error: {0} 68 | #[derive(Display)] 69 | pub struct Error(pub E); 70 | 71 | let error: Error<&str> = Error("muahaha i am an error"); 72 | assert!("oh no, an error: muahaha i am an error" == &format!("{}", error)); 73 | ``` 74 | 75 | - Two optional attributes can be added to your types next to the derive: 76 | 77 | - `#[ignore_extra_doc_attributes]` makes the macro ignore any doc 78 | comment attributes (or `///` lines) after the first. Multi-line 79 | comments using `///` are otherwise treated as an error, so use this 80 | attribute or consider switching to block doc comments (`/** */`). 81 | 82 | - `#[prefix_enum_doc_attributes]` combines the doc comment message on 83 | your enum itself with the messages for each variant, in the format 84 | “enum: variant”. When added to an enum, the doc comment on the enum 85 | becomes mandatory. When added to any other type, it has no effect. 86 | 87 | - In case you want to have an independent doc comment, the 88 | `#[displaydoc("...")` attribute may be used on the variant or struct to 89 | override it. 90 | 91 |
92 | 93 | ### FAQ 94 | 95 | 1. **Is this crate `no_std` compatible?** 96 | * 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`. 97 | 98 | 2. **Does this crate work with `Path` and `PathBuf` via the `Display` trait?** 99 | * 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! 100 | 101 | 102 | #### License 103 | 104 | 105 | Licensed under either of Apache License, Version 106 | 2.0 or MIT license at your option. 107 | 108 | 109 |
110 | 111 | 112 | Unless you explicitly state otherwise, any contribution intentionally submitted 113 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 114 | be dual licensed as above, without any additional terms or conditions. 115 | 116 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This library provides a convenient derive macro for the standard library's 2 | //! [`core::fmt::Display`] trait. 3 | //! 4 | //! [`core::fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html 5 | //! 6 | //! ```toml 7 | //! [dependencies] 8 | //! displaydoc = "0.2" 9 | //! ``` 10 | //! 11 | //! *Compiler support: requires rustc 1.56+* 12 | //! 13 | //!
14 | //! 15 | //! ## Example 16 | //! 17 | //! *Demonstration alongside the [`Error`][std::error::Error] derive macro from [`thiserror`](https://docs.rs/thiserror/1.0.25/thiserror/index.html), 18 | //! to propagate source locations from [`io::Error`][std::io::Error] with the `#[source]` attribute:* 19 | //! ```rust 20 | //! use std::io; 21 | //! use displaydoc::Display; 22 | //! use thiserror::Error; 23 | //! 24 | //! #[derive(Display, Error, Debug)] 25 | //! pub enum DataStoreError { 26 | //! /// data store disconnected 27 | //! Disconnect(#[source] io::Error), 28 | //! /// the data for key `{0}` is not available 29 | //! Redaction(String), 30 | //! /// invalid header (expected {expected:?}, found {found:?}) 31 | //! InvalidHeader { 32 | //! expected: String, 33 | //! found: String, 34 | //! }, 35 | //! /// unknown data store error 36 | //! Unknown, 37 | //! } 38 | //! 39 | //! let error = DataStoreError::Redaction("CLASSIFIED CONTENT".to_string()); 40 | //! assert!("the data for key `CLASSIFIED CONTENT` is not available" == &format!("{}", error)); 41 | //! ``` 42 | //! *Note that although [`io::Error`][std::io::Error] implements `Display`, we do not add it to the 43 | //! generated message for `DataStoreError::Disconnect`, since it is already made available via 44 | //! `#[source]`. See further context on avoiding duplication in error reports at the rust blog 45 | //! [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).* 46 | //! 47 | //!
48 | //! 49 | //! ## Details 50 | //! 51 | //! - A `fmt::Display` impl is generated for your enum if you provide 52 | //! a docstring comment on each variant as shown above in the example. The 53 | //! `Display` derive macro supports a shorthand for interpolating fields from 54 | //! the error: 55 | //! - `/// {var}` ⟶ `write!("{}", self.var)` 56 | //! - `/// {0}` ⟶ `write!("{}", self.0)` 57 | //! - `/// {var:?}` ⟶ `write!("{:?}", self.var)` 58 | //! - `/// {0:?}` ⟶ `write!("{:?}", self.0)` 59 | //! - This also works with structs and [generic types][crate::Display#generic-type-parameters]: 60 | //! ```rust 61 | //! # use displaydoc::Display; 62 | //! /// oh no, an error: {0} 63 | //! #[derive(Display)] 64 | //! pub struct Error(pub E); 65 | //! 66 | //! let error: Error<&str> = Error("muahaha i am an error"); 67 | //! assert!("oh no, an error: muahaha i am an error" == &format!("{}", error)); 68 | //! ``` 69 | //! 70 | //! - Two optional attributes can be added to your types next to the derive: 71 | //! 72 | //! - `#[ignore_extra_doc_attributes]` makes the macro ignore any doc 73 | //! comment attributes (or `///` lines) after the first. Multi-line 74 | //! comments using `///` are otherwise treated as an error, so use this 75 | //! attribute or consider switching to block doc comments (`/** */`). 76 | //! 77 | //! - `#[prefix_enum_doc_attributes]` combines the doc comment message on 78 | //! your enum itself with the messages for each variant, in the format 79 | //! “enum: variant”. When added to an enum, the doc comment on the enum 80 | //! becomes mandatory. When added to any other type, it has no effect. 81 | //! 82 | //! - In case you want to have an independent doc comment, the 83 | //! `#[displaydoc("...")` attribute may be used on the variant or struct to 84 | //! override it. 85 | //! 86 | //!
87 | //! 88 | //! ## FAQ 89 | //! 90 | //! 1. **Is this crate `no_std` compatible?** 91 | //! * 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`. 92 | //! 93 | //! 2. **Does this crate work with `Path` and `PathBuf` via the `Display` trait?** 94 | //! * 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! 95 | #![doc(html_root_url = "https://docs.rs/displaydoc/0.2.3")] 96 | #![cfg_attr(docsrs, feature(doc_cfg))] 97 | #![warn( 98 | rust_2018_idioms, 99 | unreachable_pub, 100 | bad_style, 101 | dead_code, 102 | improper_ctypes, 103 | non_shorthand_field_patterns, 104 | no_mangle_generic_items, 105 | overflowing_literals, 106 | path_statements, 107 | patterns_in_fns_without_body, 108 | unconditional_recursion, 109 | unused, 110 | unused_allocation, 111 | unused_comparisons, 112 | unused_parens, 113 | while_true 114 | )] 115 | #![allow(clippy::try_err)] 116 | 117 | #[allow(unused_extern_crates)] 118 | extern crate proc_macro; 119 | 120 | mod attr; 121 | mod expand; 122 | mod fmt; 123 | 124 | use proc_macro::TokenStream; 125 | use syn::{parse_macro_input, DeriveInput}; 126 | 127 | /// [Custom `#[derive(...)]` macro](https://doc.rust-lang.org/edition-guide/rust-2018/macros/custom-derive.html) 128 | /// for implementing [`fmt::Display`][core::fmt::Display] via doc comment attributes. 129 | /// 130 | /// ### Generic Type Parameters 131 | /// 132 | /// Type parameters to an enum or struct using this macro should *not* need to 133 | /// have an explicit `Display` constraint at the struct or enum definition 134 | /// site. A `Display` implementation for the `derive`d struct or enum is 135 | /// generated assuming each type parameter implements `Display`, but that should 136 | /// be possible without adding the constraint to the struct definition itself: 137 | /// ```rust 138 | /// use displaydoc::Display; 139 | /// 140 | /// /// oh no, an error: {0} 141 | /// #[derive(Display)] 142 | /// pub struct Error(pub E); 143 | /// 144 | /// // No need to require `E: Display`, since `displaydoc::Display` adds that implicitly. 145 | /// fn generate_error(e: E) -> Error { Error(e) } 146 | /// 147 | /// assert!("oh no, an error: muahaha" == &format!("{}", generate_error("muahaha"))); 148 | /// ``` 149 | /// 150 | /// ### Using [`Debug`][core::fmt::Debug] Implementations with Type Parameters 151 | /// However, if a type parameter must instead be constrained with the 152 | /// [`Debug`][core::fmt::Debug] trait so that some field may be printed with 153 | /// `{:?}`, that constraint must currently still also be specified redundantly 154 | /// at the struct or enum definition site. If a struct or enum field is being 155 | /// formatted with `{:?}` via [`displaydoc`][crate], and a generic type 156 | /// parameter must implement `Debug` to do that, then that struct or enum 157 | /// definition will need to propagate the `Debug` constraint to every type 158 | /// parameter it's instantiated with: 159 | /// ```rust 160 | /// use core::fmt::Debug; 161 | /// use displaydoc::Display; 162 | /// 163 | /// /// oh no, an error: {0:?} 164 | /// #[derive(Display)] 165 | /// pub struct Error(pub E); 166 | /// 167 | /// // `E: Debug` now has to propagate to callers. 168 | /// fn generate_error(e: E) -> Error { Error(e) } 169 | /// 170 | /// assert!("oh no, an error: \"cool\"" == &format!("{}", generate_error("cool"))); 171 | /// 172 | /// // Try this with a struct that doesn't impl `Display` at all, unlike `str`. 173 | /// #[derive(Debug)] 174 | /// pub struct Oh; 175 | /// assert!("oh no, an error: Oh" == &format!("{}", generate_error(Oh))); 176 | /// ``` 177 | #[proc_macro_derive( 178 | Display, 179 | attributes(ignore_extra_doc_attributes, prefix_enum_doc_attributes, displaydoc) 180 | )] 181 | pub fn derive_error(input: TokenStream) -> TokenStream { 182 | let input = parse_macro_input!(input as DeriveInput); 183 | expand::derive(&input) 184 | .unwrap_or_else(|err| err.to_compile_error()) 185 | .into() 186 | } 187 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/expand.rs: -------------------------------------------------------------------------------- 1 | use super::attr::AttrsHelper; 2 | use proc_macro2::{Span, TokenStream}; 3 | use quote::{format_ident, quote}; 4 | use syn::{ 5 | punctuated::Punctuated, 6 | token::{Colon, Comma, PathSep, Plus, Where}, 7 | Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Generics, Ident, Path, PathArguments, 8 | PathSegment, PredicateType, Result, TraitBound, TraitBoundModifier, Type, TypeParam, 9 | TypeParamBound, TypePath, WhereClause, WherePredicate, 10 | }; 11 | 12 | use std::collections::BTreeMap; 13 | 14 | pub(crate) fn derive(input: &DeriveInput) -> Result { 15 | let impls = match &input.data { 16 | Data::Struct(data) => impl_struct(input, data), 17 | Data::Enum(data) => impl_enum(input, data), 18 | Data::Union(_) => Err(Error::new_spanned(input, "Unions are not supported")), 19 | }?; 20 | 21 | let helpers = specialization(); 22 | Ok(quote! { 23 | #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] 24 | const _: () = { 25 | #helpers 26 | #impls 27 | }; 28 | }) 29 | } 30 | 31 | #[cfg(feature = "std")] 32 | fn specialization() -> TokenStream { 33 | quote! { 34 | trait DisplayToDisplayDoc { 35 | fn __displaydoc_display(&self) -> Self; 36 | } 37 | 38 | impl DisplayToDisplayDoc for &T { 39 | fn __displaydoc_display(&self) -> Self { 40 | self 41 | } 42 | } 43 | 44 | // If the `std` feature gets enabled we want to ensure that any crate 45 | // using displaydoc can still reference the std crate, which is already 46 | // being compiled in by whoever enabled the `std` feature in 47 | // `displaydoc`, even if the crates using displaydoc are no_std. 48 | extern crate std; 49 | 50 | trait PathToDisplayDoc { 51 | fn __displaydoc_display(&self) -> std::path::Display<'_>; 52 | } 53 | 54 | impl PathToDisplayDoc for std::path::Path { 55 | fn __displaydoc_display(&self) -> std::path::Display<'_> { 56 | self.display() 57 | } 58 | } 59 | 60 | impl PathToDisplayDoc for std::path::PathBuf { 61 | fn __displaydoc_display(&self) -> std::path::Display<'_> { 62 | self.display() 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[cfg(not(feature = "std"))] 69 | fn specialization() -> TokenStream { 70 | quote! {} 71 | } 72 | 73 | fn impl_struct(input: &DeriveInput, data: &DataStruct) -> Result { 74 | let ty = &input.ident; 75 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 76 | let where_clause = generate_where_clause(&input.generics, where_clause); 77 | 78 | let helper = AttrsHelper::new(&input.attrs); 79 | 80 | let display = helper.display(&input.attrs)?.map(|display| { 81 | let pat = match &data.fields { 82 | Fields::Named(fields) => { 83 | let var = fields.named.iter().map(|field| &field.ident); 84 | quote!(Self { #(#var),* }) 85 | } 86 | Fields::Unnamed(fields) => { 87 | let var = (0..fields.unnamed.len()).map(|i| format_ident!("_{}", i)); 88 | quote!(Self(#(#var),*)) 89 | } 90 | Fields::Unit => quote!(_), 91 | }; 92 | quote! { 93 | impl #impl_generics ::core::fmt::Display for #ty #ty_generics #where_clause { 94 | fn fmt(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 95 | // NB: This destructures the fields of `self` into named 96 | // variables (for unnamed fields, it uses _0, _1, etc as 97 | // above). The `#[allow(unused_variables, unused_assignments)]` 98 | // section means it doesn't have to parse the individual field 99 | // references out of the docstring. 100 | #[allow(unused_variables, unused_assignments)] 101 | let #pat = self; 102 | #display 103 | } 104 | } 105 | } 106 | }); 107 | 108 | Ok(quote! { #display }) 109 | } 110 | 111 | /// Create a `where` predicate for `ident`, without any [bound][TypeParamBound]s yet. 112 | fn new_empty_where_type_predicate(ident: Ident) -> PredicateType { 113 | let mut path_segments = Punctuated::::new(); 114 | path_segments.push_value(PathSegment { 115 | ident, 116 | arguments: PathArguments::None, 117 | }); 118 | PredicateType { 119 | lifetimes: None, 120 | bounded_ty: Type::Path(TypePath { 121 | qself: None, 122 | path: Path { 123 | leading_colon: None, 124 | segments: path_segments, 125 | }, 126 | }), 127 | colon_token: Colon { 128 | spans: [Span::call_site()], 129 | }, 130 | bounds: Punctuated::::new(), 131 | } 132 | } 133 | 134 | /// Create a `where` clause that we can add [WherePredicate]s to. 135 | fn new_empty_where_clause() -> WhereClause { 136 | WhereClause { 137 | where_token: Where { 138 | span: Span::call_site(), 139 | }, 140 | predicates: Punctuated::::new(), 141 | } 142 | } 143 | 144 | enum UseGlobalPrefix { 145 | LeadingColon, 146 | #[allow(dead_code)] 147 | NoLeadingColon, 148 | } 149 | 150 | /// Create a path with segments composed of [Idents] *without* any [PathArguments]. 151 | fn join_paths(name_segments: &[&str], use_global_prefix: UseGlobalPrefix) -> Path { 152 | let mut segments = Punctuated::::new(); 153 | assert!(!name_segments.is_empty()); 154 | segments.push_value(PathSegment { 155 | ident: Ident::new(name_segments[0], Span::call_site()), 156 | arguments: PathArguments::None, 157 | }); 158 | for name in name_segments[1..].iter() { 159 | segments.push_punct(PathSep { 160 | spans: [Span::call_site(), Span::mixed_site()], 161 | }); 162 | segments.push_value(PathSegment { 163 | ident: Ident::new(name, Span::call_site()), 164 | arguments: PathArguments::None, 165 | }); 166 | } 167 | Path { 168 | leading_colon: match use_global_prefix { 169 | UseGlobalPrefix::LeadingColon => Some(PathSep { 170 | spans: [Span::call_site(), Span::mixed_site()], 171 | }), 172 | UseGlobalPrefix::NoLeadingColon => None, 173 | }, 174 | segments, 175 | } 176 | } 177 | 178 | /// Push `new_type_predicate` onto the end of `where_clause`. 179 | fn append_where_clause_type_predicate( 180 | where_clause: &mut WhereClause, 181 | new_type_predicate: PredicateType, 182 | ) { 183 | // Push a comma at the end if there are already any `where` predicates. 184 | if !where_clause.predicates.is_empty() { 185 | where_clause.predicates.push_punct(Comma { 186 | spans: [Span::call_site()], 187 | }); 188 | } 189 | where_clause 190 | .predicates 191 | .push_value(WherePredicate::Type(new_type_predicate)); 192 | } 193 | 194 | /// Add a requirement for [core::fmt::Display] to a `where` predicate for some type. 195 | fn add_display_constraint_to_type_predicate( 196 | predicate_that_needs_a_display_impl: &mut PredicateType, 197 | ) { 198 | // Create a `Path` of `::core::fmt::Display`. 199 | let display_path = join_paths(&["core", "fmt", "Display"], UseGlobalPrefix::LeadingColon); 200 | 201 | let display_bound = TypeParamBound::Trait(TraitBound { 202 | paren_token: None, 203 | modifier: TraitBoundModifier::None, 204 | lifetimes: None, 205 | path: display_path, 206 | }); 207 | if !predicate_that_needs_a_display_impl.bounds.is_empty() { 208 | predicate_that_needs_a_display_impl.bounds.push_punct(Plus { 209 | spans: [Span::call_site()], 210 | }); 211 | } 212 | 213 | predicate_that_needs_a_display_impl 214 | .bounds 215 | .push_value(display_bound); 216 | } 217 | 218 | /// Map each declared generic type parameter to the set of all trait boundaries declared on it. 219 | /// 220 | /// These boundaries may come from the declaration site: 221 | /// pub enum E { ... } 222 | /// or a `where` clause after the parameter declarations: 223 | /// pub enum E where T: MyTrait { ... } 224 | /// This method will return the boundaries from both of those cases. 225 | fn extract_trait_constraints_from_source( 226 | where_clause: &WhereClause, 227 | type_params: &[&TypeParam], 228 | ) -> BTreeMap> { 229 | // Add trait bounds provided at the declaration site of type parameters for the struct/enum. 230 | let mut param_constraint_mapping: BTreeMap> = type_params 231 | .iter() 232 | .map(|type_param| { 233 | let trait_bounds: Vec = type_param 234 | .bounds 235 | .iter() 236 | .flat_map(|bound| match bound { 237 | TypeParamBound::Trait(trait_bound) => Some(trait_bound), 238 | _ => None, 239 | }) 240 | .cloned() 241 | .collect(); 242 | (type_param.ident.clone(), trait_bounds) 243 | }) 244 | .collect(); 245 | 246 | // Add trait bounds from `where` clauses, which may be type parameters or types containing 247 | // those parameters. 248 | for predicate in where_clause.predicates.iter() { 249 | // We only care about type and not lifetime constraints here. 250 | if let WherePredicate::Type(ref pred_ty) = predicate { 251 | let ident = match &pred_ty.bounded_ty { 252 | Type::Path(TypePath { path, qself: None }) => match path.get_ident() { 253 | None => continue, 254 | Some(ident) => ident, 255 | }, 256 | _ => continue, 257 | }; 258 | // We ignore any type constraints that aren't direct references to type 259 | // parameters of the current enum of struct definition. No types can be 260 | // constrained in a `where` clause unless they are a type parameter or a generic 261 | // type instantiated with one of the type parameters, so by only allowing single 262 | // identifiers, we can be sure that the constrained type is a type parameter 263 | // that is contained in `param_constraint_mapping`. 264 | if let Some((_, ref mut known_bounds)) = param_constraint_mapping 265 | .iter_mut() 266 | .find(|(id, _)| *id == ident) 267 | { 268 | for bound in pred_ty.bounds.iter() { 269 | // We only care about trait bounds here. 270 | if let TypeParamBound::Trait(ref bound) = bound { 271 | known_bounds.push(bound.clone()); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | param_constraint_mapping 279 | } 280 | 281 | /// Hygienically add `where _: Display` to the set of [TypeParamBound]s for `ident`, creating such 282 | /// a set if necessary. 283 | fn ensure_display_in_where_clause_for_type(where_clause: &mut WhereClause, ident: Ident) { 284 | for pred_ty in where_clause 285 | .predicates 286 | .iter_mut() 287 | // Find the `where` predicate constraining the current type param, if it exists. 288 | .flat_map(|predicate| match predicate { 289 | WherePredicate::Type(pred_ty) => Some(pred_ty), 290 | // We're looking through type constraints, not lifetime constraints. 291 | _ => None, 292 | }) 293 | { 294 | // Do a complicated destructuring in order to check if the type being constrained in this 295 | // `where` clause is the type we're looking for, so we can use the mutable reference to 296 | // `pred_ty` if so. 297 | let matches_desired_type = matches!( 298 | &pred_ty.bounded_ty, 299 | Type::Path(TypePath { path, .. }) if Some(&ident) == path.get_ident()); 300 | if matches_desired_type { 301 | add_display_constraint_to_type_predicate(pred_ty); 302 | return; 303 | } 304 | } 305 | 306 | // If there is no `where` predicate for the current type param, we will construct one. 307 | let mut new_type_predicate = new_empty_where_type_predicate(ident); 308 | add_display_constraint_to_type_predicate(&mut new_type_predicate); 309 | append_where_clause_type_predicate(where_clause, new_type_predicate); 310 | } 311 | 312 | /// For all declared type parameters, add a [core::fmt::Display] constraint, unless the type 313 | /// parameter already has any type constraint. 314 | fn ensure_where_clause_has_display_for_all_unconstrained_members( 315 | where_clause: &mut WhereClause, 316 | type_params: &[&TypeParam], 317 | ) { 318 | let param_constraint_mapping = extract_trait_constraints_from_source(where_clause, type_params); 319 | 320 | for (ident, known_bounds) in param_constraint_mapping.into_iter() { 321 | // If the type parameter has any constraints already, we don't want to touch it, to avoid 322 | // breaking use cases where a type parameter only needs to impl `Debug`, for example. 323 | if known_bounds.is_empty() { 324 | ensure_display_in_where_clause_for_type(where_clause, ident); 325 | } 326 | } 327 | } 328 | 329 | /// Generate a `where` clause that ensures all generic type parameters `impl` 330 | /// [core::fmt::Display] unless already constrained. 331 | /// 332 | /// This approach allows struct/enum definitions deriving [crate::Display] to avoid hardcoding 333 | /// a [core::fmt::Display] constraint into every type parameter. 334 | /// 335 | /// If the type parameter isn't already constrained, we add a `where _: Display` clause to our 336 | /// display implementation to expect to be able to format every enum case or struct member. 337 | /// 338 | /// In fact, we would preferably only require `where _: Display` or `where _: Debug` where the 339 | /// format string actually requires it. However, while [`std::fmt` defines a formal syntax for 340 | /// `format!()`][format syntax], it *doesn't* expose the actual logic to parse the format string, 341 | /// which appears to live in [`rustc_parse_format`]. While we use the [`syn`] crate to parse rust 342 | /// syntax, it also doesn't currently provide any method to introspect a `format!()` string. It 343 | /// would be nice to contribute this upstream in [`syn`]. 344 | /// 345 | /// [format syntax]: std::fmt#syntax 346 | /// [`rustc_parse_format`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_parse_format/index.html 347 | fn generate_where_clause(generics: &Generics, where_clause: Option<&WhereClause>) -> WhereClause { 348 | let mut where_clause = where_clause.cloned().unwrap_or_else(new_empty_where_clause); 349 | let type_params: Vec<&TypeParam> = generics.type_params().collect(); 350 | ensure_where_clause_has_display_for_all_unconstrained_members(&mut where_clause, &type_params); 351 | where_clause 352 | } 353 | 354 | fn impl_enum(input: &DeriveInput, data: &DataEnum) -> Result { 355 | let ty = &input.ident; 356 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 357 | let where_clause = generate_where_clause(&input.generics, where_clause); 358 | 359 | let helper = AttrsHelper::new(&input.attrs); 360 | 361 | let displays = data 362 | .variants 363 | .iter() 364 | .map(|variant| helper.display_with_input(&input.attrs, &variant.attrs)) 365 | .collect::>>()?; 366 | 367 | if data.variants.is_empty() { 368 | Ok(quote! { 369 | impl #impl_generics ::core::fmt::Display for #ty #ty_generics #where_clause { 370 | fn fmt(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 371 | unreachable!("empty enums cannot be instantiated and thus cannot be printed") 372 | } 373 | } 374 | }) 375 | } else if displays.iter().any(Option::is_some) { 376 | let arms = data 377 | .variants 378 | .iter() 379 | .zip(displays) 380 | .map(|(variant, display)| { 381 | let display = 382 | display.ok_or_else(|| Error::new_spanned(variant, "missing doc comment"))?; 383 | let ident = &variant.ident; 384 | Ok(match &variant.fields { 385 | Fields::Named(fields) => { 386 | let var = fields.named.iter().map(|field| &field.ident); 387 | quote!(Self::#ident { #(#var),* } => { #display }) 388 | } 389 | Fields::Unnamed(fields) => { 390 | let var = (0..fields.unnamed.len()).map(|i| format_ident!("_{}", i)); 391 | quote!(Self::#ident(#(#var),*) => { #display }) 392 | } 393 | Fields::Unit => quote!(Self::#ident => { #display }), 394 | }) 395 | }) 396 | .collect::>>()?; 397 | Ok(quote! { 398 | impl #impl_generics ::core::fmt::Display for #ty #ty_generics #where_clause { 399 | fn fmt(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 400 | #[allow(unused_variables, unused_assignments)] 401 | match self { 402 | #(#arms,)* 403 | } 404 | } 405 | } 406 | }) 407 | } else { 408 | Err(Error::new_spanned(input, "Missing doc comments")) 409 | } 410 | } 411 | --------------------------------------------------------------------------------