Skip to content

Commit

Permalink
Improves the detection of base-class hiding properties and fields
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Jan 15, 2023
1 parent aaa1529 commit 600ef6e
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 31 deletions.
38 changes: 22 additions & 16 deletions Src/FluentAssertions/Common/TypeExtensions.cs
Expand Up @@ -178,16 +178,19 @@ public static bool OverridesEquals(this Type type)
/// <returns>
/// Returns <see langword="null"/> if no such property exists.
/// </returns>
public static PropertyInfo FindProperty(this Type type, string propertyName, Type preferredType)
public static PropertyInfo FindProperty(this Type type, string propertyName)
{
List<PropertyInfo> properties =
type.GetProperties(AllInstanceMembersFlag)
.Where(pi => pi.Name == propertyName)
.ToList();
while (type != typeof(object))
{
if (type.GetProperty(propertyName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } property)
{
return property;
}

type = type.BaseType;
}

return properties.Count > 1
? properties.SingleOrDefault(p => p.PropertyType == preferredType)
: properties.SingleOrDefault();
return null;
}

/// <summary>
Expand All @@ -196,16 +199,19 @@ public static PropertyInfo FindProperty(this Type type, string propertyName, Typ
/// <returns>
/// Returns <see langword="null"/> if no such property exists.
/// </returns>
public static FieldInfo FindField(this Type type, string fieldName, Type preferredType)
public static FieldInfo FindField(this Type type, string fieldName)
{
List<FieldInfo> properties =
type.GetFields(AllInstanceMembersFlag)
.Where(pi => pi.Name == fieldName)
.ToList();
while (type != typeof(object))
{
if (type.GetField(fieldName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } field)
{
return field;
}

type = type.BaseType;
}

return properties.Count > 1
? properties.SingleOrDefault(p => p.FieldType == preferredType)
: properties.SingleOrDefault();
return null;
}

public static IEnumerable<MemberInfo> GetNonPrivateMembers(this Type typeToReflect, MemberVisibility visibility)
Expand Down
Expand Up @@ -35,7 +35,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
{
if (expectedMember.Name == expectationMemberName)
{
var member = MemberFactory.Find(subject, subjectMemberName, expectedMember.Type, parent);
var member = MemberFactory.Find(subject, subjectMemberName, parent);
if (member is null)
{
throw new ArgumentException(
Expand Down
Expand Up @@ -50,7 +50,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui

if (path.IsEquivalentTo(expectedMember.PathAndName))
{
var member = MemberFactory.Find(subject, subjectPath.MemberName, expectedMember.Type, parent);
var member = MemberFactory.Find(subject, subjectPath.MemberName, parent);
if (member is null)
{
throw new ArgumentException(
Expand Down
Expand Up @@ -15,13 +15,13 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui

if (config.IncludedProperties != MemberVisibility.None)
{
PropertyInfo propertyInfo = subject.GetType().FindProperty(expectedMember.Name, expectedMember.Type);
PropertyInfo propertyInfo = subject.GetType().FindProperty(expectedMember.Name);
subjectMember = (propertyInfo is not null) && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null;
}

if ((subjectMember is null) && config.IncludedFields != MemberVisibility.None)
{
FieldInfo fieldInfo = subject.GetType().FindField(expectedMember.Name, expectedMember.Type);
FieldInfo fieldInfo = subject.GetType().FindField(expectedMember.Name);
subjectMember = (fieldInfo is not null) ? new Field(fieldInfo, parent) : null;
}

Expand Down
Expand Up @@ -10,13 +10,13 @@ internal class TryMatchByNameRule : IMemberMatchingRule
{
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions config)
{
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name, expectedMember.Type);
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name);
if ((property is not null) && !property.IsIndexer())
{
return new Property(property, parent);
}

FieldInfo field = subject.GetType().FindField(expectedMember.Name, expectedMember.Type);
FieldInfo field = subject.GetType().FindField(expectedMember.Name);
return (field is not null) ? new Field(field, parent) : null;
}

Expand Down
6 changes: 3 additions & 3 deletions Src/FluentAssertions/Equivalency/MemberFactory.cs
Expand Up @@ -21,15 +21,15 @@ public static IMember Create(MemberInfo memberInfo, INode parent)
throw new NotSupportedException($"Don't know how to deal with a {memberInfo.MemberType}");
}

internal static IMember Find(object target, string memberName, Type preferredMemberType, INode parent)
internal static IMember Find(object target, string memberName, INode parent)
{
PropertyInfo property = target.GetType().FindProperty(memberName, preferredMemberType);
PropertyInfo property = target.GetType().FindProperty(memberName);
if ((property is not null) && !property.IsIndexer())
{
return new Property(property, parent);
}

FieldInfo field = target.GetType().FindField(memberName, preferredMemberType);
FieldInfo field = target.GetType().FindField(memberName);
return (field is not null) ? new Field(field, parent) : null;
}
}
181 changes: 175 additions & 6 deletions Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
Expand Up @@ -530,16 +530,41 @@ public void Ignores_properties_hidden_by_the_derived_class()
subject.Should().BeEquivalentTo(expectation);
}

[Fact]
public void Ignores_properties_of_the_same_runtime_types_hidden_by_the_derived_class()
{
// Arrange
var subject = new SubclassHidingStringProperty
{
Property = "DerivedValue"
};

((BaseWithStringProperty)subject).Property = "ActualBaseValue";

var expectation = new SubclassHidingStringProperty
{
Property = "DerivedValue"
};

((BaseWithStringProperty)expectation).Property = "ExpectedBaseValue";

// Act / Assert
subject.Should().BeEquivalentTo(expectation);
}

[Fact]
public void Includes_hidden_property_of_the_base_when_using_a_reference_to_the_base()
{
// Arrange
var subject = new SubclassAHidingProperty<string>
BaseWithProperty subject = new SubclassAHidingProperty<string>
{
Property = "ActualDerivedValue"
};

((BaseWithProperty)subject).Property = "BaseValue";
// FA doesn't know the compile-time type of the subject, so even though we pass a reference to the base-class,
// at run-time, it'll start finding the property on the subject starting from the run-time type, and thus ignore the
// hidden base-class field
((SubclassAHidingProperty<string>)subject).Property = "BaseValue";

AnotherBaseWithProperty expectation = new SubclassBHidingProperty<string>
{
Expand Down Expand Up @@ -617,28 +642,172 @@ public void Excluding_the_property_hiding_the_base_class_one_does_not_reveal_the
act.Should().Throw<InvalidOperationException>().WithMessage("*No members were found *");
}

public class BaseWithProperty
private class BaseWithProperty
{
public object Property { get; set; }
}

public class SubclassAHidingProperty<T> : BaseWithProperty
private class SubclassAHidingProperty<T> : BaseWithProperty
{
public new T Property { get; set; }
}

public class AnotherBaseWithProperty
private class BaseWithStringProperty
{
public string Property { get; set; }
}

private class SubclassHidingStringProperty : BaseWithStringProperty
{
public new string Property { get; set; }
}

private class AnotherBaseWithProperty
{
public object Property { get; set; }
}

public class SubclassBHidingProperty<T> : AnotherBaseWithProperty
private class SubclassBHidingProperty<T> : AnotherBaseWithProperty
{
public new T Property
{
get; set;
}
}

[Fact]
public void Ignores_fields_hidden_by_the_derived_class()
{
// Arrange
var subject = new SubclassAHidingField
{
Field = "DerivedValue"
};

((BaseWithField)subject).Field = "ActualBaseValue";

var expectation = new SubclassBHidingField
{
Field = "DerivedValue"
};

((AnotherBaseWithField)expectation).Field = "ExpectedBaseValue";

// Act / Assert
subject.Should().BeEquivalentTo(expectation, options => options.IncludingFields());
}

[Fact]
public void Includes_hidden_field_of_the_base_when_using_a_reference_to_the_base()
{
// Arrange
BaseWithField subject = new SubclassAHidingField
{
Field = "BaseValueFromSubject"
};

// FA doesn't know the compile-time type of the subject, so even though we pass a reference to the base-class,
// at run-time, it'll start finding the field on the subject starting from the run-time type, and thus ignore the
// hidden base-class field
((SubclassAHidingField)subject).Field = "BaseValueFromExpectation";

AnotherBaseWithField expectation = new SubclassBHidingField
{
Field = "ExpectedDerivedValue"
};

expectation.Field = "BaseValueFromExpectation";

// Act / Assert
subject.Should().BeEquivalentTo(expectation, options => options.IncludingFields());
}

[Fact]
public void Run_type_typing_ignores_hidden_fields_even_when_using_a_reference_to_the_base_class()
{
// Arrange
var subject = new SubclassAHidingField
{
Field = "DerivedValue"
};

((BaseWithField)subject).Field = "ActualBaseValue";

AnotherBaseWithField expectation = new SubclassBHidingField
{
Field = "DerivedValue"
};

expectation.Field = "ExpectedBaseValue";

// Act / Assert
subject.Should().BeEquivalentTo(expectation, options => options.IncludingFields().RespectingRuntimeTypes());
}

[Fact]
public void Including_the_derived_field_excludes_the_hidden_field()
{
// Arrange
var subject = new SubclassAHidingField
{
Field = "DerivedValue"
};

((BaseWithField)subject).Field = "ActualBaseValue";

var expectation = new SubclassBHidingField
{
Field = "DerivedValue"
};

((AnotherBaseWithField)expectation).Field = "ExpectedBaseValue";

// Act / Assert
subject.Should().BeEquivalentTo(expectation, options => options
.IncludingFields()
.Including(_ => _.Field));
}

[Fact]
public void Excluding_the_field_hiding_the_base_class_one_does_not_reveal_the_latter()
{
// Arrange
var subject = new SubclassAHidingField();

((BaseWithField)subject).Field = "ActualBaseValue";

var expectation = new SubclassBHidingField();

((AnotherBaseWithField)expectation).Field = "ExpectedBaseValue";

// Act
Action act = () => subject.Should().BeEquivalentTo(expectation, options => options
.IncludingFields()
.Excluding(b => b.Field));

// Assert
act.Should().Throw<InvalidOperationException>().WithMessage("*No members were found *");
}

private class BaseWithField
{
public string Field;
}

private class SubclassAHidingField : BaseWithField
{
public new string Field;
}

private class AnotherBaseWithField
{
public string Field;
}

private class SubclassBHidingField : AnotherBaseWithField
{
public new string Field;
}
}

[Fact]
Expand Down

0 comments on commit 600ef6e

Please sign in to comment.