diff --git a/googletest/src/matchers/matches_pattern.rs b/googletest/src/matchers/matches_pattern.rs index 0bfb2afd..b1d9cdb6 100644 --- a/googletest/src/matchers/matches_pattern.rs +++ b/googletest/src/matchers/matches_pattern.rs @@ -372,6 +372,18 @@ pub mod internal { } mod compile_fail_tests { + /// ```compile_fail + /// use ::googletest::prelude::*; + /// #[derive(Debug)] + /// struct Foo { a: u32 } + /// impl Foo { + /// fn b() {} + /// } + /// let actual = Foo { a: 1 }; + /// verify_that!(actual, matches_pattern!(Foo { a: eq(&1), b(): _ })); + /// ``` + fn _underscore_unsupported_for_methods() {} + /// ```compile_fail /// use ::googletest::prelude::*; /// #[derive(Debug)] diff --git a/googletest/tests/matches_pattern_test.rs b/googletest/tests/matches_pattern_test.rs index 1e45941a..39002433 100644 --- a/googletest/tests/matches_pattern_test.rs +++ b/googletest/tests/matches_pattern_test.rs @@ -150,6 +150,23 @@ fn matches_struct_containing_non_copy_field_binding_mode() -> Result<()> { verify_that!(actual, matches_pattern!(AStruct { a_string: eq("123") })) } +#[test] +fn matches_struct_with_interleaved_underscore() -> Result<()> { + #[derive(Debug)] + struct NonCopy; + #[allow(dead_code)] + #[derive(Debug)] + struct AStruct { + a: u32, + b: NonCopy, + c: u32, + } + let actual = AStruct { a: 1, b: NonCopy, c: 3 }; + + verify_that!(actual, matches_pattern!(&AStruct { a: eq(1), b: _, c: eq(3) }))?; + verify_that!(actual, matches_pattern!(AStruct { a: eq(&1), b: _, c: eq(&3) })) +} + #[test] fn has_correct_assertion_failure_message_for_single_field() -> Result<()> { #[derive(Debug)] diff --git a/googletest_macro/src/matches_pattern.rs b/googletest_macro/src/matches_pattern.rs index d1950079..7b24c6f4 100644 --- a/googletest_macro/src/matches_pattern.rs +++ b/googletest_macro/src/matches_pattern.rs @@ -204,21 +204,32 @@ impl Parse for FieldOrMethod { } /// Either field or method call matcher. E.g.: -/// * `field: starts_with("something")` +/// * `field: starts_with("something")` or `field: _` /// * `property(arg1, arg2): starts_with("something") struct FieldOrMethodPattern { ref_token: Option, field_or_method: FieldOrMethod, - matcher: Expr, + /// When `None`, it represents `_` which matches anything, meaning we should + /// ignore it. + matcher: Option, } impl Parse for FieldOrMethodPattern { fn parse(input: ParseStream) -> syn::Result { - Ok(FieldOrMethodPattern { - field_or_method: input.parse()?, - ref_token: input.parse()?, - matcher: input.parse()?, - }) + let field_or_method: FieldOrMethod = input.parse()?; + let underscore = input.parse::>()?; + match underscore { + Some(underscore) if matches!(field_or_method, FieldOrMethod::Method(_)) => compile_err( + underscore.spans[0], + "Don't match a method call against `_`. Just omit it instead.", + ), + Some(_) => Ok(FieldOrMethodPattern { field_or_method, ref_token: None, matcher: None }), + None => Ok(FieldOrMethodPattern { + field_or_method, + ref_token: input.parse()?, + matcher: Some(input.parse()?), + }), + } } } @@ -231,13 +242,17 @@ fn parse_braced_pattern_args( let mut field_names = vec![]; let field_patterns: Vec = patterns .into_iter() - .map(|FieldOrMethodPattern { ref_token, field_or_method, matcher }| match field_or_method { - FieldOrMethod::Field(ident) => { - field_names.push(ident.clone()); - quote! { field!(#struct_name . #ident, #ref_token #matcher) } - } - FieldOrMethod::Method(call) => { - quote! { property!(#struct_name . #call, #ref_token #matcher) } + .filter_map(|FieldOrMethodPattern { ref_token, field_or_method, matcher }| { + match field_or_method { + FieldOrMethod::Field(ident) => { + field_names.push(ident.clone()); + matcher.map(|matcher| { + quote! { field!(#struct_name . #ident, #ref_token #matcher) } + }) + } + FieldOrMethod::Method(call) => { + Some(quote! { property!(#struct_name . #call, #ref_token #matcher) }) + } } }) .collect();