mod nodes;
use crate::AssertStruct;
use crate::pattern::{
ComparisonOp, FieldAssertion, FieldOperation, Pattern, PatternClosure, PatternComparison,
PatternEnum, PatternMap, PatternRange, PatternSet, PatternSimple, PatternSlice, PatternString,
PatternStruct, PatternTuple, PatternWildcard, TupleElement,
};
#[cfg(feature = "regex")]
use crate::pattern::{PatternLike, PatternRegex};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use std::collections::HashSet;
use syn::{Token, punctuated::Punctuated, spanned::Spanned};
use nodes::{expand_pattern_node_ident, generate_pattern_nodes};
pub fn expand(assert: &AssertStruct) -> TokenStream {
let value = &assert.value;
let pattern = &assert.pattern;
let mut node_defs = Vec::new();
let root_ref = generate_pattern_nodes(pattern, &mut node_defs, None);
let node_constants: Vec<TokenStream> = node_defs
.iter()
.map(|(id, def)| {
let ident = Ident::new(&format!("__PATTERN_NODE_{}", id), Span::call_site());
quote! {
static #ident: ::assert_struct::__macro_support::PatternNode = #def;
}
})
.collect();
let assertion = expand_pattern_assertion("e! { #value }, pattern);
quote! {
{
#[allow(unused_assignments, clippy::neg_cmp_op_on_partial_ord, clippy::op_ref, clippy::zero_prefixed_literal, clippy::bool_comparison, clippy::redundant_pattern_matching, clippy::useless_asref)]
let __assert_struct_result = {
use std::convert::AsRef;
#(#node_constants)*
const __PATTERN_TREE: &::assert_struct::__macro_support::PatternNode = &#root_ref;
let mut __report = ::assert_struct::__macro_support::ErrorReport::new(
::std::env!("CARGO_MANIFEST_DIR"),
::std::file!(),
);
#assertion
if !__report.is_empty() {
panic!("{}", __report);
}
};
__assert_struct_result
}
}
}
fn expand_pattern_assertion(value_expr: &TokenStream, pattern: &Pattern) -> TokenStream {
match pattern {
Pattern::Simple(simple_pattern) => expand_simple_assertion(value_expr, simple_pattern),
Pattern::String(string_pattern) => expand_string_assertion(value_expr, string_pattern),
Pattern::Struct(struct_pattern) => expand_struct_assertion(value_expr, struct_pattern),
Pattern::Comparison(comparison_pattern) => {
expand_comparison_assertion(value_expr, comparison_pattern)
}
Pattern::Enum(enum_pattern) => {
expand_enum_assertion(value_expr, enum_pattern)
}
Pattern::Tuple(tuple_pattern) => {
expand_tuple_assertion(value_expr, tuple_pattern)
}
Pattern::Wildcard(_) => {
quote! {}
}
Pattern::Range(range_pattern) => {
expand_range_assertion(value_expr, range_pattern)
}
Pattern::Slice(slice_pattern) => {
expand_slice_assertion(value_expr, slice_pattern)
}
#[cfg(feature = "regex")]
Pattern::Regex(regex_pattern) => {
expand_regex_assertion(value_expr, regex_pattern)
}
#[cfg(feature = "regex")]
Pattern::Like(like_pattern) => {
expand_like_assertion(value_expr, like_pattern)
}
Pattern::Closure(closure_pattern) => {
expand_closure_assertion(value_expr, closure_pattern)
}
Pattern::Map(map_pattern) => {
expand_map_assertion(value_expr, map_pattern)
}
Pattern::Set(set_pattern) => {
expand_set_assertion(value_expr, set_pattern)
}
}
}
fn expand_struct_assertion(value_expr: &TokenStream, pattern: &PatternStruct) -> TokenStream {
let struct_path = &pattern.path;
let fields = &pattern.fields;
let rest = pattern.rest;
let Some(struct_path) = struct_path.as_ref() else {
return expand_struct_wildcard_assertion(value_expr, fields);
};
let mut unique_field_names = HashSet::new();
let field_names: Vec<_> = fields
.iter()
.filter_map(|f| {
let field_name = f.operations.root_field_name();
if unique_field_names.insert(field_name.clone()) {
Some(field_name)
} else {
None
}
})
.collect();
let rest_pattern = if rest {
quote! { , .. }
} else {
quote! {}
};
let field_assertions: Vec<_> = fields
.iter()
.map(|f| {
let field_name = f.operations.root_field_name();
let assertion = expand_field_assertion("e! { #field_name }, f);
if let Some(span) = f.pattern.span() {
quote_spanned! {span=> #assertion }
} else {
assertion
}
})
.collect();
let span = struct_path.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=>
#[allow(unreachable_patterns)]
match &#value_expr {
#struct_path { #(#field_names),* #rest_pattern } => {
#(#field_assertions)*
},
_ => {
#error_push
}
}
}
}
fn expand_struct_wildcard_assertion(
value_expr: &TokenStream,
fields: &Punctuated<FieldAssertion, Token![,]>,
) -> TokenStream {
let field_assertions: Vec<_> = fields
.iter()
.map(|f| {
let field_name = f.operations.root_field_name();
let field_pattern = &f.pattern;
let field_operations = &f.operations;
let base_field_access = quote! { (#value_expr).#field_name };
let expr = if let Some(tail_ops) = field_operations.tail_operations() {
let base_ref = quote! { &#base_field_access };
apply_field_operations(&base_ref, &tail_ops)
} else {
quote! { &#base_field_access }
};
expand_pattern_assertion(&expr, field_pattern)
})
.collect();
quote! {
#(#field_assertions)*
}
}
fn expand_field_assertion(base: &TokenStream, field_assertion: &FieldAssertion) -> TokenStream {
let field_operations = &field_assertion.operations;
let field_pattern = &field_assertion.pattern;
let expr = if let Some(tail_ops) = field_operations.tail_operations() {
apply_field_operations(base, &tail_ops)
} else {
base.clone()
};
expand_pattern_assertion(&expr, field_pattern)
}
fn apply_field_operations(base_expr: &TokenStream, operation: &FieldOperation) -> TokenStream {
match operation {
FieldOperation::Deref { count, span } => {
let mut expr = base_expr.clone();
let total_count = count + 1;
for _ in 0..total_count {
expr = quote_spanned! { *span=> *#expr };
}
expr
}
FieldOperation::Method { name, args, span } => {
if args.is_empty() {
quote_spanned! { *span=> #base_expr.#name() }
} else {
quote_spanned! { *span=> #base_expr.#name(#(#args),*) }
}
}
FieldOperation::Await { span } => {
quote_spanned! { *span=> #base_expr.await }
}
FieldOperation::NamedField { name, span } => {
quote_spanned! { *span=> #base_expr.#name }
}
FieldOperation::UnnamedField { index, span } => {
let idx = syn::Index::from(*index);
quote_spanned! { *span=> #base_expr.#idx }
}
FieldOperation::Index { index, span } => {
quote_spanned! { *span=> #base_expr[#index] }
}
FieldOperation::Chained { operations, .. } => {
let mut expr = base_expr.clone();
for op in operations {
expr = apply_field_operations(&expr, op);
}
expr
}
}
}
fn process_tuple_elements(
elements: &[TupleElement],
prefix: &str,
) -> (Vec<TokenStream>, Vec<TokenStream>) {
let mut match_patterns = Vec::new();
let mut assertions = Vec::new();
for (i, tuple_element) in elements.iter().enumerate() {
match tuple_element {
TupleElement::Positional(pattern) => {
match &**pattern {
Pattern::Wildcard(PatternWildcard { .. }) => {
match_patterns.push(quote! { _ });
}
_ => {
let name = quote::format_ident!("{}{}", prefix, i);
match_patterns.push(quote! { #name });
let assertion = expand_pattern_assertion("e! { #name }, pattern);
assertions.push(assertion);
}
}
}
TupleElement::Indexed(boxed_elem) => {
let pattern = &boxed_elem.pattern;
match pattern {
Pattern::Wildcard(PatternWildcard { .. }) => {
match_patterns.push(quote! { _ });
}
_ => {
let name = quote::format_ident!("{}{}", prefix, i);
match_patterns.push(quote! { #name });
let assertion = expand_field_assertion("e! { #name }, boxed_elem);
assertions.push(assertion);
}
}
}
}
}
(match_patterns, assertions)
}
fn expand_tuple_assertion(value_expr: &TokenStream, pattern: &PatternTuple) -> TokenStream {
let elements = &pattern.elements;
let (match_patterns, element_assertions) = process_tuple_elements(elements, "__tuple_elem_");
quote! {
#[allow(unreachable_patterns)]
match #value_expr {
(#(#match_patterns),*) => {
#(#element_assertions)*
},
_ => unreachable!("Plain tuple match should always succeed"),
}
}
}
fn expand_comparison_assertion(
value_expr: &TokenStream,
pattern: &PatternComparison,
) -> TokenStream {
let op = &pattern.op;
let expected = &pattern.expr;
let span = expected.span();
let comparison = {
match &pattern.op {
ComparisonOp::Less(_) => quote_spanned! {span=> (#value_expr).lt(&(#expected)) },
ComparisonOp::LessEqual(_) => quote_spanned! {span=> (#value_expr).le(&(#expected)) },
ComparisonOp::Greater(_) => quote_spanned! {span=> (#value_expr).gt(&(#expected)) },
ComparisonOp::GreaterEqual(_) => {
quote_spanned! {span=> (#value_expr).ge(&(#expected)) }
}
ComparisonOp::Equal(_) => quote_spanned! {span=> (#value_expr).eq(&(#expected)) },
ComparisonOp::NotEqual(_) => quote_spanned! {span=> (#value_expr).ne(&(#expected)) },
}
};
let expected_value = if matches!(op, ComparisonOp::Equal(_)) {
let expected_str = quote! { #expected }.to_string();
quote!(Some(#expected_str.to_string()))
} else {
quote!(None)
};
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
expected_value,
pattern.node_id,
);
quote_spanned! {span=>
#[allow(clippy::nonminimal_bool)]
if !(#comparison) {
#error_push
}
}
}
fn expand_enum_assertion(value_expr: &TokenStream, pattern: &PatternEnum) -> TokenStream {
let variant_path = &pattern.path;
let elements = &pattern.elements;
let span = variant_path.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
quote!(None),
pattern.node_id,
);
if elements.is_empty() {
quote_spanned! {span=>
if !matches!(#value_expr, #variant_path) {
#error_push
}
}
} else {
let (match_patterns, element_assertions) = process_tuple_elements(elements, "__elem_");
quote_spanned! {span=>
#[allow(unreachable_patterns)]
match &#value_expr {
#variant_path(#(#match_patterns),*) => {
#(#element_assertions)*
},
_ => {
#error_push
}
}
}
}
}
fn expand_range_assertion(value_expr: &TokenStream, pattern: &PatternRange) -> TokenStream {
let range = &pattern.expr;
let span = range.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=>
match &#value_expr {
#range => {},
_ => {
#error_push
}
}
}
}
fn expand_string_assertion(value_expr: &TokenStream, pattern: &PatternString) -> TokenStream {
let lit = &pattern.lit;
let span = lit.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", actual)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=> {
let __assert_struct_tmp = &#value_expr;
let actual = (*__assert_struct_tmp).as_ref();
if !matches!(actual, #lit) {
#error_push
}
}}
}
fn expand_simple_assertion(actual: &TokenStream, pattern: &PatternSimple) -> TokenStream {
let expected = &pattern.expr;
let span = expected.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #actual)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=>
if !matches!(#actual, #expected) {
#error_push
}
}
}
fn expand_slice_assertion(value_expr: &TokenStream, pattern: &PatternSlice) -> TokenStream {
let mut pattern_parts = Vec::new();
let mut bindings_and_assertions = Vec::new();
for (i, elem) in pattern.elements.iter().enumerate() {
match elem {
Pattern::Range(PatternRange {
expr: syn::Expr::Range(r),
..
}) if r.start.is_none() && r.end.is_none() => {
pattern_parts.push(quote! { .. });
}
Pattern::Wildcard(PatternWildcard { .. }) => {
pattern_parts.push(quote! { _ });
}
_ => {
let binding = quote::format_ident!("__elem_{}", i);
pattern_parts.push(quote! { #binding });
let assertion = expand_pattern_assertion("e! { #binding }, elem);
bindings_and_assertions.push(assertion);
}
}
}
let slice_expr = quote! { (#value_expr).as_slice() };
let error_push = generate_error_push(
proc_macro2::Span::call_site(),
quote!(format!("{:?}", &#value_expr)),
quote!(None),
pattern.node_id,
);
quote! {
match #slice_expr {
[#(#pattern_parts),*] => {
#(#bindings_and_assertions)*
}
_ => {
#error_push
}
}
}
}
#[cfg(feature = "regex")]
fn expand_regex_assertion(value_expr: &TokenStream, pattern: &PatternRegex) -> TokenStream {
let pattern_str = &pattern.pattern;
let span = pattern.span;
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=>
{
use ::assert_struct::Like;
let re = ::assert_struct::__macro_support::Regex::new(#pattern_str)
.expect(concat!("Invalid regex pattern: ", #pattern_str));
if !#value_expr.like(&re) {
#error_push
}
}
}
}
#[cfg(feature = "regex")]
fn expand_like_assertion(value_expr: &TokenStream, pattern: &PatternLike) -> TokenStream {
let pattern_expr = &pattern.expr;
let span = pattern_expr.span();
let actual_value = quote!(format!("{:?}", #value_expr));
let error_push = generate_error_push(span, actual_value, quote!(None), pattern.node_id);
quote_spanned! {span=>
{
use ::assert_struct::Like;
if !#value_expr.like(&#pattern_expr) {
#error_push
}
}
}
}
fn expand_closure_assertion(value_expr: &TokenStream, pattern: &PatternClosure) -> TokenStream {
let closure = &pattern.closure;
let span = closure.span();
let error_push = generate_error_push(
span,
quote!(format!("{:?}", #value_expr)),
quote!(None),
pattern.node_id,
);
quote_spanned! {span=>
{
if !::assert_struct::__macro_support::check_closure_condition(#value_expr, #closure) {
#error_push
}
}
}
}
fn expand_map_assertion(value_expr: &TokenStream, pattern: &PatternMap) -> TokenStream {
let entries = &pattern.entries;
let rest = pattern.rest;
let map_span = entries
.first()
.map(|(key, _)| key.span())
.unwrap_or_else(proc_macro2::Span::call_site);
let len_check = if !rest {
let expected_len = entries.len();
let error_push = generate_error_push(
map_span,
quote!(format!("map with {} entries", (#value_expr).len())),
quote!(Some(format!("{} entries", #expected_len))),
pattern.node_id,
);
quote_spanned! {map_span=>
if (#value_expr).len() != #expected_len {
#error_push
}
}
} else {
quote! {}
};
let key_value_assertions: Vec<TokenStream> = entries
.iter()
.map(|(key, value_pattern)| {
let key_str = quote! { #key }.to_string();
let span = key.span();
let pattern_assertion =
expand_pattern_assertion("e! { __map_value }, value_pattern);
let missing_key_error = generate_error_push(
span,
quote!("missing key".to_string()),
quote!(Some(format!("key present: {}", #key_str))),
pattern.node_id,
);
let get_expr = if matches!(
key,
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(_),
..
})
) {
quote_spanned! {span=> (#value_expr).get(&(#key).to_string()) }
} else {
quote_spanned! {span=> (#value_expr).get(&#key) }
};
quote_spanned! {span=>
match #get_expr {
Some(__map_value) => {
#pattern_assertion
}
None => {
#missing_key_error
}
}
}
})
.collect();
quote! {
#len_check
#(#key_value_assertions)*
}
}
fn expand_set_assertion(value_expr: &TokenStream, pattern: &PatternSet) -> TokenStream {
let elements = &pattern.elements;
let rest = pattern.rest;
let node_ident = expand_pattern_node_ident(pattern.node_id);
let pred_names: Vec<_> = (0..elements.len())
.map(|i| quote::format_ident!("__set_pred_{}", i))
.collect();
let pred_defs: Vec<TokenStream> = elements
.iter()
.zip(pred_names.iter())
.map(|(elem, name)| {
let assertion = expand_pattern_assertion("e! { __set_elem }, elem);
quote! {
let #name = |__set_idx: usize| -> bool {
let __set_elem = __set_coll[__set_idx];
#[allow(unused_mut)]
let mut __report = ::assert_struct::__macro_support::ErrorReport::new_probe();
#assertion
__report.is_empty()
};
}
})
.collect();
quote! {
{
let __set_coll: ::std::vec::Vec<_> = (&(#value_expr)).into_iter().collect();
#(#pred_defs)*
let __set_preds: &[&dyn ::std::ops::Fn(usize) -> bool] = &[#(&#pred_names),*];
::assert_struct::__macro_support::set_match(
__set_coll.len(),
#rest,
__set_preds,
&mut __report,
&#node_ident,
);
}
}
}
fn generate_error_push(
span: proc_macro2::Span,
actual_value: TokenStream,
expected_value: TokenStream,
node_id: usize,
) -> TokenStream {
let node_ident = expand_pattern_node_ident(node_id);
quote_spanned! {span=>
__report.push(&#node_ident, #actual_value, #expected_value);
}
}