diff --git a/src/System.Runtime/ref/System.Runtime.cs b/src/System.Runtime/ref/System.Runtime.cs index 0e0504e47971..c2fca7984d7a 100644 --- a/src/System.Runtime/ref/System.Runtime.cs +++ b/src/System.Runtime/ref/System.Runtime.cs @@ -5667,6 +5667,7 @@ protected MemberInfo() { } public abstract object[] GetCustomAttributes(Type attributeType, bool inherit); public abstract bool IsDefined(Type attributeType, bool inherit); public virtual System.Collections.Generic.IList GetCustomAttributesData() { throw null; } + public virtual bool HasSameMetadataDefinitionAs(System.Reflection.MemberInfo other) { throw null; } } [Flags] public enum MemberTypes diff --git a/src/System.Runtime/tests/System.Runtime.Tests.csproj b/src/System.Runtime/tests/System.Runtime.Tests.csproj index faba8325cab6..23234b7eab9f 100644 --- a/src/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/System.Runtime/tests/System.Runtime.Tests.csproj @@ -144,6 +144,7 @@ + diff --git a/src/System.Runtime/tests/System/Reflection/MemberInfoTests.netcoreapp.cs b/src/System.Runtime/tests/System/Reflection/MemberInfoTests.netcoreapp.cs new file mode 100644 index 000000000000..4a93db228b95 --- /dev/null +++ b/src/System.Runtime/tests/System/Reflection/MemberInfoTests.netcoreapp.cs @@ -0,0 +1,491 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Diagnostics; +using System.Collections.Generic; + +using Xunit; + +#pragma warning disable 0649 // Yes, we know - test class have fields we don't assign to. + +namespace System.Reflection.Tests +{ + public static class MemberInfoNetCoreAppTests + { + [Fact] + public static void HasSameMetadataDefinitionAs_GenericClassMembers() + { + Type tGeneric = typeof(GenericTestClass<>); + IEnumerable methodsOnGeneric = tGeneric.GetTypeInfo().GetDeclaredMethods(nameof(GenericTestClass.Foo)); + + List typeInsts = new List(); + foreach (MethodInfo method in methodsOnGeneric) + { + Debug.Assert(method.GetParameters().Length == 1); + Type parameterType = method.GetParameters()[0].ParameterType; + typeInsts.Add(tGeneric.MakeGenericType(parameterType)); + } + typeInsts.Add(tGeneric); + CrossTestHasSameMethodDefinitionAs(typeInsts.ToArray()); + } + + private static void CrossTestHasSameMethodDefinitionAs(params Type[] types) + { + Assert.All(types, + delegate (Type type1) + { + Assert.All(type1.GenerateTestMemberList(), + delegate (MemberInfo m1) + { + MarkerAttribute mark1 = m1.GetCustomAttribute(); + if (mark1 == null) + return; + + Assert.All(types, + delegate (Type type2) + { + Assert.All(type2.GenerateTestMemberList(), + delegate (MemberInfo m2) + { + MarkerAttribute mark2 = m2.GetCustomAttribute(); + if (mark2 == null) + return; + + bool hasSameMetadata = m1.HasSameMetadataDefinitionAs(m2); + Assert.Equal(hasSameMetadata, m2.HasSameMetadataDefinitionAs(m1)); + + if (hasSameMetadata) + { + Assert.Equal(mark1.Mark, mark2.Mark); + } + else + { + Assert.NotEqual(mark1.Mark, mark2.Mark); + } + } + ); + } + ); + } + ); + } + ); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_ReflectedTypeNotPartOfComparison() + { + Type tBase = typeof(GenericTestClass<>); + Type tDerived = typeof(DerivedFromGenericTestClass); + BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + + IEnumerable baseMembers = tBase.GetMembers(bf).Where(m => m.IsDefined(typeof(MarkerAttribute))); + baseMembers = baseMembers.Where(bm => !(bm is ConstructorInfo)); // Constructors cannot be seen from derived types. + IEnumerable derivedMembers = tDerived.GetMembers(bf).Where(m => m.IsDefined(typeof(MarkerAttribute))); + Assert.All(baseMembers, + delegate (MemberInfo baseMember) + { + MemberInfo matchingDerivedMember = derivedMembers.Single(dm => dm.HasSameMarkAs(baseMember)); + Assert.True(baseMember.HasSameMetadataDefinitionAs(matchingDerivedMember)); + Assert.True(matchingDerivedMember.HasSameMetadataDefinitionAs(matchingDerivedMember)); + } + ); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_ConstructedGenericMethods() + { + Type t1 = typeof(TestClassWithGenericMethod<>); + Type theT = t1.GetTypeInfo().GenericTypeParameters[0]; + + MethodInfo mooNormal = t1.GetConfirmedMethod(nameof(TestClassWithGenericMethod.Moo), theT); + MethodInfo mooGeneric = t1.GetTypeInfo().GetDeclaredMethods("Moo").Single(m => m.IsGenericMethod); + + MethodInfo mooInst = mooGeneric.MakeGenericMethod(typeof(int)); + Assert.True(mooGeneric.HasSameMetadataDefinitionAs(mooInst)); + Assert.True(mooInst.HasSameMetadataDefinitionAs(mooGeneric)); + + MethodInfo mooInst2 = mooGeneric.MakeGenericMethod(typeof(double)); + Assert.True(mooInst2.HasSameMetadataDefinitionAs(mooInst)); + Assert.True(mooInst.HasSameMetadataDefinitionAs(mooInst2)); + + Type t2 = typeof(TestClassWithGenericMethod); + MethodInfo mooNormalOnT2 = t2.GetConfirmedMethod(nameof(TestClassWithGenericMethod.Moo), typeof(int)); + Assert.False(mooInst.HasSameMetadataDefinitionAs(mooNormalOnT2)); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_NamedAndGenericTypes() + { + Type tnong = typeof(TestClass); + Type tnong2 = typeof(TestClass2); + Type tg = typeof(GenericTestClass<>); + Type tginst1 = typeof(GenericTestClass); + Type tginst2 = typeof(GenericTestClass); + + + Assert.True(tnong.HasSameMetadataDefinitionAs(tnong)); + Assert.True(tnong2.HasSameMetadataDefinitionAs(tnong2)); + Assert.True(tg.HasSameMetadataDefinitionAs(tg)); + Assert.True(tginst1.HasSameMetadataDefinitionAs(tginst1)); + Assert.True(tginst2.HasSameMetadataDefinitionAs(tginst2)); + + Assert.True(tg.HasSameMetadataDefinitionAs(tginst1)); + Assert.True(tginst1.HasSameMetadataDefinitionAs(tginst1)); + Assert.True(tginst1.HasSameMetadataDefinitionAs(tginst2)); + + Assert.False(tnong.HasSameMetadataDefinitionAs(tnong2)); + Assert.False(tnong.HasSameMetadataDefinitionAs(tg)); + Assert.False(tg.HasSameMetadataDefinitionAs(tnong)); + Assert.False(tnong.HasSameMetadataDefinitionAs(tginst1)); + Assert.False(tginst1.HasSameMetadataDefinitionAs(tnong)); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_GenericTypeParameters() + { + Type theT = typeof(GenericTestClass<>).GetTypeInfo().GenericTypeParameters[0]; + Type theT2 = typeof(TestClassWithGenericMethod<>).GetTypeInfo().GenericTypeParameters[0]; + + BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.ExactBinding; + MethodInfo mooNong = theT2.GetMethod("Moo", bf, null, new Type[] { theT2 }, null); + MethodInfo mooGeneric = typeof(TestClassWithGenericMethod<>).GetTypeInfo().GetDeclaredMethods("Moo").Single(m => m.IsGenericMethod); + Type theM = mooGeneric.GetGenericArguments()[0]; + + Assert.True(theT.HasSameMetadataDefinitionAs(theT)); + Assert.False(theT2.HasSameMetadataDefinitionAs(theT)); + Assert.False(theT.HasSameMetadataDefinitionAs(theT2)); + Assert.False(theM.HasSameMetadataDefinitionAs(theT)); + Assert.False(theM.HasSameMetadataDefinitionAs(theT2)); + Assert.False(theT2.HasSameMetadataDefinitionAs(theM)); + Assert.False(theT.HasSameMetadataDefinitionAs(theM)); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_Twins() + { + // This situation is particularly treacherous for CoreRT as the .Net Native toolchain can and does assign + // the same native metadata tokens to identically structured members in unrelated types. + Type twin1 = typeof(Twin1); + Type twin2 = typeof(Twin2); + + Assert.All(twin1.GenerateTestMemberList(), + delegate (MemberInfo m1) + { + Assert.All(twin2.GenerateTestMemberList(), + delegate (MemberInfo m2) + { + Assert.False(m1.HasSameMetadataDefinitionAs(m2)); + } + ); + } + ); + } + + private class Twin1 + { + public Twin1() { } + public int Field1; + public Action Event1; + public void Method1() { } + public int Property1 { get; set; } + } + + private class Twin2 + { + public Twin2() { } + public int Field1; + public Action Event1; + public void Method1() { } + public int Property1 { get; set; } + } + + [Fact] + [OuterLoop] // Time-consuming. + public static void HasSameMetadataDefinitionAs_CrossAssembly() + { + // Make sure that identical tokens in different assemblies don't confuse the api. + foreach (Type t1 in typeof(object).Assembly.DefinedTypes) + { + foreach (Type t2 in typeof(MemberInfoNetCoreAppTests).Assembly.DefinedTypes) + { + Assert.False(t1.HasSameMetadataDefinitionAs(t2)); + } + } + } + + [Theory] + [MemberData(nameof(NegativeTypeData))] + public static void HasSameMetadataDefinitionAs_Negative_NonRuntimeType(Type type) + { + Type mockType = new MockType(); + Assert.False(type.HasSameMetadataDefinitionAs(mockType)); + Assert.All(type.GenerateTestMemberList(), + delegate (MemberInfo member) + { + Assert.False(member.HasSameMetadataDefinitionAs(mockType)); + } + ); + } + + [Theory] + [MemberData(nameof(NegativeTypeData))] + public static void HasSameMetadataDefinitionAs_Negative_Null(Type type) + { + AssertExtensions.Throws("other", () => type.HasSameMetadataDefinitionAs(null)); + Assert.All(type.GenerateTestMemberList(), + delegate (MemberInfo member) + { + AssertExtensions.Throws("other", () => member.HasSameMetadataDefinitionAs(null)); + } + ); + } + + public static IEnumerable NegativeTypeData => NegativeTypeDataRaw.Select(t => new object[] { t }); + private static IEnumerable NegativeTypeDataRaw + { + get + { + yield return typeof(TestClass); + yield return typeof(GenericTestClass<>); + yield return typeof(GenericTestClass); + + yield return typeof(int[]); + yield return typeof(int).MakeArrayType(1); + yield return typeof(int[,]); + yield return typeof(int).MakeByRefType(); + yield return typeof(int).MakePointerType(); + + yield return typeof(GenericTestClass<>).GetTypeInfo().GenericTypeParameters[0]; + if (PlatformDetection.IsWindows) + yield return Type.GetTypeFromCLSID(new Guid("DCA66D18-E253-4695-9E08-35B54420AFA2")); + } + } + + [Fact] + public static void HasSameMetadataDefinitionAs__CornerCase_HasElementTypes() + { + // HasSameMetadataDefinitionAs on a array/byref/pointer type is uninteresting (they'll never be an actual member of a type) + // but for future compat, we'll establish their known behavior here. Since these types all return a MetadataToken of 0x02000000, + // they'll all "match" each other. + + Type[] types = + { + typeof(int[]), + typeof(double[]), + typeof(int).MakeArrayType(1), + typeof(double).MakeArrayType(1), + typeof(int[,]), + typeof(double[,]), + typeof(int).MakeByRefType(), + typeof(double).MakeByRefType(), + typeof(int).MakePointerType(), + typeof(double).MakePointerType(), + }; + + Assert.All(types, + delegate (Type t1) + { + Assert.All(types, t2 => Assert.True(t1.HasSameMetadataDefinitionAs(t2))); + } + ); + } + + [Fact] + public static void HasSameMetadataDefinitionAs_CornerCase_ArrayMethods() + { + // The magic methods and constructors exposed on array types do not have metadata backing and report a MetadataToken of 0x06000000 + // and hence compare identically with each other. This may be surprising but this test records that fact for future compat. + // + Type[] arrayTypes = + { + typeof(int[]), + typeof(double[]), + typeof(int).MakeArrayType(1), + typeof(double).MakeArrayType(1), + typeof(int[,]), + typeof(double[,]), + }; + + List members = new List(); + foreach (Type arrayType in arrayTypes) + { + foreach (MemberInfo member in arrayType.GetMembers(BindingFlags.Public|BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + if (member is MethodBase) + { + members.Add(member); + } + } + } + + Assert.All(members, + delegate (MemberInfo member1) + { + Assert.All(members, + delegate (MemberInfo member2) + { + if (member1.MemberType == member2.MemberType) + Assert.True(member1.HasSameMetadataDefinitionAs(member2)); + else + Assert.False(member1.HasSameMetadataDefinitionAs(member2)); + } + ); + } + ); + } + + [ConditionalFact(nameof(PlatformDetection) + "." + nameof(PlatformDetection.IsWindows))] + public static void HasSameMetadataDefinitionAs_CornerCase_CLSIDConstructor() + { + // HasSameMetadataDefinitionAs on a GetTypeFromCLSID type is uninteresting (they'll never be an actual member of a type) + // but for future compat, we'll establish their known behavior here. Since these types and their constructors all return the same + // MetadataToken, they'll all "match" each other. + + Type t1 = Type.GetTypeFromCLSID(new Guid("23A54889-7738-4F16-8AB2-CB23F8E756BE")); + Type t2 = Type.GetTypeFromCLSID(new Guid("DCA66D18-E253-4695-9E08-35B54420AFA2")); + + Assert.True(t1.HasSameMetadataDefinitionAs(t2)); + + ConstructorInfo c1 = t1.GetConfirmedConstructor(); + ConstructorInfo c2 = t2.GetConfirmedConstructor(); + + Assert.True(c1.HasSameMetadataDefinitionAs(c2)); + } + + + private static bool HasSameMarkAs(this MemberInfo m1, MemberInfo m2) + { + MarkerAttribute marker1 = m1.GetCustomAttribute(); + Assert.NotNull(marker1); + + MarkerAttribute marker2 = m2.GetCustomAttribute(); + Assert.NotNull(marker2); + + return marker1.Mark == marker2.Mark; + } + + private static MethodInfo GetConfirmedMethod(this Type t, string name, params Type[] parameterTypes) + { + BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.ExactBinding; + MethodInfo method = t.GetMethod(name, bf, null, parameterTypes, null); + Assert.NotNull(method); + return method; + } + + private static ConstructorInfo GetConfirmedConstructor(this Type t, params Type[] parameterTypes) + { + BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.ExactBinding; + ConstructorInfo ctor = t.GetConstructor(bf, null, parameterTypes, null); + Assert.NotNull(ctor); + return ctor; + } + + private static IEnumerable GenerateTestMemberList(this Type t) + { + if (t.IsGenericTypeDefinition) + { + foreach (Type gp in t.GetTypeInfo().GenericTypeParameters) + { + yield return gp; + } + } + foreach (MemberInfo m in t.GetTypeInfo().DeclaredMembers) + { + yield return m; + MethodInfo method = m as MethodInfo; + if (method != null && method.IsGenericMethodDefinition) + { + foreach (Type mgp in method.GetGenericArguments()) + { + yield return mgp; + } + yield return method.MakeGenericMethod(method.GetGenericArguments().Select(ga => typeof(object)).ToArray()); + } + } + } + + private class TestClassWithGenericMethod + { + public void Moo(T t) { } + public void Moo(M m) { } + } + + private class TestClass + { + public void Foo() { } + public void Foo(object o) { } + public void Foo(string s) { } + public void Bar() { } + } + + private class TestClass2 { } + + + private class GenericTestClass + { + [Marker(1)] + public void Foo(object o) { } + [Marker(2)] + public void Foo(int i) { } + [Marker(3)] + public void Foo(T t) { } + [Marker(4)] + public void Foo(double d) { } + [Marker(5)] + public void Foo(string s) { } + [Marker(6)] + public void Foo(int[] s) { } + [Marker(7)] + public void Foo(U t) { } + [Marker(8)] + public void Foo(T t) { } + [Marker(9)] + public void Foo(T t) { } + + [Marker(101)] + public GenericTestClass() { } + [Marker(102)] + public GenericTestClass(T t) { } + [Marker(103)] + public GenericTestClass(int t) { } + + [Marker(201)] + public int Field1; + [Marker(202)] + public int Field2; + [Marker(203)] + public T Field3; + + [Marker(301)] + public int Property1 { get { throw null; } set { throw null; } } + [Marker(302)] + public int Property2 { get { throw null; } set { throw null; } } + [Marker(303)] + public T Property3 { get { throw null; } set { throw null; } } + + [Marker(401)] + public event Action Event1 { add { } remove { } } + [Marker(402)] + public event Action Event2 { add { } remove { } } + [Marker(403)] + public event Action Event3 { add { } remove { } } + } + + private class DerivedFromGenericTestClass : GenericTestClass { } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)] + public class MarkerAttribute : Attribute + { + public MarkerAttribute(int mark) + { + Mark = mark; + } + + public readonly int Mark; + } + } +} +