Skip to content

Commit

Permalink
BeEquivalentTo will now find and can map subject properties that are …
Browse files Browse the repository at this point in the history
…implemented through an explicitly-implemented interface
  • Loading branch information
Dennis Doomen authored and dennisdoomen committed Aug 13, 2023
1 parent 5f94c6b commit 1d0fd6d
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 131 deletions.
44 changes: 17 additions & 27 deletions Src/FluentAssertions/Common/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ internal static class TypeExtensions
private const BindingFlags PublicInstanceMembersFlag =
BindingFlags.Public | BindingFlags.Instance;

private const BindingFlags AllInstanceMembersFlag =
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

private const BindingFlags AllStaticAndInstanceMembersFlag =
PublicInstanceMembersFlag | BindingFlags.NonPublic | BindingFlags.Static;

Expand Down Expand Up @@ -176,24 +173,21 @@ public static bool OverridesEquals(this Type type)
}

/// <summary>
/// Finds the property by a case-sensitive name.
/// Finds the property by a case-sensitive name and with a certain visibility.
/// </summary>
/// <remarks>
/// If both a normal property and one that was implemented through an explicit interface implementation with the same name exist,
/// then the normal property will be returned.
/// </remarks>
/// <returns>
/// Returns <see langword="null"/> if no such property exists.
/// </returns>
public static PropertyInfo FindProperty(this Type type, string propertyName)
public static PropertyInfo FindProperty(this Type type, string propertyName, MemberVisibility memberVisibility)
{
while (type != typeof(object))
{
if (type.GetProperty(propertyName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } property)
{
return property;
}

type = type.BaseType;
}
var properties = type.GetNonPrivateProperties(memberVisibility);

return null;
return Array.Find(properties, p =>
p.Name == propertyName || p.Name.EndsWith("." + propertyName, StringComparison.OrdinalIgnoreCase));
}

/// <summary>
Expand All @@ -202,19 +196,12 @@ public static PropertyInfo FindProperty(this Type type, string propertyName)
/// <returns>
/// Returns <see langword="null"/> if no such property exists.
/// </returns>
public static FieldInfo FindField(this Type type, string fieldName)
public static FieldInfo FindField(this Type type, string fieldName, MemberVisibility memberVisibility)
{
while (type != typeof(object))
{
if (type.GetField(fieldName, AllInstanceMembersFlag | BindingFlags.DeclaredOnly) is { } field)
{
return field;
}
var fields = type.GetNonPrivateFields(memberVisibility);

type = type.BaseType;
}

return null;
return Array.Find(fields, p =>
p.Name == fieldName || p.Name.EndsWith("." + fieldName, StringComparison.OrdinalIgnoreCase));
}

public static MemberInfo[] GetNonPrivateMembers(this Type typeToReflect, MemberVisibility visibility)
Expand Down Expand Up @@ -318,8 +305,11 @@ public static bool IsIndexer(this PropertyInfo member)

public static ConstructorInfo GetConstructor(this Type type, IEnumerable<Type> parameterTypes)
{
const BindingFlags allInstanceMembersFlag =
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

return type
.GetConstructors(AllInstanceMembersFlag)
.GetConstructors(allInstanceMembersFlag)
.SingleOrDefault(m => m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes));
}

Expand Down
57 changes: 43 additions & 14 deletions Src/FluentAssertions/Common/TypeMemberReflector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,45 @@ private static PropertyInfo[] LoadNonPrivateProperties(Type typeToReflect, Membe
{
IEnumerable<PropertyInfo> query =
from propertyInfo in GetPropertiesFromHierarchy(typeToReflect, visibility)
where HasNonPrivateGetter(propertyInfo)
where HasNonPrivateGetter(propertyInfo, visibility)
where !propertyInfo.IsIndexer()
select propertyInfo;

return query.ToArray();
}

private static List<PropertyInfo> GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
private static PropertyInfo[] GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
{
bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal);
bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);

