proptest_derive/attr.rs
1// Copyright 2018 The proptest developers
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Provides a parser from syn attributes to our logical model.
10
11use quote::ToTokens;
12use syn::parse::Parser;
13use syn::punctuated::Punctuated;
14use syn::{self, Attribute, Expr, Ident, Lit, Meta, Type};
15
16use crate::error::{self, Ctx, DeriveResult};
17use crate::interp;
18use crate::util;
19
20//==============================================================================
21// Public API
22//==============================================================================
23
24/// Parsed attributes in our logical model.
25#[derive(Clone)]
26pub struct ParsedAttributes {
27 /// If we've been ordered to skip this item.
28 /// This is only valid for enum variants.
29 pub skip: bool,
30 /// The potential weight assigned to an enum variant.
31 /// This must be `None` for things that are not enum variants.
32 pub weight: Option<u32>,
33 /// The mode for `Parameters` to use. See that type for more.
34 pub params: ParamsMode,
35 /// The mode for `Strategy` to use. See that type for more.
36 pub strategy: StratMode,
37 /// Filter expressions if any.
38 pub filter: Vec<syn::Expr>,
39 /// True if no_bound was specified.
40 pub no_bound: bool,
41}
42
43/// The mode for the associated item `Strategy` to use.
44#[derive(Clone)]
45pub enum StratMode {
46 /// This means that no explicit strategy was specified
47 /// and that we thus should use `Arbitrary` for whatever
48 /// it is that needs a strategy.
49 Arbitrary,
50 /// This means that an explicit value has been provided.
51 /// The result of this is to use a strategy that always
52 /// returns the given value.
53 Value(Expr),
54 /// This means that an explicit strategy has been provided.
55 /// This strategy will be used to generate whatever it
56 /// is that the attribute was set on.
57 Strategy(Expr),
58 /// This means that an explicit *regex* strategy has been provided.
59 /// We don't reuse `Strategy(..)` so that we can produce better and
60 /// more tailored error messages.
61 Regex(Expr),
62}
63
64/// The mode for the associated item `Parameters` to use.
65#[derive(Clone)]
66pub enum ParamsMode {
67 /// Nothing has been specified. The children are now free to
68 /// specify their parameters, and if nothing is specified, then
69 /// `<X as Arbitrary>::Parameters` will be used for a type `X`.
70 Passthrough,
71 /// We've been ordered to use the Default value of
72 /// `<X as Arbitrary>::Parameters` for some field where applicable.
73 /// For the top level item, this means that `Parameters` will be
74 /// the unit type. For children, it means that this child should
75 /// not count towards the product type that is being built up.
76 Default,
77 /// An explicit type has been specified on some item.
78 /// If the top level item has this specified on it, this means
79 /// that `Parameters` will have the given type.
80 /// If it is specified on a child of the top level item, this
81 /// entails that the given type will be added to the resultant
82 /// product type.
83 Specified(Type),
84}
85
86impl ParamsMode {
87 /// Returns `true` iff the mode was explicitly set.
88 pub fn is_set(&self) -> bool {
89 if let ParamsMode::Passthrough = *self {
90 false
91 } else {
92 true
93 }
94 }
95
96 /// Converts the mode to an `Option` of an `Option` of a type
97 /// where the outer `Option` is `None` iff the mode wasn't set
98 /// and the inner `Option` is `None` iff the mode was `Default`.
99 pub fn into_option(self) -> Option<Option<Type>> {
100 use self::ParamsMode::*;
101 match self {
102 Passthrough => None,
103 Specified(ty) => Some(Some(ty)),
104 Default => Some(None),
105 }
106 }
107}
108
109impl StratMode {
110 /// Returns `true` iff the mode was explicitly set.
111 pub fn is_set(&self) -> bool {
112 if let StratMode::Arbitrary = self {
113 false
114 } else {
115 true
116 }
117 }
118}
119
120/// Parse the attributes specified on an item and parsed by syn
121/// into our logical model that we work with.
122pub fn parse_attributes(
123 ctx: Ctx,
124 attrs: &[Attribute],
125) -> DeriveResult<ParsedAttributes> {
126 let attrs = parse_attributes_base(ctx, attrs)?;
127 if attrs.no_bound {
128 error::no_bound_set_on_non_tyvar(ctx);
129 }
130 Ok(attrs)
131}
132
133/// Parse the attributes specified on a type definition...
134pub fn parse_top_attributes(
135 ctx: Ctx,
136 attrs: &[Attribute],
137) -> DeriveResult<ParsedAttributes> {
138 parse_attributes_base(ctx, attrs)
139}
140
141/// Parses the attributes specified on an item and parsed by syn
142/// and returns true if we've been ordered to not set an `Arbitrary`
143/// bound on the given type variable the attributes are from,
144/// no matter what.
145pub fn has_no_bound(ctx: Ctx, attrs: &[Attribute]) -> DeriveResult<bool> {
146 let attrs = parse_attributes_base(ctx, attrs)?;
147 error::if_anything_specified(ctx, &attrs, error::TY_VAR);
148 Ok(attrs.no_bound)
149}
150
151/// Parse the attributes specified on an item and parsed by syn
152/// into our logical model that we work with.
153fn parse_attributes_base(
154 ctx: Ctx,
155 attrs: &[Attribute],
156) -> DeriveResult<ParsedAttributes> {
157 let acc = parse_accumulate(ctx, attrs);
158
159 Ok(ParsedAttributes {
160 skip: acc.skip.is_some(),
161 weight: acc.weight,
162 filter: acc.filter,
163 // Process params and no_params together to see which one to use.
164 params: parse_params_mode(ctx, acc.no_params, acc.params)?,
165 // Process strategy and value together to see which one to use.
166 strategy: parse_strat_mode(ctx, acc.strategy, acc.value, acc.regex)?,
167 no_bound: acc.no_bound.is_some(),
168 })
169}
170
171//==============================================================================
172// Internals: Initialization
173//==============================================================================
174
175/// The internal state of the attribute parser.
176#[derive(Default)]
177struct ParseAcc {
178 skip: Option<()>,
179 weight: Option<u32>,
180 no_params: Option<()>,
181 params: Option<Type>,
182 strategy: Option<Expr>,
183 value: Option<Expr>,
184 regex: Option<Expr>,
185 filter: Vec<Expr>,
186 no_bound: Option<()>,
187}
188
189//==============================================================================
190// Internals: Extraction & Filtering
191//==============================================================================
192
193fn parse_accumulate(ctx: Ctx, attrs: &[Attribute]) -> ParseAcc {
194 let mut state = ParseAcc::default();
195
196 // Get rid of attributes we don't care about:
197 for attr in attrs {
198 if is_proptest_attr(&attr) {
199 // Flatten attributes so we deal with them uniformly.
200 state = extract_modifiers(ctx, &attr)
201 .into_iter()
202 // Accumulate attributes into a form for final processing.
203 .fold(state, |state, meta| dispatch_attribute(ctx, state, meta))
204 }
205 }
206
207 state
208}
209
210/// Returns `true` iff the attribute has to do with proptest.
211/// Otherwise, the attribute is irrevant to us and we will simply
212/// ignore it in our processing.
213fn is_proptest_attr(attr: &Attribute) -> bool {
214 util::eq_simple_path("proptest", attr.path())
215}
216
217/// Extract all individual attributes inside one `#[proptest(..)]`.
218/// We do this to treat all pieces uniformly whether a single
219/// `#[proptest(..)]` was used or many. This simplifies the
220/// logic somewhat.
221fn extract_modifiers(ctx: Ctx, attr: &Attribute) -> Vec<Meta> {
222 // Ensure we've been given an outer attribute form.
223 if !is_outer_attr(&attr) {
224 error::inner_attr(ctx);
225 }
226
227 match &attr.meta {
228 Meta::List(list) => {
229 if syn::parse2::<Lit>(list.tokens.clone()).is_ok() {
230 error::immediate_literals(ctx);
231 } else {
232 let parser = Punctuated::<Meta, Token![,]>::parse_separated_nonempty;
233 let metas = parser.parse2(list.tokens.clone()).unwrap();
234 return metas.into_iter().collect();
235 }
236 }
237 Meta::Path(_) => error::bare_proptest_attr(ctx),
238 Meta::NameValue(_) => error::literal_set_proptest(ctx),
239 }
240
241 vec![]
242}
243
244/// Returns true iff the given attribute is an outer one, i.e: `#[<attr>]`.
245/// An inner attribute is the other possibility and has the syntax `#![<attr>]`.
246/// Note that `<attr>` is a meta-variable for the contents inside.
247fn is_outer_attr(attr: &Attribute) -> bool {
248 syn::AttrStyle::Outer == attr.style
249}
250
251//==============================================================================
252// Internals: Dispatch
253//==============================================================================
254
255/// Dispatches an attribute modifier to handlers and
256/// let's them add stuff into our accumulartor.
257fn dispatch_attribute(ctx: Ctx, mut acc: ParseAcc, meta: Meta) -> ParseAcc {
258 // Dispatch table for attributes:
259 let path = meta.path();
260 if let Some(name) = path.get_ident().map(ToString::to_string) {
261 match name.as_ref() {
262 // Valid modifiers:
263 "skip" => parse_skip(ctx, &mut acc, meta),
264 "w" | "weight" => parse_weight(ctx, &mut acc, &meta),
265 "no_params" => parse_no_params(ctx, &mut acc, meta),
266 "params" => parse_params(ctx, &mut acc, meta),
267 "strategy" => parse_strategy(ctx, &mut acc, &meta),
268 "value" => parse_value(ctx, &mut acc, &meta),
269 "regex" => parse_regex(ctx, &mut acc, &meta),
270 "filter" => parse_filter(ctx, &mut acc, &meta),
271 "no_bound" => parse_no_bound(ctx, &mut acc, meta),
272 // Invalid modifiers:
273 name => dispatch_unknown_mod(ctx, name),
274 }
275 } else {
276 // Occurs when passed path is something other than a single ident
277 error::unkown_modifier(ctx, &path.into_token_stream().to_string());
278 }
279 acc
280}
281
282fn dispatch_unknown_mod(ctx: Ctx, name: &str) {
283 match name {
284 "no_bounds" => error::did_you_mean(ctx, name, "no_bound"),
285 "weights" | "weighted" => error::did_you_mean(ctx, name, "weight"),
286 "strat" | "strategies" => error::did_you_mean(ctx, name, "strategy"),
287 "values" | "valued" | "fix" | "fixed" => {
288 error::did_you_mean(ctx, name, "value")
289 }
290 "regexes" | "regexp" | "re" => error::did_you_mean(ctx, name, "regex"),
291 "param" | "parameters" => error::did_you_mean(ctx, name, "params"),
292 "no_param" | "no_parameters" => {
293 error::did_you_mean(ctx, name, "no_params")
294 }
295 name => error::unkown_modifier(ctx, name),
296 // TODO: consider levenshtein distance.
297 }
298}
299
300//==============================================================================
301// Internals: no_bound
302//==============================================================================
303
304/// Parse a no_bound attribute.
305/// Valid forms are:
306/// + `#[proptest(no_bound)]`
307fn parse_no_bound(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
308 parse_bare_modifier(ctx, &mut acc.no_bound, meta, error::no_bound_malformed)
309}
310
311//==============================================================================
312// Internals: Skip
313//==============================================================================
314
315/// Parse a skip attribute.
316/// Valid forms are:
317/// + `#[proptest(skip)]`
318fn parse_skip(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
319 parse_bare_modifier(ctx, &mut acc.skip, meta, error::skip_malformed)
320}
321
322//==============================================================================
323// Internals: Weight
324//==============================================================================
325
326/// Parses a weight.
327/// Valid forms are:
328/// + `#[proptest(weight = <integer>)]`
329/// + `#[proptest(weight = "<expr>")]`
330/// + `#[proptest(weight(<integer>))]`
331/// + `#[proptest(weight("<expr>""))]`
332///
333/// The `<integer>` must also fit within an `u32` and be unsigned.
334fn parse_weight(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
335 use std::u32;
336 error_if_set(ctx, &acc.weight, &meta);
337
338 // Convert to value if possible:
339 let value = normalize_meta(meta.clone())
340 .and_then(extract_lit)
341 .and_then(extract_expr)
342 // Evaluate the expression into a value:
343 .as_ref()
344 .and_then(interp::eval_expr)
345 // Ensure that `val` fits within an `u32` as proptest requires that:
346 .filter(|&value| value <= u128::from(u32::MAX))
347 .map(|value| value as u32);
348
349 if let v @ Some(_) = value {
350 acc.weight = v;
351 } else {
352 error::weight_malformed(ctx, meta)
353 }
354}
355
356//==============================================================================
357// Internals: Filter
358//==============================================================================
359
360/// Parses an explicit value as a strategy.
361/// Valid forms are:
362/// + `#[proptest(filter(<ident>))]`
363/// + `#[proptest(filter = "<expr>")]`
364/// + `#[proptest(filter("<expr>")]`
365fn parse_filter(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
366 if let Some(filter) = match normalize_meta(meta.clone()) {
367 Some(NormMeta::Lit(Lit::Str(lit))) => lit.parse().ok(),
368 Some(NormMeta::Word(ident)) => Some(parse_quote!( #ident )),
369 _ => None,
370 } {
371 acc.filter.push(filter);
372 } else {
373 error::filter_malformed(ctx, meta)
374 }
375}
376
377//==============================================================================
378// Internals: Strategy
379//==============================================================================
380
381/// Parses an explicit value as a strategy.
382/// Valid forms are:
383/// + `#[proptest(regex = "<string>")]`
384/// + `#[proptest(regex("<string>")]`
385/// + `#[proptest(regex(<ident>)]`
386fn parse_regex(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
387 error_if_set(ctx, &acc.regex, &meta);
388
389 if let expr @ Some(_) = match normalize_meta(meta.clone()) {
390 Some(NormMeta::Word(fun)) => Some(function_call(fun)),
391 Some(NormMeta::Lit(lit @ Lit::Str(_))) => Some(lit_to_expr(lit)),
392 _ => None,
393 } {
394 acc.regex = expr;
395 } else {
396 error::regex_malformed(ctx)
397 }
398}
399
400/// Parses an explicit value as a strategy.
401/// Valid forms are:
402/// + `#[proptest(value = <literal>)]`
403/// + `#[proptest(value = "<expr>")]`
404/// + `#[proptest(value("<expr>")]`
405/// + `#[proptest(value(<literal>)]`
406/// + `#[proptest(value(<ident>)]`
407fn parse_value(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
408 parse_strategy_base(ctx, &mut acc.value, meta)
409}
410
411/// Parses an explicit strategy.
412/// Valid forms are:
413/// + `#[proptest(strategy = <literal>)]`
414/// + `#[proptest(strategy = "<expr>")]`
415/// + `#[proptest(strategy("<expr>")]`
416/// + `#[proptest(strategy(<literal>)]`
417/// + `#[proptest(strategy(<ident>)]`
418fn parse_strategy(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
419 parse_strategy_base(ctx, &mut acc.strategy, meta)
420}
421
422/// Parses an explicit strategy. This is a helper.
423/// Valid forms are:
424/// + `#[proptest(<meta.name()> = <literal>)]`
425/// + `#[proptest(<meta.name()> = "<expr>")]`
426/// + `#[proptest(<meta.name()>("<expr>")]`
427/// + `#[proptest(<meta.name()>(<literal>)]`
428/// + `#[proptest(<meta.name()>(<ident>)]`
429fn parse_strategy_base(ctx: Ctx, loc: &mut Option<Expr>, meta: &Meta) {
430 error_if_set(ctx, &loc, &meta);
431
432 if let expr @ Some(_) = match normalize_meta(meta.clone()) {
433 Some(NormMeta::Word(fun)) => Some(function_call(fun)),
434 Some(NormMeta::Lit(lit)) => extract_expr(lit),
435 _ => None,
436 } {
437 *loc = expr;
438 } else {
439 error::strategy_malformed(ctx, meta)
440 }
441}
442
443/// Combines any parsed explicit strategy, value, and regex into a single
444/// value and fails if both an explicit strategy / value / regex was set.
445/// Only one of them can be set, or none.
446fn parse_strat_mode(
447 ctx: Ctx,
448 strat: Option<Expr>,
449 value: Option<Expr>,
450 regex: Option<Expr>,
451) -> DeriveResult<StratMode> {
452 Ok(match (strat, value, regex) {
453 (None, None, None) => StratMode::Arbitrary,
454 (None, None, Some(re)) => StratMode::Regex(re),
455 (None, Some(vl), None) => StratMode::Value(vl),
456 (Some(st), None, None) => StratMode::Strategy(st),
457 _ => error::overspecified_strat(ctx)?,
458 })
459}
460
461//==============================================================================
462// Internals: Parameters
463//==============================================================================
464
465/// Combines a potentially set `params` and `no_params` into a single value
466/// and fails if both have been set. Only one of them can be set, or none.
467fn parse_params_mode(
468 ctx: Ctx,
469 no_params: Option<()>,
470 ty_params: Option<Type>,
471) -> DeriveResult<ParamsMode> {
472 Ok(match (no_params, ty_params) {
473 (None, None) => ParamsMode::Passthrough,
474 (None, Some(ty)) => ParamsMode::Specified(ty),
475 (Some(_), None) => ParamsMode::Default,
476 (Some(_), Some(_)) => error::overspecified_param(ctx)?,
477 })
478}
479
480/// Parses an explicit Parameters type.
481///
482/// Valid forms are:
483/// + `#[proptest(params(<type>)]`
484/// + `#[proptest(params("<type>")]`
485/// + `#[proptest(params = "<type>"]`
486///
487/// The latter form is required for more complex types.
488fn parse_params(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
489 error_if_set(ctx, &acc.params, &meta);
490
491 let typ = match normalize_meta(meta) {
492 // Form is: `#[proptest(params(<type>)]`.
493 Some(NormMeta::Word(ident)) => Some(ident_to_type(ident)),
494 // Form is: `#[proptest(params = "<type>"]` or,
495 // Form is: `#[proptest(params("<type>")]`..
496 Some(NormMeta::Lit(Lit::Str(lit))) => lit.parse().ok(),
497 _ => None,
498 };
499
500 if let typ @ Some(_) = typ {
501 acc.params = typ;
502 } else {
503 error::param_malformed(ctx)
504 }
505}
506
507/// Parses an order to use the default Parameters type and value.
508/// Valid forms are:
509/// + `#[proptest(no_params)]`
510fn parse_no_params(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
511 parse_bare_modifier(
512 ctx,
513 &mut acc.no_params,
514 meta,
515 error::no_params_malformed,
516 )
517}
518
519//==============================================================================
520// Internals: Utilities
521//==============================================================================
522
523/// Parses a bare attribute of the form `#[proptest(<attr>)]` and sets `loc`.
524fn parse_bare_modifier(
525 ctx: Ctx,
526 loc: &mut Option<()>,
527 meta: Meta,
528 malformed: fn(Ctx),
529) {
530 error_if_set(ctx, loc, &meta);
531
532 if let Some(NormMeta::Plain) = normalize_meta(meta) {
533 *loc = Some(());
534 } else {
535 malformed(ctx);
536 }
537}
538
539/// Emits a "set again" error iff the given option `.is_some()`.
540fn error_if_set<T>(ctx: Ctx, loc: &Option<T>, meta: &Meta) {
541 if loc.is_some() {
542 error::set_again(ctx, meta)
543 }
544}
545
546/// Constructs a type out of an identifier.
547fn ident_to_type(ident: Ident) -> Type {
548 Type::Path(syn::TypePath {
549 qself: None,
550 path: ident.into(),
551 })
552}
553
554/// Extract a `lit` in `NormMeta::Lit(<lit>)`.
555fn extract_lit(meta: NormMeta) -> Option<Lit> {
556 if let NormMeta::Lit(lit) = meta {
557 Some(lit)
558 } else {
559 None
560 }
561}
562
563/// Extract expression out of literal if possible.
564fn extract_expr(lit: Lit) -> Option<Expr> {
565 match lit {
566 Lit::Str(lit) => lit.parse().ok(),
567 lit @ Lit::Int(_) => Some(lit_to_expr(lit)),
568 // TODO(centril): generalize to other literals, e.g. floats
569 _ => None,
570 }
571}
572
573/// Construct an expression from a literal.
574fn lit_to_expr(lit: Lit) -> Expr {
575 syn::ExprLit { attrs: vec![], lit }.into()
576}
577
578/// Construct a function call expression for an identifier.
579fn function_call(fun: Ident) -> Expr {
580 parse_quote!( #fun() )
581}
582
583/// Normalized `Meta` into all the forms we will possibly accept.
584#[derive(Debug)]
585enum NormMeta {
586 /// Accepts: `#[proptest(<word>)]`
587 Plain,
588 /// Accepts: `#[proptest(<word> = <lit>)]` and `#[proptest(<word>(<lit>))]`
589 Lit(Lit),
590 /// Accepts: `#[proptest(<word>(<word>))`.
591 Word(Ident),
592}
593
594/// Normalize a `meta: Meta` into the forms accepted in `#[proptest(<meta>)]`.
595fn normalize_meta(meta: Meta) -> Option<NormMeta> {
596 match meta {
597 Meta::Path(_) => Some(NormMeta::Plain),
598 Meta::NameValue(nv) => match nv.value {
599 Expr::Lit(elit) => Some(NormMeta::Lit(elit.lit)),
600 _ => None,
601 }
602 Meta::List(ml) => {
603 let mut output: Option<NormMeta> = None;
604
605 if let Ok(lit) = syn::parse2(ml.tokens.clone()) {
606 output = Some(NormMeta::Lit(lit));
607 } else if let Ok(ident) = syn::parse2(ml.tokens.clone()) {
608 output = Some(NormMeta::Word(ident));
609 }
610
611 output
612 }
613 }
614}