Skip to content

Commit

Permalink
Similar to native match pattern for tuple structs, make `matches_patt…
Browse files Browse the repository at this point in the history
…ern` enforce exhaustive field checks for tuple structs by default, to be suppressed by explicit `..` at the end of the pattern.

This change is not backward-compatible since it enforces field exhaustiveness by default in the absence of a trailing `..` in the pattern.

Fixes #447

PiperOrigin-RevId: 704273542
  • Loading branch information
marcianx authored and copybara-github committed Dec 13, 2024
1 parent 99bcfaa commit dbfddfb
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 9 deletions.
18 changes: 18 additions & 0 deletions googletest/src/matchers/matches_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,15 @@ mod compile_fail_tests {
/// ```
fn _dot_dot_supported_only_at_end_of_struct_pattern() {}

/// ```compile_fail
/// use ::googletest::prelude::*;
/// #[derive(Debug)]
/// struct Foo(u32, u32);
/// let actual = Foo(1, 2);
/// verify_that!(actual, matches_pattern!(Foo(eq(&1), .., )));
/// ```
fn _dot_dot_supported_only_at_end_of_tuple_struct_pattern() {}

/// ```compile_fail
/// use ::googletest::prelude::*;
/// #[derive(Debug)]
Expand All @@ -472,4 +481,13 @@ mod compile_fail_tests {
/// verify_that!(actual, matches_pattern!(Foo::Bar { a: eq(&1) }));
/// ```
fn _unexhaustive_enum_struct_field_check_requires_dot_dot() {}

/// ```compile_fail
/// use ::googletest::prelude::*;
/// #[derive(Debug)]
/// struct Foo(u32, u32, u32);
/// let actual = Foo(1, 2, 3);
/// verify_that!(actual, matches_pattern!(Foo(eq(&1), eq(&2) )));
/// ```
fn _unexhaustive_tuple_struct_field_check_requires_dot_dot() {}
}
22 changes: 22 additions & 0 deletions googletest/tests/matches_pattern_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,28 @@ fn matches_tuple_struct_with_interleaved_underscore() -> Result<()> {
verify_that!(actual, matches_pattern!(AStruct(eq(&1), _, eq(&3))))
}

#[test]
fn matches_tuple_struct_non_exhaustive() -> Result<()> {
#[allow(dead_code)]
#[derive(Debug)]
struct AStruct(i32, u32);
let actual = AStruct(1, 3);

verify_that!(actual, matches_pattern!(&AStruct(_, ..)))?;
verify_that!(actual, matches_pattern!(AStruct(_, ..)))
}

#[test]
fn matches_generic_tuple_struct_exhaustively() -> Result<()> {
#[allow(dead_code)]
#[derive(Debug)]
struct AStruct<T>(T, u32);
let actual = AStruct(1, 3);

verify_that!(actual, matches_pattern!(&AStruct(_, _)))?;
verify_that!(actual, matches_pattern!(AStruct(_, _)))
}

#[test]
fn matches_enum_without_field() -> Result<()> {
#[derive(Debug)]
Expand Down
40 changes: 31 additions & 9 deletions googletest_macro/src/matches_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream, TokenTree};
use quote::quote;
use syn::{
parse::{Parse, ParseStream, Parser as _},
parse_macro_input,
punctuated::Punctuated,
Expr, ExprCall, Pat, Token,
parse_macro_input, Expr, ExprCall, Pat, Token,
};

/// This is an implementation detail of `googletest::matches_pattern!`. It
Expand Down Expand Up @@ -164,22 +162,46 @@ fn parse_tuple_pattern_args(
struct_name: TokenStream,
group_content: TokenStream,
) -> syn::Result<TokenStream> {
let parser = Punctuated::<MaybeTupleFieldPattern, Token![,]>::parse_terminated;
let fields = parser
.parse2(group_content)?
let (patterns, non_exhaustive) =
parse_list_terminated_pattern::<MaybeTupleFieldPattern>.parse2(group_content)?;
let field_count = patterns.len();
let field_patterns = patterns
.into_iter()
.enumerate()
.filter_map(|(index, maybe_pattern)| maybe_pattern.0.map(|pattern| (index, pattern)))
.map(|(index, TupleFieldPattern { ref_token, matcher })| {
let index = syn::Index::from(index);
quote! { googletest::matchers::field!(#struct_name.#index, #ref_token #matcher) }
});
Ok(quote! {

let matcher = quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::is(
stringify!(#struct_name),
all!( #(#fields),* )
all!( #(#field_patterns),* )
)
})
};

// Do an exhaustiveness check only if the pattern doesn't end with `..`.
if non_exhaustive {
Ok(matcher)
} else {
let empty_fields = std::iter::repeat(quote! { _ }).take(field_count);
Ok(quote! {
googletest::matchers::__internal_unstable_do_not_depend_on_these::compile_assert_and_match(
|actual| {
// Exhaustively check that all field names are specified.
match actual {
#struct_name ( #(#empty_fields),* ) => (),
// The pattern below is unreachable if the type is a struct (as opposed to
// an enum). Since the macro can't know which it is, we always include it
// and just tell the compiler not to complain.
#[allow(unreachable_patterns)]
_ => {},
}
},
#matcher)
})
}
}

////////////////////////////////////////////////////////////////////////////////
Expand Down

0 comments on commit dbfddfb

Please sign in to comment.