return GetMembersFromHierarchy(typeToReflect, type =>
{
return type
.GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly)
.Where(property => property.GetMethod?.IsPrivate == false)
.Where(property => includeInternals || property.GetMethod is { IsAssembly: false, IsFamilyOrAssembly: false })
.Where(property => includeInternal || !IsInternal(property))
.Select(property => new
{
Property = property,
IsExplicit = IsExplicitImplementation(property)
})
.OrderBy(x => x.IsExplicit)
.Select(x => x.Property)
.ToArray();
});
}

private static bool IsInternal(PropertyInfo property)
{
return property.GetMethod is { IsAssembly: true } or { IsFamilyOrAssembly: true };
}

private static bool IsExplicitImplementation(PropertyInfo property)
{
return property.GetMethod?.IsPrivate == true &&
property.SetMethod?.IsPrivate == true &&
property.Name.Contains('.', StringComparison.Ordinal);
}

private static FieldInfo[] LoadNonPrivateFields(Type typeToReflect, MemberVisibility visibility)
{
IEnumerable<FieldInfo> query =
Expand All @@ -63,21 +81,26 @@ from fieldInfo in GetFieldsFromHierarchy(typeToReflect, visibility)
return query.ToArray();
}

private static List<FieldInfo> GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
private static FieldInfo[] GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
{
bool includeInternals = memberVisibility.HasFlag(MemberVisibility.Internal);
bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);

return GetMembersFromHierarchy(typeToReflect, type =>
{
return type
.GetFields(AllInstanceMembersFlag)
.Where(field => !field.IsPrivate)
.Where(field => includeInternals || (!field.IsAssembly && !field.IsFamilyOrAssembly))
.Where(field => includeInternal || !IsInternal(field))
.ToArray();
});
}

private static List<TMemberInfo> GetMembersFromHierarchy<TMemberInfo>(
private static bool IsInternal(FieldInfo field)
{
return field.IsAssembly || field.IsFamilyOrAssembly;
}

private static TMemberInfo[] GetMembersFromHierarchy<TMemberInfo>(
Type typeToReflect,
Func<Type, IEnumerable<TMemberInfo>> getMembers)
where TMemberInfo : MemberInfo
Expand All @@ -90,7 +113,7 @@ private static List<TMemberInfo> GetMembersFromHierarchy<TMemberInfo>(
return GetClassMembers(typeToReflect, getMembers);
}

private static List<TMemberInfo> GetInterfaceMembers<TMemberInfo>(Type typeToReflect,
private static TMemberInfo[] GetInterfaceMembers<TMemberInfo>(Type typeToReflect,
Func<Type, IEnumerable<TMemberInfo>> getMembers)
where TMemberInfo : MemberInfo
{
Expand Down Expand Up @@ -123,10 +146,10 @@ private static List<TMemberInfo> GetInterfaceMembers<TMemberInfo>(Type typeToRef
members.InsertRange(0, newPropertyInfos);
}

return members;
return members.ToArray();
}

private static List<TMemberInfo> GetClassMembers<TMemberInfo>(Type typeToReflect,
private static TMemberInfo[] GetClassMembers<TMemberInfo>(Type typeToReflect,
Func<Type, IEnumerable<TMemberInfo>> getMembers)
where TMemberInfo : MemberInfo
{
Expand All @@ -145,12 +168,18 @@ private static List<TMemberInfo> GetClassMembers<TMemberInfo>(Type typeToReflect
typeToReflect = typeToReflect.BaseType;
}

return members;
return members.ToArray();
}

private static bool HasNonPrivateGetter(PropertyInfo propertyInfo)
private static bool HasNonPrivateGetter(PropertyInfo propertyInfo, MemberVisibility visibility)
{
MethodInfo getMethod = propertyInfo.GetGetMethod(nonPublic: true);

if (visibility.HasFlag(MemberVisibility.ExplicitlyImplemented))
{
return getMethod is { IsPrivate: false, IsFamily: false } or { IsPrivate: true, IsFinal: true };
}

return getMethod is { IsPrivate: false, IsFamily: false };
}
}
20 changes: 8 additions & 12 deletions Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui

if (options.IncludedProperties != MemberVisibility.None)
{
PropertyInfo propertyInfo = subject.GetType().FindProperty(expectedMember.Name);
PropertyInfo propertyInfo = subject.GetType().FindProperty(
expectedMember.Name,
options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);

subjectMember = propertyInfo is not null && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null;
}

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

if ((subjectMember is null || !options.UseRuntimeTyping) && ExpectationImplementsMemberExplicitly(subject, expectedMember))
{
subjectMember = expectedMember;
subjectMember = fieldInfo is not null ? new Field(fieldInfo, parent) : null;
}

if (subjectMember is null)
Expand All @@ -49,11 +50,6 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
return subjectMember;
}

private static bool ExpectationImplementsMemberExplicitly(object expectation, IMember subjectMember)
{
return subjectMember.DeclaringType.IsInstanceOfType(expectation);
}

/// <inheritdoc />
/// <filterpriority>2</filterpriority>
public override string ToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ internal class TryMatchByNameRule : IMemberMatchingRule
{
public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions options)
{
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name);
PropertyInfo property = subject.GetType().FindProperty(expectedMember.Name,
options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);

if (property is not null && !property.IsIndexer())
{
return new Property(property, parent);
}

FieldInfo field = subject.GetType().FindField(expectedMember.Name);
FieldInfo field = subject.GetType()
.FindField(expectedMember.Name, options.IncludedFields);

return field is not null ? new Field(field, parent) : null;
}

