use quote::ToTokens;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{self, Attribute, Expr, Ident, Lit, Meta, Type};
use crate::error::{self, Ctx, DeriveResult};
use crate::interp;
use crate::util;
#[derive(Clone)]
pub struct ParsedAttributes {
pub skip: bool,
pub weight: Option<u32>,
pub params: ParamsMode,
pub strategy: StratMode,
pub filter: Vec<syn::Expr>,
pub no_bound: bool,
}
#[derive(Clone)]
pub enum StratMode {
Arbitrary,
Value(Expr),
Strategy(Expr),
Regex(Expr),
}
#[derive(Clone)]
pub enum ParamsMode {
Passthrough,
Default,
Specified(Type),
}
impl ParamsMode {
pub fn is_set(&self) -> bool {
if let ParamsMode::Passthrough = *self {
false
} else {
true
}
}
pub fn into_option(self) -> Option<Option<Type>> {
use self::ParamsMode::*;
match self {
Passthrough => None,
Specified(ty) => Some(Some(ty)),
Default => Some(None),
}
}
}
impl StratMode {
pub fn is_set(&self) -> bool {
if let StratMode::Arbitrary = self {
false
} else {
true
}
}
}
pub fn parse_attributes(
ctx: Ctx,
attrs: &[Attribute],
) -> DeriveResult<ParsedAttributes> {
let attrs = parse_attributes_base(ctx, attrs)?;
if attrs.no_bound {
error::no_bound_set_on_non_tyvar(ctx);
}
Ok(attrs)
}
pub fn parse_top_attributes(
ctx: Ctx,
attrs: &[Attribute],
) -> DeriveResult<ParsedAttributes> {
parse_attributes_base(ctx, attrs)
}
pub fn has_no_bound(ctx: Ctx, attrs: &[Attribute]) -> DeriveResult<bool> {
let attrs = parse_attributes_base(ctx, attrs)?;
error::if_anything_specified(ctx, &attrs, error::TY_VAR);
Ok(attrs.no_bound)
}
fn parse_attributes_base(
ctx: Ctx,
attrs: &[Attribute],
) -> DeriveResult<ParsedAttributes> {
let acc = parse_accumulate(ctx, attrs);
Ok(ParsedAttributes {
skip: acc.skip.is_some(),
weight: acc.weight,
filter: acc.filter,
params: parse_params_mode(ctx, acc.no_params, acc.params)?,
strategy: parse_strat_mode(ctx, acc.strategy, acc.value, acc.regex)?,
no_bound: acc.no_bound.is_some(),
})
}
#[derive(Default)]
struct ParseAcc {
skip: Option<()>,
weight: Option<u32>,
no_params: Option<()>,
params: Option<Type>,
strategy: Option<Expr>,
value: Option<Expr>,
regex: Option<Expr>,
filter: Vec<Expr>,
no_bound: Option<()>,
}
fn parse_accumulate(ctx: Ctx, attrs: &[Attribute]) -> ParseAcc {
let mut state = ParseAcc::default();
for attr in attrs {
if is_proptest_attr(&attr) {
state = extract_modifiers(ctx, &attr)
.into_iter()
.fold(state, |state, meta| dispatch_attribute(ctx, state, meta))
}
}
state
}
fn is_proptest_attr(attr: &Attribute) -> bool {
util::eq_simple_path("proptest", attr.path())
}
fn extract_modifiers(ctx: Ctx, attr: &Attribute) -> Vec<Meta> {
if !is_outer_attr(&attr) {
error::inner_attr(ctx);
}
match &attr.meta {
Meta::List(list) => {
if syn::parse2::<Lit>(list.tokens.clone()).is_ok() {
error::immediate_literals(ctx);
} else {
let parser = Punctuated::<Meta, Token![,]>::parse_separated_nonempty;
let metas = parser.parse2(list.tokens.clone()).unwrap();
return metas.into_iter().collect();
}
}
Meta::Path(_) => error::bare_proptest_attr(ctx),
Meta::NameValue(_) => error::literal_set_proptest(ctx),
}
vec![]
}
fn is_outer_attr(attr: &Attribute) -> bool {
syn::AttrStyle::Outer == attr.style
}
fn dispatch_attribute(ctx: Ctx, mut acc: ParseAcc, meta: Meta) -> ParseAcc {
let path = meta.path();
if let Some(name) = path.get_ident().map(ToString::to_string) {
match name.as_ref() {
"skip" => parse_skip(ctx, &mut acc, meta),
"w" | "weight" => parse_weight(ctx, &mut acc, &meta),
"no_params" => parse_no_params(ctx, &mut acc, meta),
"params" => parse_params(ctx, &mut acc, meta),
"strategy" => parse_strategy(ctx, &mut acc, &meta),
"value" => parse_value(ctx, &mut acc, &meta),
"regex" => parse_regex(ctx, &mut acc, &meta),
"filter" => parse_filter(ctx, &mut acc, &meta),
"no_bound" => parse_no_bound(ctx, &mut acc, meta),
name => dispatch_unknown_mod(ctx, name),
}
} else {
error::unkown_modifier(ctx, &path.into_token_stream().to_string());
}
acc
}
fn dispatch_unknown_mod(ctx: Ctx, name: &str) {
match name {
"no_bounds" => error::did_you_mean(ctx, name, "no_bound"),
"weights" | "weighted" => error::did_you_mean(ctx, name, "weight"),
"strat" | "strategies" => error::did_you_mean(ctx, name, "strategy"),
"values" | "valued" | "fix" | "fixed" => {
error::did_you_mean(ctx, name, "value")
}
"regexes" | "regexp" | "re" => error::did_you_mean(ctx, name, "regex"),
"param" | "parameters" => error::did_you_mean(ctx, name, "params"),
"no_param" | "no_parameters" => {
error::did_you_mean(ctx, name, "no_params")
}
name => error::unkown_modifier(ctx, name),
}
}
fn parse_no_bound(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
parse_bare_modifier(ctx, &mut acc.no_bound, meta, error::no_bound_malformed)
}
fn parse_skip(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
parse_bare_modifier(ctx, &mut acc.skip, meta, error::skip_malformed)
}
fn parse_weight(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
use std::u32;
error_if_set(ctx, &acc.weight, &meta);
let value = normalize_meta(meta.clone())
.and_then(extract_lit)
.and_then(extract_expr)
.as_ref()
.and_then(interp::eval_expr)
.filter(|&value| value <= u128::from(u32::MAX))
.map(|value| value as u32);
if let v @ Some(_) = value {
acc.weight = v;
} else {
error::weight_malformed(ctx, meta)
}
}
fn parse_filter(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
if let Some(filter) = match normalize_meta(meta.clone()) {
Some(NormMeta::Lit(Lit::Str(lit))) => lit.parse().ok(),
Some(NormMeta::Word(ident)) => Some(parse_quote!( #ident )),
_ => None,
} {
acc.filter.push(filter);
} else {
error::filter_malformed(ctx, meta)
}
}
fn parse_regex(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
error_if_set(ctx, &acc.regex, &meta);
if let expr @ Some(_) = match normalize_meta(meta.clone()) {
Some(NormMeta::Word(fun)) => Some(function_call(fun)),
Some(NormMeta::Lit(lit @ Lit::Str(_))) => Some(lit_to_expr(lit)),
_ => None,
} {
acc.regex = expr;
} else {
error::regex_malformed(ctx)
}
}
fn parse_value(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
parse_strategy_base(ctx, &mut acc.value, meta)
}
fn parse_strategy(ctx: Ctx, acc: &mut ParseAcc, meta: &Meta) {
parse_strategy_base(ctx, &mut acc.strategy, meta)
}
fn parse_strategy_base(ctx: Ctx, loc: &mut Option<Expr>, meta: &Meta) {
error_if_set(ctx, &loc, &meta);
if let expr @ Some(_) = match normalize_meta(meta.clone()) {
Some(NormMeta::Word(fun)) => Some(function_call(fun)),
Some(NormMeta::Lit(lit)) => extract_expr(lit),
_ => None,
} {
*loc = expr;
} else {
error::strategy_malformed(ctx, meta)
}
}
fn parse_strat_mode(
ctx: Ctx,
strat: Option<Expr>,
value: Option<Expr>,
regex: Option<Expr>,
) -> DeriveResult<StratMode> {
Ok(match (strat, value, regex) {
(None, None, None) => StratMode::Arbitrary,
(None, None, Some(re)) => StratMode::Regex(re),
(None, Some(vl), None) => StratMode::Value(vl),
(Some(st), None, None) => StratMode::Strategy(st),
_ => error::overspecified_strat(ctx)?,
})
}
fn parse_params_mode(
ctx: Ctx,
no_params: Option<()>,
ty_params: Option<Type>,
) -> DeriveResult<ParamsMode> {
Ok(match (no_params, ty_params) {
(None, None) => ParamsMode::Passthrough,
(None, Some(ty)) => ParamsMode::Specified(ty),
(Some(_), None) => ParamsMode::Default,
(Some(_), Some(_)) => error::overspecified_param(ctx)?,
})
}
fn parse_params(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
error_if_set(ctx, &acc.params, &meta);
let typ = match normalize_meta(meta) {
Some(NormMeta::Word(ident)) => Some(ident_to_type(ident)),
Some(NormMeta::Lit(Lit::Str(lit))) => lit.parse().ok(),
_ => None,
};
if let typ @ Some(_) = typ {
acc.params = typ;
} else {
error::param_malformed(ctx)
}
}
fn parse_no_params(ctx: Ctx, acc: &mut ParseAcc, meta: Meta) {
parse_bare_modifier(
ctx,
&mut acc.no_params,
meta,
error::no_params_malformed,
)
}
fn parse_bare_modifier(
ctx: Ctx,
loc: &mut Option<()>,
meta: Meta,
malformed: fn(Ctx),
) {
error_if_set(ctx, loc, &meta);
if let Some(NormMeta::Plain) = normalize_meta(meta) {
*loc = Some(());
} else {
malformed(ctx);
}
}
fn error_if_set<T>(ctx: Ctx, loc: &Option<T>, meta: &Meta) {
if loc.is_some() {
error::set_again(ctx, meta)
}
}
fn ident_to_type(ident: Ident) -> Type {
Type::Path(syn::TypePath {
qself: None,
path: ident.into(),
})
}
fn extract_lit(meta: NormMeta) -> Option<Lit> {
if let NormMeta::Lit(lit) = meta {
Some(lit)
} else {
None
}
}
fn extract_expr(lit: Lit) -> Option<Expr> {
match lit {
Lit::Str(lit) => lit.parse().ok(),
lit @ Lit::Int(_) => Some(lit_to_expr(lit)),
_ => None,
}
}
fn lit_to_expr(lit: Lit) -> Expr {
syn::ExprLit { attrs: vec![], lit }.into()
}
fn function_call(fun: Ident) -> Expr {
parse_quote!( #fun() )
}
#[derive(Debug)]
enum NormMeta {
Plain,
Lit(Lit),
Word(Ident),
}
fn normalize_meta(meta: Meta) -> Option<NormMeta> {
match meta {
Meta::Path(_) => Some(NormMeta::Plain),
Meta::NameValue(nv) => match nv.value {
Expr::Lit(elit) => Some(NormMeta::Lit(elit.lit)),
_ => None,
}
Meta::List(ml) => {
let mut output: Option<NormMeta> = None;
if let Ok(lit) = syn::parse2(ml.tokens.clone()) {
output = Some(NormMeta::Lit(lit));
} else if let Ok(ident) = syn::parse2(ml.tokens.clone()) {
output = Some(NormMeta::Word(ident));
}
output
}
}
}