Skip to content

Commit 43ce704

Browse files
committed
Refactor error detection and reporting
This allows using non-literal discriminants - fixes #20. Also fixes #12 and #13.
1 parent c2d07d5 commit 43ce704

File tree

8 files changed

+250
-81
lines changed

8 files changed

+250
-81
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ enumflags2 = "^0.6"
2020

2121
- [x] Uses enums to represent individual flags—a set of flags is a separate type from a single flag.
2222
- [x] Detects incorrect BitFlags at compile time.
23-
- Non-unique bits.
24-
- Missing values.
25-
- Flags larger than the chosen `repr`.
2623
- [x] Has a similar API compared to the popular [bitflags](https://crates.io/crates/bitflags) crate.
2724
- [x] Does not expose the generated types explicity. The user interacts exclusively with `struct BitFlags<Enum>;`.
2825
- [x] The debug formatter prints the binary flag value as well as the flag enums: `BitFlags(0b1111, [A, B, C, D])`.

enumflags/Cargo.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ readme = "../README.md"
99
keywords = ["enum", "bitflag", "flag", "bitflags"]
1010
documentation = "https://docs.rs/enumflags2"
1111

12-
[dependencies]
13-
enumflags2_derive = { version = "0.6.1", path = "../enumflags_derive" }
14-
serde = { version = "^1.0.0", default-features = false, optional = true }
12+
[dependencies.enumflags2_derive]
13+
version = "0.6.1"
14+
path = "../enumflags_derive"
15+
16+
[dependencies.serde]
17+
version = "^1.0.0"
18+
default-features = false
19+
optional = true
1520

1621
[features]
1722
std = []
23+
not_literal = ["enumflags2_derive/not_literal"]

enumflags/src/lib.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,17 @@
4343
//!
4444
//! ## Optional Feature Flags
4545
//!
46-
//! - [`serde`](https://serde.rs/) implements `Serialize` and `Deserialize` for `BitFlags<T>`.
46+
//! - [`serde`](https://serde.rs/) implements `Serialize` and `Deserialize`
47+
//! for `BitFlags<T>`.
4748
//! - `std` implements `std::error::Error` for `FromBitsError`.
49+
//! - `not_literal` enables a workaround that allows using discriminant
50+
//! expressions that can't be evaluated at macro expansion time. Notably,
51+
//! this includes using pre-existing constants.
52+
//!
53+
//! This is disabled by default because of the high potential for confusing
54+
//! error messages - if a flag doesn't have exactly one bit set, the error
55+
//! message will be "attempt to subtract with overflow", pointing at the
56+
//! relevant flag.
4857
//!
4958
//! ### Migrating from 0.5
5059
//!
@@ -143,7 +152,7 @@ pub mod _internal {
143152

144153
// Re-export libcore so the macro doesn't inject "extern crate" downstream.
145154
pub mod core {
146-
pub use core::{convert, option, ops};
155+
pub use core::{convert, option, ops, compile_error};
147156
}
148157
}
149158

enumflags_derive/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ license = "MIT or Apache-2.0"
77
repository = "https://github.com/NieDzejkob/enumflags2"
88
keywords = ["enum", "bitflag", "flag", "bitflags"]
99

10+
[lib]
11+
proc-macro = true
12+
1013
[dependencies]
1114
syn = "^1.0"
1215
quote = "^1.0"
1316
proc-macro2 = "^1.0"
1417

15-
[lib]
16-
proc-macro = true
18+
[features]
19+
not_literal = []

enumflags_derive/src/lib.rs

Lines changed: 154 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,105 +4,189 @@ extern crate proc_macro;
44
extern crate quote;
55
extern crate syn;
66
extern crate proc_macro2;
7-
use syn::{Data, Ident, DeriveInput, DataEnum};
7+
use syn::{Data, Ident, DeriveInput, DataEnum, spanned::Spanned};
88
use proc_macro2::TokenStream;
99
use proc_macro2::Span;
10-
use quote::ToTokens;
1110
use std::convert::From;
1211

12+
/// Shorthand for a quoted `compile_error!`.
13+
macro_rules! error {
14+
($span:expr => $($x:tt)*) => {
15+
quote_spanned!($span =>
16+
::enumflags2::_internal::core::compile_error!($($x)*);
17+
)
18+
};
19+
($($x:tt)*) => {
20+
quote!(
21+
::enumflags2::_internal::core::compile_error!($($x)*);
22+
)
23+
}
24+
}
25+
1326
#[proc_macro_derive(BitFlags_internal)]
14-
pub fn derive_enum_flags(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
27+
pub fn derive_enum_flags(input: proc_macro::TokenStream)
28+
-> proc_macro::TokenStream
29+
{
1530
let ast: DeriveInput = syn::parse(input).unwrap();
1631

1732
match ast.data {
18-
Data::Enum(ref data) => gen_enumflags(&ast.ident, &ast, data).into(),
19-
_ => panic!("`derive(BitFlags)` may only be applied to enums"),
33+
Data::Enum(ref data) => {
34+
gen_enumflags(&ast.ident, &ast, data)
35+
.unwrap_or_else(|err| err)
36+
.into()
37+
}
38+
_ => error!("BitFlags can only be derived on enums").into(),
39+
}
40+
}
41+
42+
#[derive(Debug)]
43+
enum EvaluationError {
44+
LiteralOutOfRange(Span),
45+
UnsupportedOperation(Span),
46+
}
47+
48+
impl From<EvaluationError> for TokenStream {
49+
fn from(why: EvaluationError) -> TokenStream {
50+
use EvaluationError::*;
51+
52+
match why {
53+
LiteralOutOfRange(span) => {
54+
error!(span => "Integer literal out of range")
55+
}
56+
UnsupportedOperation(span) => {
57+
error!(span => "This kind of discriminant expression is \
58+
not supported.\n\
59+
hint: Enable the \"not_literal\" feature to \
60+
use a workaround.\n\
61+
note: This is not enabled by default due to the \
62+
high potential for confusing error messages \
63+
(see documentation).")
64+
}
65+
}
2066
}
2167
}
2268

23-
fn fold_expr(expr: &syn::Expr) -> u64 {
69+
/// Try to evaluate the expression given.
70+
fn fold_expr(expr: &syn::Expr) -> Result<u64, EvaluationError> {
2471
use syn::Expr;
72+
use EvaluationError::*;
2573
match expr {
2674
Expr::Lit(ref expr_lit) => {
2775
match expr_lit.lit {
28-
syn::Lit::Int(ref lit_int) => lit_int.base10_parse().expect("Int literal out of range"),
29-
_ => panic!("Only Int literals are supported")
76+
syn::Lit::Int(ref lit_int) => {
77+
lit_int.base10_parse()
78+
.or_else(|_| Err(LiteralOutOfRange(expr.span())))
79+
}
80+
_ => Err(UnsupportedOperation(expr.span()))
3081
}
3182
},
3283
Expr::Binary(ref expr_binary) => {
33-
let l = fold_expr(&expr_binary.left);
34-
let r = fold_expr(&expr_binary.right);
84+
let l = fold_expr(&expr_binary.left)?;
85+
let r = fold_expr(&expr_binary.right)?;
3586
match &expr_binary.op {
36-
syn::BinOp::Shl(_) => l << r,
37-
op => panic!("{} not supported", op.to_token_stream())
87+
syn::BinOp::Shl(_) => Ok(l << r),
88+
_ => Err(UnsupportedOperation(expr_binary.span()))
3889
}
3990
}
40-
_ => panic!("Only literals are supported")
91+
_ => Err(UnsupportedOperation(expr.span()))
4192
}
4293
}
4394

44-
fn extract_repr(attrs: &[syn::Attribute]) -> Option<syn::Ident> {
45-
attrs
46-
.iter()
47-
.filter_map(|a| {
48-
if let syn::Meta::List(ref meta) = a.parse_meta().expect("Metalist") {
49-
if meta.path.is_ident("repr") {
50-
return meta.nested
51-
.iter()
52-
.filter_map(|mi| {
53-
if let syn::NestedMeta::Meta(syn::Meta::Path(path)) =
54-
mi
55-
{
56-
return path.get_ident().cloned();
95+
/// Given a list of attributes, find the `repr`, if any, and return the integer
96+
/// type specified.
97+
fn extract_repr(attrs: &[syn::Attribute])
98+
-> Result<Option<syn::Ident>, TokenStream>
99+
{
100+
use syn::{Meta, NestedMeta};
101+
attrs.iter()
102+
.find_map(|attr| {
103+
match attr.parse_meta() {
104+
Err(why) => {
105+
let error = format!("Couldn't parse attribute: {}", why);
106+
Some(Err(error!(attr.span() => #error)))
107+
}
108+
Ok(Meta::List(ref meta)) if meta.path.is_ident("repr") => {
109+
meta.nested.iter()
110+
.find_map(|mi| match mi {
111+
NestedMeta::Meta(Meta::Path(path)) => {
112+
path.get_ident().cloned()
113+
.map(Ok)
57114
}
58-
None
115+
_ => None
59116
})
60-
.nth(0);
61117
}
118+
Ok(_) => None
62119
}
63-
None
64120
})
65-
.nth(0)
121+
.transpose()
66122
}
67123

68-
fn gen_enumflags(ident: &Ident, item: &DeriveInput, data: &DataEnum) -> TokenStream {
69-
let span = Span::call_site();
70-
let variants = data.variants.iter().map(|v| &v.ident);
71-
let flag_values: Vec<_> =
72-
data.variants.iter()
73-
.map(|v| v.discriminant.as_ref()
74-
.map(|d| fold_expr(&d.1)).expect("No discriminant"))
75-
.collect();
76-
let variants_len = flag_values.len();
77-
let names = flag_values.iter().map(|_| &ident);
78-
let ty = extract_repr(&item.attrs)
79-
.unwrap_or_else(|| Ident::new("usize", span));
80-
81-
let mut flags_seen = 0;
82-
for (&flag, variant) in flag_values.iter().zip(variants.clone()) {
83-
if flag == 0 || !flag.is_power_of_two() {
84-
panic!("Each flag must have exactly one bit set, and {ident}::{variant} = {flag:#b} doesn't",
85-
ident = ident,
86-
variant = variant,
87-
flag = flag
88-
);
89-
} else if flags_seen & flag != 0 {
90-
panic!("Flag {} collides with {}",
91-
variant,
92-
flag_values.iter()
93-
.zip(variants.clone())
94-
.find(|(&other_flag, _)| flag == other_flag)
95-
.unwrap()
96-
.1
97-
);
124+
/// Returns Ok with deferred checks (not_literal), or Err with error!
125+
fn verify_flag_values<'a>(
126+
// starts with underscore to silence warnings when not_literal
127+
// are disabled
128+
_type_name: &Ident,
129+
variants: impl Iterator<Item=&'a syn::Variant>
130+
) -> Result<TokenStream, TokenStream> {
131+
#[cfg_attr(not(feature = "not_literal"), allow(unused_mut))]
132+
let mut deferred_checks: Vec<TokenStream> = vec![];
133+
for variant in variants {
134+
let discr = variant.discriminant.as_ref()
135+
.ok_or_else(|| error!(variant.span() =>
136+
"Please add an explicit discriminant"))?;
137+
match fold_expr(&discr.1) {
138+
Ok(flag) => {
139+
if !flag.is_power_of_two() {
140+
return Err(error!(variant.discriminant.as_ref()
141+
.unwrap().1.span() =>
142+
"Flags must have exactly one set bit."));
143+
}
144+
}
145+
#[cfg(feature = "not_literal")]
146+
Err(EvaluationError::UnsupportedOperation(_)) => {
147+
let variant_name = &variant.ident;
148+
// adapted from static-assertions-rs by nvzqz (MIT/Apache-2.0)
149+
deferred_checks.push(quote_spanned!(variant.span() =>
150+
#[allow(unknown_lints, eq_op)]
151+
const _: [(); 0 - !(
152+
(#_type_name::#variant_name as u64).wrapping_sub(1) &
153+
(#_type_name::#variant_name as u64) == 0 &&
154+
(#_type_name::#variant_name as u64) != 0
155+
) as usize] = [];
156+
));
157+
}
158+
Err(why) => Err(why)?,
98159
}
99-
100-
flags_seen |= flag;
101160
}
102161

103-
let std_path = quote_spanned!(span=> ::enumflags2::_internal::core);
104-
quote_spanned!{
105-
span =>
162+
Ok(quote!(
163+
#(#deferred_checks)*
164+
))
165+
}
166+
167+
fn gen_enumflags(ident: &Ident, item: &DeriveInput, data: &DataEnum)
168+
-> Result<TokenStream, TokenStream>
169+
{
170+
let span = Span::call_site();
171+
// for quote! interpolation
172+
let variants = data.variants.iter().map(|v| &v.ident);
173+
let variants_len = data.variants.len();
174+
let names = std::iter::repeat(&ident);
175+
let ty = extract_repr(&item.attrs)?
176+
.unwrap_or_else(|| Ident::new("usize", span));
177+
178+
let deferred = verify_flag_values(ident, data.variants.iter())?;
179+
let std_path = quote_spanned!(span => ::enumflags2::_internal::core);
180+
let all = if variants_len == 0 {
181+
quote!(0)
182+
} else {
183+
let names = names.clone();
184+
let variants = variants.clone();
185+
quote!(#(#names::#variants as #ty)|*)
186+
};
187+
188+
Ok(quote_spanned! {
189+
span => #deferred
106190
impl #std_path::ops::Not for #ident {
107191
type Output = ::enumflags2::BitFlags<#ident>;
108192
fn not(self) -> Self::Output {
@@ -138,21 +222,23 @@ fn gen_enumflags(ident: &Ident, item: &DeriveInput, data: &DataEnum) -> TokenStr
138222
type Type = #ty;
139223

140224
fn all() -> Self::Type {
141-
(#(#flag_values)|*) as #ty
225+
// make sure it's evaluated at compile time
226+
const VALUE: #ty = #all as #ty;
227+
VALUE
142228
}
143229

144230
fn bits(self) -> Self::Type {
145231
self as #ty
146232
}
147233

148234
fn flag_list() -> &'static [Self] {
149-
const VARIANTS: [#ident; #variants_len] = [#(#names :: #variants, )*];
235+
const VARIANTS: [#ident; #variants_len] = [#(#names :: #variants),*];
150236
&VARIANTS
151237
}
152238

153239
fn bitflags_type_name() -> &'static str {
154240
concat!("BitFlags<", stringify!(#ident), ">")
155241
}
156242
}
157-
}
243+
})
158244
}

test_suite/Cargo.toml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ name = "bitflags-test-suite"
33
version = "0.0.0"
44
publish = false
55

6-
[dev-dependencies]
6+
[dependencies.enumflags2]
7+
path = "../enumflags"
8+
features = ["serde", "not_literal"]
79

8-
enumflags2 = {path = "../enumflags", features = ["serde"]}
9-
serde = {version = "1", features = ["derive"] }
10+
[dependencies.serde]
11+
version = "1"
12+
features = ["derive"]
1013

1114
[[test]]
1215
name = "bitflags-test"
@@ -47,3 +50,13 @@ edition = "2018"
4750
name = "serde"
4851
path = "tests/serde.rs"
4952
edition = "2018"
53+
54+
[[test]]
55+
name = "not_literal"
56+
path = "tests/not_literal.rs"
57+
edition = "2018"
58+
59+
[lib]
60+
name = "compile_fail"
61+
path = "compile_fail.rs"
62+
edition = "2018"

0 commit comments

Comments
 (0)