From 94d2740242f9f724ea546e18bdb0c4fd05d2cb2e Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Fri, 18 Oct 2024 20:53:55 +0200 Subject: [PATCH] SAVEPOINT --- Build/_build.csproj.DotSettings | 5 +- FluentAssertions.sln.DotSettings | 3 + .../Common/TypeMemberReflector.cs | 187 ++++++----------- .../Equivalency/MemberVisibility.cs | 3 +- .../TypeExtensionsSpecs.GetProperties.cs | 191 ++++++++++++++++++ .../{Types => Common}/TypeExtensionsSpecs.cs | 4 +- .../FluentAssertions.Specs.csproj | 1 + 7 files changed, 266 insertions(+), 128 deletions(-) create mode 100644 Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs rename Tests/FluentAssertions.Specs/{Types => Common}/TypeExtensionsSpecs.cs (99%) diff --git a/Build/_build.csproj.DotSettings b/Build/_build.csproj.DotSettings index 9aac7d8e8d..28494fb0c6 100644 --- a/Build/_build.csproj.DotSettings +++ b/Build/_build.csproj.DotSettings @@ -13,6 +13,8 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> True True True @@ -21,4 +23,5 @@ True True True - True + True + True diff --git a/FluentAssertions.sln.DotSettings b/FluentAssertions.sln.DotSettings index 692c8c68da..8e755b4509 100644 --- a/FluentAssertions.sln.DotSettings +++ b/FluentAssertions.sln.DotSettings @@ -104,6 +104,8 @@ UseExplicitType <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> @@ -154,6 +156,7 @@ True True True + True D:\Workspaces\FluentAssertions\Default.testsettings 4 False diff --git a/Src/FluentAssertions/Common/TypeMemberReflector.cs b/Src/FluentAssertions/Common/TypeMemberReflector.cs index ea65969cdf..1f9f87d16e 100644 --- a/Src/FluentAssertions/Common/TypeMemberReflector.cs +++ b/Src/FluentAssertions/Common/TypeMemberReflector.cs @@ -21,149 +21,88 @@ public TypeMemberReflector(Type typeToReflect, MemberVisibility visibility) Members = Properties.Concat(Fields).ToArray(); } - public MemberInfo[] Members { get; } - - public PropertyInfo[] Properties { get; } - - public FieldInfo[] Fields { get; } - - private static PropertyInfo[] LoadProperties(Type typeToReflect, MemberVisibility visibility) + private PropertyInfo[] LoadProperties(Type typeToReflect, Enum visibility) { - List query = GetPropertiesFromHierarchy(typeToReflect, visibility); - - return query.ToArray(); - } - - private static List GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) + public static PropertyInfo[] GetProperties2(this Type type, MemberVisibility visibility) { - bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal); - bool includeExplicitlyImplemented = memberVisibility.HasFlag(MemberVisibility.ExplicitlyImplemented); + var collectedProperties = new HashSet(); + var properties = new List(); - return GetMembersFromHierarchy(typeToReflect, type => + // Start with the given type and iterate up the inheritance chain + while (type != null && type != typeof(object)) { - return - from p in type.GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly) - where p.GetMethod is { } getMethod - && (IsPublic(getMethod) || (includeExplicitlyImplemented && IsExplicitlyImplemented(getMethod))) - && (includeInternal || !IsInternal(getMethod)) - && !p.IsIndexer() - orderby IsExplicitImplementation(p) - select p; - }); - } - - private static bool IsPublic(MethodBase getMethod) => - !getMethod.IsPrivate && !getMethod.IsFamily && !getMethod.IsFamilyAndAssembly; - - private static bool IsExplicitlyImplemented(MethodBase getMethod) => - getMethod.IsPrivate && getMethod.IsFinal; - - private static bool IsInternal(MethodBase getMethod) => - getMethod.IsAssembly || getMethod.IsFamilyOrAssembly; - - private static bool IsExplicitImplementation(PropertyInfo property) - { - return property.GetMethod!.IsPrivate && - property.SetMethod?.IsPrivate != false && - property.Name.Contains('.', StringComparison.Ordinal); - } + // Add all properties declared in the current type (including new ones) + PropertyInfo[] allProperties = type.GetProperties( + BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); - private static FieldInfo[] LoadFields(Type typeToReflect, MemberVisibility visibility) - { - List query = GetFieldsFromHierarchy(typeToReflect, visibility); - - return query.ToArray(); - } - - private static List GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility) - { - bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal); - - return GetMembersFromHierarchy(typeToReflect, type => - { - return type - .GetFields(AllInstanceMembersFlag) - .Where(field => IsPublic(field)) - .Where(field => includeInternal || !IsInternal(field)); - }); - } - - private static bool IsPublic(FieldInfo field) => - !field.IsPrivate && !field.IsFamily && !field.IsFamilyAndAssembly; - - private static bool IsInternal(FieldInfo field) - { - return field.IsAssembly || field.IsFamilyOrAssembly; - } - - private static List GetMembersFromHierarchy( - Type typeToReflect, - Func> getMembers) - where TMemberInfo : MemberInfo - { - if (typeToReflect.IsInterface) - { - return GetInterfaceMembers(typeToReflect, getMembers); - } - - return GetClassMembers(typeToReflect, getMembers); - } - - private static List GetInterfaceMembers(Type typeToReflect, - Func> getMembers) - where TMemberInfo : MemberInfo - { - List members = new(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(typeToReflect); - queue.Enqueue(typeToReflect); - - while (queue.Count > 0) - { - Type subType = queue.Dequeue(); - - foreach (Type subInterface in subType.GetInterfaces()) + if (visibility.HasFlag(MemberVisibility.Public) || visibility.HasFlag(MemberVisibility.Internal) || + visibility.HasFlag(MemberVisibility.ExplicitlyImplemented)) { - if (considered.Contains(subInterface)) + foreach (var prop in allProperties) { - continue; + if (!collectedProperties.Contains(prop.Name) && !IsExplicitlyImplemented(prop) && HasVisibility(visibility, prop)) + { + properties.Add(prop); + collectedProperties.Add(prop.Name); + } } + } - considered.Add(subInterface); - queue.Enqueue(subInterface); + if (visibility.HasFlag(MemberVisibility.ExplicitlyImplemented)) + { + foreach (var prop in allProperties) + { + if (IsExplicitlyImplemented(prop)) + { + var name = prop.Name.Split('.').Last(); + if (!collectedProperties.Contains(name)) + { + properties.Add(prop); + collectedProperties.Add(name); + } + } + } } - IEnumerable typeMembers = getMembers(subType); + if (visibility.HasFlag(MemberVisibility.DefaultInterfaceProperties)) + { + // Add explicitly implemented interface properties (not included above) + var interfaces = type.GetInterfaces(); - IEnumerable newPropertyInfos = typeMembers.Where(x => !members.Contains(x)); + foreach (var iface in interfaces) + { + foreach (var prop in iface.GetProperties()) + { + if (!collectedProperties.Contains(prop.Name) && !prop.GetMethod!.IsAbstract) + { + properties.Add(prop); + collectedProperties.Add(prop.Name); + } + } + } + } - members.InsertRange(0, newPropertyInfos); + // Move to the base type + type = type.BaseType; } - return members; + return properties.ToArray(); } - private static List GetClassMembers(Type typeToReflect, - Func> getMembers) - where TMemberInfo : MemberInfo + private static bool IsExplicitlyImplemented(PropertyInfo prop) { - List members = new(); + return prop.Name.Contains('.', StringComparison.InvariantCultureIgnoreCase); + } - while (typeToReflect != null) - { - foreach (var memberInfo in getMembers(typeToReflect)) - { - if (members.TrueForAll(mi => mi.Name != memberInfo.Name)) - { - members.Add(memberInfo); - } - } + private static bool HasVisibility(MemberVisibility visibility, PropertyInfo prop) => + (visibility.HasFlag(MemberVisibility.Public) && prop.GetMethod?.IsPublic is true) || + (visibility.HasFlag(MemberVisibility.Internal) && prop.GetMethod?.IsAssembly is true); +throw new NotImplementedException(); + } - typeToReflect = typeToReflect.BaseType; - } + public MemberInfo[] Members { get; } - return members; - } + public PropertyInfo[] Properties { get; } + + public FieldInfo[] Fields { get; } } diff --git a/Src/FluentAssertions/Equivalency/MemberVisibility.cs b/Src/FluentAssertions/Equivalency/MemberVisibility.cs index e64b798a81..a0eddebf20 100644 --- a/Src/FluentAssertions/Equivalency/MemberVisibility.cs +++ b/Src/FluentAssertions/Equivalency/MemberVisibility.cs @@ -12,5 +12,6 @@ public enum MemberVisibility None = 0, Internal = 1, Public = 2, - ExplicitlyImplemented = 4 + ExplicitlyImplemented = 4, + DefaultInterfaceProperties = 8 } diff --git a/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs new file mode 100644 index 0000000000..ab9ed234c5 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs @@ -0,0 +1,191 @@ +#if NETCOREAPP3_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions.Equivalency; +using Xunit; + +namespace FluentAssertions.Specs.Common; + +public partial class TypeExtensionsSpecs +{ + public class GetProperties + { + [Fact] + public void Can_get_all_public_explicit_and_default_interface_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented | + MemberVisibility.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + new + { + Name = $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + }, + new { Name = "DefaultProperty", PropertyType = typeof(string) } + }); + } + + [Fact] + public void Can_get_normal_public_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Public); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + }); + } + + [Fact] + public void Can_get_explicit_properties_only() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.ExplicitlyImplemented); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new + { + Name = $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + }, + }); + } + + [Fact] + public void Prefers_normal_property_over_explicitly_implemented_one() + { + // Act + var properties = typeof(ClassWithExplicitAndNormalProperty) + .GetProperties2(MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new + { + Name = "ExplicitlyImplementedProperty", + PropertyType = typeof(int) + }, + }); + } + + [Fact] + public void Can_get_default_interface_properties_only() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "DefaultProperty", PropertyType = typeof(string) }, + }); + } + + [Fact] + public void Can_get_internal_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Internal); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "InternalProperty", PropertyType = typeof(bool) }, + }); + } + + private class SuperClass : BaseClass, IInterfaceWithDefaultProperty + { + public string NormalProperty { get; set; } + + public new int NewProperty { get; set; } + + internal bool InternalProperty { get; set; } + + string IInterfaceWithSingleProperty.ExplicitlyImplementedProperty { get; set; } + + public string InterfaceProperty { get; set; } + } + + private class ClassWithExplicitAndNormalProperty : IInterfaceWithSingleProperty + { + string IInterfaceWithSingleProperty.ExplicitlyImplementedProperty { get; set; } + + public int ExplicitlyImplementedProperty { get; set; } + } + + private class BaseClass + { + public string NewProperty { get; set; } + } + + private interface IInterfaceWithDefaultProperty : IInterfaceWithSingleProperty + { + string InterfaceProperty { get; set; } + + string DefaultProperty => "Default"; + } + + private interface IInterfaceWithSingleProperty + { + string ExplicitlyImplementedProperty { get; set; } + } + } +} + +internal static class TypeReflector +{ + public static FieldInfo[] GetFields2(this Type type, MemberVisibility visibility) + { + var collectedFields = new HashSet(); + var fields = new List(); + + while (type != null && type != typeof(object)) + { + // Add all properties declared in the current type (including new ones) + FieldInfo[] files = type.GetFields( + BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + + foreach (var field in files) + { + if (!collectedFields.Contains(field.Name) && HasVisibility(visibility, field)) + { + fields.Add(field); + collectedFields.Add(field.Name); + } + } + + // Move to the base type + type = type.BaseType; + } + + return fields.ToArray(); + } + + private static bool HasVisibility(MemberVisibility visibility, FieldInfo field) => + (visibility.HasFlag(MemberVisibility.Public) && field.IsPublic) || + (visibility.HasFlag(MemberVisibility.Internal) && field.IsAssembly); +} + +#endif diff --git a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs similarity index 99% rename from Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs rename to Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs index 73ef4247ed..24fdf10663 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs @@ -7,9 +7,9 @@ using FluentAssertions.Common; using Xunit; -namespace FluentAssertions.Specs.Types; +namespace FluentAssertions.Specs.Common; -public class TypeExtensionsSpecs +public partial class TypeExtensionsSpecs { [Fact] public void When_comparing_types_and_types_are_same_it_should_return_true() diff --git a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj index d5c5a52b90..b2b56399a7 100644 --- a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj +++ b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj @@ -7,6 +7,7 @@ false $(NoWarn),IDE0052,1573,1591,1712,CS8002 full + 12