From 3638efa129a74cc2226038d30b564b27a1beff46 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 23 Feb 2023 14:21:00 -0600 Subject: [PATCH] Support specifying ranges of alternate values (#107) For simplicity, we require both ends of the range to be specified an literals. This only works if the `complex-expressions` feature is enabled, as `syn` can't parse ranges without its `full` feature being enabled, which significantly increases compile times. We give a useful error if you attempt to use ranges without this feature being enabled. * Bump minimum rustversion number Pre-1.0.4 it pulled in syn with the "full" feature, which broke some of our tests for testing when full is disabled, in minimal versions mode. --- num_enum/Cargo.toml | 2 +- num_enum/tests/from_primitive.rs | 27 +++++ num_enum/tests/try_build.rs | 34 +++++- .../compile_fail/exhaustive_enum.stderr | 7 -- .../alternate_exprs_with_range.rs | 11 ++ .../alternate_exprs_with_range.stderr | 5 + ...ternate_exprs_range_missing_lower_bound.rs | 11 ++ ...ate_exprs_range_missing_lower_bound.stderr | 5 + ...ternate_exprs_range_missing_upper_bound.rs | 11 ++ ...ate_exprs_range_missing_upper_bound.stderr | 5 + ...lternate_exprs_range_nonlit_lower_bound.rs | 13 +++ ...nate_exprs_range_nonlit_lower_bound.stderr | 5 + ...lternate_exprs_range_nonlit_upper_bound.rs | 13 +++ ...nate_exprs_range_nonlit_upper_bound.stderr | 5 + .../alternate_exprs_range_swapped_bounds.rs | 11 ++ ...lternate_exprs_range_swapped_bounds.stderr | 5 + .../tests/try_build/pass/exhaustive_enum.rs | 78 ------------- .../pass/exhaustive_enum_try_from.rs | 40 +++++++ .../exhaustive_enum_via_alternatives.rs} | 0 .../pass/exhaustive_enum_via_default.rs | 39 +++++++ .../alternate_exprs_exhaustive_with_range.rs | 11 ++ ...ternate_exprs_non_exhaustive_with_range.rs | 11 ++ num_enum/tests/try_from_primitive.rs | 58 +++++++++ num_enum_derive/src/lib.rs | 110 +++++++++++++++--- 24 files changed, 415 insertions(+), 102 deletions(-) delete mode 100644 num_enum/tests/try_build/compile_fail/exhaustive_enum.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.stderr create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.rs create mode 100644 num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.stderr delete mode 100644 num_enum/tests/try_build/pass/exhaustive_enum.rs create mode 100644 num_enum/tests/try_build/pass/exhaustive_enum_try_from.rs rename num_enum/tests/try_build/{compile_fail/exhaustive_enum.rs => pass/exhaustive_enum_via_alternatives.rs} (100%) create mode 100644 num_enum/tests/try_build/pass/exhaustive_enum_via_default.rs create mode 100644 num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_exhaustive_with_range.rs create mode 100644 num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_non_exhaustive_with_range.rs diff --git a/num_enum/Cargo.toml b/num_enum/Cargo.toml index 7eaac34..33030cc 100644 --- a/num_enum/Cargo.toml +++ b/num_enum/Cargo.toml @@ -33,6 +33,6 @@ num_enum_derive = { version = "0.5.10", path = "../num_enum_derive", default-fea [dev-dependencies] anyhow = "1.0.14" -rustversion = "1" +rustversion = "1.0.4" trybuild = "1.0.49" walkdir = "2" diff --git a/num_enum/tests/from_primitive.rs b/num_enum/tests/from_primitive.rs index d727db2..1be33fc 100644 --- a/num_enum/tests/from_primitive.rs +++ b/num_enum/tests/from_primitive.rs @@ -115,3 +115,30 @@ fn from_primitive_number_catch_all() { let two = Enum::from_primitive(2_u8); assert_eq!(two, Enum::NonZero(2_u8)); } + +#[cfg(feature = "complex-expressions")] +#[test] +fn from_primitive_number_with_inclusive_range() { + #[derive(Debug, Eq, PartialEq, FromPrimitive)] + #[repr(u8)] + enum Enum { + Zero = 0, + #[num_enum(alternatives = [2..=255])] + NonZero, + } + + let zero = Enum::from_primitive(0_u8); + assert_eq!(zero, Enum::Zero); + + let one = Enum::from_primitive(1_u8); + assert_eq!(one, Enum::NonZero); + + let two = Enum::from_primitive(2_u8); + assert_eq!(two, Enum::NonZero); + + let three = Enum::from_primitive(3_u8); + assert_eq!(three, Enum::NonZero); + + let twofivefive = Enum::from_primitive(255_u8); + assert_eq!(twofivefive, Enum::NonZero); +} diff --git a/num_enum/tests/try_build.rs b/num_enum/tests/try_build.rs index aff23ee..3c771d1 100644 --- a/num_enum/tests/try_build.rs +++ b/num_enum/tests/try_build.rs @@ -8,14 +8,42 @@ fn trybuild() { let mut _renamer = None; + let compile_fail_dir = directory.join("compile_fail"); + // Sometimes error messages change on beta/nightly - allow alternate errors on those. - _renamer = Some(Renamer::rename(directory.join("compile_fail")).unwrap()); + _renamer = Some(Renamer::rename(compile_fail_dir.clone()).unwrap()); let fail = trybuild::TestCases::new(); - fail.compile_fail(directory.join("compile_fail/*.rs")); + fail.compile_fail(compile_fail_dir.join("*.rs")); + add_feature_dirs(&compile_fail_dir, &fail, ExpectedResult::Fail); let pass = trybuild::TestCases::new(); - pass.pass(directory.join("pass/*.rs")); + let pass_dir = directory.join("pass"); + pass.pass(pass_dir.join("*.rs")); + add_feature_dirs(&pass_dir, &pass, ExpectedResult::Pass); +} + +enum ExpectedResult { + Pass, + Fail, +} + +fn add_feature_dirs( + parent_dir: &Path, + test_cases: &trybuild::TestCases, + expected_result: ExpectedResult, +) { + let features_dir = parent_dir.join("features"); + let feature_specific_dir = if cfg!(feature = "complex-expressions") { + features_dir.join("complex-expressions") + } else { + features_dir.join("!complex-expressions") + }; + let tests = feature_specific_dir.join("*.rs"); + match expected_result { + ExpectedResult::Pass => test_cases.pass(tests), + ExpectedResult::Fail => test_cases.compile_fail(tests), + } } struct Renamer(Vec); diff --git a/num_enum/tests/try_build/compile_fail/exhaustive_enum.stderr b/num_enum/tests/try_build/compile_fail/exhaustive_enum.stderr deleted file mode 100644 index 3efcc87..0000000 --- a/num_enum/tests/try_build/compile_fail/exhaustive_enum.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: #[derive(num_enum::FromPrimitive)] requires enum to be exhaustive, or a variant marked with `#[default]`, `#[num_enum(default)]`, or `#[num_enum(catch_all)` - --> $DIR/exhaustive_enum.rs:1:10 - | -1 | #[derive(num_enum::FromPrimitive)] - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the derive macro `num_enum::FromPrimitive` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.rs b/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.rs new file mode 100644 index 0000000..9de1028 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::FromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [2..=255])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.stderr b/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.stderr new file mode 100644 index 0000000..a60cc4d --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.stderr @@ -0,0 +1,5 @@ +error: Ranges are only supported as num_enum alternate values if the `complex-expressions` feature of the crate `num_enum` is enabled + --> tests/try_build/compile_fail/features/!complex-expressions/alternate_exprs_with_range.rs:5:5 + | +5 | #[num_enum(alternatives = [2..=255])] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.rs b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.rs new file mode 100644 index 0000000..bafbfdb --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [..255])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.stderr b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.stderr new file mode 100644 index 0000000..7dc3d59 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.stderr @@ -0,0 +1,5 @@ +error: When ranges are used for alternate values, both bounds most be explicitly specified numeric literals + --> tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_lower_bound.rs:5:32 + | +5 | #[num_enum(alternatives = [..255])] + | ^^^^^ diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.rs b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.rs new file mode 100644 index 0000000..2bc2854 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [2..])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.stderr b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.stderr new file mode 100644 index 0000000..f458858 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.stderr @@ -0,0 +1,5 @@ +error: When ranges are used for alternate values, both bounds most be explicitly specified numeric literals + --> tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_missing_upper_bound.rs:5:32 + | +5 | #[num_enum(alternatives = [2..])] + | ^^^ diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.rs b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.rs new file mode 100644 index 0000000..a083acb --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.rs @@ -0,0 +1,13 @@ +const TWO: u8 = 2; + +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [TWO..=255])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.stderr b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.stderr new file mode 100644 index 0000000..40249eb --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.stderr @@ -0,0 +1,5 @@ +error: When ranges are used for alternate values, both bounds most be explicitly specified numeric literals + --> tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_lower_bound.rs:7:32 + | +7 | #[num_enum(alternatives = [TWO..=255])] + | ^^^^^^^^^ diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.rs b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.rs new file mode 100644 index 0000000..c7b3edc --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.rs @@ -0,0 +1,13 @@ +const TWOFIVEFIVE: u8 = 255; + +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [2..=TWOFIVEFIVE])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.stderr b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.stderr new file mode 100644 index 0000000..78863b6 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.stderr @@ -0,0 +1,5 @@ +error: When ranges are used for alternate values, both bounds most be explicitly specified numeric literals + --> tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_nonlit_upper_bound.rs:7:32 + | +7 | #[num_enum(alternatives = [2..=TWOFIVEFIVE])] + | ^^^^^^^^^^^^^^^ diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.rs b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.rs new file mode 100644 index 0000000..dc17669 --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [255..=2])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.stderr b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.stderr new file mode 100644 index 0000000..b933eda --- /dev/null +++ b/num_enum/tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.stderr @@ -0,0 +1,5 @@ +error: When using ranges for alternate values, upper bound must not be less than lower bound + --> tests/try_build/compile_fail/features/complex-expressions/alternate_exprs_range_swapped_bounds.rs:5:32 + | +5 | #[num_enum(alternatives = [255..=2])] + | ^^^^^^^ diff --git a/num_enum/tests/try_build/pass/exhaustive_enum.rs b/num_enum/tests/try_build/pass/exhaustive_enum.rs deleted file mode 100644 index c590051..0000000 --- a/num_enum/tests/try_build/pass/exhaustive_enum.rs +++ /dev/null @@ -1,78 +0,0 @@ -#[derive(num_enum::FromPrimitive)] -#[repr(u8)] -enum ExhaustiveFrom { - #[num_enum(default)] - #[num_enum(alternatives = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] - A = 0, - #[num_enum(alternatives = [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])] - B = 16, - #[num_enum(alternatives = [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])] - C = 32, - #[num_enum(alternatives = [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63])] - D = 48, - #[num_enum(alternatives = [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])] - E = 64, - #[num_enum(alternatives = [81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95])] - F = 80, - #[num_enum(alternatives = [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111])] - G = 96, - #[num_enum(alternatives = [113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127])] - H = 112, - #[num_enum(alternatives = [129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])] - I = 128, - #[num_enum(alternatives = [145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159])] - J = 144, - #[num_enum(alternatives = [161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175])] - K = 160, - #[num_enum(alternatives = [177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191])] - L = 176, - #[num_enum(alternatives = [193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207])] - M = 192, - #[num_enum(alternatives = [209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223])] - N = 208, - #[num_enum(alternatives = [225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239])] - O = 224, - #[num_enum(alternatives = [241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255])] - P = 240, -} - -#[derive(num_enum::TryFromPrimitive)] -#[repr(u8)] -enum ExhaustiveTryFrom { - #[num_enum(alternatives = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] - A = 0, - #[num_enum(alternatives = [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])] - B = 16, - #[num_enum(alternatives = [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])] - C = 32, - #[num_enum(alternatives = [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63])] - D = 48, - #[num_enum(alternatives = [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])] - E = 64, - #[num_enum(alternatives = [81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95])] - F = 80, - #[num_enum(alternatives = [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111])] - G = 96, - #[num_enum(alternatives = [113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127])] - H = 112, - #[num_enum(alternatives = [129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])] - I = 128, - #[num_enum(alternatives = [145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159])] - J = 144, - #[num_enum(alternatives = [161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175])] - K = 160, - #[num_enum(alternatives = [177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191])] - L = 176, - #[num_enum(alternatives = [193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207])] - M = 192, - #[num_enum(alternatives = [209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223])] - N = 208, - #[num_enum(alternatives = [225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239])] - O = 224, - #[num_enum(alternatives = [241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255])] - P = 240, -} - -fn main() { - -} diff --git a/num_enum/tests/try_build/pass/exhaustive_enum_try_from.rs b/num_enum/tests/try_build/pass/exhaustive_enum_try_from.rs new file mode 100644 index 0000000..4e9f892 --- /dev/null +++ b/num_enum/tests/try_build/pass/exhaustive_enum_try_from.rs @@ -0,0 +1,40 @@ +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum ExhaustiveTryFrom { + #[num_enum(alternatives = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] + A = 0, + #[num_enum(alternatives = [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])] + B = 16, + #[num_enum(alternatives = [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])] + C = 32, + #[num_enum(alternatives = [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63])] + D = 48, + #[num_enum(alternatives = [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])] + E = 64, + #[num_enum(alternatives = [81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95])] + F = 80, + #[num_enum(alternatives = [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111])] + G = 96, + #[num_enum(alternatives = [113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127])] + H = 112, + #[num_enum(alternatives = [129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])] + I = 128, + #[num_enum(alternatives = [145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159])] + J = 144, + #[num_enum(alternatives = [161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175])] + K = 160, + #[num_enum(alternatives = [177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191])] + L = 176, + #[num_enum(alternatives = [193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207])] + M = 192, + #[num_enum(alternatives = [209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223])] + N = 208, + #[num_enum(alternatives = [225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239])] + O = 224, + #[num_enum(alternatives = [241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255])] + P = 240, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/compile_fail/exhaustive_enum.rs b/num_enum/tests/try_build/pass/exhaustive_enum_via_alternatives.rs similarity index 100% rename from num_enum/tests/try_build/compile_fail/exhaustive_enum.rs rename to num_enum/tests/try_build/pass/exhaustive_enum_via_alternatives.rs diff --git a/num_enum/tests/try_build/pass/exhaustive_enum_via_default.rs b/num_enum/tests/try_build/pass/exhaustive_enum_via_default.rs new file mode 100644 index 0000000..eb3c357 --- /dev/null +++ b/num_enum/tests/try_build/pass/exhaustive_enum_via_default.rs @@ -0,0 +1,39 @@ +#[derive(num_enum::FromPrimitive)] +#[repr(u8)] +enum ExhaustiveFrom { + #[num_enum(default)] + #[num_enum(alternatives = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] + A = 0, + #[num_enum(alternatives = [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])] + B = 16, + #[num_enum(alternatives = [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])] + C = 32, + #[num_enum(alternatives = [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63])] + D = 48, + #[num_enum(alternatives = [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79])] + E = 64, + #[num_enum(alternatives = [81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95])] + F = 80, + #[num_enum(alternatives = [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111])] + G = 96, + #[num_enum(alternatives = [113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127])] + H = 112, + #[num_enum(alternatives = [129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])] + I = 128, + #[num_enum(alternatives = [145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159])] + J = 144, + #[num_enum(alternatives = [161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175])] + K = 160, + #[num_enum(alternatives = [177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191])] + L = 176, + #[num_enum(alternatives = [193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207])] + M = 192, + #[num_enum(alternatives = [209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223])] + N = 208, + #[num_enum(alternatives = [225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239])] + O = 224, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_exhaustive_with_range.rs b/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_exhaustive_with_range.rs new file mode 100644 index 0000000..9de1028 --- /dev/null +++ b/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_exhaustive_with_range.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::FromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [2..=255])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_non_exhaustive_with_range.rs b/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_non_exhaustive_with_range.rs new file mode 100644 index 0000000..0c7dd52 --- /dev/null +++ b/num_enum/tests/try_build/pass/features/complex-expressions/alternate_exprs_non_exhaustive_with_range.rs @@ -0,0 +1,11 @@ +#[derive(num_enum::TryFromPrimitive)] +#[repr(u8)] +enum Numbers { + Zero = 0, + #[num_enum(alternatives = [2..255])] + NonZero = 1, +} + +fn main() { + +} diff --git a/num_enum/tests/try_from_primitive.rs b/num_enum/tests/try_from_primitive.rs index ffccf3a..e6d19f6 100644 --- a/num_enum/tests/try_from_primitive.rs +++ b/num_enum/tests/try_from_primitive.rs @@ -236,6 +236,64 @@ mod complex { let seven: Result = 7u8.try_into(); assert_eq!(seven, Ok(Enum::Seven)); } + + #[derive(Debug, Eq, PartialEq, TryFromPrimitive)] + #[repr(u8)] + enum EnumWithExclusiveRange { + Zero = 0, + #[num_enum(alternatives = [2..4])] + OneOrTwoOrThree, + } + + #[test] + fn different_values_with_exclusive_range() { + let zero: Result = 0u8.try_into(); + assert_eq!(zero, Ok(EnumWithExclusiveRange::Zero)); + + let one: Result = 1u8.try_into(); + assert_eq!(one, Ok(EnumWithExclusiveRange::OneOrTwoOrThree)); + + let two: Result = 2u8.try_into(); + assert_eq!(two, Ok(EnumWithExclusiveRange::OneOrTwoOrThree)); + + let three: Result = 3u8.try_into(); + assert_eq!(three, Ok(EnumWithExclusiveRange::OneOrTwoOrThree)); + + let four: Result = 4u8.try_into(); + assert_eq!( + four.unwrap_err().to_string(), + "No discriminant in enum `EnumWithExclusiveRange` matches the value `4`", + ); + } + + #[derive(Debug, Eq, PartialEq, TryFromPrimitive)] + #[repr(u8)] + enum EnumWithInclusiveRange { + Zero = 0, + #[num_enum(alternatives = [2..=3])] + OneOrTwoOrThree, + } + + #[test] + fn different_values_with_inclusive_range() { + let zero: Result = 0u8.try_into(); + assert_eq!(zero, Ok(EnumWithInclusiveRange::Zero)); + + let one: Result = 1u8.try_into(); + assert_eq!(one, Ok(EnumWithInclusiveRange::OneOrTwoOrThree)); + + let two: Result = 2u8.try_into(); + assert_eq!(two, Ok(EnumWithInclusiveRange::OneOrTwoOrThree)); + + let three: Result = 3u8.try_into(); + assert_eq!(three, Ok(EnumWithInclusiveRange::OneOrTwoOrThree)); + + let four: Result = 4u8.try_into(); + assert_eq!( + four.unwrap_err().to_string(), + "No discriminant in enum `EnumWithInclusiveRange` matches the value `4`", + ); + } } #[test] diff --git a/num_enum_derive/src/lib.rs b/num_enum_derive/src/lib.rs index c64c1de..600730f 100644 --- a/num_enum_derive/src/lib.rs +++ b/num_enum_derive/src/lib.rs @@ -1,3 +1,6 @@ +// Not supported by MSRV +#![allow(clippy::uninlined_format_args)] + extern crate proc_macro; use proc_macro::TokenStream; @@ -63,6 +66,63 @@ fn parse_discriminant(val_exp: &Expr) -> Result { } } +#[cfg(feature = "complex-expressions")] +fn parse_alternative_values(val_expr: &Expr) -> Result> { + fn range_expr_value_to_number( + parent_range_expr: &Expr, + range_bound_value: &Option>, + ) -> Result { + // Avoid needing to calculate what the lower and upper bound would be - these are type dependent, + // and also may not be obvious in context (e.g. an omitted bound could reasonably mean "from the last discriminant" or "from the lower bound of the type"). + if let Some(range_bound_value) = range_bound_value { + let range_bound_value = parse_discriminant(range_bound_value.as_ref())?; + // If non-literals are used, we can't expand to the mapped values, so can't write a nice match statement or do exhaustiveness checking. + // Require literals instead. + if let DiscriminantValue::Literal(value) = range_bound_value { + return Ok(value); + } + } + die!(parent_range_expr => "When ranges are used for alternate values, both bounds most be explicitly specified numeric literals") + } + + if let Expr::Range(syn::ExprRange { + from, to, limits, .. + }) = val_expr + { + let lower = range_expr_value_to_number(val_expr, from)?; + let upper = range_expr_value_to_number(val_expr, to)?; + // While this is technically allowed in Rust, and results in an empty range, it's almost certainly a mistake in this context. + if lower > upper { + die!(val_expr => "When using ranges for alternate values, upper bound must not be less than lower bound"); + } + let mut values = Vec::with_capacity((upper - lower) as usize); + let mut next = lower; + loop { + match limits { + syn::RangeLimits::HalfOpen(..) => { + if next == upper { + break; + } + } + syn::RangeLimits::Closed(..) => { + if next > upper { + break; + } + } + } + values.push(DiscriminantValue::Literal(next)); + next += 1; + } + return Ok(values); + } + parse_discriminant(val_expr).map(|v| vec![v]) +} + +#[cfg(not(feature = "complex-expressions"))] +fn parse_alternative_values(val_expr: &Expr) -> Result> { + parse_discriminant(val_expr).map(|v| vec![v]) +} + mod kw { syn::custom_keyword!(default); syn::custom_keyword!(catch_all); @@ -211,7 +271,13 @@ impl EnumInfo { if let Some(suffix) = suffix { if let Ok(bits) = suffix.parse::() { let variants = 1usize.checked_shl(bits); - return Ok(variants.map_or(false, |v| v == self.variants.len())); + return Ok(variants.map_or(false, |v| { + v == self + .variants + .iter() + .map(|v| v.alternative_values.len() + 1) + .sum() + })); } } } @@ -427,6 +493,15 @@ impl Parse for EnumInfo { } } Err(err) => { + if cfg!(not(feature = "complex-expressions")) { + let attribute_str = format!("{}", attribute.tokens); + if attribute_str.contains("alternatives") + && attribute_str.contains("..") + { + // Give a nice error message suggesting how to fix the problem. + die!(attribute => "Ranges are only supported as num_enum alternate values if the `complex-expressions` feature of the crate `num_enum` is enabled".to_string()) + } + } die!(attribute => format!("Invalid attribute: {}", err) ); @@ -458,20 +533,29 @@ impl Parse for EnumInfo { } // Deal with the alternative values. - let alternate_values = raw_alternative_values - .iter() - .map(parse_discriminant) - .collect::>>()?; - - debug_assert_eq!(alternate_values.len(), raw_alternative_values.len()); + let mut flattened_alternative_values = Vec::new(); + let mut flattened_raw_alternative_values = Vec::new(); + for raw_alternative_value in raw_alternative_values { + let expanded_values = parse_alternative_values(&raw_alternative_value)?; + for expanded_value in expanded_values { + flattened_alternative_values.push(expanded_value); + flattened_raw_alternative_values.push(raw_alternative_value.clone()) + } + } - if !alternate_values.is_empty() { - let alternate_int_values = alternate_values + if !flattened_alternative_values.is_empty() { + let alternate_int_values = flattened_alternative_values .into_iter() .map(|v| { match v { DiscriminantValue::Literal(value) => Ok(value), DiscriminantValue::Expr(expr) => { + if let Expr::Range(_) = expr { + if cfg!(not(feature = "complex-expressions")) { + // Give a nice error message suggesting how to fix the problem. + die!(expr => "Ranges are only supported as num_enum alternate values if the `complex-expressions` feature of the crate `num_enum` is enabled".to_string()) + } + } // We can't do uniqueness checking on non-literals, so we don't allow them as alternate values. // We could probably allow them, but there doesn't seem to be much of a use-case, // and it's easier to give good error messages about duplicate values this way, @@ -491,7 +575,7 @@ impl Parse for EnumInfo { .iter() .position(|&x| x == canonical_value_int) { - die!(&raw_alternative_values[index] => format!("'{}' in the alternative values is already attributed as the discriminant of this variant", canonical_value_int)); + die!(&flattened_raw_alternative_values[index] => format!("'{}' in the alternative values is already attributed as the discriminant of this variant", canonical_value_int)); } } @@ -508,14 +592,14 @@ impl Parse for EnumInfo { if sorted_alternate_int_values.first().unwrap() <= last_upper_val { for (index, val) in alternate_int_values.iter().enumerate() { if discriminant_int_val_set.contains(val) { - die!(&raw_alternative_values[index] => format!("'{}' in the alternative values is already attributed to a previous variant", val)); + die!(&flattened_raw_alternative_values[index] => format!("'{}' in the alternative values is already attributed to a previous variant", val)); } } } } // Reconstruct the alternative_values vec of Expr but sorted. - raw_alternative_values = sorted_alternate_int_values + flattened_raw_alternative_values = sorted_alternate_int_values .iter() .map(|val| literal(val.to_owned())) .collect(); @@ -535,7 +619,7 @@ impl Parse for EnumInfo { is_default, is_catch_all, canonical_value: discriminant, - alternative_values: raw_alternative_values, + alternative_values: flattened_raw_alternative_values, }); // Get the next value for the discriminant.