diff --git a/subprojects/thiserror/.github/FUNDING.yml b/subprojects/thiserror/.github/FUNDING.yml new file mode 100644 index 0000000..7507077 --- /dev/null +++ b/subprojects/thiserror/.github/FUNDING.yml @@ -0,0 +1 @@ +github: dtolnay diff --git a/subprojects/thiserror/.github/workflows/ci.yml b/subprojects/thiserror/.github/workflows/ci.yml new file mode 100644 index 0000000..8320187 --- /dev/null +++ b/subprojects/thiserror/.github/workflows/ci.yml @@ -0,0 +1,128 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + schedule: [cron: "40 1 * * *"] + +permissions: + contents: read + +env: + RUSTFLAGS: -Dwarnings + +jobs: + pre_ci: + uses: dtolnay/.github/.github/workflows/pre_ci.yml@master + + test: + name: Rust ${{matrix.rust}} + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: [nightly, beta, stable, 1.81.0, 1.76.0] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.rust}} + components: rust-src + - name: Enable type layout randomization + run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV + if: matrix.rust == 'nightly' + - name: Enable nightly-only tests + run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=thiserror_nightly_testing >> $GITHUB_ENV + if: matrix.rust == 'nightly' + - run: cargo test --workspace --exclude thiserror_no_std_test + - run: cargo test --manifest-path tests/no-std/Cargo.toml + if: matrix.rust != '1.76.0' + - run: cargo test --no-default-features + - uses: actions/upload-artifact@v4 + if: matrix.rust == 'nightly' && always() + with: + name: Cargo.lock + path: Cargo.lock + continue-on-error: true + + msrv: + name: Rust 1.68.0 + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@1.68.0 + with: + components: rust-src + - run: cargo check + + minimal: + name: Minimal versions + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo generate-lockfile -Z minimal-versions + - run: cargo check --locked + + doc: + name: Documentation + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RUSTDOCFLAGS: -Dwarnings + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + - uses: dtolnay/install@cargo-docs-rs + - run: cargo docs-rs + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@nightly + with: + components: clippy, rust-src + - run: cargo clippy --tests --workspace -- -Dclippy::all -Dclippy::pedantic + + miri: + name: Miri + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@miri + - run: cargo miri setup + - run: cargo miri test + env: + MIRIFLAGS: -Zmiri-strict-provenance + + outdated: + name: Outdated + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/install@cargo-outdated + - run: cargo outdated --workspace --exit-code 1 diff --git a/subprojects/thiserror/.gitignore b/subprojects/thiserror/.gitignore new file mode 100644 index 0000000..e9e2199 --- /dev/null +++ b/subprojects/thiserror/.gitignore @@ -0,0 +1,2 @@ +/target/ +/Cargo.lock diff --git a/subprojects/thiserror/Cargo.toml b/subprojects/thiserror/Cargo.toml new file mode 100644 index 0000000..a4364bd --- /dev/null +++ b/subprojects/thiserror/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "thiserror" +version = "2.0.17" +authors = ["David Tolnay "] +categories = ["rust-patterns", "no-std"] +description = "derive(Error)" +documentation = "https://docs.rs/thiserror" +edition = "2021" +keywords = ["error", "error-handling", "derive"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/dtolnay/thiserror" +rust-version = "1.68" + +[features] +default = ["std"] + +# Std feature enables support for formatting std::path::{Path, PathBuf} +# conveniently in an error message. +# +# #[derive(Error, Debug)] +# #[error("failed to create configuration file {path}")] +# pub struct MyError { +# pub path: PathBuf, +# pub source: std::io::Error, +# } +# +# Without std, this would need to be written #[error("... {}", path.display())]. +std = [] + +[dependencies] +thiserror-impl = { version = "=2.0.17", path = "impl" } + +[dev-dependencies] +anyhow = "1.0.73" +ref-cast = "1.0.18" +rustversion = "1.0.13" +trybuild = { version = "1.0.108", features = ["diff"] } + +[workspace] +members = ["impl", "tests/no-std"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = [ + "--generate-link-to-definition", + "--generate-macro-expansion", + "--extern-html-root-url=core=https://doc.rust-lang.org", + "--extern-html-root-url=alloc=https://doc.rust-lang.org", + "--extern-html-root-url=std=https://doc.rust-lang.org", +] diff --git a/subprojects/thiserror/LICENSE-APACHE b/subprojects/thiserror/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/subprojects/thiserror/LICENSE-APACHE @@ -0,0 +1,176 @@ + 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 diff --git a/subprojects/thiserror/LICENSE-MIT b/subprojects/thiserror/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/subprojects/thiserror/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/subprojects/thiserror/README.md b/subprojects/thiserror/README.md new file mode 100644 index 0000000..f05e2ec --- /dev/null +++ b/subprojects/thiserror/README.md @@ -0,0 +1,238 @@ +derive(Error) +============= + +[github](https://github.com/dtolnay/thiserror) +[crates.io](https://crates.io/crates/thiserror) +[docs.rs](https://docs.rs/thiserror) +[build status](https://github.com/dtolnay/thiserror/actions?query=branch%3Amaster) + +This library provides a convenient derive macro for the standard library's +[`std::error::Error`] trait. + +[`std::error::Error`]: https://doc.rust-lang.org/std/error/trait.Error.html + +```toml +[dependencies] +thiserror = "2" +``` + +*Compiler support: requires rustc 1.68+* + +
+ +## Example + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DataStoreError { + #[error("data store disconnected")] + Disconnect(#[from] io::Error), + #[error("the data for key `{0}` is not available")] + Redaction(String), + #[error("invalid header (expected {expected:?}, found {found:?})")] + InvalidHeader { + expected: String, + found: String, + }, + #[error("unknown data store error")] + Unknown, +} +``` + +
+ +## Details + +- Thiserror deliberately does not appear in your public API. You get the same + thing as if you had written an implementation of `std::error::Error` by hand, + and switching from handwritten impls to thiserror or vice versa is not a + breaking change. + +- Errors may be enums, structs with named fields, tuple structs, or unit + structs. + +- A `Display` impl is generated for your error if you provide `#[error("...")]` + messages on the struct or each variant of your enum, as shown above in the + example. + + The messages support a shorthand for interpolating fields from the error. + + - `#[error("{var}")]` ⟶ `write!("{}", self.var)` + - `#[error("{0}")]` ⟶ `write!("{}", self.0)` + - `#[error("{var:?}")]` ⟶ `write!("{:?}", self.var)` + - `#[error("{0:?}")]` ⟶ `write!("{:?}", self.0)` + + These shorthands can be used together with any additional format args, which + may be arbitrary expressions. For example: + + ```rust + #[derive(Error, Debug)] + pub enum Error { + #[error("invalid rdo_lookahead_frames {0} (expected < {max})", max = i32::MAX)] + InvalidLookahead(u32), + } + ``` + + If one of the additional expression arguments needs to refer to a field of the + struct or enum, then refer to named fields as `.var` and tuple fields as `.0`. + + ```rust + #[derive(Error, Debug)] + pub enum Error { + #[error("first letter must be lowercase but was {:?}", first_char(.0))] + WrongCase(String), + #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)] + OutOfBounds { idx: usize, limits: Limits }, + } + ``` + +- A `From` impl is generated for each variant that contains a `#[from]` + attribute. + + The variant using `#[from]` must not contain any other fields beyond the + source error (and possibly a backtrace — see below). Usually `#[from]` + fields are unnamed, but `#[from]` is allowed on a named field too. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + Io(#[from] io::Error), + Glob(#[from] globset::Error), + } + ``` + +- The Error trait's `source()` method is implemented to return whichever field + has a `#[source]` attribute or is named `source`, if any. This is for + identifying the underlying lower level error that caused your error. + + The `#[from]` attribute always implies that the same field is `#[source]`, so + you don't ever need to specify both attributes. + + Any error type that implements `std::error::Error` or dereferences to `dyn + std::error::Error` will work as a source. + + ```rust + #[derive(Error, Debug)] + pub struct MyError { + msg: String, + #[source] // optional if field name is `source` + source: anyhow::Error, + } + ``` + +- The Error trait's `provide()` method is implemented to provide whichever field + has a type named `Backtrace`, if any, as a `std::backtrace::Backtrace`. Using + `Backtrace` in errors requires a nightly compiler with Rust version 1.73 or + newer. + + ```rust + use std::backtrace::Backtrace; + + #[derive(Error, Debug)] + pub struct MyError { + msg: String, + backtrace: Backtrace, // automatically detected + } + ``` + +- If a field is both a source (named `source`, or has `#[source]` or `#[from]` + attribute) *and* is marked `#[backtrace]`, then the Error trait's `provide()` + method is forwarded to the source's `provide` so that both layers of the error + share the same backtrace. The `#[backtrace]` attribute requires a nightly + compiler with Rust version 1.73 or newer. + + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + Io { + #[backtrace] + source: io::Error, + }, + } + ``` + +- For variants that use `#[from]` and also contain a `Backtrace` field, a + backtrace is captured from within the `From` impl. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + Io { + #[from] + source: io::Error, + backtrace: Backtrace, + }, + } + ``` + +- Errors may use `error(transparent)` to forward the source and Display methods + straight through to an underlying error without adding an additional message. + This would be appropriate for enums that need an "anything else" variant. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + ... + + #[error(transparent)] + Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error + } + ``` + + Another use case is hiding implementation details of an error representation + behind an opaque error type, so that the representation is able to evolve + without breaking the crate's public API. + + ```rust + // PublicError is public, but opaque and easy to keep compatible. + #[derive(Error, Debug)] + #[error(transparent)] + pub struct PublicError(#[from] ErrorRepr); + + impl PublicError { + // Accessors for anything we do want to expose publicly. + } + + // Private and free to change across minor version of the crate. + #[derive(Error, Debug)] + enum ErrorRepr { + ... + } + ``` + +- See also the [`anyhow`] library for a convenient single error type to use in + application code. + + [`anyhow`]: https://github.com/dtolnay/anyhow + +
+ +## Comparison to anyhow + +Use thiserror if you care about designing your own dedicated error type(s) so +that the caller receives exactly the information that you choose in the event of +failure. This most often applies to library-like code. Use [Anyhow] if you don't +care what error type your functions return, you just want it to be easy. This is +common in application-like code. + +[Anyhow]: https://github.com/dtolnay/anyhow + +
+ +#### 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/subprojects/thiserror/build.rs b/subprojects/thiserror/build.rs new file mode 100644 index 0000000..d1558fb --- /dev/null +++ b/subprojects/thiserror/build.rs @@ -0,0 +1,195 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::io::ErrorKind; +use std::iter; +use std::path::{Path, PathBuf}; +use std::process::{self, Command, Stdio}; +use std::str; + +const PRIVATE: &str = "\ +#[doc(hidden)] +pub mod __private$$ { + #[doc(hidden)] + pub use crate::private::*; +} +"; + +fn main() { + println!("cargo:rerun-if-changed=build/probe.rs"); + + println!("cargo:rustc-check-cfg=cfg(error_generic_member_access)"); + println!("cargo:rustc-check-cfg=cfg(thiserror_nightly_testing)"); + println!("cargo:rustc-check-cfg=cfg(thiserror_no_backtrace_type)"); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let patch_version = env::var("CARGO_PKG_VERSION_PATCH").unwrap(); + let module = PRIVATE.replace("$$", &patch_version); + fs::write(out_dir.join("private.rs"), module).unwrap(); + + let error_generic_member_access; + let consider_rustc_bootstrap; + if compile_probe(false) { + // This is a nightly or dev compiler, so it supports unstable features + // regardless of RUSTC_BOOTSTRAP. No need to rerun build script if + // RUSTC_BOOTSTRAP is changed. + error_generic_member_access = true; + consider_rustc_bootstrap = false; + } else if let Some(rustc_bootstrap) = env::var_os("RUSTC_BOOTSTRAP") { + if compile_probe(true) { + // This is a stable or beta compiler for which the user has set + // RUSTC_BOOTSTRAP to turn on unstable features. Rerun build script + // if they change it. + error_generic_member_access = true; + consider_rustc_bootstrap = true; + } else if rustc_bootstrap == "1" { + // This compiler does not support the generic member access API in + // the form that thiserror expects. No need to pay attention to + // RUSTC_BOOTSTRAP. + error_generic_member_access = false; + consider_rustc_bootstrap = false; + } else { + // This is a stable or beta compiler for which RUSTC_BOOTSTRAP is + // set to restrict the use of unstable features by this crate. + error_generic_member_access = false; + consider_rustc_bootstrap = true; + } + } else { + // Without RUSTC_BOOTSTRAP, this compiler does not support the generic + // member access API in the form that thiserror expects, but try again + // if the user turns on unstable features. + error_generic_member_access = false; + consider_rustc_bootstrap = true; + } + + if error_generic_member_access { + println!("cargo:rustc-cfg=error_generic_member_access"); + } + + if consider_rustc_bootstrap { + println!("cargo:rerun-if-env-changed=RUSTC_BOOTSTRAP"); + } + + // core::error::Error stabilized in Rust 1.81 + // https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#coreerrorerror + let rustc = rustc_minor_version(); + if cfg!(not(feature = "std")) && rustc.map_or(false, |rustc| rustc < 81) { + println!("cargo:rustc-cfg=feature=\"std\""); + } + + let Some(rustc) = rustc else { + return; + }; + + // std::backtrace::Backtrace stabilized in Rust 1.65 + // https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html#stabilized-apis + if rustc < 65 { + println!("cargo:rustc-cfg=thiserror_no_backtrace_type"); + } +} + +fn compile_probe(rustc_bootstrap: bool) -> bool { + if env::var_os("RUSTC_STAGE").is_some() { + // We are running inside rustc bootstrap. This is a highly non-standard + // environment with issues such as: + // + // https://github.com/rust-lang/cargo/issues/11138 + // https://github.com/rust-lang/rust/issues/114839 + // + // Let's just not use nightly features here. + return false; + } + + let rustc = cargo_env_var("RUSTC"); + let out_dir = cargo_env_var("OUT_DIR"); + let out_subdir = Path::new(&out_dir).join("probe"); + let probefile = Path::new("build").join("probe.rs"); + + if let Err(err) = fs::create_dir(&out_subdir) { + if err.kind() != ErrorKind::AlreadyExists { + eprintln!("Failed to create {}: {}", out_subdir.display(), err); + process::exit(1); + } + } + + let rustc_wrapper = env::var_os("RUSTC_WRAPPER").filter(|wrapper| !wrapper.is_empty()); + let rustc_workspace_wrapper = + env::var_os("RUSTC_WORKSPACE_WRAPPER").filter(|wrapper| !wrapper.is_empty()); + let mut rustc = rustc_wrapper + .into_iter() + .chain(rustc_workspace_wrapper) + .chain(iter::once(rustc)); + let mut cmd = Command::new(rustc.next().unwrap()); + cmd.args(rustc); + + if !rustc_bootstrap { + cmd.env_remove("RUSTC_BOOTSTRAP"); + } + + cmd.stderr(Stdio::null()) + .arg("--edition=2018") + .arg("--crate-name=thiserror") + .arg("--crate-type=lib") + .arg("--cap-lints=allow") + .arg("--emit=dep-info,metadata") + .arg("--out-dir") + .arg(&out_subdir) + .arg(probefile); + + if let Some(target) = env::var_os("TARGET") { + cmd.arg("--target").arg(target); + } + + // If Cargo wants to set RUSTFLAGS, use that. + if let Ok(rustflags) = env::var("CARGO_ENCODED_RUSTFLAGS") { + if !rustflags.is_empty() { + for arg in rustflags.split('\x1f') { + cmd.arg(arg); + } + } + } + + let success = match cmd.status() { + Ok(status) => status.success(), + Err(_) => false, + }; + + // Clean up to avoid leaving nondeterministic absolute paths in the dep-info + // file in OUT_DIR, which causes nonreproducible builds in build systems + // that treat the entire OUT_DIR as an artifact. + if let Err(err) = fs::remove_dir_all(&out_subdir) { + // libc::ENOTEMPTY + // Some filesystems (NFSv3) have timing issues under load where '.nfs*' + // dummy files can continue to get created for a short period after the + // probe command completes, breaking remove_dir_all. + // To be replaced with ErrorKind::DirectoryNotEmpty (Rust 1.83+). + const ENOTEMPTY: i32 = 39; + + if !(err.kind() == ErrorKind::NotFound + || (cfg!(target_os = "linux") && err.raw_os_error() == Some(ENOTEMPTY))) + { + eprintln!("Failed to clean up {}: {}", out_subdir.display(), err); + process::exit(1); + } + } + + success +} + +fn rustc_minor_version() -> Option { + let rustc = cargo_env_var("RUSTC"); + let output = Command::new(rustc).arg("--version").output().ok()?; + let version = str::from_utf8(&output.stdout).ok()?; + let mut pieces = version.split('.'); + if pieces.next() != Some("rustc 1") { + return None; + } + pieces.next()?.parse().ok() +} + +fn cargo_env_var(key: &str) -> OsString { + env::var_os(key).unwrap_or_else(|| { + eprintln!("Environment variable ${key} is not set during execution of build script"); + process::exit(1); + }) +} diff --git a/subprojects/thiserror/build/probe.rs b/subprojects/thiserror/build/probe.rs new file mode 100644 index 0000000..ee126d4 --- /dev/null +++ b/subprojects/thiserror/build/probe.rs @@ -0,0 +1,33 @@ +// This code exercises the surface area that we expect of the Error generic +// member access API. If the current toolchain is able to compile it, then +// thiserror is able to provide backtrace support. + +#![no_std] +#![feature(error_generic_member_access)] + +use core::error::{Error, Request}; +use core::fmt::{self, Debug, Display}; + +struct MyError(Thing); +struct Thing; + +impl Debug for MyError { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } +} + +impl Display for MyError { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } +} + +impl Error for MyError { + fn provide<'a>(&'a self, request: &mut Request<'a>) { + request.provide_ref(&self.0); + } +} + +// Include in sccache cache key. +const _: Option<&str> = option_env!("RUSTC_BOOTSTRAP"); diff --git a/subprojects/thiserror/impl/Cargo.toml b/subprojects/thiserror/impl/Cargo.toml new file mode 100644 index 0000000..1d7d2f9 --- /dev/null +++ b/subprojects/thiserror/impl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "thiserror-impl" +version = "2.0.17" +authors = ["David Tolnay "] +description = "Implementation detail of the `thiserror` crate" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/dtolnay/thiserror" +rust-version = "1.68" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.74" +quote = "1.0.35" +syn = "2.0.87" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = [ + "--generate-link-to-definition", + "--generate-macro-expansion", + "--extern-html-root-url=core=https://doc.rust-lang.org", + "--extern-html-root-url=alloc=https://doc.rust-lang.org", + "--extern-html-root-url=std=https://doc.rust-lang.org", + "--extern-html-root-url=proc_macro=https://doc.rust-lang.org", +] diff --git a/subprojects/thiserror/impl/LICENSE-APACHE b/subprojects/thiserror/impl/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/subprojects/thiserror/impl/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/subprojects/thiserror/impl/LICENSE-MIT b/subprojects/thiserror/impl/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/subprojects/thiserror/impl/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/subprojects/thiserror/impl/src/ast.rs b/subprojects/thiserror/impl/src/ast.rs new file mode 100644 index 0000000..77f9583 --- /dev/null +++ b/subprojects/thiserror/impl/src/ast.rs @@ -0,0 +1,185 @@ +use crate::attr::{self, Attrs}; +use crate::generics::ParamsInScope; +use crate::unraw::{IdentUnraw, MemberUnraw}; +use proc_macro2::Span; +use std::fmt::{self, Display}; +use syn::{ + Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Generics, Ident, Index, Result, Type, +}; + +pub enum Input<'a> { + Struct(Struct<'a>), + Enum(Enum<'a>), +} + +pub struct Struct<'a> { + pub attrs: Attrs<'a>, + pub ident: Ident, + pub generics: &'a Generics, + pub fields: Vec>, +} + +pub struct Enum<'a> { + pub attrs: Attrs<'a>, + pub ident: Ident, + pub generics: &'a Generics, + pub variants: Vec>, +} + +pub struct Variant<'a> { + pub original: &'a syn::Variant, + pub attrs: Attrs<'a>, + pub ident: Ident, + pub fields: Vec>, +} + +pub struct Field<'a> { + pub original: &'a syn::Field, + pub attrs: Attrs<'a>, + pub member: MemberUnraw, + pub ty: &'a Type, + pub contains_generic: bool, +} + +#[derive(Copy, Clone)] +pub enum ContainerKind { + Struct, + TupleStruct, + UnitStruct, + StructVariant, + TupleVariant, + UnitVariant, +} + +impl<'a> Input<'a> { + pub fn from_syn(node: &'a DeriveInput) -> Result { + match &node.data { + Data::Struct(data) => Struct::from_syn(node, data).map(Input::Struct), + Data::Enum(data) => Enum::from_syn(node, data).map(Input::Enum), + Data::Union(_) => Err(Error::new_spanned( + node, + "union as errors are not supported", + )), + } + } +} + +impl<'a> Struct<'a> { + fn from_syn(node: &'a DeriveInput, data: &'a DataStruct) -> Result { + let mut attrs = attr::get(&node.attrs)?; + let scope = ParamsInScope::new(&node.generics); + let fields = Field::multiple_from_syn(&data.fields, &scope)?; + if let Some(display) = &mut attrs.display { + let container = ContainerKind::from_struct(data); + display.expand_shorthand(&fields, container)?; + } + Ok(Struct { + attrs, + ident: node.ident.clone(), + generics: &node.generics, + fields, + }) + } +} + +impl<'a> Enum<'a> { + fn from_syn(node: &'a DeriveInput, data: &'a DataEnum) -> Result { + let attrs = attr::get(&node.attrs)?; + let scope = ParamsInScope::new(&node.generics); + let variants = data + .variants + .iter() + .map(|node| { + let mut variant = Variant::from_syn(node, &scope)?; + if variant.attrs.display.is_none() + && variant.attrs.transparent.is_none() + && variant.attrs.fmt.is_none() + { + variant.attrs.display.clone_from(&attrs.display); + variant.attrs.transparent = attrs.transparent; + variant.attrs.fmt.clone_from(&attrs.fmt); + } + if let Some(display) = &mut variant.attrs.display { + let container = ContainerKind::from_variant(node); + display.expand_shorthand(&variant.fields, container)?; + } + Ok(variant) + }) + .collect::>()?; + Ok(Enum { + attrs, + ident: node.ident.clone(), + generics: &node.generics, + variants, + }) + } +} + +impl<'a> Variant<'a> { + fn from_syn(node: &'a syn::Variant, scope: &ParamsInScope<'a>) -> Result { + let attrs = attr::get(&node.attrs)?; + Ok(Variant { + original: node, + attrs, + ident: node.ident.clone(), + fields: Field::multiple_from_syn(&node.fields, scope)?, + }) + } +} + +impl<'a> Field<'a> { + fn multiple_from_syn(fields: &'a Fields, scope: &ParamsInScope<'a>) -> Result> { + fields + .iter() + .enumerate() + .map(|(i, field)| Field::from_syn(i, field, scope)) + .collect() + } + + fn from_syn(i: usize, node: &'a syn::Field, scope: &ParamsInScope<'a>) -> Result { + Ok(Field { + original: node, + attrs: attr::get(&node.attrs)?, + member: match &node.ident { + Some(name) => MemberUnraw::Named(IdentUnraw::new(name.clone())), + None => MemberUnraw::Unnamed(Index { + index: i as u32, + span: Span::call_site(), + }), + }, + ty: &node.ty, + contains_generic: scope.intersects(&node.ty), + }) + } +} + +impl ContainerKind { + fn from_struct(node: &DataStruct) -> Self { + match node.fields { + Fields::Named(_) => ContainerKind::Struct, + Fields::Unnamed(_) => ContainerKind::TupleStruct, + Fields::Unit => ContainerKind::UnitStruct, + } + } + + fn from_variant(node: &syn::Variant) -> Self { + match node.fields { + Fields::Named(_) => ContainerKind::StructVariant, + Fields::Unnamed(_) => ContainerKind::TupleVariant, + Fields::Unit => ContainerKind::UnitVariant, + } + } +} + +impl Display for ContainerKind { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(match self { + ContainerKind::Struct => "struct", + ContainerKind::TupleStruct => "tuple struct", + ContainerKind::UnitStruct => "unit struct", + ContainerKind::StructVariant => "struct variant", + ContainerKind::TupleVariant => "tuple variant", + ContainerKind::UnitVariant => "unit variant", + }) + } +} diff --git a/subprojects/thiserror/impl/src/attr.rs b/subprojects/thiserror/impl/src/attr.rs new file mode 100644 index 0000000..7ad83e0 --- /dev/null +++ b/subprojects/thiserror/impl/src/attr.rs @@ -0,0 +1,358 @@ +use proc_macro2::{Delimiter, Group, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use std::collections::BTreeSet as Set; +use syn::parse::discouraged::Speculative; +use syn::parse::{End, ParseStream}; +use syn::{ + braced, bracketed, parenthesized, token, Attribute, Error, ExprPath, Ident, Index, LitFloat, + LitInt, LitStr, Meta, Result, Token, +}; + +pub struct Attrs<'a> { + pub display: Option>, + pub source: Option>, + pub backtrace: Option<&'a Attribute>, + pub from: Option>, + pub transparent: Option>, + pub fmt: Option>, +} + +#[derive(Clone)] +pub struct Display<'a> { + pub original: &'a Attribute, + pub fmt: LitStr, + pub args: TokenStream, + pub requires_fmt_machinery: bool, + pub has_bonus_display: bool, + pub infinite_recursive: bool, + pub implied_bounds: Set<(usize, Trait)>, + pub bindings: Vec<(Ident, TokenStream)>, +} + +#[derive(Copy, Clone)] +pub struct Source<'a> { + pub original: &'a Attribute, + pub span: Span, +} + +#[derive(Copy, Clone)] +pub struct From<'a> { + pub original: &'a Attribute, + pub span: Span, +} + +#[derive(Copy, Clone)] +pub struct Transparent<'a> { + pub original: &'a Attribute, + pub span: Span, +} + +#[derive(Clone)] +pub struct Fmt<'a> { + pub original: &'a Attribute, + pub path: ExprPath, +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub enum Trait { + Debug, + Display, + Octal, + LowerHex, + UpperHex, + Pointer, + Binary, + LowerExp, + UpperExp, +} + +pub fn get(input: &[Attribute]) -> Result { + let mut attrs = Attrs { + display: None, + source: None, + backtrace: None, + from: None, + transparent: None, + fmt: None, + }; + + for attr in input { + if attr.path().is_ident("error") { + parse_error_attribute(&mut attrs, attr)?; + } else if attr.path().is_ident("source") { + attr.meta.require_path_only()?; + if attrs.source.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[source] attribute")); + } + let span = (attr.pound_token.span) + .join(attr.bracket_token.span.join()) + .unwrap_or(attr.path().get_ident().unwrap().span()); + attrs.source = Some(Source { + original: attr, + span, + }); + } else if attr.path().is_ident("backtrace") { + attr.meta.require_path_only()?; + if attrs.backtrace.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[backtrace] attribute")); + } + attrs.backtrace = Some(attr); + } else if attr.path().is_ident("from") { + match attr.meta { + Meta::Path(_) => {} + Meta::List(_) | Meta::NameValue(_) => { + // Assume this is meant for derive_more crate or something. + continue; + } + } + if attrs.from.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[from] attribute")); + } + let span = (attr.pound_token.span) + .join(attr.bracket_token.span.join()) + .unwrap_or(attr.path().get_ident().unwrap().span()); + attrs.from = Some(From { + original: attr, + span, + }); + } + } + + Ok(attrs) +} + +fn parse_error_attribute<'a>(attrs: &mut Attrs<'a>, attr: &'a Attribute) -> Result<()> { + mod kw { + syn::custom_keyword!(transparent); + syn::custom_keyword!(fmt); + } + + attr.parse_args_with(|input: ParseStream| { + let lookahead = input.lookahead1(); + let fmt = if lookahead.peek(LitStr) { + input.parse::()? + } else if lookahead.peek(kw::transparent) { + let kw: kw::transparent = input.parse()?; + if attrs.transparent.is_some() { + return Err(Error::new_spanned( + attr, + "duplicate #[error(transparent)] attribute", + )); + } + attrs.transparent = Some(Transparent { + original: attr, + span: kw.span, + }); + return Ok(()); + } else if lookahead.peek(kw::fmt) { + input.parse::()?; + input.parse::()?; + let path: ExprPath = input.parse()?; + if attrs.fmt.is_some() { + return Err(Error::new_spanned( + attr, + "duplicate #[error(fmt = ...)] attribute", + )); + } + attrs.fmt = Some(Fmt { + original: attr, + path, + }); + return Ok(()); + } else { + return Err(lookahead.error()); + }; + + let args = if input.is_empty() || input.peek(Token![,]) && input.peek2(End) { + input.parse::>()?; + TokenStream::new() + } else { + parse_token_expr(input, false)? + }; + + let requires_fmt_machinery = !args.is_empty(); + + let display = Display { + original: attr, + fmt, + args, + requires_fmt_machinery, + has_bonus_display: false, + infinite_recursive: false, + implied_bounds: Set::new(), + bindings: Vec::new(), + }; + if attrs.display.is_some() { + return Err(Error::new_spanned( + attr, + "only one #[error(...)] attribute is allowed", + )); + } + attrs.display = Some(display); + Ok(()) + }) +} + +fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result { + let mut tokens = Vec::new(); + while !input.is_empty() { + if input.peek(token::Group) { + let group: TokenTree = input.parse()?; + tokens.push(group); + begin_expr = false; + continue; + } + + if begin_expr && input.peek(Token![.]) { + if input.peek2(Ident) { + input.parse::()?; + begin_expr = false; + continue; + } else if input.peek2(LitInt) { + input.parse::()?; + let int: Index = input.parse()?; + tokens.push({ + let ident = format_ident!("_{}", int.index, span = int.span); + TokenTree::Ident(ident) + }); + begin_expr = false; + continue; + } else if input.peek2(LitFloat) { + let ahead = input.fork(); + ahead.parse::()?; + let float: LitFloat = ahead.parse()?; + let repr = float.to_string(); + let mut indices = repr.split('.').map(syn::parse_str::); + if let (Some(Ok(first)), Some(Ok(second)), None) = + (indices.next(), indices.next(), indices.next()) + { + input.advance_to(&ahead); + tokens.push({ + let ident = format_ident!("_{}", first, span = float.span()); + TokenTree::Ident(ident) + }); + tokens.push({ + let mut punct = Punct::new('.', Spacing::Alone); + punct.set_span(float.span()); + TokenTree::Punct(punct) + }); + tokens.push({ + let mut literal = Literal::u32_unsuffixed(second.index); + literal.set_span(float.span()); + TokenTree::Literal(literal) + }); + begin_expr = false; + continue; + } + } + } + + begin_expr = input.peek(Token![break]) + || input.peek(Token![continue]) + || input.peek(Token![if]) + || input.peek(Token![in]) + || input.peek(Token![match]) + || input.peek(Token![mut]) + || input.peek(Token![return]) + || input.peek(Token![while]) + || input.peek(Token![+]) + || input.peek(Token![&]) + || input.peek(Token![!]) + || input.peek(Token![^]) + || input.peek(Token![,]) + || input.peek(Token![/]) + || input.peek(Token![=]) + || input.peek(Token![>]) + || input.peek(Token![<]) + || input.peek(Token![|]) + || input.peek(Token![%]) + || input.peek(Token![;]) + || input.peek(Token![*]) + || input.peek(Token![-]); + + let token: TokenTree = if input.peek(token::Paren) { + let content; + let delimiter = parenthesized!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Parenthesis, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else if input.peek(token::Brace) { + let content; + let delimiter = braced!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Brace, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else if input.peek(token::Bracket) { + let content; + let delimiter = bracketed!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Bracket, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else { + input.parse()? + }; + tokens.push(token); + } + Ok(TokenStream::from_iter(tokens)) +} + +impl ToTokens for Display<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + if self.infinite_recursive { + let span = self.fmt.span(); + tokens.extend(quote_spanned! {span=> + #[warn(unconditional_recursion)] + fn _fmt() { _fmt() } + }); + } + + let fmt = &self.fmt; + let args = &self.args; + + // Currently `write!(f, "text")` produces less efficient code than + // `f.write_str("text")`. We recognize the case when the format string + // has no braces and no interpolated values, and generate simpler code. + let write = if self.requires_fmt_machinery { + quote! { + ::core::write!(__formatter, #fmt #args) + } + } else { + quote! { + __formatter.write_str(#fmt) + } + }; + + tokens.extend(if self.bindings.is_empty() { + write + } else { + let locals = self.bindings.iter().map(|(local, _value)| local); + let values = self.bindings.iter().map(|(_local, value)| value); + quote! { + match (#(#values,)*) { + (#(#locals,)*) => #write + } + } + }); + } +} + +impl ToTokens for Trait { + fn to_tokens(&self, tokens: &mut TokenStream) { + let trait_name = match self { + Trait::Debug => "Debug", + Trait::Display => "Display", + Trait::Octal => "Octal", + Trait::LowerHex => "LowerHex", + Trait::UpperHex => "UpperHex", + Trait::Pointer => "Pointer", + Trait::Binary => "Binary", + Trait::LowerExp => "LowerExp", + Trait::UpperExp => "UpperExp", + }; + let ident = Ident::new(trait_name, Span::call_site()); + tokens.extend(quote!(::core::fmt::#ident)); + } +} diff --git a/subprojects/thiserror/impl/src/expand.rs b/subprojects/thiserror/impl/src/expand.rs new file mode 100644 index 0000000..97466dd --- /dev/null +++ b/subprojects/thiserror/impl/src/expand.rs @@ -0,0 +1,584 @@ +use crate::ast::{Enum, Field, Input, Struct}; +use crate::attr::Trait; +use crate::fallback; +use crate::generics::InferredBounds; +use crate::private; +use crate::unraw::MemberUnraw; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use std::collections::BTreeSet as Set; +use syn::{DeriveInput, GenericArgument, PathArguments, Result, Token, Type}; + +pub fn derive(input: &DeriveInput) -> TokenStream { + match try_expand(input) { + Ok(expanded) => expanded, + // If there are invalid attributes in the input, expand to an Error impl + // anyway to minimize spurious secondary errors in other code that uses + // this type as an Error. + Err(error) => fallback::expand(input, error), + } +} + +fn try_expand(input: &DeriveInput) -> Result { + let input = Input::from_syn(input)?; + input.validate()?; + Ok(match input { + Input::Struct(input) => impl_struct(input), + Input::Enum(input) => impl_enum(input), + }) +} + +fn impl_struct(input: Struct) -> TokenStream { + let ty = call_site_ident(&input.ident); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut error_inferred_bounds = InferredBounds::new(); + + let source_body = if let Some(transparent_attr) = &input.attrs.transparent { + let only_field = &input.fields[0]; + if only_field.contains_generic { + error_inferred_bounds.insert(only_field.ty, quote!(::thiserror::#private::Error)); + } + let member = &only_field.member; + Some(quote_spanned! {transparent_attr.span=> + ::thiserror::#private::Error::source(self.#member.as_dyn_error()) + }) + } else if let Some(source_field) = input.source_field() { + let source = &source_field.member; + if source_field.contains_generic { + let ty = unoptional_type(source_field.ty); + error_inferred_bounds.insert(ty, quote!(::thiserror::#private::Error + 'static)); + } + let asref = if type_is_option(source_field.ty) { + Some(quote_spanned!(source.span()=> .as_ref()?)) + } else { + None + }; + let dyn_error = quote_spanned! {source_field.source_span()=> + self.#source #asref.as_dyn_error() + }; + Some(quote! { + ::core::option::Option::Some(#dyn_error) + }) + } else { + None + }; + let source_method = source_body.map(|body| { + quote! { + fn source(&self) -> ::core::option::Option<&(dyn ::thiserror::#private::Error + 'static)> { + use ::thiserror::#private::AsDynError as _; + #body + } + } + }); + + let provide_method = input.backtrace_field().map(|backtrace_field| { + let request = quote!(request); + let backtrace = &backtrace_field.member; + let body = if let Some(source_field) = input.source_field() { + let source = &source_field.member; + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {source.span()=> + if let ::core::option::Option::Some(source) = &self.#source { + source.thiserror_provide(#request); + } + } + } else { + quote_spanned! {source.span()=> + self.#source.thiserror_provide(#request); + } + }; + let self_provide = if source == backtrace { + None + } else if type_is_option(backtrace_field.ty) { + Some(quote! { + if let ::core::option::Option::Some(backtrace) = &self.#backtrace { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + }) + } else { + Some(quote! { + #request.provide_ref::<::thiserror::#private::Backtrace>(&self.#backtrace); + }) + }; + quote! { + use ::thiserror::#private::ThiserrorProvide as _; + #source_provide + #self_provide + } + } else if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = &self.#backtrace { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + } + } else { + quote! { + #request.provide_ref::<::thiserror::#private::Backtrace>(&self.#backtrace); + } + }; + quote! { + fn provide<'_request>(&'_request self, #request: &mut ::core::error::Request<'_request>) { + #body + } + } + }); + + let mut display_implied_bounds = Set::new(); + let display_body = if input.attrs.transparent.is_some() { + let only_field = &input.fields[0].member; + display_implied_bounds.insert((0, Trait::Display)); + Some(quote! { + ::core::fmt::Display::fmt(&self.#only_field, __formatter) + }) + } else if let Some(display) = &input.attrs.display { + display_implied_bounds.clone_from(&display.implied_bounds); + let use_as_display = use_as_display(display.has_bonus_display); + let pat = fields_pat(&input.fields); + Some(quote! { + #use_as_display + #[allow(unused_variables, deprecated)] + let Self #pat = self; + #display + }) + } else { + None + }; + let display_impl = display_body.map(|body| { + let mut display_inferred_bounds = InferredBounds::new(); + for (field, bound) in display_implied_bounds { + let field = &input.fields[field]; + if field.contains_generic { + display_inferred_bounds.insert(field.ty, bound); + } + } + let display_where_clause = display_inferred_bounds.augment_where_clause(input.generics); + quote! { + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::core::fmt::Display for #ty #ty_generics #display_where_clause { + #[allow(clippy::used_underscore_binding)] + fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + #body + } + } + } + }); + + let from_impl = input.from_field().map(|from_field| { + let span = from_field.attrs.from.unwrap().span; + let backtrace_field = input.distinct_backtrace_field(); + let from = unoptional_type(from_field.ty); + let source_var = Ident::new("source", span); + let body = from_initializer(from_field, backtrace_field, &source_var); + let from_function = quote! { + fn from(#source_var: #from) -> Self { + #ty #body + } + }; + let from_impl = quote_spanned! {span=> + #[automatically_derived] + impl #impl_generics ::core::convert::From<#from> for #ty #ty_generics #where_clause { + #from_function + } + }; + Some(quote! { + #[allow( + deprecated, + unused_qualifications, + clippy::elidable_lifetime_names, + clippy::needless_lifetimes, + )] + #from_impl + }) + }); + + if input.generics.type_params().next().is_some() { + let self_token = ::default(); + error_inferred_bounds.insert(self_token, Trait::Debug); + error_inferred_bounds.insert(self_token, Trait::Display); + } + let error_where_clause = error_inferred_bounds.augment_where_clause(input.generics); + + quote! { + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::thiserror::#private::Error for #ty #ty_generics #error_where_clause { + #source_method + #provide_method + } + #display_impl + #from_impl + } +} + +fn impl_enum(input: Enum) -> TokenStream { + let ty = call_site_ident(&input.ident); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut error_inferred_bounds = InferredBounds::new(); + + let source_method = if input.has_source() { + let arms = input.variants.iter().map(|variant| { + let ident = &variant.ident; + if let Some(transparent_attr) = &variant.attrs.transparent { + let only_field = &variant.fields[0]; + if only_field.contains_generic { + error_inferred_bounds.insert(only_field.ty, quote!(::thiserror::#private::Error)); + } + let member = &only_field.member; + let source = quote_spanned! {transparent_attr.span=> + ::thiserror::#private::Error::source(transparent.as_dyn_error()) + }; + quote! { + #ty::#ident {#member: transparent} => #source, + } + } else if let Some(source_field) = variant.source_field() { + let source = &source_field.member; + if source_field.contains_generic { + let ty = unoptional_type(source_field.ty); + error_inferred_bounds.insert(ty, quote!(::thiserror::#private::Error + 'static)); + } + let asref = if type_is_option(source_field.ty) { + Some(quote_spanned!(source.span()=> .as_ref()?)) + } else { + None + }; + let varsource = quote!(source); + let dyn_error = quote_spanned! {source_field.source_span()=> + #varsource #asref.as_dyn_error() + }; + quote! { + #ty::#ident {#source: #varsource, ..} => ::core::option::Option::Some(#dyn_error), + } + } else { + quote! { + #ty::#ident {..} => ::core::option::Option::None, + } + } + }); + Some(quote! { + fn source(&self) -> ::core::option::Option<&(dyn ::thiserror::#private::Error + 'static)> { + use ::thiserror::#private::AsDynError as _; + #[allow(deprecated)] + match self { + #(#arms)* + } + } + }) + } else { + None + }; + + let provide_method = if input.has_backtrace() { + let request = quote!(request); + let arms = input.variants.iter().map(|variant| { + let ident = &variant.ident; + match (variant.backtrace_field(), variant.source_field()) { + (Some(backtrace_field), Some(source_field)) + if backtrace_field.attrs.backtrace.is_none() => + { + let backtrace = &backtrace_field.member; + let source = &source_field.member; + let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {source.span()=> + if let ::core::option::Option::Some(source) = #varsource { + source.thiserror_provide(#request); + } + } + } else { + quote_spanned! {source.span()=> + #varsource.thiserror_provide(#request); + } + }; + let self_provide = if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = backtrace { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + } + } else { + quote! { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + }; + quote! { + #ty::#ident { + #backtrace: backtrace, + #source: #varsource, + .. + } => { + use ::thiserror::#private::ThiserrorProvide as _; + #source_provide + #self_provide + } + } + } + (Some(backtrace_field), Some(source_field)) + if backtrace_field.member == source_field.member => + { + let backtrace = &backtrace_field.member; + let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {backtrace.span()=> + if let ::core::option::Option::Some(source) = #varsource { + source.thiserror_provide(#request); + } + } + } else { + quote_spanned! {backtrace.span()=> + #varsource.thiserror_provide(#request); + } + }; + quote! { + #ty::#ident {#backtrace: #varsource, ..} => { + use ::thiserror::#private::ThiserrorProvide as _; + #source_provide + } + } + } + (Some(backtrace_field), _) => { + let backtrace = &backtrace_field.member; + let body = if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = backtrace { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + } + } else { + quote! { + #request.provide_ref::<::thiserror::#private::Backtrace>(backtrace); + } + }; + quote! { + #ty::#ident {#backtrace: backtrace, ..} => { + #body + } + } + } + (None, _) => quote! { + #ty::#ident {..} => {} + }, + } + }); + Some(quote! { + fn provide<'_request>(&'_request self, #request: &mut ::core::error::Request<'_request>) { + #[allow(deprecated)] + match self { + #(#arms)* + } + } + }) + } else { + None + }; + + let display_impl = if input.has_display() { + let mut display_inferred_bounds = InferredBounds::new(); + let has_bonus_display = input.variants.iter().any(|v| { + v.attrs + .display + .as_ref() + .map_or(false, |display| display.has_bonus_display) + }); + let use_as_display = use_as_display(has_bonus_display); + let void_deref = if input.variants.is_empty() { + Some(quote!(*)) + } else { + None + }; + let arms = input.variants.iter().map(|variant| { + let mut display_implied_bounds = Set::new(); + let display = if let Some(display) = &variant.attrs.display { + display_implied_bounds.clone_from(&display.implied_bounds); + display.to_token_stream() + } else if let Some(fmt) = &variant.attrs.fmt { + let fmt_path = &fmt.path; + let vars = variant.fields.iter().map(|field| match &field.member { + MemberUnraw::Named(ident) => ident.to_local(), + MemberUnraw::Unnamed(index) => format_ident!("_{}", index), + }); + quote!(#fmt_path(#(#vars,)* __formatter)) + } else { + let only_field = match &variant.fields[0].member { + MemberUnraw::Named(ident) => ident.to_local(), + MemberUnraw::Unnamed(index) => format_ident!("_{}", index), + }; + display_implied_bounds.insert((0, Trait::Display)); + quote!(::core::fmt::Display::fmt(#only_field, __formatter)) + }; + for (field, bound) in display_implied_bounds { + let field = &variant.fields[field]; + if field.contains_generic { + display_inferred_bounds.insert(field.ty, bound); + } + } + let ident = &variant.ident; + let pat = fields_pat(&variant.fields); + quote! { + #ty::#ident #pat => #display + } + }); + let arms = arms.collect::>(); + let display_where_clause = display_inferred_bounds.augment_where_clause(input.generics); + Some(quote! { + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::core::fmt::Display for #ty #ty_generics #display_where_clause { + fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + #use_as_display + #[allow(unused_variables, deprecated, clippy::used_underscore_binding)] + match #void_deref self { + #(#arms,)* + } + } + } + }) + } else { + None + }; + + let from_impls = input.variants.iter().filter_map(|variant| { + let from_field = variant.from_field()?; + let span = from_field.attrs.from.unwrap().span; + let backtrace_field = variant.distinct_backtrace_field(); + let variant = &variant.ident; + let from = unoptional_type(from_field.ty); + let source_var = Ident::new("source", span); + let body = from_initializer(from_field, backtrace_field, &source_var); + let from_function = quote! { + fn from(#source_var: #from) -> Self { + #ty::#variant #body + } + }; + let from_impl = quote_spanned! {span=> + #[automatically_derived] + impl #impl_generics ::core::convert::From<#from> for #ty #ty_generics #where_clause { + #from_function + } + }; + Some(quote! { + #[allow( + deprecated, + unused_qualifications, + clippy::elidable_lifetime_names, + clippy::needless_lifetimes, + )] + #from_impl + }) + }); + + if input.generics.type_params().next().is_some() { + let self_token = ::default(); + error_inferred_bounds.insert(self_token, Trait::Debug); + error_inferred_bounds.insert(self_token, Trait::Display); + } + let error_where_clause = error_inferred_bounds.augment_where_clause(input.generics); + + quote! { + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::thiserror::#private::Error for #ty #ty_generics #error_where_clause { + #source_method + #provide_method + } + #display_impl + #(#from_impls)* + } +} + +// Create an ident with which we can expand `impl Trait for #ident {}` on a +// deprecated type without triggering deprecation warning on the generated impl. +pub(crate) fn call_site_ident(ident: &Ident) -> Ident { + let mut ident = ident.clone(); + ident.set_span(ident.span().resolved_at(Span::call_site())); + ident +} + +fn fields_pat(fields: &[Field]) -> TokenStream { + let mut members = fields.iter().map(|field| &field.member).peekable(); + match members.peek() { + Some(MemberUnraw::Named(_)) => quote!({ #(#members),* }), + Some(MemberUnraw::Unnamed(_)) => { + let vars = members.map(|member| match member { + MemberUnraw::Unnamed(index) => format_ident!("_{}", index), + MemberUnraw::Named(_) => unreachable!(), + }); + quote!((#(#vars),*)) + } + None => quote!({}), + } +} + +fn use_as_display(needs_as_display: bool) -> Option { + if needs_as_display { + Some(quote! { + use ::thiserror::#private::AsDisplay as _; + }) + } else { + None + } +} + +fn from_initializer( + from_field: &Field, + backtrace_field: Option<&Field>, + source_var: &Ident, +) -> TokenStream { + let from_member = &from_field.member; + let some_source = if type_is_option(from_field.ty) { + quote!(::core::option::Option::Some(#source_var)) + } else { + quote!(#source_var) + }; + let backtrace = backtrace_field.map(|backtrace_field| { + let backtrace_member = &backtrace_field.member; + if type_is_option(backtrace_field.ty) { + quote! { + #backtrace_member: ::core::option::Option::Some(::thiserror::#private::Backtrace::capture()), + } + } else { + quote! { + #backtrace_member: ::core::convert::From::from(::thiserror::#private::Backtrace::capture()), + } + } + }); + quote!({ + #from_member: #some_source, + #backtrace + }) +} + +fn type_is_option(ty: &Type) -> bool { + type_parameter_of_option(ty).is_some() +} + +fn unoptional_type(ty: &Type) -> TokenStream { + let unoptional = type_parameter_of_option(ty).unwrap_or(ty); + quote!(#unoptional) +} + +fn type_parameter_of_option(ty: &Type) -> Option<&Type> { + let path = match ty { + Type::Path(ty) => &ty.path, + _ => return None, + }; + + let last = path.segments.last().unwrap(); + if last.ident != "Option" { + return None; + } + + let bracketed = match &last.arguments { + PathArguments::AngleBracketed(bracketed) => bracketed, + _ => return None, + }; + + if bracketed.args.len() != 1 { + return None; + } + + match &bracketed.args[0] { + GenericArgument::Type(arg) => Some(arg), + _ => None, + } +} diff --git a/subprojects/thiserror/impl/src/fallback.rs b/subprojects/thiserror/impl/src/fallback.rs new file mode 100644 index 0000000..9914aa5 --- /dev/null +++ b/subprojects/thiserror/impl/src/fallback.rs @@ -0,0 +1,33 @@ +use crate::expand::call_site_ident; +use crate::private; +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn expand(input: &DeriveInput, error: syn::Error) -> TokenStream { + let ty = call_site_ident(&input.ident); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let error = error.to_compile_error(); + + quote! { + #error + + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::thiserror::#private::Error for #ty #ty_generics #where_clause + where + // Work around trivial bounds being unstable. + // https://github.com/rust-lang/rust/issues/48214 + for<'workaround> #ty #ty_generics: ::core::fmt::Debug, + {} + + #[allow(unused_qualifications)] + #[automatically_derived] + impl #impl_generics ::core::fmt::Display for #ty #ty_generics #where_clause { + fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::unreachable!() + } + } + } +} diff --git a/subprojects/thiserror/impl/src/fmt.rs b/subprojects/thiserror/impl/src/fmt.rs new file mode 100644 index 0000000..2988ca3 --- /dev/null +++ b/subprojects/thiserror/impl/src/fmt.rs @@ -0,0 +1,323 @@ +use crate::ast::{ContainerKind, Field}; +use crate::attr::{Display, Trait}; +use crate::private; +use crate::scan_expr::scan_expr; +use crate::unraw::{IdentUnraw, MemberUnraw}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use quote::{format_ident, quote, quote_spanned, ToTokens as _}; +use std::collections::{BTreeSet, HashMap}; +use std::iter; +use syn::ext::IdentExt; +use syn::parse::discouraged::Speculative; +use syn::parse::{Error, ParseStream, Parser, Result}; +use syn::{Expr, Ident, Index, LitStr, Token}; + +impl Display<'_> { + pub fn expand_shorthand(&mut self, fields: &[Field], container: ContainerKind) -> Result<()> { + let raw_args = self.args.clone(); + let FmtArguments { + named: user_named_args, + first_unnamed, + } = explicit_named_args.parse2(raw_args).unwrap(); + + let mut member_index = HashMap::new(); + let mut extra_positional_arguments_allowed = true; + for (i, field) in fields.iter().enumerate() { + member_index.insert(&field.member, i); + extra_positional_arguments_allowed &= matches!(&field.member, MemberUnraw::Named(_)); + } + + let span = self.fmt.span(); + let fmt = self.fmt.value(); + let mut read = fmt.as_str(); + let mut out = String::new(); + let mut has_bonus_display = false; + let mut infinite_recursive = false; + let mut implied_bounds = BTreeSet::new(); + let mut bindings = Vec::new(); + let mut macro_named_args = BTreeSet::new(); + + self.requires_fmt_machinery = self.requires_fmt_machinery || fmt.contains('}'); + + while let Some(brace) = read.find('{') { + self.requires_fmt_machinery = true; + out += &read[..brace + 1]; + read = &read[brace + 1..]; + if read.starts_with('{') { + out.push('{'); + read = &read[1..]; + continue; + } + let next = match read.chars().next() { + Some(next) => next, + None => return Ok(()), + }; + let member = match next { + '0'..='9' => { + let int = take_int(&mut read); + if !extra_positional_arguments_allowed { + if let Some(first_unnamed) = &first_unnamed { + let msg = format!("ambiguous reference to positional arguments by number in a {container}; change this to a named argument"); + return Err(Error::new_spanned(first_unnamed, msg)); + } + } + match int.parse::() { + Ok(index) => MemberUnraw::Unnamed(Index { index, span }), + Err(_) => return Ok(()), + } + } + 'a'..='z' | 'A'..='Z' | '_' => { + if read.starts_with("r#") { + continue; + } + let repr = take_ident(&mut read); + if repr == "_" { + // Invalid. Let rustc produce the diagnostic. + out += repr; + continue; + } + let ident = IdentUnraw::new(Ident::new(repr, span)); + if user_named_args.contains(&ident) { + // Refers to a named argument written by the user, not to field. + out += repr; + continue; + } + MemberUnraw::Named(ident) + } + _ => continue, + }; + let end_spec = match read.find('}') { + Some(end_spec) => end_spec, + None => return Ok(()), + }; + let mut bonus_display = false; + let bound = match read[..end_spec].chars().next_back() { + Some('?') => Trait::Debug, + Some('o') => Trait::Octal, + Some('x') => Trait::LowerHex, + Some('X') => Trait::UpperHex, + Some('p') => Trait::Pointer, + Some('b') => Trait::Binary, + Some('e') => Trait::LowerExp, + Some('E') => Trait::UpperExp, + Some(_) => Trait::Display, + None => { + bonus_display = true; + has_bonus_display = true; + Trait::Display + } + }; + infinite_recursive |= member == *"self" && bound == Trait::Display; + let field = match member_index.get(&member) { + Some(&field) => field, + None => { + out += &member.to_string(); + continue; + } + }; + implied_bounds.insert((field, bound)); + let formatvar_prefix = if bonus_display { + "__display" + } else if bound == Trait::Pointer { + "__pointer" + } else { + "__field" + }; + let mut formatvar = IdentUnraw::new(match &member { + MemberUnraw::Unnamed(index) => format_ident!("{}{}", formatvar_prefix, index), + MemberUnraw::Named(ident) => { + format_ident!("{}_{}", formatvar_prefix, ident.to_string()) + } + }); + while user_named_args.contains(&formatvar) { + formatvar = IdentUnraw::new(format_ident!("_{}", formatvar.to_string())); + } + formatvar.set_span(span); + out += &formatvar.to_string(); + if !macro_named_args.insert(formatvar.clone()) { + // Already added to bindings by a previous use. + continue; + } + let mut binding_value = match &member { + MemberUnraw::Unnamed(index) => format_ident!("_{}", index), + MemberUnraw::Named(ident) => ident.to_local(), + }; + binding_value.set_span(span.resolved_at(fields[field].member.span())); + let wrapped_binding_value = if bonus_display { + quote_spanned!(span=> #binding_value.as_display()) + } else if bound == Trait::Pointer { + quote!(::thiserror::#private::Var(#binding_value)) + } else { + binding_value.into_token_stream() + }; + bindings.push((formatvar.to_local(), wrapped_binding_value)); + } + + out += read; + self.fmt = LitStr::new(&out, self.fmt.span()); + self.has_bonus_display = has_bonus_display; + self.infinite_recursive = infinite_recursive; + self.implied_bounds = implied_bounds; + self.bindings = bindings; + Ok(()) + } +} + +struct FmtArguments { + named: BTreeSet, + first_unnamed: Option, +} + +#[allow(clippy::unnecessary_wraps)] +fn explicit_named_args(input: ParseStream) -> Result { + let ahead = input.fork(); + if let Ok(set) = try_explicit_named_args(&ahead) { + input.advance_to(&ahead); + return Ok(set); + } + + let ahead = input.fork(); + if let Ok(set) = fallback_explicit_named_args(&ahead) { + input.advance_to(&ahead); + return Ok(set); + } + + input.parse::().unwrap(); + Ok(FmtArguments { + named: BTreeSet::new(), + first_unnamed: None, + }) +} + +fn try_explicit_named_args(input: ParseStream) -> Result { + let mut syn_full = None; + let mut args = FmtArguments { + named: BTreeSet::new(), + first_unnamed: None, + }; + + while !input.is_empty() { + input.parse::()?; + if input.is_empty() { + break; + } + + let mut begin_unnamed = None; + if input.peek(Ident::peek_any) && input.peek2(Token![=]) && !input.peek2(Token![==]) { + let ident: IdentUnraw = input.parse()?; + input.parse::()?; + args.named.insert(ident); + } else { + begin_unnamed = Some(input.fork()); + } + + let ahead = input.fork(); + if *syn_full.get_or_insert_with(is_syn_full) && ahead.parse::().is_ok() { + input.advance_to(&ahead); + } else { + scan_expr(input)?; + } + + if let Some(begin_unnamed) = begin_unnamed { + if args.first_unnamed.is_none() { + args.first_unnamed = Some(between(&begin_unnamed, input)); + } + } + } + + Ok(args) +} + +fn fallback_explicit_named_args(input: ParseStream) -> Result { + let mut args = FmtArguments { + named: BTreeSet::new(), + first_unnamed: None, + }; + + while !input.is_empty() { + if input.peek(Token![,]) + && input.peek2(Ident::peek_any) + && input.peek3(Token![=]) + && !input.peek3(Token![==]) + { + input.parse::()?; + let ident: IdentUnraw = input.parse()?; + input.parse::()?; + args.named.insert(ident); + } else { + input.parse::()?; + } + } + + Ok(args) +} + +fn is_syn_full() -> bool { + // Expr::Block contains syn::Block which contains Vec. In the + // current version of Syn, syn::Stmt is exhaustive and could only plausibly + // represent `trait Trait {}` in Stmt::Item which contains syn::Item. Most + // of the point of syn's non-"full" mode is to avoid compiling Item and the + // entire expansive syntax tree it comprises. So the following expression + // being parsed to Expr::Block is a reliable indication that "full" is + // enabled. + let test = quote!({ + trait Trait {} + }); + match syn::parse2(test) { + Ok(Expr::Verbatim(_)) | Err(_) => false, + Ok(Expr::Block(_)) => true, + Ok(_) => unreachable!(), + } +} + +fn take_int<'a>(read: &mut &'a str) -> &'a str { + let mut int_len = 0; + for ch in read.chars() { + match ch { + '0'..='9' => int_len += 1, + _ => break, + } + } + let (int, rest) = read.split_at(int_len); + *read = rest; + int +} + +fn take_ident<'a>(read: &mut &'a str) -> &'a str { + let mut ident_len = 0; + for ch in read.chars() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident_len += 1, + _ => break, + } + } + let (ident, rest) = read.split_at(ident_len); + *read = rest; + ident +} + +fn between<'a>(begin: ParseStream<'a>, end: ParseStream<'a>) -> TokenStream { + let end = end.cursor(); + let mut cursor = begin.cursor(); + let mut tokens = TokenStream::new(); + + while cursor < end { + let (tt, next) = cursor.token_tree().unwrap(); + + if end < next { + if let Some((inside, _span, _after)) = cursor.group(Delimiter::None) { + cursor = inside; + continue; + } + if tokens.is_empty() { + tokens.extend(iter::once(tt)); + } + break; + } + + tokens.extend(iter::once(tt)); + cursor = next; + } + + tokens +} diff --git a/subprojects/thiserror/impl/src/generics.rs b/subprojects/thiserror/impl/src/generics.rs new file mode 100644 index 0000000..26fe0a9 --- /dev/null +++ b/subprojects/thiserror/impl/src/generics.rs @@ -0,0 +1,83 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap as Map, BTreeSet as Set}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, GenericArgument, Generics, Ident, PathArguments, Token, Type, WhereClause}; + +pub struct ParamsInScope<'a> { + names: Set<&'a Ident>, +} + +impl<'a> ParamsInScope<'a> { + pub fn new(generics: &'a Generics) -> Self { + ParamsInScope { + names: generics.type_params().map(|param| ¶m.ident).collect(), + } + } + + pub fn intersects(&self, ty: &Type) -> bool { + let mut found = false; + crawl(self, ty, &mut found); + found + } +} + +fn crawl(in_scope: &ParamsInScope, ty: &Type, found: &mut bool) { + if let Type::Path(ty) = ty { + if let Some(qself) = &ty.qself { + crawl(in_scope, &qself.ty, found); + } else { + let front = ty.path.segments.first().unwrap(); + if front.arguments.is_none() && in_scope.names.contains(&front.ident) { + *found = true; + } + } + for segment in &ty.path.segments { + if let PathArguments::AngleBracketed(arguments) = &segment.arguments { + for arg in &arguments.args { + if let GenericArgument::Type(ty) = arg { + crawl(in_scope, ty, found); + } + } + } + } + } +} + +pub struct InferredBounds { + bounds: Map, Punctuated)>, + order: Vec, +} + +impl InferredBounds { + pub fn new() -> Self { + InferredBounds { + bounds: Map::new(), + order: Vec::new(), + } + } + + pub fn insert(&mut self, ty: impl ToTokens, bound: impl ToTokens) { + let ty = ty.to_token_stream(); + let bound = bound.to_token_stream(); + let entry = self.bounds.entry(ty.to_string()); + if let Entry::Vacant(_) = entry { + self.order.push(ty); + } + let (set, tokens) = entry.or_default(); + if set.insert(bound.to_string()) { + tokens.push(bound); + } + } + + pub fn augment_where_clause(&self, generics: &Generics) -> WhereClause { + let mut generics = generics.clone(); + let where_clause = generics.make_where_clause(); + for ty in &self.order { + let (_set, bounds) = &self.bounds[&ty.to_string()]; + where_clause.predicates.push(parse_quote!(#ty: #bounds)); + } + generics.where_clause.unwrap() + } +} diff --git a/subprojects/thiserror/impl/src/lib.rs b/subprojects/thiserror/impl/src/lib.rs new file mode 100644 index 0000000..25890f2 --- /dev/null +++ b/subprojects/thiserror/impl/src/lib.rs @@ -0,0 +1,55 @@ +#![allow( + clippy::blocks_in_conditions, + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::enum_glob_use, + clippy::expl_impl_clone_on_copy, // https://github.com/rust-lang/rust-clippy/issues/15842 + clippy::manual_find, + clippy::manual_let_else, + clippy::manual_map, + clippy::map_unwrap_or, + clippy::module_name_repetitions, + clippy::needless_pass_by_value, + clippy::range_plus_one, + clippy::single_match_else, + clippy::struct_field_names, + clippy::too_many_lines, + clippy::wrong_self_convention +)] +#![allow(unknown_lints, mismatched_lifetime_syntaxes)] + +extern crate proc_macro; + +mod ast; +mod attr; +mod expand; +mod fallback; +mod fmt; +mod generics; +mod prop; +mod scan_expr; +mod unraw; +mod valid; + +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{ToTokens, TokenStreamExt as _}; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(Error, attributes(backtrace, error, from, source))] +pub fn derive_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand::derive(&input).into() +} + +#[allow(non_camel_case_types)] +struct private; + +impl ToTokens for private { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.append(Ident::new( + concat!("__private", env!("CARGO_PKG_VERSION_PATCH")), + Span::call_site(), + )); + } +} diff --git a/subprojects/thiserror/impl/src/prop.rs b/subprojects/thiserror/impl/src/prop.rs new file mode 100644 index 0000000..0a101fc --- /dev/null +++ b/subprojects/thiserror/impl/src/prop.rs @@ -0,0 +1,148 @@ +use crate::ast::{Enum, Field, Struct, Variant}; +use crate::unraw::MemberUnraw; +use proc_macro2::Span; +use syn::Type; + +impl Struct<'_> { + pub(crate) fn from_field(&self) -> Option<&Field> { + from_field(&self.fields) + } + + pub(crate) fn source_field(&self) -> Option<&Field> { + source_field(&self.fields) + } + + pub(crate) fn backtrace_field(&self) -> Option<&Field> { + backtrace_field(&self.fields) + } + + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { + let backtrace_field = self.backtrace_field()?; + distinct_backtrace_field(backtrace_field, self.from_field()) + } +} + +impl Enum<'_> { + pub(crate) fn has_source(&self) -> bool { + self.variants + .iter() + .any(|variant| variant.source_field().is_some() || variant.attrs.transparent.is_some()) + } + + pub(crate) fn has_backtrace(&self) -> bool { + self.variants + .iter() + .any(|variant| variant.backtrace_field().is_some()) + } + + pub(crate) fn has_display(&self) -> bool { + self.attrs.display.is_some() + || self.attrs.transparent.is_some() + || self.attrs.fmt.is_some() + || self + .variants + .iter() + .any(|variant| variant.attrs.display.is_some() || variant.attrs.fmt.is_some()) + || self + .variants + .iter() + .all(|variant| variant.attrs.transparent.is_some()) + } +} + +impl Variant<'_> { + pub(crate) fn from_field(&self) -> Option<&Field> { + from_field(&self.fields) + } + + pub(crate) fn source_field(&self) -> Option<&Field> { + source_field(&self.fields) + } + + pub(crate) fn backtrace_field(&self) -> Option<&Field> { + backtrace_field(&self.fields) + } + + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { + let backtrace_field = self.backtrace_field()?; + distinct_backtrace_field(backtrace_field, self.from_field()) + } +} + +impl Field<'_> { + pub(crate) fn is_backtrace(&self) -> bool { + type_is_backtrace(self.ty) + } + + pub(crate) fn source_span(&self) -> Span { + if let Some(source_attr) = &self.attrs.source { + source_attr.span + } else if let Some(from_attr) = &self.attrs.from { + from_attr.span + } else { + self.member.span() + } + } +} + +fn from_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.from.is_some() { + return Some(field); + } + } + None +} + +fn source_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.from.is_some() || field.attrs.source.is_some() { + return Some(field); + } + } + for field in fields { + match &field.member { + MemberUnraw::Named(ident) if ident == "source" => return Some(field), + _ => {} + } + } + None +} + +fn backtrace_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.backtrace.is_some() { + return Some(field); + } + } + for field in fields { + if field.is_backtrace() { + return Some(field); + } + } + None +} + +// The #[backtrace] field, if it is not the same as the #[from] field. +fn distinct_backtrace_field<'a, 'b>( + backtrace_field: &'a Field<'b>, + from_field: Option<&Field>, +) -> Option<&'a Field<'b>> { + if from_field.map_or(false, |from_field| { + from_field.member == backtrace_field.member + }) { + None + } else { + Some(backtrace_field) + } +} + +fn type_is_backtrace(ty: &Type) -> bool { + let path = match ty { + Type::Path(ty) => &ty.path, + _ => return false, + }; + + let last = path.segments.last().unwrap(); + last.ident == "Backtrace" && last.arguments.is_empty() +} diff --git a/subprojects/thiserror/impl/src/scan_expr.rs b/subprojects/thiserror/impl/src/scan_expr.rs new file mode 100644 index 0000000..155b5b6 --- /dev/null +++ b/subprojects/thiserror/impl/src/scan_expr.rs @@ -0,0 +1,264 @@ +use self::{Action::*, Input::*}; +use proc_macro2::{Delimiter, Ident, Spacing, TokenTree}; +use syn::parse::{ParseStream, Result}; +use syn::{AngleBracketedGenericArguments, BinOp, Expr, ExprPath, Lifetime, Lit, Token, Type}; + +enum Input { + Keyword(&'static str), + Punct(&'static str), + ConsumeAny, + ConsumeBinOp, + ConsumeBrace, + ConsumeDelimiter, + ConsumeIdent, + ConsumeLifetime, + ConsumeLiteral, + ConsumeNestedBrace, + ExpectPath, + ExpectTurbofish, + ExpectType, + CanBeginExpr, + Otherwise, + Empty, +} + +enum Action { + SetState(&'static [(Input, Action)]), + IncDepth, + DecDepth, + Finish, +} + +static INIT: [(Input, Action); 28] = [ + (ConsumeDelimiter, SetState(&POSTFIX)), + (Keyword("async"), SetState(&ASYNC)), + (Keyword("break"), SetState(&BREAK_LABEL)), + (Keyword("const"), SetState(&CONST)), + (Keyword("continue"), SetState(&CONTINUE)), + (Keyword("for"), SetState(&FOR)), + (Keyword("if"), IncDepth), + (Keyword("let"), SetState(&PATTERN)), + (Keyword("loop"), SetState(&BLOCK)), + (Keyword("match"), IncDepth), + (Keyword("move"), SetState(&CLOSURE)), + (Keyword("return"), SetState(&RETURN)), + (Keyword("static"), SetState(&CLOSURE)), + (Keyword("unsafe"), SetState(&BLOCK)), + (Keyword("while"), IncDepth), + (Keyword("yield"), SetState(&RETURN)), + (Keyword("_"), SetState(&POSTFIX)), + (Punct("!"), SetState(&INIT)), + (Punct("#"), SetState(&[(ConsumeDelimiter, SetState(&INIT))])), + (Punct("&"), SetState(&REFERENCE)), + (Punct("*"), SetState(&INIT)), + (Punct("-"), SetState(&INIT)), + (Punct("..="), SetState(&INIT)), + (Punct(".."), SetState(&RANGE)), + (Punct("|"), SetState(&CLOSURE_ARGS)), + (ConsumeLifetime, SetState(&[(Punct(":"), SetState(&INIT))])), + (ConsumeLiteral, SetState(&POSTFIX)), + (ExpectPath, SetState(&PATH)), +]; + +static POSTFIX: [(Input, Action); 10] = [ + (Keyword("as"), SetState(&[(ExpectType, SetState(&POSTFIX))])), + (Punct("..="), SetState(&INIT)), + (Punct(".."), SetState(&RANGE)), + (Punct("."), SetState(&DOT)), + (Punct("?"), SetState(&POSTFIX)), + (ConsumeBinOp, SetState(&INIT)), + (Punct("="), SetState(&INIT)), + (ConsumeNestedBrace, SetState(&IF_THEN)), + (ConsumeDelimiter, SetState(&POSTFIX)), + (Empty, Finish), +]; + +static ASYNC: [(Input, Action); 3] = [ + (Keyword("move"), SetState(&ASYNC)), + (Punct("|"), SetState(&CLOSURE_ARGS)), + (ConsumeBrace, SetState(&POSTFIX)), +]; + +static BLOCK: [(Input, Action); 1] = [(ConsumeBrace, SetState(&POSTFIX))]; + +static BREAK_LABEL: [(Input, Action); 2] = [ + (ConsumeLifetime, SetState(&BREAK_VALUE)), + (Otherwise, SetState(&BREAK_VALUE)), +]; + +static BREAK_VALUE: [(Input, Action); 3] = [ + (ConsumeNestedBrace, SetState(&IF_THEN)), + (CanBeginExpr, SetState(&INIT)), + (Otherwise, SetState(&POSTFIX)), +]; + +static CLOSURE: [(Input, Action); 6] = [ + (Keyword("async"), SetState(&CLOSURE)), + (Keyword("move"), SetState(&CLOSURE)), + (Punct(","), SetState(&CLOSURE)), + (Punct(">"), SetState(&CLOSURE)), + (Punct("|"), SetState(&CLOSURE_ARGS)), + (ConsumeLifetime, SetState(&CLOSURE)), +]; + +static CLOSURE_ARGS: [(Input, Action); 2] = [ + (Punct("|"), SetState(&CLOSURE_RET)), + (ConsumeAny, SetState(&CLOSURE_ARGS)), +]; + +static CLOSURE_RET: [(Input, Action); 2] = [ + (Punct("->"), SetState(&[(ExpectType, SetState(&BLOCK))])), + (Otherwise, SetState(&INIT)), +]; + +static CONST: [(Input, Action); 2] = [ + (Punct("|"), SetState(&CLOSURE_ARGS)), + (ConsumeBrace, SetState(&POSTFIX)), +]; + +static CONTINUE: [(Input, Action); 2] = [ + (ConsumeLifetime, SetState(&POSTFIX)), + (Otherwise, SetState(&POSTFIX)), +]; + +static DOT: [(Input, Action); 3] = [ + (Keyword("await"), SetState(&POSTFIX)), + (ConsumeIdent, SetState(&METHOD)), + (ConsumeLiteral, SetState(&POSTFIX)), +]; + +static FOR: [(Input, Action); 2] = [ + (Punct("<"), SetState(&CLOSURE)), + (Otherwise, SetState(&PATTERN)), +]; + +static IF_ELSE: [(Input, Action); 2] = [(Keyword("if"), SetState(&INIT)), (ConsumeBrace, DecDepth)]; +static IF_THEN: [(Input, Action); 2] = + [(Keyword("else"), SetState(&IF_ELSE)), (Otherwise, DecDepth)]; + +static METHOD: [(Input, Action); 1] = [(ExpectTurbofish, SetState(&POSTFIX))]; + +static PATH: [(Input, Action); 4] = [ + (Punct("!="), SetState(&INIT)), + (Punct("!"), SetState(&INIT)), + (ConsumeNestedBrace, SetState(&IF_THEN)), + (Otherwise, SetState(&POSTFIX)), +]; + +static PATTERN: [(Input, Action); 15] = [ + (ConsumeDelimiter, SetState(&PATTERN)), + (Keyword("box"), SetState(&PATTERN)), + (Keyword("in"), IncDepth), + (Keyword("mut"), SetState(&PATTERN)), + (Keyword("ref"), SetState(&PATTERN)), + (Keyword("_"), SetState(&PATTERN)), + (Punct("!"), SetState(&PATTERN)), + (Punct("&"), SetState(&PATTERN)), + (Punct("..="), SetState(&PATTERN)), + (Punct(".."), SetState(&PATTERN)), + (Punct("="), SetState(&INIT)), + (Punct("@"), SetState(&PATTERN)), + (Punct("|"), SetState(&PATTERN)), + (ConsumeLiteral, SetState(&PATTERN)), + (ExpectPath, SetState(&PATTERN)), +]; + +static RANGE: [(Input, Action); 6] = [ + (Punct("..="), SetState(&INIT)), + (Punct(".."), SetState(&RANGE)), + (Punct("."), SetState(&DOT)), + (ConsumeNestedBrace, SetState(&IF_THEN)), + (Empty, Finish), + (Otherwise, SetState(&INIT)), +]; + +static RAW: [(Input, Action); 3] = [ + (Keyword("const"), SetState(&INIT)), + (Keyword("mut"), SetState(&INIT)), + (Otherwise, SetState(&POSTFIX)), +]; + +static REFERENCE: [(Input, Action); 3] = [ + (Keyword("mut"), SetState(&INIT)), + (Keyword("raw"), SetState(&RAW)), + (Otherwise, SetState(&INIT)), +]; + +static RETURN: [(Input, Action); 2] = [ + (CanBeginExpr, SetState(&INIT)), + (Otherwise, SetState(&POSTFIX)), +]; + +pub(crate) fn scan_expr(input: ParseStream) -> Result<()> { + let mut state = INIT.as_slice(); + let mut depth = 0usize; + 'table: loop { + for rule in state { + if match rule.0 { + Input::Keyword(expected) => input.step(|cursor| match cursor.ident() { + Some((ident, rest)) if ident == expected => Ok((true, rest)), + _ => Ok((false, *cursor)), + })?, + Input::Punct(expected) => input.step(|cursor| { + let begin = *cursor; + let mut cursor = begin; + for (i, ch) in expected.chars().enumerate() { + match cursor.punct() { + Some((punct, _)) if punct.as_char() != ch => break, + Some((_, rest)) if i == expected.len() - 1 => { + return Ok((true, rest)); + } + Some((punct, rest)) if punct.spacing() == Spacing::Joint => { + cursor = rest; + } + _ => break, + } + } + Ok((false, begin)) + })?, + Input::ConsumeAny => input.parse::>()?.is_some(), + Input::ConsumeBinOp => input.parse::().is_ok(), + Input::ConsumeBrace | Input::ConsumeNestedBrace => { + (matches!(rule.0, Input::ConsumeBrace) || depth > 0) + && input.step(|cursor| match cursor.group(Delimiter::Brace) { + Some((_inside, _span, rest)) => Ok((true, rest)), + None => Ok((false, *cursor)), + })? + } + Input::ConsumeDelimiter => input.step(|cursor| match cursor.any_group() { + Some((_inside, _delimiter, _span, rest)) => Ok((true, rest)), + None => Ok((false, *cursor)), + })?, + Input::ConsumeIdent => input.parse::>()?.is_some(), + Input::ConsumeLifetime => input.parse::>()?.is_some(), + Input::ConsumeLiteral => input.parse::>()?.is_some(), + Input::ExpectPath => { + input.parse::()?; + true + } + Input::ExpectTurbofish => { + if input.peek(Token![::]) { + input.parse::()?; + } + true + } + Input::ExpectType => { + Type::without_plus(input)?; + true + } + Input::CanBeginExpr => Expr::peek(input), + Input::Otherwise => true, + Input::Empty => input.is_empty() || input.peek(Token![,]), + } { + state = match rule.1 { + Action::SetState(next) => next, + Action::IncDepth => (depth += 1, &INIT).1, + Action::DecDepth => (depth -= 1, &POSTFIX).1, + Action::Finish => return if depth == 0 { Ok(()) } else { break }, + }; + continue 'table; + } + } + return Err(input.error("unsupported expression")); + } +} diff --git a/subprojects/thiserror/impl/src/unraw.rs b/subprojects/thiserror/impl/src/unraw.rs new file mode 100644 index 0000000..73b9970 --- /dev/null +++ b/subprojects/thiserror/impl/src/unraw.rs @@ -0,0 +1,142 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::ToTokens; +use std::cmp::Ordering; +use std::fmt::{self, Display}; +use std::hash::{Hash, Hasher}; +use syn::ext::IdentExt as _; +use syn::parse::{Parse, ParseStream, Result}; +use syn::Index; + +#[derive(Clone)] +#[repr(transparent)] +pub struct IdentUnraw(Ident); + +impl IdentUnraw { + pub fn new(ident: Ident) -> Self { + IdentUnraw(ident) + } + + pub fn to_local(&self) -> Ident { + let unraw = self.0.unraw(); + let repr = unraw.to_string(); + if syn::parse_str::(&repr).is_err() { + if let "_" | "super" | "self" | "Self" | "crate" = repr.as_str() { + // Some identifiers are never allowed to appear as raw, like r#self and r#_. + } else { + return Ident::new_raw(&repr, Span::call_site()); + } + } + unraw + } + + pub fn set_span(&mut self, span: Span) { + self.0.set_span(span); + } +} + +impl Display for IdentUnraw { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.0.unraw(), formatter) + } +} + +impl Eq for IdentUnraw {} + +impl PartialEq for IdentUnraw { + fn eq(&self, other: &Self) -> bool { + PartialEq::eq(&self.0.unraw(), &other.0.unraw()) + } +} + +impl PartialEq for IdentUnraw { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl Ord for IdentUnraw { + fn cmp(&self, other: &Self) -> Ordering { + Ord::cmp(&self.0.unraw(), &other.0.unraw()) + } +} + +impl PartialOrd for IdentUnraw { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Self::cmp(self, other)) + } +} + +impl Parse for IdentUnraw { + fn parse(input: ParseStream) -> Result { + input.call(Ident::parse_any).map(IdentUnraw::new) + } +} + +impl ToTokens for IdentUnraw { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.unraw().to_tokens(tokens); + } +} + +#[derive(Clone)] +pub enum MemberUnraw { + Named(IdentUnraw), + Unnamed(Index), +} + +impl MemberUnraw { + pub fn span(&self) -> Span { + match self { + MemberUnraw::Named(ident) => ident.0.span(), + MemberUnraw::Unnamed(index) => index.span, + } + } +} + +impl Display for MemberUnraw { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + MemberUnraw::Named(this) => Display::fmt(this, formatter), + MemberUnraw::Unnamed(this) => Display::fmt(&this.index, formatter), + } + } +} + +impl Eq for MemberUnraw {} + +impl PartialEq for MemberUnraw { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (MemberUnraw::Named(this), MemberUnraw::Named(other)) => this == other, + (MemberUnraw::Unnamed(this), MemberUnraw::Unnamed(other)) => this == other, + _ => false, + } + } +} + +impl PartialEq for MemberUnraw { + fn eq(&self, other: &str) -> bool { + match self { + MemberUnraw::Named(this) => this == other, + MemberUnraw::Unnamed(_) => false, + } + } +} + +impl Hash for MemberUnraw { + fn hash(&self, hasher: &mut H) { + match self { + MemberUnraw::Named(ident) => ident.0.unraw().hash(hasher), + MemberUnraw::Unnamed(index) => index.hash(hasher), + } + } +} + +impl ToTokens for MemberUnraw { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + MemberUnraw::Named(ident) => ident.to_local().to_tokens(tokens), + MemberUnraw::Unnamed(index) => index.to_tokens(tokens), + } + } +} diff --git a/subprojects/thiserror/impl/src/valid.rs b/subprojects/thiserror/impl/src/valid.rs new file mode 100644 index 0000000..21bd885 --- /dev/null +++ b/subprojects/thiserror/impl/src/valid.rs @@ -0,0 +1,248 @@ +use crate::ast::{Enum, Field, Input, Struct, Variant}; +use crate::attr::Attrs; +use syn::{Error, GenericArgument, PathArguments, Result, Type}; + +impl Input<'_> { + pub(crate) fn validate(&self) -> Result<()> { + match self { + Input::Struct(input) => input.validate(), + Input::Enum(input) => input.validate(), + } + } +} + +impl Struct<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + if let Some(transparent) = self.attrs.transparent { + if self.fields.len() != 1 { + return Err(Error::new_spanned( + transparent.original, + "#[error(transparent)] requires exactly one field", + )); + } + if let Some(source) = self.fields.iter().find_map(|f| f.attrs.source) { + return Err(Error::new_spanned( + source.original, + "transparent error struct can't contain #[source]", + )); + } + } + if let Some(fmt) = &self.attrs.fmt { + return Err(Error::new_spanned( + fmt.original, + "#[error(fmt = ...)] is only supported in enums; for a struct, handwrite your own Display impl", + )); + } + check_field_attrs(&self.fields)?; + for field in &self.fields { + field.validate()?; + } + Ok(()) + } +} + +impl Enum<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + let has_display = self.has_display(); + for variant in &self.variants { + variant.validate()?; + if has_display + && variant.attrs.display.is_none() + && variant.attrs.transparent.is_none() + && variant.attrs.fmt.is_none() + { + return Err(Error::new_spanned( + variant.original, + "missing #[error(\"...\")] display attribute", + )); + } + } + Ok(()) + } +} + +impl Variant<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + if self.attrs.transparent.is_some() { + if self.fields.len() != 1 { + return Err(Error::new_spanned( + self.original, + "#[error(transparent)] requires exactly one field", + )); + } + if let Some(source) = self.fields.iter().find_map(|f| f.attrs.source) { + return Err(Error::new_spanned( + source.original, + "transparent variant can't contain #[source]", + )); + } + } + check_field_attrs(&self.fields)?; + for field in &self.fields { + field.validate()?; + } + Ok(()) + } +} + +impl Field<'_> { + fn validate(&self) -> Result<()> { + if let Some(unexpected_display_attr) = if let Some(display) = &self.attrs.display { + Some(display.original) + } else if let Some(fmt) = &self.attrs.fmt { + Some(fmt.original) + } else { + None + } { + return Err(Error::new_spanned( + unexpected_display_attr, + "not expected here; the #[error(...)] attribute belongs on top of a struct or an enum variant", + )); + } + Ok(()) + } +} + +fn check_non_field_attrs(attrs: &Attrs) -> Result<()> { + if let Some(from) = &attrs.from { + return Err(Error::new_spanned( + from.original, + "not expected here; the #[from] attribute belongs on a specific field", + )); + } + if let Some(source) = &attrs.source { + return Err(Error::new_spanned( + source.original, + "not expected here; the #[source] attribute belongs on a specific field", + )); + } + if let Some(backtrace) = &attrs.backtrace { + return Err(Error::new_spanned( + backtrace, + "not expected here; the #[backtrace] attribute belongs on a specific field", + )); + } + if attrs.transparent.is_some() { + if let Some(display) = &attrs.display { + return Err(Error::new_spanned( + display.original, + "cannot have both #[error(transparent)] and a display attribute", + )); + } + if let Some(fmt) = &attrs.fmt { + return Err(Error::new_spanned( + fmt.original, + "cannot have both #[error(transparent)] and #[error(fmt = ...)]", + )); + } + } else if let (Some(display), Some(_)) = (&attrs.display, &attrs.fmt) { + return Err(Error::new_spanned( + display.original, + "cannot have both #[error(fmt = ...)] and a format arguments attribute", + )); + } + + Ok(()) +} + +fn check_field_attrs(fields: &[Field]) -> Result<()> { + let mut from_field = None; + let mut source_field = None; + let mut backtrace_field = None; + let mut has_backtrace = false; + for field in fields { + if let Some(from) = field.attrs.from { + if from_field.is_some() { + return Err(Error::new_spanned( + from.original, + "duplicate #[from] attribute", + )); + } + from_field = Some(field); + } + if let Some(source) = field.attrs.source { + if source_field.is_some() { + return Err(Error::new_spanned( + source.original, + "duplicate #[source] attribute", + )); + } + source_field = Some(field); + } + if let Some(backtrace) = field.attrs.backtrace { + if backtrace_field.is_some() { + return Err(Error::new_spanned( + backtrace, + "duplicate #[backtrace] attribute", + )); + } + backtrace_field = Some(field); + has_backtrace = true; + } + if let Some(transparent) = field.attrs.transparent { + return Err(Error::new_spanned( + transparent.original, + "#[error(transparent)] needs to go outside the enum or struct, not on an individual field", + )); + } + has_backtrace |= field.is_backtrace(); + } + if let (Some(from_field), Some(source_field)) = (from_field, source_field) { + if from_field.member != source_field.member { + return Err(Error::new_spanned( + from_field.attrs.from.unwrap().original, + "#[from] is only supported on the source field, not any other field", + )); + } + } + if let Some(from_field) = from_field { + let max_expected_fields = match backtrace_field { + Some(backtrace_field) => 1 + (from_field.member != backtrace_field.member) as usize, + None => 1 + has_backtrace as usize, + }; + if fields.len() > max_expected_fields { + return Err(Error::new_spanned( + from_field.attrs.from.unwrap().original, + "deriving From requires no fields other than source and backtrace", + )); + } + } + if let Some(source_field) = source_field.or(from_field) { + if contains_non_static_lifetime(source_field.ty) { + return Err(Error::new_spanned( + &source_field.original.ty, + "non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static", + )); + } + } + Ok(()) +} + +fn contains_non_static_lifetime(ty: &Type) -> bool { + match ty { + Type::Path(ty) => { + let bracketed = match &ty.path.segments.last().unwrap().arguments { + PathArguments::AngleBracketed(bracketed) => bracketed, + _ => return false, + }; + for arg in &bracketed.args { + match arg { + GenericArgument::Type(ty) if contains_non_static_lifetime(ty) => return true, + GenericArgument::Lifetime(lifetime) if lifetime.ident != "static" => { + return true + } + _ => {} + } + } + false + } + Type::Reference(ty) => ty + .lifetime + .as_ref() + .map_or(false, |lifetime| lifetime.ident != "static"), + _ => false, // maybe implement later if there are common other cases + } +} diff --git a/subprojects/thiserror/rust-toolchain.toml b/subprojects/thiserror/rust-toolchain.toml new file mode 100644 index 0000000..20fe888 --- /dev/null +++ b/subprojects/thiserror/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +components = ["rust-src"] diff --git a/subprojects/thiserror/src/aserror.rs b/subprojects/thiserror/src/aserror.rs new file mode 100644 index 0000000..ac91cc8 --- /dev/null +++ b/subprojects/thiserror/src/aserror.rs @@ -0,0 +1,50 @@ +use core::error::Error; +use core::panic::UnwindSafe; + +#[doc(hidden)] +pub trait AsDynError<'a>: Sealed { + fn as_dyn_error(&self) -> &(dyn Error + 'a); +} + +impl<'a, T: Error + 'a> AsDynError<'a> for T { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + Sync + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + Sync + UnwindSafe + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +#[doc(hidden)] +pub trait Sealed {} +impl Sealed for T {} +impl Sealed for dyn Error + '_ {} +impl Sealed for dyn Error + Send + '_ {} +impl Sealed for dyn Error + Send + Sync + '_ {} +impl Sealed for dyn Error + Send + Sync + UnwindSafe + '_ {} diff --git a/subprojects/thiserror/src/display.rs b/subprojects/thiserror/src/display.rs new file mode 100644 index 0000000..e544657 --- /dev/null +++ b/subprojects/thiserror/src/display.rs @@ -0,0 +1,82 @@ +use core::fmt::Display; +#[cfg(feature = "std")] +use std::path::{self, Path, PathBuf}; + +#[doc(hidden)] +pub trait AsDisplay<'a>: Sealed { + // TODO: convert to generic associated type. + // https://github.com/dtolnay/thiserror/pull/253 + type Target: Display; + + fn as_display(&'a self) -> Self::Target; +} + +impl<'a, T> AsDisplay<'a> for &T +where + T: Display + ?Sized + 'a, +{ + type Target = &'a T; + + fn as_display(&'a self) -> Self::Target { + *self + } +} + +#[cfg(feature = "std")] +impl<'a> AsDisplay<'a> for Path { + type Target = path::Display<'a>; + + #[inline] + fn as_display(&'a self) -> Self::Target { + self.display() + } +} + +#[cfg(feature = "std")] +impl<'a> AsDisplay<'a> for PathBuf { + type Target = path::Display<'a>; + + #[inline] + fn as_display(&'a self) -> Self::Target { + self.display() + } +} + +#[doc(hidden)] +pub trait Sealed {} +impl Sealed for &T {} +#[cfg(feature = "std")] +impl Sealed for Path {} +#[cfg(feature = "std")] +impl Sealed for PathBuf {} + +// Add a synthetic second impl of AsDisplay to prevent the "single applicable +// impl" rule from making too weird inference decision based on the single impl +// for &T, which could lead to code that compiles with thiserror's std feature +// off but breaks under feature unification when std is turned on by an +// unrelated crate. +#[cfg(not(feature = "std"))] +mod placeholder { + use super::{AsDisplay, Sealed}; + use core::fmt::{self, Display}; + + #[allow(dead_code)] + pub struct Placeholder; + + impl<'a> AsDisplay<'a> for Placeholder { + type Target = Self; + + #[inline] + fn as_display(&'a self) -> Self::Target { + Placeholder + } + } + + impl Display for Placeholder { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unreachable!() + } + } + + impl Sealed for Placeholder {} +} diff --git a/subprojects/thiserror/src/lib.rs b/subprojects/thiserror/src/lib.rs new file mode 100644 index 0000000..155272d --- /dev/null +++ b/subprojects/thiserror/src/lib.rs @@ -0,0 +1,291 @@ +//! [![github]](https://github.com/dtolnay/thiserror) [![crates-io]](https://crates.io/crates/thiserror) [![docs-rs]](https://docs.rs/thiserror) +//! +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//!
+//! +//! This library provides a convenient derive macro for the standard library's +//! [`std::error::Error`] trait. +//! +//!
+//! +//! # Example +//! +//! ```rust +//! # use std::io; +//! use thiserror::Error; +//! +//! #[derive(Error, Debug)] +//! pub enum DataStoreError { +//! #[error("data store disconnected")] +//! Disconnect(#[from] io::Error), +//! #[error("the data for key `{0}` is not available")] +//! Redaction(String), +//! #[error("invalid header (expected {expected:?}, found {found:?})")] +//! InvalidHeader { +//! expected: String, +//! found: String, +//! }, +//! #[error("unknown data store error")] +//! Unknown, +//! } +//! ``` +//! +//!
+//! +//! # Details +//! +//! - Thiserror deliberately does not appear in your public API. You get the +//! same thing as if you had written an implementation of +//! [`std::error::Error`] by hand, and switching from handwritten impls to +//! thiserror or vice versa is not a breaking change. +//! +//! - Errors may be enums, structs with named fields, tuple structs, or unit +//! structs. +//! +//! - A [`Display`] impl is generated for your error if you provide +//! `#[error("...")]` messages on the struct or each variant of your enum, as +//! shown above in the example. +//! +//! The messages support a shorthand for interpolating fields from the error. +//! +//! - `#[error("{var}")]` ⟶ `write!("{}", self.var)` +//! - `#[error("{0}")]` ⟶ `write!("{}", self.0)` +//! - `#[error("{var:?}")]` ⟶ `write!("{:?}", self.var)` +//! - `#[error("{0:?}")]` ⟶ `write!("{:?}", self.0)` +//! +//! These shorthands can be used together with any additional format args, +//! which may be arbitrary expressions. For example: +//! +//! ```rust +//! # use core::i32; +//! # use thiserror::Error; +//! # +//! #[derive(Error, Debug)] +//! pub enum Error { +//! #[error("invalid rdo_lookahead_frames {0} (expected < {max})", max = i32::MAX)] +//! InvalidLookahead(u32), +//! } +//! ``` +//! +//! If one of the additional expression arguments needs to refer to a field of +//! the struct or enum, then refer to named fields as `.var` and tuple fields +//! as `.0`. +//! +//! ```rust +//! # use thiserror::Error; +//! # +//! # fn first_char(s: &String) -> char { +//! # s.chars().next().unwrap() +//! # } +//! # +//! # #[derive(Debug)] +//! # struct Limits { +//! # lo: usize, +//! # hi: usize, +//! # } +//! # +//! #[derive(Error, Debug)] +//! pub enum Error { +//! #[error("first letter must be lowercase but was {:?}", first_char(.0))] +//! WrongCase(String), +//! #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)] +//! OutOfBounds { idx: usize, limits: Limits }, +//! } +//! ``` +//! +//! - A [`From`] impl is generated for each variant that contains a `#[from]` +//! attribute. +//! +//! The variant using `#[from]` must not contain any other fields beyond the +//! source error (and possibly a backtrace — see below). Usually +//! `#[from]` fields are unnamed, but `#[from]` is allowed on a named field +//! too. +//! +//! ```rust +//! # use core::fmt::{self, Display}; +//! # use std::io; +//! # use thiserror::Error; +//! # +//! # mod globset { +//! # #[derive(thiserror::Error, Debug)] +//! # #[error("...")] +//! # pub struct Error; +//! # } +//! # +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! Io(#[from] io::Error), +//! Glob(#[from] globset::Error), +//! } +//! # +//! # impl Display for MyError { +//! # fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { +//! # unimplemented!() +//! # } +//! # } +//! ``` +//! +//! - The Error trait's [`source()`] method is implemented to return whichever +//! field has a `#[source]` attribute or is named `source`, if any. This is +//! for identifying the underlying lower level error that caused your error. +//! +//! The `#[from]` attribute always implies that the same field is `#[source]`, +//! so you don't ever need to specify both attributes. +//! +//! Any error type that implements `std::error::Error` or dereferences to `dyn +//! std::error::Error` will work as a source. +//! +//! ```rust +//! # use core::fmt::{self, Display}; +//! # use thiserror::Error; +//! # +//! #[derive(Error, Debug)] +//! pub struct MyError { +//! msg: String, +//! #[source] // optional if field name is `source` +//! source: anyhow::Error, +//! } +//! # +//! # impl Display for MyError { +//! # fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { +//! # unimplemented!() +//! # } +//! # } +//! ``` +//! +//! - The Error trait's [`provide()`] method is implemented to provide whichever +//! field has a type named `Backtrace`, if any, as a +//! [`std::backtrace::Backtrace`]. Using `Backtrace` in errors requires a +//! nightly compiler with Rust version 1.73 or newer. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! use std::backtrace::Backtrace; +//! +//! #[derive(Error, Debug)] +//! pub struct MyError { +//! msg: String, +//! backtrace: Backtrace, // automatically detected +//! } +//! # }; +//! ``` +//! +//! - If a field is both a source (named `source`, or has `#[source]` or +//! `#[from]` attribute) *and* is marked `#[backtrace]`, then the Error +//! trait's [`provide()`] method is forwarded to the source's `provide` so +//! that both layers of the error share the same backtrace. The `#[backtrace]` +//! attribute requires a nightly compiler with Rust version 1.73 or newer. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! Io { +//! #[backtrace] +//! source: io::Error, +//! }, +//! } +//! # }; +//! ``` +//! +//! - For variants that use `#[from]` and also contain a `Backtrace` field, a +//! backtrace is captured from within the `From` impl. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! Io { +//! #[from] +//! source: io::Error, +//! backtrace: Backtrace, +//! }, +//! } +//! # }; +//! ``` +//! +//! - Errors may use `error(transparent)` to forward the source and [`Display`] +//! methods straight through to an underlying error without adding an +//! additional message. This would be appropriate for enums that need an +//! "anything else" variant. +//! +//! ``` +//! # use thiserror::Error; +//! # +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! # /* +//! ... +//! # */ +//! +//! #[error(transparent)] +//! Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error +//! } +//! ``` +//! +//! Another use case is hiding implementation details of an error +//! representation behind an opaque error type, so that the representation is +//! able to evolve without breaking the crate's public API. +//! +//! ``` +//! # use thiserror::Error; +//! # +//! // PublicError is public, but opaque and easy to keep compatible. +//! #[derive(Error, Debug)] +//! #[error(transparent)] +//! pub struct PublicError(#[from] ErrorRepr); +//! +//! impl PublicError { +//! // Accessors for anything we do want to expose publicly. +//! } +//! +//! // Private and free to change across minor version of the crate. +//! #[derive(Error, Debug)] +//! enum ErrorRepr { +//! # /* +//! ... +//! # */ +//! } +//! ``` +//! +//! - See also the [`anyhow`] library for a convenient single error type to use +//! in application code. +//! +//! [`anyhow`]: https://github.com/dtolnay/anyhow +//! [`source()`]: std::error::Error::source +//! [`provide()`]: std::error::Error::provide +//! [`Display`]: std::fmt::Display + +#![no_std] +#![doc(html_root_url = "https://docs.rs/thiserror/2.0.17")] +#![allow( + clippy::elidable_lifetime_names, + clippy::module_name_repetitions, + clippy::needless_lifetimes, + clippy::return_self_not_must_use, + clippy::wildcard_imports +)] +#![cfg_attr(error_generic_member_access, feature(error_generic_member_access))] + +#[cfg(all(thiserror_nightly_testing, not(error_generic_member_access)))] +compile_error!("Build script probe failed to compile."); + +#[cfg(feature = "std")] +extern crate std; +#[cfg(feature = "std")] +extern crate std as core; + +mod aserror; +mod display; +#[cfg(error_generic_member_access)] +mod provide; +mod var; + +pub use thiserror_impl::*; + +mod private; + +include!(concat!(env!("OUT_DIR"), "/private.rs")); diff --git a/subprojects/thiserror/src/private.rs b/subprojects/thiserror/src/private.rs new file mode 100644 index 0000000..545aa8e --- /dev/null +++ b/subprojects/thiserror/src/private.rs @@ -0,0 +1,14 @@ +#[doc(hidden)] +pub use crate::aserror::AsDynError; +#[doc(hidden)] +pub use crate::display::AsDisplay; +#[cfg(error_generic_member_access)] +#[doc(hidden)] +pub use crate::provide::ThiserrorProvide; +#[doc(hidden)] +pub use crate::var::Var; +#[doc(hidden)] +pub use core::error::Error; +#[cfg(all(feature = "std", not(thiserror_no_backtrace_type)))] +#[doc(hidden)] +pub use std::backtrace::Backtrace; diff --git a/subprojects/thiserror/src/provide.rs b/subprojects/thiserror/src/provide.rs new file mode 100644 index 0000000..4b2f06a --- /dev/null +++ b/subprojects/thiserror/src/provide.rs @@ -0,0 +1,20 @@ +use core::error::{Error, Request}; + +#[doc(hidden)] +pub trait ThiserrorProvide: Sealed { + fn thiserror_provide<'a>(&'a self, request: &mut Request<'a>); +} + +impl ThiserrorProvide for T +where + T: Error + ?Sized, +{ + #[inline] + fn thiserror_provide<'a>(&'a self, request: &mut Request<'a>) { + self.provide(request); + } +} + +#[doc(hidden)] +pub trait Sealed {} +impl Sealed for T {} diff --git a/subprojects/thiserror/src/var.rs b/subprojects/thiserror/src/var.rs new file mode 100644 index 0000000..ecfcd85 --- /dev/null +++ b/subprojects/thiserror/src/var.rs @@ -0,0 +1,9 @@ +use core::fmt::{self, Pointer}; + +pub struct Var<'a, T: ?Sized>(pub &'a T); + +impl<'a, T: Pointer + ?Sized> Pointer for Var<'a, T> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + Pointer::fmt(self.0, formatter) + } +} diff --git a/subprojects/thiserror/tests/compiletest.rs b/subprojects/thiserror/tests/compiletest.rs new file mode 100644 index 0000000..23a6a06 --- /dev/null +++ b/subprojects/thiserror/tests/compiletest.rs @@ -0,0 +1,7 @@ +#[rustversion::attr(not(nightly), ignore = "requires nightly")] +#[cfg_attr(miri, ignore = "incompatible with miri")] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/subprojects/thiserror/tests/no-std/Cargo.toml b/subprojects/thiserror/tests/no-std/Cargo.toml new file mode 100644 index 0000000..8b03d2f --- /dev/null +++ b/subprojects/thiserror/tests/no-std/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "thiserror_no_std_test" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2021" +publish = false + +[lib] +path = "test.rs" + +[dependencies] +thiserror = { path = "../..", default-features = false } diff --git a/subprojects/thiserror/tests/no-std/test.rs b/subprojects/thiserror/tests/no-std/test.rs new file mode 100644 index 0000000..da7899c --- /dev/null +++ b/subprojects/thiserror/tests/no-std/test.rs @@ -0,0 +1,58 @@ +#![no_std] + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Error::E")] + E(#[from] SourceError), +} + +#[derive(Error, Debug)] +#[error("SourceError {field}")] +pub struct SourceError { + pub field: i32, +} + +#[cfg(test)] +mod tests { + use crate::{Error, SourceError}; + use core::error::Error as _; + use core::fmt::{self, Write}; + use core::mem; + + struct Buf<'a>(&'a mut [u8]); + + impl Write for Buf<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + if s.len() <= self.0.len() { + let (out, rest) = mem::take(&mut self.0).split_at_mut(s.len()); + out.copy_from_slice(s.as_bytes()); + self.0 = rest; + Ok(()) + } else { + Err(fmt::Error) + } + } + } + + #[test] + fn test() { + let source = SourceError { field: -1 }; + let error = Error::from(source); + + let source = error + .source() + .unwrap() + .downcast_ref::() + .unwrap(); + + let mut msg = [b'~'; 17]; + write!(Buf(&mut msg), "{error}").unwrap(); + assert_eq!(msg, *b"Error::E~~~~~~~~~"); + + let mut msg = [b'~'; 17]; + write!(Buf(&mut msg), "{source}").unwrap(); + assert_eq!(msg, *b"SourceError -1~~~"); + } +} diff --git a/subprojects/thiserror/tests/test_backtrace.rs b/subprojects/thiserror/tests/test_backtrace.rs new file mode 100644 index 0000000..cc25676 --- /dev/null +++ b/subprojects/thiserror/tests/test_backtrace.rs @@ -0,0 +1,289 @@ +#![cfg(feature = "std")] +#![cfg_attr(thiserror_nightly_testing, feature(error_generic_member_access))] + +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct Inner; + +#[cfg(thiserror_nightly_testing)] +#[derive(Error, Debug)] +#[error("...")] +pub struct InnerBacktrace { + backtrace: std::backtrace::Backtrace, +} + +#[cfg(thiserror_nightly_testing)] +pub mod structs { + use super::{Inner, InnerBacktrace}; + use std::backtrace::Backtrace; + use std::error::{self, Error}; + use std::sync::Arc; + use thiserror::Error; + + mod not_backtrace { + #[derive(Debug)] + pub struct Backtrace; + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct PlainBacktrace { + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ExplicitBacktrace { + #[backtrace] + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct NotBacktrace { + backtrace: crate::structs::not_backtrace::r#Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptBacktrace { + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ArcBacktrace { + #[backtrace] + backtrace: Arc, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct BacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct CombinedBacktraceFrom { + #[from] + #[backtrace] + source: InnerBacktrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptBacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ArcBacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Arc, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct AnyhowBacktrace { + #[backtrace] + source: anyhow::Error, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct BoxDynErrorBacktrace { + #[backtrace] + source: Box, + } + + #[test] + fn test_backtrace() { + let error = PlainBacktrace { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ExplicitBacktrace { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktrace { + backtrace: Some(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktrace { + backtrace: Arc::new(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = CombinedBacktraceFrom::from(InnerBacktrace { + backtrace: Backtrace::capture(), + }); + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = AnyhowBacktrace { + source: anyhow::Error::msg("..."), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BoxDynErrorBacktrace { + source: Box::new(PlainBacktrace { + backtrace: Backtrace::capture(), + }), + }; + assert!(error::request_ref::(&error).is_some()); + } +} + +#[cfg(thiserror_nightly_testing)] +pub mod enums { + use super::{Inner, InnerBacktrace}; + use std::backtrace::Backtrace; + use std::error; + use std::sync::Arc; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum PlainBacktrace { + #[error("...")] + Test { backtrace: Backtrace }, + } + + #[derive(Error, Debug)] + pub enum ExplicitBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum OptBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum ArcBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Arc, + }, + } + + #[derive(Error, Debug)] + pub enum BacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum CombinedBacktraceFrom { + #[error("...")] + Test { + #[from] + #[backtrace] + source: InnerBacktrace, + }, + } + + #[derive(Error, Debug)] + pub enum OptBacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum ArcBacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Arc, + }, + } + + #[test] + fn test_backtrace() { + let error = PlainBacktrace::Test { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ExplicitBacktrace::Test { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktrace::Test { + backtrace: Some(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktrace::Test { + backtrace: Arc::new(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = CombinedBacktraceFrom::from(InnerBacktrace { + backtrace: Backtrace::capture(), + }); + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + } +} + +#[test] +#[cfg_attr( + not(thiserror_nightly_testing), + ignore = "requires `--cfg=thiserror_nightly_testing`" +)] +fn test_backtrace() {} diff --git a/subprojects/thiserror/tests/test_display.rs b/subprojects/thiserror/tests/test_display.rs new file mode 100644 index 0000000..cace226 --- /dev/null +++ b/subprojects/thiserror/tests/test_display.rs @@ -0,0 +1,478 @@ +#![allow( + clippy::elidable_lifetime_names, + clippy::needless_lifetimes, + clippy::needless_raw_string_hashes, + clippy::trivially_copy_pass_by_ref, + clippy::uninlined_format_args +)] + +use core::fmt::{self, Display}; +use thiserror::Error; + +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_braced() { + #[derive(Error, Debug)] + #[error("braced error: {msg}")] + struct Error { + msg: String, + } + + let msg = "T".to_owned(); + assert("braced error: T", Error { msg }); +} + +#[test] +fn test_braced_unused() { + #[derive(Error, Debug)] + #[error("braced error")] + struct Error { + extra: usize, + } + + assert("braced error", Error { extra: 0 }); +} + +#[test] +fn test_tuple() { + #[derive(Error, Debug)] + #[error("tuple error: {0}")] + struct Error(usize); + + assert("tuple error: 0", Error(0)); +} + +#[test] +fn test_unit() { + #[derive(Error, Debug)] + #[error("unit error")] + struct Error; + + assert("unit error", Error); +} + +#[test] +fn test_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("braced error: {id}")] + Braced { id: usize }, + #[error("tuple error: {0}")] + Tuple(usize), + #[error("unit error")] + Unit, + } + + assert("braced error: 0", Error::Braced { id: 0 }); + assert("tuple error: 0", Error::Tuple(0)); + assert("unit error", Error::Unit); +} + +#[test] +fn test_constants() { + #[derive(Error, Debug)] + #[error("{MSG}: {id:?} (code {CODE:?})")] + struct Error { + id: &'static str, + } + + const MSG: &str = "failed to do"; + const CODE: usize = 9; + + assert("failed to do: \"\" (code 9)", Error { id: "" }); +} + +#[test] +fn test_inherit() { + #[derive(Error, Debug)] + #[error("{0}")] + enum Error { + Some(&'static str), + #[error("other error")] + Other(&'static str), + } + + assert("some error", Error::Some("some error")); + assert("other error", Error::Other("...")); +} + +#[test] +fn test_brace_escape() { + #[derive(Error, Debug)] + #[error("fn main() {{}}")] + struct Error; + + assert("fn main() {}", Error); +} + +#[test] +fn test_expr() { + #[derive(Error, Debug)] + #[error("1 + 1 = {}", 1 + 1)] + struct Error; + assert("1 + 1 = 2", Error); +} + +#[test] +fn test_nested() { + #[derive(Error, Debug)] + #[error("!bool = {}", not(.0))] + struct Error(bool); + + #[allow(clippy::trivially_copy_pass_by_ref)] + fn not(bool: &bool) -> bool { + !*bool + } + + assert("!bool = false", Error(true)); +} + +#[test] +fn test_match() { + #[derive(Error, Debug)] + #[error("{intro}: {0}", intro = match .1 { + Some(n) => format!("error occurred with {}", n), + None => "there was an empty error".to_owned(), + })] + struct Error(String, Option); + + assert( + "error occurred with 1: ...", + Error("...".to_owned(), Some(1)), + ); + assert( + "there was an empty error: ...", + Error("...".to_owned(), None), + ); +} + +#[test] +fn test_nested_display() { + // Same behavior as the one in `test_match`, but without String allocations. + #[derive(Error, Debug)] + #[error("{}", { + struct Msg<'a>(&'a String, &'a Option); + impl<'a> Display for Msg<'a> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self.1 { + Some(n) => write!(formatter, "error occurred with {}", n), + None => write!(formatter, "there was an empty error"), + }?; + write!(formatter, ": {}", self.0) + } + } + Msg(.0, .1) + })] + struct Error(String, Option); + + assert( + "error occurred with 1: ...", + Error("...".to_owned(), Some(1)), + ); + assert( + "there was an empty error: ...", + Error("...".to_owned(), None), + ); +} + +#[test] +fn test_void() { + #[allow(clippy::empty_enums)] + #[derive(Error, Debug)] + #[error("...")] + pub enum Error {} + + let _: Error; +} + +#[test] +fn test_mixed() { + #[derive(Error, Debug)] + #[error("a={a} :: b={} :: c={c} :: d={d}", 1, c = 2, d = 3)] + struct Error { + a: usize, + d: usize, + } + + assert("a=0 :: b=1 :: c=2 :: d=3", Error { a: 0, d: 0 }); +} + +#[test] +fn test_ints() { + #[derive(Error, Debug)] + enum Error { + #[error("error {0}")] + Tuple(usize, usize), + #[error("error {0}", '?')] + Struct { v: usize }, + } + + assert("error 9", Error::Tuple(9, 0)); + assert("error ?", Error::Struct { v: 0 }); +} + +#[test] +fn test_trailing_comma() { + #[derive(Error, Debug)] + #[error( + "error {0}", + )] + #[rustfmt::skip] + struct Error(char); + + assert("error ?", Error('?')); +} + +#[test] +fn test_field() { + #[derive(Debug)] + struct Inner { + data: usize, + } + + #[derive(Error, Debug)] + #[error("{}", .0.data)] + struct Error(Inner); + + assert("0", Error(Inner { data: 0 })); +} + +#[test] +fn test_nested_tuple_field() { + #[derive(Debug)] + struct Inner(usize); + + #[derive(Error, Debug)] + #[error("{}", .0.0)] + struct Error(Inner); + + assert("0", Error(Inner(0))); +} + +#[test] +fn test_pointer() { + #[derive(Error, Debug)] + #[error("{field:p}")] + pub struct Struct { + field: Box, + } + + let s = Struct { + field: Box::new(-1), + }; + assert_eq!(s.to_string(), format!("{:p}", s.field)); +} + +#[test] +fn test_macro_rules_variant_from_call_site() { + // Regression test for https://github.com/dtolnay/thiserror/issues/86 + + macro_rules! decl_error { + ($variant:ident($value:ident)) => { + #[derive(Error, Debug)] + pub enum Error0 { + #[error("{0:?}")] + $variant($value), + } + + #[derive(Error, Debug)] + #[error("{0:?}")] + pub enum Error1 { + $variant($value), + } + }; + } + + decl_error!(Repro(u8)); + + assert("0", Error0::Repro(0)); + assert("0", Error1::Repro(0)); +} + +#[test] +fn test_macro_rules_message_from_call_site() { + // Regression test for https://github.com/dtolnay/thiserror/issues/398 + + macro_rules! decl_error { + ($($errors:tt)*) => { + #[derive(Error, Debug)] + pub enum Error { + $($errors)* + } + }; + } + + decl_error! { + #[error("{0}")] + Unnamed(u8), + #[error("{x}")] + Named { x: u8 }, + } + + assert("0", Error::Unnamed(0)); + assert("0", Error::Named { x: 0 }); +} + +#[test] +fn test_raw() { + #[derive(Error, Debug)] + #[error("braced raw error: {fn}")] + struct Error { + r#fn: &'static str, + } + + assert("braced raw error: T", Error { r#fn: "T" }); +} + +#[test] +fn test_raw_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("braced raw error: {fn}")] + Braced { r#fn: &'static str }, + } + + assert("braced raw error: T", Error::Braced { r#fn: "T" }); +} + +#[test] +fn test_keyword() { + #[derive(Error, Debug)] + #[error("error: {type}", type = 1)] + struct Error; + + assert("error: 1", Error); +} + +#[test] +fn test_self() { + #[derive(Error, Debug)] + #[error("error: {self:?}")] + struct Error; + + assert("error: Error", Error); +} + +#[test] +fn test_str_special_chars() { + #[derive(Error, Debug)] + pub enum Error { + #[error("brace left {{")] + BraceLeft, + #[error("brace left 2 \x7B\x7B")] + BraceLeft2, + #[error("brace left 3 \u{7B}\u{7B}")] + BraceLeft3, + #[error("brace right }}")] + BraceRight, + #[error("brace right 2 \x7D\x7D")] + BraceRight2, + #[error("brace right 3 \u{7D}\u{7D}")] + BraceRight3, + #[error( + "new_\ +line" + )] + NewLine, + #[error("escape24 \u{78}")] + Escape24, + } + + assert("brace left {", Error::BraceLeft); + assert("brace left 2 {", Error::BraceLeft2); + assert("brace left 3 {", Error::BraceLeft3); + assert("brace right }", Error::BraceRight); + assert("brace right 2 }", Error::BraceRight2); + assert("brace right 3 }", Error::BraceRight3); + assert("new_line", Error::NewLine); + assert("escape24 x", Error::Escape24); +} + +#[test] +fn test_raw_str() { + #[derive(Error, Debug)] + pub enum Error { + #[error(r#"raw brace left {{"#)] + BraceLeft, + #[error(r#"raw brace left 2 \x7B"#)] + BraceLeft2, + #[error(r#"raw brace right }}"#)] + BraceRight, + #[error(r#"raw brace right 2 \x7D"#)] + BraceRight2, + } + + assert(r#"raw brace left {"#, Error::BraceLeft); + assert(r#"raw brace left 2 \x7B"#, Error::BraceLeft2); + assert(r#"raw brace right }"#, Error::BraceRight); + assert(r#"raw brace right 2 \x7D"#, Error::BraceRight2); +} + +mod util { + use core::fmt::{self, Octal}; + + pub fn octal(value: &T, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "0o{:o}", value) + } +} + +#[test] +fn test_fmt_path() { + fn unit(formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("unit=") + } + + fn pair(k: &i32, v: &i32, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "pair={k}:{v}") + } + + #[derive(Error, Debug)] + pub enum Error { + #[error(fmt = unit)] + Unit, + #[error(fmt = pair)] + Tuple(i32, i32), + #[error(fmt = pair)] + Entry { k: i32, v: i32 }, + #[error(fmt = crate::util::octal)] + I16(i16), + #[error(fmt = crate::util::octal::)] + I32 { n: i32 }, + #[error(fmt = core::fmt::Octal::fmt)] + I64(i64), + #[error("...{0}")] + Other(bool), + } + + assert("unit=", Error::Unit); + assert("pair=10:0", Error::Tuple(10, 0)); + assert("pair=10:0", Error::Entry { k: 10, v: 0 }); + assert("0o777", Error::I16(0o777)); + assert("0o777", Error::I32 { n: 0o777 }); + assert("777", Error::I64(0o777)); + assert("...false", Error::Other(false)); +} + +#[test] +fn test_fmt_path_inherited() { + #[derive(Error, Debug)] + #[error(fmt = crate::util::octal)] + pub enum Error { + I16(i16), + I32 { + n: i32, + }, + #[error(fmt = core::fmt::Octal::fmt)] + I64(i64), + #[error("...{0}")] + Other(bool), + } + + assert("0o777", Error::I16(0o777)); + assert("0o777", Error::I32 { n: 0o777 }); + assert("777", Error::I64(0o777)); + assert("...false", Error::Other(false)); +} diff --git a/subprojects/thiserror/tests/test_error.rs b/subprojects/thiserror/tests/test_error.rs new file mode 100644 index 0000000..eb52cef --- /dev/null +++ b/subprojects/thiserror/tests/test_error.rs @@ -0,0 +1,56 @@ +#![allow(dead_code)] + +use core::fmt::{self, Display}; +use std::io; +use thiserror::Error; + +macro_rules! unimplemented_display { + ($ty:ty) => { + impl Display for $ty { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } + } + }; +} + +#[derive(Error, Debug)] +struct BracedError { + msg: String, + pos: usize, +} + +#[derive(Error, Debug)] +struct TupleError(String, usize); + +#[derive(Error, Debug)] +struct UnitError; + +#[derive(Error, Debug)] +struct WithSource { + #[source] + cause: io::Error, +} + +#[derive(Error, Debug)] +struct WithAnyhow { + #[source] + cause: anyhow::Error, +} + +#[derive(Error, Debug)] +enum EnumError { + Braced { + #[source] + cause: io::Error, + }, + Tuple(#[source] io::Error), + Unit, +} + +unimplemented_display!(BracedError); +unimplemented_display!(TupleError); +unimplemented_display!(UnitError); +unimplemented_display!(WithSource); +unimplemented_display!(WithAnyhow); +unimplemented_display!(EnumError); diff --git a/subprojects/thiserror/tests/test_expr.rs b/subprojects/thiserror/tests/test_expr.rs new file mode 100644 index 0000000..1872fb5 --- /dev/null +++ b/subprojects/thiserror/tests/test_expr.rs @@ -0,0 +1,118 @@ +#![allow(clippy::iter_cloned_collect, clippy::uninlined_format_args)] + +use core::fmt::Display; +#[cfg(feature = "std")] +use std::path::PathBuf; +use thiserror::Error; + +// Some of the elaborate cases from the rcc codebase, which is a C compiler in +// Rust. https://github.com/jyn514/rcc/blob/0.8.0/src/data/error.rs +#[derive(Error, Debug)] +pub enum CompilerError { + #[error("cannot shift {} by {maximum} or more bits (got {current})", if *.is_left { "left" } else { "right" })] + TooManyShiftBits { + is_left: bool, + maximum: u64, + current: u64, + }, + + #[error("#error {}", (.0).iter().copied().collect::>().join(" "))] + User(Vec<&'static str>), + + #[error("overflow while parsing {}integer literal", + if let Some(signed) = .is_signed { + if *signed { "signed "} else { "unsigned "} + } else { + "" + } + )] + IntegerOverflow { is_signed: Option }, + + #[error("overflow while parsing {}integer literal", match .is_signed { + Some(true) => "signed ", + Some(false) => "unsigned ", + None => "", + })] + IntegerOverflow2 { is_signed: Option }, +} + +// Examples drawn from Rustup. +#[derive(Error, Debug)] +pub enum RustupError { + #[error( + "toolchain '{name}' does not contain component {component}{}", + .suggestion + .as_ref() + .map_or_else(String::new, |s| format!("; did you mean '{}'?", s)), + )] + UnknownComponent { + name: String, + component: String, + suggestion: Option, + }, +} + +#[track_caller] +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_rcc() { + assert( + "cannot shift left by 32 or more bits (got 50)", + CompilerError::TooManyShiftBits { + is_left: true, + maximum: 32, + current: 50, + }, + ); + + assert("#error A B C", CompilerError::User(vec!["A", "B", "C"])); + + assert( + "overflow while parsing signed integer literal", + CompilerError::IntegerOverflow { + is_signed: Some(true), + }, + ); +} + +#[test] +fn test_rustup() { + assert( + "toolchain 'nightly' does not contain component clipy; did you mean 'clippy'?", + RustupError::UnknownComponent { + name: "nightly".to_owned(), + component: "clipy".to_owned(), + suggestion: Some("clippy".to_owned()), + }, + ); +} + +// Regression test for https://github.com/dtolnay/thiserror/issues/335 +#[cfg(feature = "std")] +#[test] +#[allow(non_snake_case)] +fn test_assoc_type_equality_constraint() { + pub trait Trait: Display { + type A; + } + + impl Trait for i32 { + type A = i32; + } + + #[derive(Error, Debug)] + #[error("{A} {b}", b = &0 as &dyn Trait)] + pub struct Error { + pub A: PathBuf, + } + + assert( + "... 0", + Error { + A: PathBuf::from("..."), + }, + ); +} diff --git a/subprojects/thiserror/tests/test_from.rs b/subprojects/thiserror/tests/test_from.rs new file mode 100644 index 0000000..51af40b --- /dev/null +++ b/subprojects/thiserror/tests/test_from.rs @@ -0,0 +1,64 @@ +#![allow(clippy::extra_unused_type_parameters)] + +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct { + #[from] + source: io::Error, +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStructOptional { + #[from] + source: Option, +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorTuple(#[from] io::Error); + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorTupleOptional(#[from] Option); + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Test { + #[from] + source: io::Error, + }, +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnumOptional { + Test { + #[from] + source: Option, + }, +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum Many { + Any(#[from] anyhow::Error), + Io(#[from] io::Error), +} + +fn assert_impl>() {} + +#[test] +fn test_from() { + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); +} diff --git a/subprojects/thiserror/tests/test_generics.rs b/subprojects/thiserror/tests/test_generics.rs new file mode 100644 index 0000000..bcbfee0 --- /dev/null +++ b/subprojects/thiserror/tests/test_generics.rs @@ -0,0 +1,205 @@ +#![allow(clippy::needless_late_init, clippy::uninlined_format_args)] + +use core::fmt::{self, Debug, Display}; +use core::str::FromStr; +use thiserror::Error; + +pub struct NoFormat; + +#[derive(Debug)] +pub struct DebugOnly; + +pub struct DisplayOnly; + +impl Display for DisplayOnly { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("display only") + } +} + +#[derive(Debug)] +pub struct DebugAndDisplay; + +impl Display for DebugAndDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("debug and display") + } +} + +// Should expand to: +// +// impl Display for EnumDebugField +// where +// E: Debug; +// +// impl Error for EnumDebugField +// where +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumDebugGeneric { + #[error("{0:?}")] + FatalError(E), +} + +// Should expand to: +// +// impl Display for EnumFromGeneric; +// +// impl Error for EnumFromGeneric +// where +// EnumDebugGeneric: Error + 'static, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumFromGeneric { + #[error("enum from generic")] + Source(#[from] EnumDebugGeneric), +} + +// Should expand to: +// +// impl Display +// for EnumCompound +// where +// HasDisplay: Display, +// HasDebug: Debug; +// +// impl Error +// for EnumCompound +// where +// Self: Debug + Display; +// +#[derive(Error)] +pub enum EnumCompound { + #[error("{0} {1:?}")] + DisplayDebug(HasDisplay, HasDebug), + #[error("{0}")] + Display(HasDisplay, HasNeither), + #[error("{1:?}")] + Debug(HasNeither, HasDebug), +} + +impl Debug for EnumCompound { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("EnumCompound") + } +} + +#[test] +fn test_display_enum_compound() { + let mut instance: EnumCompound; + + instance = EnumCompound::DisplayDebug(DisplayOnly, DebugOnly); + assert_eq!(format!("{}", instance), "display only DebugOnly"); + + instance = EnumCompound::Display(DisplayOnly, NoFormat); + assert_eq!(format!("{}", instance), "display only"); + + instance = EnumCompound::Debug(NoFormat, DebugOnly); + assert_eq!(format!("{}", instance), "DebugOnly"); +} + +// Should expand to: +// +// impl Display for EnumTransparentGeneric +// where +// E: Display; +// +// impl Error for EnumTransparentGeneric +// where +// E: Error, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumTransparentGeneric { + #[error(transparent)] + Other(E), +} + +// Should expand to: +// +// impl Display for StructDebugGeneric +// where +// E: Debug; +// +// impl Error for StructDebugGeneric +// where +// Self: Debug + Display; +// +#[derive(Error, Debug)] +#[error("{underlying:?}")] +pub struct StructDebugGeneric { + pub underlying: E, +} + +// Should expand to: +// +// impl Error for StructFromGeneric +// where +// StructDebugGeneric: Error + 'static, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub struct StructFromGeneric { + #[from] + pub source: StructDebugGeneric, +} + +// Should expand to: +// +// impl Display for StructTransparentGeneric +// where +// E: Display; +// +// impl Error for StructTransparentGeneric +// where +// E: Error, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +#[error(transparent)] +pub struct StructTransparentGeneric(pub E); + +// Should expand to: +// +// impl Display for AssociatedTypeError +// where +// T::Err: Display; +// +// impl Error for AssociatedTypeError +// where +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum AssociatedTypeError { + #[error("couldn't parse matrix")] + Other, + #[error("couldn't parse entry: {0}")] + EntryParseError(T::Err), +} + +// Regression test for https://github.com/dtolnay/thiserror/issues/345 +#[test] +fn test_no_bound_on_named_fmt() { + #[derive(Error, Debug)] + #[error("{thing}", thing = "...")] + struct Error { + thing: T, + } + + let error = Error { thing: DebugOnly }; + assert_eq!(error.to_string(), "..."); +} + +#[test] +fn test_multiple_bound() { + #[derive(Error, Debug)] + #[error("0x{thing:x} 0x{thing:X}")] + pub struct Error { + thing: T, + } + + let error = Error { thing: 0xFFi32 }; + assert_eq!(error.to_string(), "0xff 0xFF"); +} diff --git a/subprojects/thiserror/tests/test_lints.rs b/subprojects/thiserror/tests/test_lints.rs new file mode 100644 index 0000000..802ad50 --- /dev/null +++ b/subprojects/thiserror/tests/test_lints.rs @@ -0,0 +1,98 @@ +#![allow(clippy::mixed_attributes_style)] + +use thiserror::Error; + +pub use std::error::Error; + +#[test] +fn test_allow_attributes() { + #![deny(clippy::allow_attributes)] + + #[derive(Error, Debug)] + #[error("...")] + pub struct MyError(#[from] anyhow::Error); + + let _: MyError; +} + +#[test] +fn test_unused_qualifications() { + #![deny(unused_qualifications)] + + // Expansion of derive(Error) macro can't know whether something like + // std::error::Error is already imported in the caller's scope so it must + // suppress unused_qualifications. + + #[derive(Error, Debug)] + #[error("...")] + pub struct MyError; + + let _: MyError; +} + +#[test] +fn test_needless_lifetimes() { + #![allow(dead_code)] + #![deny(clippy::elidable_lifetime_names, clippy::needless_lifetimes)] + + #[derive(Error, Debug)] + #[error("...")] + pub enum MyError<'a> { + A(#[from] std::io::Error), + B(&'a ()), + } + + let _: MyError; +} + +#[test] +fn test_deprecated() { + #![deny(deprecated)] + + #[derive(Error, Debug)] + #[deprecated] + #[error("...")] + pub struct DeprecatedStruct; + + #[derive(Error, Debug)] + #[error("{message} {}", .message)] + pub struct DeprecatedStructField { + #[deprecated] + message: String, + } + + #[derive(Error, Debug)] + #[deprecated] + pub enum DeprecatedEnum { + #[error("...")] + Variant, + } + + #[derive(Error, Debug)] + pub enum DeprecatedVariant { + #[deprecated] + #[error("...")] + Variant, + } + + #[derive(Error, Debug)] + pub enum DeprecatedFrom { + #[error(transparent)] + Variant( + #[from] + #[allow(deprecated)] + DeprecatedStruct, + ), + } + + #[allow(deprecated)] + let _: DeprecatedStruct; + #[allow(deprecated)] + let _: DeprecatedStructField; + #[allow(deprecated)] + let _ = DeprecatedEnum::Variant; + #[allow(deprecated)] + let _ = DeprecatedVariant::Variant; + #[allow(deprecated)] + let _ = DeprecatedFrom::Variant(DeprecatedStruct); +} diff --git a/subprojects/thiserror/tests/test_option.rs b/subprojects/thiserror/tests/test_option.rs new file mode 100644 index 0000000..21cd5e1 --- /dev/null +++ b/subprojects/thiserror/tests/test_option.rs @@ -0,0 +1,109 @@ +#![cfg(feature = "std")] +#![cfg_attr(thiserror_nightly_testing, feature(error_generic_member_access))] + +#[cfg(thiserror_nightly_testing)] +pub mod structs { + use std::backtrace::Backtrace; + use thiserror::Error; + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceNoBacktrace { + #[source] + pub source: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceAlwaysBacktrace { + #[source] + pub source: Option, + pub backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct NoSourceOptBacktrace { + #[backtrace] + pub backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct AlwaysSourceOptBacktrace { + pub source: anyhow::Error, + #[backtrace] + pub backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceOptBacktrace { + #[source] + pub source: Option, + #[backtrace] + pub backtrace: Option, + } +} + +#[cfg(thiserror_nightly_testing)] +pub mod enums { + use std::backtrace::Backtrace; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum OptSourceNoBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + }, + } + + #[derive(Error, Debug)] + pub enum OptSourceAlwaysBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum NoSourceOptBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum AlwaysSourceOptBacktrace { + #[error("...")] + Test { + source: anyhow::Error, + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum OptSourceOptBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + #[backtrace] + backtrace: Option, + }, + } +} + +#[test] +#[cfg_attr( + not(thiserror_nightly_testing), + ignore = "requires `--cfg=thiserror_nightly_testing`" +)] +fn test_option() {} diff --git a/subprojects/thiserror/tests/test_path.rs b/subprojects/thiserror/tests/test_path.rs new file mode 100644 index 0000000..fa85c1d --- /dev/null +++ b/subprojects/thiserror/tests/test_path.rs @@ -0,0 +1,54 @@ +#![cfg(feature = "std")] + +use core::fmt::Display; +use ref_cast::RefCast; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("failed to read '{file}'")] +struct StructPathBuf { + file: PathBuf, +} + +#[derive(Error, Debug, RefCast)] +#[repr(C)] +#[error("failed to read '{file}'")] +struct StructPath { + file: Path, +} + +#[derive(Error, Debug)] +enum EnumPathBuf { + #[error("failed to read '{0}'")] + Read(PathBuf), +} + +#[derive(Error, Debug)] +#[error("{tail}")] +pub struct UnsizedError { + pub head: i32, + pub tail: str, +} + +#[derive(Error, Debug)] +pub enum BothError { + #[error("display:{0} debug:{0:?}")] + DisplayDebug(PathBuf), + #[error("debug:{0:?} display:{0}")] + DebugDisplay(PathBuf), +} + +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_display() { + let path = Path::new("/thiserror"); + let file = path.to_owned(); + assert("failed to read '/thiserror'", StructPathBuf { file }); + let file = path.to_owned(); + assert("failed to read '/thiserror'", EnumPathBuf::Read(file)); + assert("failed to read '/thiserror'", StructPath::ref_cast(path)); +} diff --git a/subprojects/thiserror/tests/test_source.rs b/subprojects/thiserror/tests/test_source.rs new file mode 100644 index 0000000..29968be --- /dev/null +++ b/subprojects/thiserror/tests/test_source.rs @@ -0,0 +1,82 @@ +use std::error::Error as StdError; +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("implicit source")] +pub struct ImplicitSource { + source: io::Error, +} + +#[derive(Error, Debug)] +#[error("explicit source")] +pub struct ExplicitSource { + source: String, + #[source] + io: io::Error, +} + +#[derive(Error, Debug)] +#[error("boxed source")] +pub struct BoxedSource { + #[source] + source: Box, +} + +#[test] +fn test_implicit_source() { + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = ImplicitSource { source: io }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_explicit_source() { + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = ExplicitSource { + source: String::new(), + io, + }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_boxed_source() { + let source = Box::new(io::Error::new(io::ErrorKind::Other, "oh no!")); + let error = BoxedSource { source }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +macro_rules! error_from_macro { + ($($variants:tt)*) => { + #[derive(Error)] + #[derive(Debug)] + pub enum MacroSource { + $($variants)* + } + } +} + +// Test that we generate impls with the proper hygiene +#[rustfmt::skip] +error_from_macro! { + #[error("Something")] + Variant(#[from] io::Error) +} + +#[test] +fn test_not_source() { + #[derive(Error, Debug)] + #[error("{source} ==> {destination}")] + pub struct NotSource { + r#source: char, + destination: char, + } + + let error = NotSource { + source: 'S', + destination: 'D', + }; + assert_eq!(error.to_string(), "S ==> D"); + assert!(error.source().is_none()); +} diff --git a/subprojects/thiserror/tests/test_transparent.rs b/subprojects/thiserror/tests/test_transparent.rs new file mode 100644 index 0000000..ee30f5b --- /dev/null +++ b/subprojects/thiserror/tests/test_transparent.rs @@ -0,0 +1,96 @@ +use anyhow::anyhow; +use std::error::Error as _; +use std::io; +use thiserror::Error; + +#[test] +fn test_transparent_struct() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Error(ErrorKind); + + #[derive(Error, Debug)] + enum ErrorKind { + #[error("E0")] + E0, + #[error("E1")] + E1(#[from] io::Error), + } + + let error = Error(ErrorKind::E0); + assert_eq!("E0", error.to_string()); + assert!(error.source().is_none()); + + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = Error(ErrorKind::from(io)); + assert_eq!("E1", error.to_string()); + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_transparent_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("this failed")] + This, + #[error(transparent)] + Other(anyhow::Error), + } + + let error = Error::This; + assert_eq!("this failed", error.to_string()); + + let error = Error::Other(anyhow!("inner").context("outer")); + assert_eq!("outer", error.to_string()); + assert_eq!("inner", error.source().unwrap().to_string()); +} + +#[test] +fn test_transparent_enum_with_default_message() { + #[derive(Error, Debug)] + #[error("this failed: {0}_{1}")] + enum Error { + This(i32, i32), + #[error(transparent)] + Other(anyhow::Error), + } + + let error = Error::This(-1, -1); + assert_eq!("this failed: -1_-1", error.to_string()); + + let error = Error::Other(anyhow!("inner").context("outer")); + assert_eq!("outer", error.to_string()); + assert_eq!("inner", error.source().unwrap().to_string()); +} + +#[test] +fn test_anyhow() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Any(#[from] anyhow::Error); + + let error = Any::from(anyhow!("inner").context("outer")); + assert_eq!("outer", error.to_string()); + assert_eq!("inner", error.source().unwrap().to_string()); +} + +#[test] +fn test_non_static() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Error<'a> { + inner: ErrorKind<'a>, + } + + #[derive(Error, Debug)] + enum ErrorKind<'a> { + #[error("unexpected token: {:?}", token)] + Unexpected { token: &'a str }, + } + + let error = Error { + inner: ErrorKind::Unexpected { token: "error" }, + }; + assert_eq!("unexpected token: \"error\"", error.to_string()); + assert!(error.source().is_none()); +} diff --git a/subprojects/thiserror/tests/ui/bad-field-attr.rs b/subprojects/thiserror/tests/ui/bad-field-attr.rs new file mode 100644 index 0000000..d5429b2 --- /dev/null +++ b/subprojects/thiserror/tests/ui/bad-field-attr.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(#[error(transparent)] std::io::Error); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/bad-field-attr.stderr b/subprojects/thiserror/tests/ui/bad-field-attr.stderr new file mode 100644 index 0000000..5fb5744 --- /dev/null +++ b/subprojects/thiserror/tests/ui/bad-field-attr.stderr @@ -0,0 +1,5 @@ +error: #[error(transparent)] needs to go outside the enum or struct, not on an individual field + --> tests/ui/bad-field-attr.rs:5:18 + | +5 | pub struct Error(#[error(transparent)] std::io::Error); + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/concat-display.rs b/subprojects/thiserror/tests/ui/concat-display.rs new file mode 100644 index 0000000..8b53cc0 --- /dev/null +++ b/subprojects/thiserror/tests/ui/concat-display.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +macro_rules! error_type { + ($name:ident, $what:expr) => { + // Use #[error("invalid {}", $what)] instead. + + #[derive(Error, Debug)] + #[error(concat!("invalid ", $what))] + pub struct $name; + }; +} + +error_type!(Error, "foo"); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/concat-display.stderr b/subprojects/thiserror/tests/ui/concat-display.stderr new file mode 100644 index 0000000..73718ae --- /dev/null +++ b/subprojects/thiserror/tests/ui/concat-display.stderr @@ -0,0 +1,10 @@ +error: expected one of: string literal, `transparent`, `fmt` + --> tests/ui/concat-display.rs:8:17 + | + 8 | #[error(concat!("invalid ", $what))] + | ^^^^^^ +... +13 | error_type!(Error, "foo"); + | ------------------------- in this macro invocation + | + = note: this error originates in the macro `error_type` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/subprojects/thiserror/tests/ui/display-underscore.rs b/subprojects/thiserror/tests/ui/display-underscore.rs new file mode 100644 index 0000000..335614b --- /dev/null +++ b/subprojects/thiserror/tests/ui/display-underscore.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("{_}")] +pub struct Error; + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/display-underscore.stderr b/subprojects/thiserror/tests/ui/display-underscore.stderr new file mode 100644 index 0000000..36882b9 --- /dev/null +++ b/subprojects/thiserror/tests/ui/display-underscore.stderr @@ -0,0 +1,7 @@ +error: invalid format string: invalid argument name `_` + --> tests/ui/display-underscore.rs:4:11 + | +4 | #[error("{_}")] + | ^ invalid argument name in format string + | + = note: argument name cannot be a single underscore diff --git a/subprojects/thiserror/tests/ui/duplicate-enum-source.rs b/subprojects/thiserror/tests/ui/duplicate-enum-source.rs new file mode 100644 index 0000000..15e579f --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-enum-source.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ErrorEnum { + Confusing { + #[source] + a: std::io::Error, + #[source] + b: anyhow::Error, + }, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/duplicate-enum-source.stderr b/subprojects/thiserror/tests/ui/duplicate-enum-source.stderr new file mode 100644 index 0000000..4a4b2d3 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-enum-source.stderr @@ -0,0 +1,5 @@ +error: duplicate #[source] attribute + --> tests/ui/duplicate-enum-source.rs:8:9 + | +8 | #[source] + | ^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/duplicate-fmt.rs b/subprojects/thiserror/tests/ui/duplicate-fmt.rs new file mode 100644 index 0000000..32f7a23 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-fmt.rs @@ -0,0 +1,23 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("...")] +#[error("...")] +pub struct Error; + +#[derive(Error, Debug)] +#[error(fmt = core::fmt::Octal::fmt)] +#[error(fmt = core::fmt::LowerHex::fmt)] +pub enum FmtFmt {} + +#[derive(Error, Debug)] +#[error(fmt = core::fmt::Octal::fmt)] +#[error(transparent)] +pub enum FmtTransparent {} + +#[derive(Error, Debug)] +#[error(fmt = core::fmt::Octal::fmt)] +#[error("...")] +pub enum FmtDisplay {} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/duplicate-fmt.stderr b/subprojects/thiserror/tests/ui/duplicate-fmt.stderr new file mode 100644 index 0000000..a6c9932 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-fmt.stderr @@ -0,0 +1,23 @@ +error: only one #[error(...)] attribute is allowed + --> tests/ui/duplicate-fmt.rs:5:1 + | +5 | #[error("...")] + | ^^^^^^^^^^^^^^^ + +error: duplicate #[error(fmt = ...)] attribute + --> tests/ui/duplicate-fmt.rs:10:1 + | +10 | #[error(fmt = core::fmt::LowerHex::fmt)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: cannot have both #[error(transparent)] and #[error(fmt = ...)] + --> tests/ui/duplicate-fmt.rs:14:1 + | +14 | #[error(fmt = core::fmt::Octal::fmt)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: cannot have both #[error(fmt = ...)] and a format arguments attribute + --> tests/ui/duplicate-fmt.rs:20:1 + | +20 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/duplicate-struct-source.rs b/subprojects/thiserror/tests/ui/duplicate-struct-source.rs new file mode 100644 index 0000000..569df8d --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-struct-source.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub struct ErrorStruct { + #[source] + a: std::io::Error, + #[source] + b: anyhow::Error, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/duplicate-struct-source.stderr b/subprojects/thiserror/tests/ui/duplicate-struct-source.stderr new file mode 100644 index 0000000..c8de574 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-struct-source.stderr @@ -0,0 +1,5 @@ +error: duplicate #[source] attribute + --> tests/ui/duplicate-struct-source.rs:7:5 + | +7 | #[source] + | ^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/duplicate-transparent.rs b/subprojects/thiserror/tests/ui/duplicate-transparent.rs new file mode 100644 index 0000000..49c0e46 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-transparent.rs @@ -0,0 +1,8 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +#[error(transparent)] +pub struct Error(anyhow::Error); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/duplicate-transparent.stderr b/subprojects/thiserror/tests/ui/duplicate-transparent.stderr new file mode 100644 index 0000000..a830879 --- /dev/null +++ b/subprojects/thiserror/tests/ui/duplicate-transparent.stderr @@ -0,0 +1,5 @@ +error: duplicate #[error(transparent)] attribute + --> tests/ui/duplicate-transparent.rs:5:1 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/expression-fallback.rs b/subprojects/thiserror/tests/ui/expression-fallback.rs new file mode 100644 index 0000000..7269129 --- /dev/null +++ b/subprojects/thiserror/tests/ui/expression-fallback.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("".yellow)] +pub struct ArgError; + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/expression-fallback.stderr b/subprojects/thiserror/tests/ui/expression-fallback.stderr new file mode 100644 index 0000000..8d22c3a --- /dev/null +++ b/subprojects/thiserror/tests/ui/expression-fallback.stderr @@ -0,0 +1,24 @@ +error: expected `,`, found `.` + --> tests/ui/expression-fallback.rs:4:11 + | +4 | #[error("".yellow)] + | ^ expected `,` + +error: argument never used + --> tests/ui/expression-fallback.rs:4:12 + | +4 | #[error("".yellow)] + | -- ^^^^^^ argument never used + | | + | formatting specifier missing + | +help: format specifiers use curly braces, consider adding a format specifier + | +4 | #[error("{}".yellow)] + | ++ + +error[E0425]: cannot find value `yellow` in this scope + --> tests/ui/expression-fallback.rs:4:12 + | +4 | #[error("".yellow)] + | ^^^^^^ not found in this scope diff --git a/subprojects/thiserror/tests/ui/fallback-impl-with-display.rs b/subprojects/thiserror/tests/ui/fallback-impl-with-display.rs new file mode 100644 index 0000000..23dcf28 --- /dev/null +++ b/subprojects/thiserror/tests/ui/fallback-impl-with-display.rs @@ -0,0 +1,14 @@ +use core::fmt::{self, Display}; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error] +pub struct MyError; + +impl Display for MyError { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/fallback-impl-with-display.stderr b/subprojects/thiserror/tests/ui/fallback-impl-with-display.stderr new file mode 100644 index 0000000..6bd3730 --- /dev/null +++ b/subprojects/thiserror/tests/ui/fallback-impl-with-display.stderr @@ -0,0 +1,16 @@ +error: expected attribute arguments in parentheses: #[error(...)] + --> tests/ui/fallback-impl-with-display.rs:5:3 + | +5 | #[error] + | ^^^^^ + +error[E0119]: conflicting implementations of trait `std::fmt::Display` for type `MyError` + --> tests/ui/fallback-impl-with-display.rs:4:10 + | +4 | #[derive(Error, Debug)] + | ^^^^^ conflicting implementation for `MyError` +... +8 | impl Display for MyError { + | ------------------------ first implementation here + | + = note: this error originates in the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/subprojects/thiserror/tests/ui/from-backtrace-backtrace.rs b/subprojects/thiserror/tests/ui/from-backtrace-backtrace.rs new file mode 100644 index 0000000..3b781ac --- /dev/null +++ b/subprojects/thiserror/tests/ui/from-backtrace-backtrace.rs @@ -0,0 +1,15 @@ +// https://github.com/dtolnay/thiserror/issues/163 + +use std::backtrace::Backtrace; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct Error( + #[from] + #[backtrace] + std::io::Error, + Backtrace, +); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/from-backtrace-backtrace.stderr b/subprojects/thiserror/tests/ui/from-backtrace-backtrace.stderr new file mode 100644 index 0000000..5c0b9a3 --- /dev/null +++ b/subprojects/thiserror/tests/ui/from-backtrace-backtrace.stderr @@ -0,0 +1,5 @@ +error: deriving From requires no fields other than source and backtrace + --> tests/ui/from-backtrace-backtrace.rs:9:5 + | +9 | #[from] + | ^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/from-not-source.rs b/subprojects/thiserror/tests/ui/from-not-source.rs new file mode 100644 index 0000000..ad72867 --- /dev/null +++ b/subprojects/thiserror/tests/ui/from-not-source.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub struct Error { + #[source] + source: std::io::Error, + #[from] + other: anyhow::Error, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/from-not-source.stderr b/subprojects/thiserror/tests/ui/from-not-source.stderr new file mode 100644 index 0000000..9713601 --- /dev/null +++ b/subprojects/thiserror/tests/ui/from-not-source.stderr @@ -0,0 +1,5 @@ +error: #[from] is only supported on the source field, not any other field + --> tests/ui/from-not-source.rs:7:5 + | +7 | #[from] + | ^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.rs b/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.rs new file mode 100644 index 0000000..0a0bcbe --- /dev/null +++ b/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error] +pub struct MyError; + +fn main() { + // No error on the following line. Thiserror emits an Error impl despite the + // bad attribute. + _ = &MyError as &dyn std::error::Error; +} diff --git a/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.stderr b/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.stderr new file mode 100644 index 0000000..b98c31e --- /dev/null +++ b/subprojects/thiserror/tests/ui/invalid-input-impl-anyway.stderr @@ -0,0 +1,5 @@ +error: expected attribute arguments in parentheses: #[error(...)] + --> tests/ui/invalid-input-impl-anyway.rs:4:3 + | +4 | #[error] + | ^^^^^ diff --git a/subprojects/thiserror/tests/ui/lifetime.rs b/subprojects/thiserror/tests/ui/lifetime.rs new file mode 100644 index 0000000..a82909d --- /dev/null +++ b/subprojects/thiserror/tests/ui/lifetime.rs @@ -0,0 +1,24 @@ +use core::fmt::Debug; +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("error")] +struct Error<'a>(#[from] Inner<'a>); + +#[derive(Error, Debug)] +#[error("{0}")] +struct Inner<'a>(&'a str); + +#[derive(Error, Debug)] +enum Enum<'a> { + #[error("error")] + Foo(#[from] Generic<&'a str>), +} + +#[derive(Error, Debug)] +#[error("{0:?}")] +struct Generic(T); + +fn main() -> Result<(), Error<'static>> { + Err(Error(Inner("some text"))) +} diff --git a/subprojects/thiserror/tests/ui/lifetime.stderr b/subprojects/thiserror/tests/ui/lifetime.stderr new file mode 100644 index 0000000..8b58136 --- /dev/null +++ b/subprojects/thiserror/tests/ui/lifetime.stderr @@ -0,0 +1,11 @@ +error: non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static + --> tests/ui/lifetime.rs:6:26 + | +6 | struct Error<'a>(#[from] Inner<'a>); + | ^^^^^^^^^ + +error: non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static + --> tests/ui/lifetime.rs:15:17 + | +15 | Foo(#[from] Generic<&'a str>), + | ^^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/missing-display.rs b/subprojects/thiserror/tests/ui/missing-display.rs new file mode 100644 index 0000000..31e23fe --- /dev/null +++ b/subprojects/thiserror/tests/ui/missing-display.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum MyError { + First, + Second, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/missing-display.stderr b/subprojects/thiserror/tests/ui/missing-display.stderr new file mode 100644 index 0000000..fe7472e --- /dev/null +++ b/subprojects/thiserror/tests/ui/missing-display.stderr @@ -0,0 +1,19 @@ +error[E0277]: `MyError` doesn't implement `std::fmt::Display` + --> tests/ui/missing-display.rs:4:10 + | +3 | #[derive(Error, Debug)] + | ----- in this derive macro expansion +4 | pub enum MyError { + | ^^^^^^^ unsatisfied trait bound + | +help: the trait `std::fmt::Display` is not implemented for `MyError` + --> tests/ui/missing-display.rs:4:1 + | +4 | pub enum MyError { + | ^^^^^^^^^^^^^^^^ +note: required by a bound in `std::error::Error` + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^ required by this bound in `Error` + = note: this error originates in the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/subprojects/thiserror/tests/ui/missing-fmt.rs b/subprojects/thiserror/tests/ui/missing-fmt.rs new file mode 100644 index 0000000..d52fbdf --- /dev/null +++ b/subprojects/thiserror/tests/ui/missing-fmt.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("...")] + A(usize), + B(usize), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/missing-fmt.stderr b/subprojects/thiserror/tests/ui/missing-fmt.stderr new file mode 100644 index 0000000..c0be373 --- /dev/null +++ b/subprojects/thiserror/tests/ui/missing-fmt.stderr @@ -0,0 +1,5 @@ +error: missing #[error("...")] display attribute + --> tests/ui/missing-fmt.rs:7:5 + | +7 | B(usize), + | ^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/no-display.rs b/subprojects/thiserror/tests/ui/no-display.rs new file mode 100644 index 0000000..d804e00 --- /dev/null +++ b/subprojects/thiserror/tests/ui/no-display.rs @@ -0,0 +1,18 @@ +use thiserror::Error; + +#[derive(Debug)] +struct NoDisplay; + +#[derive(Error, Debug)] +#[error("thread: {thread}")] +pub struct Error { + thread: NoDisplay, +} + +#[derive(Error, Debug)] +#[error("thread: {thread:o}")] +pub struct ErrorOctal { + thread: NoDisplay, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/no-display.stderr b/subprojects/thiserror/tests/ui/no-display.stderr new file mode 100644 index 0000000..9750582 --- /dev/null +++ b/subprojects/thiserror/tests/ui/no-display.stderr @@ -0,0 +1,46 @@ +error[E0599]: the method `as_display` exists for reference `&NoDisplay`, but its trait bounds were not satisfied + --> tests/ui/no-display.rs:7:9 + | +4 | struct NoDisplay; + | ---------------- doesn't satisfy `NoDisplay: std::fmt::Display` +... +7 | #[error("thread: {thread}")] + | ^^^^^^^^^^^^^^^^^^ method cannot be called on `&NoDisplay` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NoDisplay: std::fmt::Display` + which is required by `&NoDisplay: AsDisplay<'_>` +note: the trait `std::fmt::Display` must be implemented + --> $RUST/core/src/fmt/mod.rs + | + | pub trait Display: PointeeSized { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `as_display`, perhaps you need to implement it: + candidate #1: `AsDisplay` + +error[E0277]: the trait bound `NoDisplay: Octal` is not satisfied + --> tests/ui/no-display.rs:13:9 + | +12 | #[derive(Error, Debug)] + | ----- in this derive macro expansion +13 | #[error("thread: {thread:o}")] + | ^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `Octal` is not implemented for `NoDisplay` + --> tests/ui/no-display.rs:4:1 + | + 4 | struct NoDisplay; + | ^^^^^^^^^^^^^^^^ + = help: the following other types implement trait `Octal`: + &T + &mut T + NonZero + Saturating + Wrapping + i128 + i16 + i32 + and $N others + = note: required for `&NoDisplay` to implement `Octal` + = note: this error originates in the macro `$crate::format_args` which comes from the expansion of the derive macro `Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/subprojects/thiserror/tests/ui/numbered-positional-tuple.rs b/subprojects/thiserror/tests/ui/numbered-positional-tuple.rs new file mode 100644 index 0000000..6deb658 --- /dev/null +++ b/subprojects/thiserror/tests/ui/numbered-positional-tuple.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)] +pub struct Error(u32); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/numbered-positional-tuple.stderr b/subprojects/thiserror/tests/ui/numbered-positional-tuple.stderr new file mode 100644 index 0000000..ab13371 --- /dev/null +++ b/subprojects/thiserror/tests/ui/numbered-positional-tuple.stderr @@ -0,0 +1,5 @@ +error: ambiguous reference to positional arguments by number in a tuple struct; change this to a named argument + --> tests/ui/numbered-positional-tuple.rs:4:61 + | +4 | #[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)] + | ^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/raw-identifier.rs b/subprojects/thiserror/tests/ui/raw-identifier.rs new file mode 100644 index 0000000..e7e66d0 --- /dev/null +++ b/subprojects/thiserror/tests/ui/raw-identifier.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("error: {r#fn}")] +pub struct Error { + r#fn: &'static str, +} + +fn main() { + let r#fn = "..."; + let _ = format!("error: {r#fn}"); +} diff --git a/subprojects/thiserror/tests/ui/raw-identifier.stderr b/subprojects/thiserror/tests/ui/raw-identifier.stderr new file mode 100644 index 0000000..a3ce94d --- /dev/null +++ b/subprojects/thiserror/tests/ui/raw-identifier.stderr @@ -0,0 +1,21 @@ +error: invalid format string: raw identifiers are not supported + --> tests/ui/raw-identifier.rs:4:18 + | +4 | #[error("error: {r#fn}")] + | --^^ + | | + | raw identifier used here in format string + | help: remove the `r#` + | + = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#` + +error: invalid format string: raw identifiers are not supported + --> tests/ui/raw-identifier.rs:11:30 + | +11 | let _ = format!("error: {r#fn}"); + | --^^ + | | + | raw identifier used here in format string + | help: remove the `r#` + | + = note: identifiers in format strings can be keywords and don't need to be prefixed with `r#` diff --git a/subprojects/thiserror/tests/ui/same-from-type.rs b/subprojects/thiserror/tests/ui/same-from-type.rs new file mode 100644 index 0000000..0ebdf45 --- /dev/null +++ b/subprojects/thiserror/tests/ui/same-from-type.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to open")] + OpenFile(#[from] std::io::Error), + #[error("failed to close")] + CloseFile(#[from] std::io::Error), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/same-from-type.stderr b/subprojects/thiserror/tests/ui/same-from-type.stderr new file mode 100644 index 0000000..a655163 --- /dev/null +++ b/subprojects/thiserror/tests/ui/same-from-type.stderr @@ -0,0 +1,8 @@ +error[E0119]: conflicting implementations of trait `From` for type `Error` + --> tests/ui/same-from-type.rs:8:15 + | +6 | OpenFile(#[from] std::io::Error), + | ------- first implementation here +7 | #[error("failed to close")] +8 | CloseFile(#[from] std::io::Error), + | ^^^^^^^ conflicting implementation for `Error` diff --git a/subprojects/thiserror/tests/ui/source-enum-not-error.rs b/subprojects/thiserror/tests/ui/source-enum-not-error.rs new file mode 100644 index 0000000..dae2285 --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-enum-not-error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Debug)] +pub struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Broken { source: NotError }, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/source-enum-not-error.stderr b/subprojects/thiserror/tests/ui/source-enum-not-error.stderr new file mode 100644 index 0000000..649d77d --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-enum-not-error.stderr @@ -0,0 +1,22 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&NotError`, but its trait bounds were not satisfied + --> tests/ui/source-enum-not-error.rs:9:14 + | +4 | pub struct NotError; + | ------------------- doesn't satisfy `NotError: AsDynError<'_>` or `NotError: std::error::Error` +... +9 | Broken { source: NotError }, + | ^^^^^^ method cannot be called on `&NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` + `&NotError: std::error::Error` + which is required by `&NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `as_dyn_error`, perhaps you need to implement it: + candidate #1: `AsDynError` diff --git a/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.rs b/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.rs new file mode 100644 index 0000000..a877c2c --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Debug)] +pub struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Broken(#[source] NotError), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.stderr b/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.stderr new file mode 100644 index 0000000..dc97a4b --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-enum-unnamed-field-not-error.stderr @@ -0,0 +1,22 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&NotError`, but its trait bounds were not satisfied + --> tests/ui/source-enum-unnamed-field-not-error.rs:9:12 + | +4 | pub struct NotError; + | ------------------- doesn't satisfy `NotError: AsDynError<'_>` or `NotError: std::error::Error` +... +9 | Broken(#[source] NotError), + | ^^^^^^^^^ method cannot be called on `&NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` + `&NotError: std::error::Error` + which is required by `&NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `as_dyn_error`, perhaps you need to implement it: + candidate #1: `AsDynError` diff --git a/subprojects/thiserror/tests/ui/source-struct-not-error.rs b/subprojects/thiserror/tests/ui/source-struct-not-error.rs new file mode 100644 index 0000000..d59df1e --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-struct-not-error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Debug)] +struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct { + source: NotError, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/source-struct-not-error.stderr b/subprojects/thiserror/tests/ui/source-struct-not-error.stderr new file mode 100644 index 0000000..07cd67a --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-struct-not-error.stderr @@ -0,0 +1,20 @@ +error[E0599]: the method `as_dyn_error` exists for struct `NotError`, but its trait bounds were not satisfied + --> tests/ui/source-struct-not-error.rs:9:5 + | +4 | struct NotError; + | --------------- method `as_dyn_error` not found for this struct because it doesn't satisfy `NotError: AsDynError<'_>` or `NotError: std::error::Error` +... +9 | source: NotError, + | ^^^^^^ method cannot be called on `NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `as_dyn_error`, perhaps you need to implement it: + candidate #1: `AsDynError` diff --git a/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.rs b/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.rs new file mode 100644 index 0000000..160b6b2 --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug)] +struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct(#[source] NotError); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.stderr b/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.stderr new file mode 100644 index 0000000..1f5350b --- /dev/null +++ b/subprojects/thiserror/tests/ui/source-struct-unnamed-field-not-error.stderr @@ -0,0 +1,20 @@ +error[E0599]: the method `as_dyn_error` exists for struct `NotError`, but its trait bounds were not satisfied + --> tests/ui/source-struct-unnamed-field-not-error.rs:8:24 + | +4 | struct NotError; + | --------------- method `as_dyn_error` not found for this struct because it doesn't satisfy `NotError: AsDynError<'_>` or `NotError: std::error::Error` +... +8 | pub struct ErrorStruct(#[source] NotError); + | ^^^^^^^^^ method cannot be called on `NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `as_dyn_error`, perhaps you need to implement it: + candidate #1: `AsDynError` diff --git a/subprojects/thiserror/tests/ui/struct-with-fmt.rs b/subprojects/thiserror/tests/ui/struct-with-fmt.rs new file mode 100644 index 0000000..73bf79f --- /dev/null +++ b/subprojects/thiserror/tests/ui/struct-with-fmt.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(fmt = core::fmt::Octal::fmt)] +pub struct Error(i32); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/struct-with-fmt.stderr b/subprojects/thiserror/tests/ui/struct-with-fmt.stderr new file mode 100644 index 0000000..00463be --- /dev/null +++ b/subprojects/thiserror/tests/ui/struct-with-fmt.stderr @@ -0,0 +1,5 @@ +error: #[error(fmt = ...)] is only supported in enums; for a struct, handwrite your own Display impl + --> tests/ui/struct-with-fmt.rs:4:1 + | +4 | #[error(fmt = core::fmt::Octal::fmt)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/transparent-display.rs b/subprojects/thiserror/tests/ui/transparent-display.rs new file mode 100644 index 0000000..2a59f18 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-display.rs @@ -0,0 +1,8 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +#[error("...")] +pub struct Error(anyhow::Error); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-display.stderr b/subprojects/thiserror/tests/ui/transparent-display.stderr new file mode 100644 index 0000000..54d958b --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-display.stderr @@ -0,0 +1,5 @@ +error: cannot have both #[error(transparent)] and a display attribute + --> tests/ui/transparent-display.rs:5:1 + | +5 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/transparent-enum-many.rs b/subprojects/thiserror/tests/ui/transparent-enum-many.rs new file mode 100644 index 0000000..e2a73a4 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-many.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(anyhow::Error, String), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-enum-many.stderr b/subprojects/thiserror/tests/ui/transparent-enum-many.stderr new file mode 100644 index 0000000..a9adfa5 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-many.stderr @@ -0,0 +1,6 @@ +error: #[error(transparent)] requires exactly one field + --> tests/ui/transparent-enum-many.rs:5:5 + | +5 | / #[error(transparent)] +6 | | Other(anyhow::Error, String), + | |________________________________^ diff --git a/subprojects/thiserror/tests/ui/transparent-enum-not-error.rs b/subprojects/thiserror/tests/ui/transparent-enum-not-error.rs new file mode 100644 index 0000000..80ccfc9 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-not-error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other { message: String }, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-enum-not-error.stderr b/subprojects/thiserror/tests/ui/transparent-enum-not-error.stderr new file mode 100644 index 0000000..bb836d4 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-not-error.stderr @@ -0,0 +1,20 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&String`, but its trait bounds were not satisfied + --> tests/ui/transparent-enum-not-error.rs:5:13 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `&String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- doesn't satisfy `String: AsDynError<'_>` or `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `&String: std::error::Error` + which is required by `&String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/subprojects/thiserror/tests/ui/transparent-enum-source.rs b/subprojects/thiserror/tests/ui/transparent-enum-source.rs new file mode 100644 index 0000000..3849f66 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-source.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(#[source] anyhow::Error), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-enum-source.stderr b/subprojects/thiserror/tests/ui/transparent-enum-source.stderr new file mode 100644 index 0000000..ccb9067 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-source.stderr @@ -0,0 +1,5 @@ +error: transparent variant can't contain #[source] + --> tests/ui/transparent-enum-source.rs:6:11 + | +6 | Other(#[source] anyhow::Error), + | ^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.rs b/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.rs new file mode 100644 index 0000000..87c32e0 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(String), +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.stderr b/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.stderr new file mode 100644 index 0000000..f337c59 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-enum-unnamed-field-not-error.stderr @@ -0,0 +1,20 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&String`, but its trait bounds were not satisfied + --> tests/ui/transparent-enum-unnamed-field-not-error.rs:5:13 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `&String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- doesn't satisfy `String: AsDynError<'_>` or `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `&String: std::error::Error` + which is required by `&String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/subprojects/thiserror/tests/ui/transparent-struct-many.rs b/subprojects/thiserror/tests/ui/transparent-struct-many.rs new file mode 100644 index 0000000..18f2466 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-many.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error { + inner: anyhow::Error, + what: String, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-struct-many.stderr b/subprojects/thiserror/tests/ui/transparent-struct-many.stderr new file mode 100644 index 0000000..c0e3806 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-many.stderr @@ -0,0 +1,5 @@ +error: #[error(transparent)] requires exactly one field + --> tests/ui/transparent-struct-many.rs:4:1 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/transparent-struct-not-error.rs b/subprojects/thiserror/tests/ui/transparent-struct-not-error.rs new file mode 100644 index 0000000..811ff53 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-not-error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error { + message: String, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-struct-not-error.stderr b/subprojects/thiserror/tests/ui/transparent-struct-not-error.stderr new file mode 100644 index 0000000..ee50d03 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-not-error.stderr @@ -0,0 +1,18 @@ +error[E0599]: the method `as_dyn_error` exists for struct `String`, but its trait bounds were not satisfied + --> tests/ui/transparent-struct-not-error.rs:4:9 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- doesn't satisfy `String: AsDynError<'_>` or `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/subprojects/thiserror/tests/ui/transparent-struct-source.rs b/subprojects/thiserror/tests/ui/transparent-struct-source.rs new file mode 100644 index 0000000..d4512c2 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-source.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(#[source] anyhow::Error); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-struct-source.stderr b/subprojects/thiserror/tests/ui/transparent-struct-source.stderr new file mode 100644 index 0000000..3012ca3 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-source.stderr @@ -0,0 +1,5 @@ +error: transparent error struct can't contain #[source] + --> tests/ui/transparent-struct-source.rs:5:18 + | +5 | pub struct Error(#[source] anyhow::Error); + | ^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.rs b/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.rs new file mode 100644 index 0000000..b4f7fbb --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(String); + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.stderr b/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.stderr new file mode 100644 index 0000000..c3d6c00 --- /dev/null +++ b/subprojects/thiserror/tests/ui/transparent-struct-unnamed-field-not-error.stderr @@ -0,0 +1,18 @@ +error[E0599]: the method `as_dyn_error` exists for struct `String`, but its trait bounds were not satisfied + --> tests/ui/transparent-struct-unnamed-field-not-error.rs:4:9 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- doesn't satisfy `String: AsDynError<'_>` or `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/subprojects/thiserror/tests/ui/unconditional-recursion.rs b/subprojects/thiserror/tests/ui/unconditional-recursion.rs new file mode 100644 index 0000000..035b15e --- /dev/null +++ b/subprojects/thiserror/tests/ui/unconditional-recursion.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("{self}")] +pub struct Error; + +fn main() { + __FAIL__; +} diff --git a/subprojects/thiserror/tests/ui/unconditional-recursion.stderr b/subprojects/thiserror/tests/ui/unconditional-recursion.stderr new file mode 100644 index 0000000..568e891 --- /dev/null +++ b/subprojects/thiserror/tests/ui/unconditional-recursion.stderr @@ -0,0 +1,21 @@ +error[E0425]: cannot find value `__FAIL__` in this scope + --> tests/ui/unconditional-recursion.rs:8:5 + | +8 | __FAIL__; + | ^^^^^^^^ not found in this scope + +warning: function cannot return without recursing + --> tests/ui/unconditional-recursion.rs:4:9 + | +4 | #[error("{self}")] + | ^^^^^^^^ + | | + | cannot return without recursing + | recursive call site + | + = help: a `loop` may express intention better if this is on purpose +note: the lint level is defined here + --> tests/ui/unconditional-recursion.rs:4:9 + | +4 | #[error("{self}")] + | ^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/unexpected-field-fmt.rs b/subprojects/thiserror/tests/ui/unexpected-field-fmt.rs new file mode 100644 index 0000000..7c439d9 --- /dev/null +++ b/subprojects/thiserror/tests/ui/unexpected-field-fmt.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + What { + #[error("...")] + io: std::io::Error, + }, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/unexpected-field-fmt.stderr b/subprojects/thiserror/tests/ui/unexpected-field-fmt.stderr new file mode 100644 index 0000000..bf3c24d --- /dev/null +++ b/subprojects/thiserror/tests/ui/unexpected-field-fmt.stderr @@ -0,0 +1,5 @@ +error: not expected here; the #[error(...)] attribute belongs on top of a struct or an enum variant + --> tests/ui/unexpected-field-fmt.rs:6:9 + | +6 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/unexpected-struct-source.rs b/subprojects/thiserror/tests/ui/unexpected-struct-source.rs new file mode 100644 index 0000000..f396494 --- /dev/null +++ b/subprojects/thiserror/tests/ui/unexpected-struct-source.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[source] +pub struct Error; + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/unexpected-struct-source.stderr b/subprojects/thiserror/tests/ui/unexpected-struct-source.stderr new file mode 100644 index 0000000..6f15841 --- /dev/null +++ b/subprojects/thiserror/tests/ui/unexpected-struct-source.stderr @@ -0,0 +1,5 @@ +error: not expected here; the #[source] attribute belongs on a specific field + --> tests/ui/unexpected-struct-source.rs:4:1 + | +4 | #[source] + | ^^^^^^^^^ diff --git a/subprojects/thiserror/tests/ui/union.rs b/subprojects/thiserror/tests/ui/union.rs new file mode 100644 index 0000000..cd6a934 --- /dev/null +++ b/subprojects/thiserror/tests/ui/union.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error)] +pub union U { + msg: &'static str, + num: usize, +} + +fn main() {} diff --git a/subprojects/thiserror/tests/ui/union.stderr b/subprojects/thiserror/tests/ui/union.stderr new file mode 100644 index 0000000..3ec4d71 --- /dev/null +++ b/subprojects/thiserror/tests/ui/union.stderr @@ -0,0 +1,8 @@ +error: union as errors are not supported + --> tests/ui/union.rs:4:1 + | +4 | / pub union U { +5 | | msg: &'static str, +6 | | num: usize, +7 | | } + | |_^