Expand Down
4 changes: 2 additions & 2 deletions Src/FluentAssertions/Equivalency/MemberFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ public static IMember Create(MemberInfo memberInfo, INode parent)

internal static IMember Find(object target, string memberName, INode parent)
{
PropertyInfo property = target.GetType().FindProperty(memberName);
PropertyInfo property = target.GetType().FindProperty(memberName, MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented);

if (property is not null && !property.IsIndexer())
{
return new Property(property, parent);
}

FieldInfo field = target.GetType().FindField(memberName);
FieldInfo field = target.GetType().FindField(memberName, MemberVisibility.Public);
return field is not null ? new Field(field, parent) : null;
}
}
3 changes: 2 additions & 1 deletion Src/FluentAssertions/Equivalency/MemberVisibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public enum MemberVisibility
{
None = 0,
Internal = 1,
Public = 2
Public = 2,
ExplicitlyImplemented = 4
}
10 changes: 8 additions & 2 deletions Tests/Approval.Tests/ApiApproval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ public Task ApproveApi(string frameworkVersion)
string assemblyPath = Uri.UnescapeDataString(uri.Path);
var containingDirectory = Path.GetDirectoryName(assemblyPath);
var configurationName = new DirectoryInfo(containingDirectory).Parent.Name;

var assemblyFile = Path.GetFullPath(
Path.Combine(
GetSourceDirectory(),
Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion, "FluentAssertions.dll")));
Path.Combine("..", "..", "Src", "FluentAssertions", "bin", configurationName, frameworkVersion,
"FluentAssertions.dll")));

var assembly = Assembly.LoadFile(Path.GetFullPath(assemblyFile));
var publicApi = assembly.GeneratePublicApi(options: null);
Expand All @@ -53,6 +55,7 @@ public static Task<CompareResult> OnlyIncludeChanges(string received, string ver
var diff = InlineDiffBuilder.Diff(verified, received);

var builder = new StringBuilder();

foreach (var line in diff.Lines)
{
switch (line.Type)
Expand All @@ -79,9 +82,12 @@ private class TargetFrameworksTheoryData : TheoryData<string>
{
public TargetFrameworksTheoryData()
{
var csproj = Path.Combine(GetSourceDirectory(), Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj"));
var csproj = Path.Combine(GetSourceDirectory(),
Path.Combine("..", "..", "Src", "FluentAssertions", "FluentAssertions.csproj"));

var project = XDocument.Load(csproj);
var targetFrameworks = project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks");

foreach (string targetFramework in targetFrameworks!.Value.Split(';'))
{
Add(targetFramework);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ namespace FluentAssertions.Equivalency
None = 0,
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
{
Expand Down
Loading

0 comments on commit 1d0fd6d

Please sign in to comment.