diff --git a/Cargo.lock b/Cargo.lock index ad07b73..f32809b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,181 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "basic-toml" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1" +dependencies = [ + "serde", +] + [[package]] name = "error-iter" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8070547d90d1b98debb6626421d742c897942bbb78f047694a5eb769495eccd6" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "myn" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950af707b8005ae33bb0f0b20cb32b09750e7375085606dc45b2f2078a34e52" +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + [[package]] name = "onlyerror" version = "0.1.2" dependencies = [ "error-iter", "myn", + "rustversion", + "trybuild", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "trybuild" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501dbdbb99861e4ab6b60eb6a7493956a9defb644fd034bc4a5ef27c693c8a3a" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index e451156..0ea054f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,14 @@ std = [] [lib] proc-macro = true +[[test]] +name = "compile_and_fail" +path = "compile_tests/compiler.rs" + [dependencies] myn = "0.1" [dev-dependencies] error-iter = "0.4" +rustversion = "1.0.12" +trybuild = "1.0.80" diff --git a/compile_tests/compiler.rs b/compile_tests/compiler.rs new file mode 100644 index 0000000..23d1178 --- /dev/null +++ b/compile_tests/compiler.rs @@ -0,0 +1,15 @@ +#[test] +fn compile_tests() { + let t = trybuild::TestCases::new(); + t.pass("compile_tests/empty.rs"); + t.pass("compile_tests/one_comment.rs"); + t.pass("compile_tests/one_param.rs"); + t.compile_fail("compile_tests/one_non_signed.rs"); + t.pass("compile_tests/multiple_variant.rs"); + t.compile_fail("compile_tests/multiple_non_signed.rs"); + t.compile_fail("compile_tests/multiple_one_non_signed.rs"); + t.pass("compile_tests/no_display.rs"); + if rustversion::cfg!(since(1.68.0)) { + t.compile_fail("compile_tests/no_display_no_impl.rs"); + } +} diff --git a/compile_tests/empty.rs b/compile_tests/empty.rs new file mode 100644 index 0000000..4a2e580 --- /dev/null +++ b/compile_tests/empty.rs @@ -0,0 +1,4 @@ +#[derive(Debug, onlyerror::Error)] +enum Error {} + +fn main() {} diff --git a/compile_tests/multiple_non_signed.rs b/compile_tests/multiple_non_signed.rs new file mode 100644 index 0000000..736b4c2 --- /dev/null +++ b/compile_tests/multiple_non_signed.rs @@ -0,0 +1,8 @@ +#[derive(Debug, onlyerror::Error)] +enum Error { + First, + Second(usize), + Third { key: String, value: Vec }, +} + +fn main() {} diff --git a/compile_tests/multiple_non_signed.stderr b/compile_tests/multiple_non_signed.stderr new file mode 100644 index 0000000..716bbdc --- /dev/null +++ b/compile_tests/multiple_non_signed.stderr @@ -0,0 +1,5 @@ +error: Required error message is missing + --> compile_tests/multiple_non_signed.rs:3:5 + | +3 | First, + | ^^^^^ diff --git a/compile_tests/multiple_one_non_signed.rs b/compile_tests/multiple_one_non_signed.rs new file mode 100644 index 0000000..b4bc5fc --- /dev/null +++ b/compile_tests/multiple_one_non_signed.rs @@ -0,0 +1,13 @@ +#[derive(Debug, onlyerror::Error)] +enum Error { + /// First + First, + #[error("Second with {0}")] + Second(usize), + Third { + key: String, + value: Vec, + }, +} + +fn main() {} diff --git a/compile_tests/multiple_one_non_signed.stderr b/compile_tests/multiple_one_non_signed.stderr new file mode 100644 index 0000000..12eac27 --- /dev/null +++ b/compile_tests/multiple_one_non_signed.stderr @@ -0,0 +1,5 @@ +error: Required error message is missing + --> compile_tests/multiple_one_non_signed.rs:7:5 + | +7 | Third { + | ^^^^^ diff --git a/compile_tests/multiple_variant.rs b/compile_tests/multiple_variant.rs new file mode 100644 index 0000000..1fad7f2 --- /dev/null +++ b/compile_tests/multiple_variant.rs @@ -0,0 +1,13 @@ +#![allow(dead_code)] + +#[derive(Debug, onlyerror::Error)] +enum Error { + /// First + First, + #[error("Second with {0}")] + Second(usize), + #[error("Third with {key} and {value:?}")] + Third { key: String, value: Vec }, +} + +fn main() {} diff --git a/compile_tests/no_display.rs b/compile_tests/no_display.rs new file mode 100644 index 0000000..cf6952e --- /dev/null +++ b/compile_tests/no_display.rs @@ -0,0 +1,17 @@ +#![allow(dead_code)] + +#[derive(Debug, onlyerror::Error)] +#[no_display] +enum Error { + First, + Second(usize), + Third { key: String, value: Vec }, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { + write!(f, "Should work") + } +} + +fn main() {} diff --git a/compile_tests/no_display_no_impl.rs b/compile_tests/no_display_no_impl.rs new file mode 100644 index 0000000..b7af50f --- /dev/null +++ b/compile_tests/no_display_no_impl.rs @@ -0,0 +1,14 @@ +#![allow(dead_code)] + +#[derive(Debug, onlyerror::Error)] +#[no_display] +enum Error { + /// First + First, + #[error("Second with {0}")] + Second(usize), + #[error("Third with {key} and {value:?}")] + Third { key: String, value: Vec }, +} + +fn main() {} diff --git a/compile_tests/no_display_no_impl.stderr b/compile_tests/no_display_no_impl.stderr new file mode 100644 index 0000000..84c3cdc --- /dev/null +++ b/compile_tests/no_display_no_impl.stderr @@ -0,0 +1,11 @@ +error[E0277]: `Error` doesn't implement `std::fmt::Display` + --> compile_tests/no_display_no_impl.rs:3:17 + | +3 | #[derive(Debug, onlyerror::Error)] + | ^^^^^^^^^^^^^^^^ `Error` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `Error` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `std::error::Error` + --> $RUST/core/src/error.rs + = note: this error originates in the derive macro `onlyerror::Error` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/compile_tests/one_comment.rs b/compile_tests/one_comment.rs new file mode 100644 index 0000000..f4ee70c --- /dev/null +++ b/compile_tests/one_comment.rs @@ -0,0 +1,9 @@ +#![allow(dead_code)] + +#[derive(Debug, onlyerror::Error)] +enum Error { + /// One + One, +} + +fn main() {} diff --git a/compile_tests/one_non_signed.rs b/compile_tests/one_non_signed.rs new file mode 100644 index 0000000..5377602 --- /dev/null +++ b/compile_tests/one_non_signed.rs @@ -0,0 +1,6 @@ +#[derive(Debug, onlyerror::Error)] +enum Error { + One, +} + +fn main() {} diff --git a/compile_tests/one_non_signed.stderr b/compile_tests/one_non_signed.stderr new file mode 100644 index 0000000..6d8547a --- /dev/null +++ b/compile_tests/one_non_signed.stderr @@ -0,0 +1,5 @@ +error: Required error message is missing + --> compile_tests/one_non_signed.rs:3:5 + | +3 | One, + | ^^^ diff --git a/compile_tests/one_param.rs b/compile_tests/one_param.rs new file mode 100644 index 0000000..dd90b36 --- /dev/null +++ b/compile_tests/one_param.rs @@ -0,0 +1,9 @@ +#![allow(dead_code)] + +#[derive(Debug, onlyerror::Error)] +enum Error { + #[error("One")] + One, +} + +fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 3b16f1b..105b7c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,9 +54,8 @@ //! //! - Only `enum` types are supported by the [`Error`] macro. //! - Only inline string interpolations are supported by the derived `Display` impl. -//! - Either all variants must be given an error message, or none. -//! - In the latter case, you must hand-implement `Display`. This is a constraint required by the -//! `Error` trait. +//! - Either all variants must be given an error message, or `#[no_display]` attribute must be set +//! to enum with hand-written `Display` implementation //! - `From` impls are only derived for `#[from]` and `#[source]` attributes, not implicitly for any //! field names. //! - `Backtrace` is not supported. @@ -89,7 +88,7 @@ use std::{rc::Rc, str::FromStr as _}; mod parser; #[allow(clippy::too_many_lines)] -#[proc_macro_derive(Error, attributes(error, from, source))] +#[proc_macro_derive(Error, attributes(error, from, source, no_display))] pub fn derive_error(input: TokenStream) -> TokenStream { let ast = match Error::parse(input) { Ok(ast) => ast, @@ -127,10 +126,11 @@ pub fn derive_error(input: TokenStream) -> TokenStream { ErrorSource::None => None, }) .collect::(); - let display = ast - .variants - .iter() - .map(|v| { + + let display_impl = if ast.no_display { + String::new() + } else { + let display = ast.variants.iter().map(|v| { let name = &v.name; let display = &v.display; @@ -145,7 +145,7 @@ pub fn derive_error(input: TokenStream) -> TokenStream { .collect::(); Ok(match &v.ty { - VariantType::Unit => format!("Self::{name} => write!(f, {display:?})?,"), + VariantType::Unit => format!("Self::{name} => write!(f, {display:?}),"), VariantType::Tuple => { let fields = (0..v.fields.len()) .map(|i| { @@ -156,19 +156,16 @@ pub fn derive_error(input: TokenStream) -> TokenStream { } }) .collect::(); - format!("Self::{name}({fields}) => write!(f, {display:?}, {display_fields})?,") + format!("Self::{name}({fields}) => write!(f, {display:?}, {display_fields}),") } VariantType::Struct => { format!( "Self::{name} {{ {display_fields} .. }} => \ - write!(f, {display:?}, {display_fields})?," + write!(f, {display:?}, {display_fields})," ) } }) - }) - .collect::>(); - - let display_impl = if display.iter().any(Result::is_ok) { + }); let mut display_matches = String::new(); for res in display { match res { @@ -178,6 +175,9 @@ pub fn derive_error(input: TokenStream) -> TokenStream { Ok(msg) => display_matches.push_str(&msg), } } + display_matches.push_str(&format!( + "_ => unsafe {{ ::{std_crate}::hint::unreachable_unchecked()}}" + )); format!( r#"impl ::{std_crate}::fmt::Display for {name} {{ @@ -187,12 +187,9 @@ pub fn derive_error(input: TokenStream) -> TokenStream { match self {{ {display_matches} }} - ::{std_crate}::result::Result::Ok(()) }} }}"# ) - } else { - String::new() }; let from_impls = ast diff --git a/src/parser.rs b/src/parser.rs index 8eeae1f..f39c9c9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,6 +7,7 @@ use std::rc::Rc; pub(crate) struct Error { pub(crate) name: Ident, pub(crate) variants: Vec, + pub(crate) no_display: bool, } #[derive(Debug)] @@ -48,7 +49,7 @@ pub(crate) struct OrderedMap { impl Error { pub(crate) fn parse(input: TokenStream) -> Result { let mut input = input.into_token_iter(); - input.parse_attributes()?; + let attributes = input.parse_attributes()?; input.parse_visibility()?; input.expect_ident("enum")?; let name = input.as_ident()?; @@ -61,7 +62,13 @@ impl Error { } match input.next() { - None => Ok(Self { name, variants }), + None => Ok(Self { + name, + variants, + no_display: attributes + .into_iter() + .any(|attr| attr.name.to_string() == "no_display"), + }), tree => Err(spanned_error("Unexpected token", tree.as_span())), } } @@ -74,7 +81,14 @@ impl Variant { let mut fields = HashMap::new(); let mut source = ErrorSource::None; - let ty = if let Ok(group) = input.as_group() { + let group = if let Some(TokenTree::Group(group)) = input.peek() { + let group = group.clone(); + input.next(); + Some(group) + } else { + None + }; + let ty = if let Some(group) = group { let (ty, map) = match group.delimiter() { Delimiter::Parenthesis => (VariantType::Tuple, parse_tuple_fields(group.stream())?), Delimiter::Brace => (VariantType::Struct, parse_struct_fields(group.stream())?), @@ -121,14 +135,8 @@ impl Variant { ty } else { - // Unit variants can have an optional value. - if let Some(tree) = input.peek() { - if matches!(tree, TokenTree::Literal(_) | TokenTree::Ident(_)) { - input.next(); - input.expect_punct(',')?; - } - } - + // Skip everything before ',' + while input.expect_punct(',').is_err() {} VariantType::Unit };