1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System;
6 using System.Diagnostics;
7 using System.Linq;
8 using System.Linq.Expressions;
9 using System.Reflection;
10 
11 namespace Microsoft.CSharp.RuntimeBinder
12 {
13     internal static class RuntimeBinderExtensions
14     {
IsNullableType(this Type type)15         public static bool IsNullableType(this Type type)
16         {
17             return type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
18         }
19 
20         // This method is intended as a means to detect when MemberInfos are the same,
21         // modulo the fact that they can appear to have different but equivalent local
22         // No-PIA types. It is by the symbol table to determine whether
23         // or not members have been added to an AggSym or not.
IsEquivalentTo(this MemberInfo mi1, MemberInfo mi2)24         public static bool IsEquivalentTo(this MemberInfo mi1, MemberInfo mi2)
25         {
26             if (mi1 == null || mi2 == null)
27             {
28                 return mi1 == null && mi2 == null;
29             }
30 
31 #if UNSUPPORTEDAPI
32             if (mi1 == mi2 || (mi1.DeclaringType.IsGenericallyEqual(mi2.DeclaringType) && mi1.MetadataToken == mi2.MetadataToken))
33 #else
34             if (mi1.Equals(mi2))
35 #endif
36             {
37                 return true;
38             }
39 
40             if (mi1 is MethodInfo method1)
41             {
42                 if (!(mi2 is MethodInfo method2) || method1.IsGenericMethod != method2.IsGenericMethod)
43                 {
44                     return false;
45                 }
46 
47                 if (method1.IsGenericMethod)
48                 {
49                     method1 = method1.GetGenericMethodDefinition();
50                     method2 = method2.GetGenericMethodDefinition();
51 
52                     if (method1.GetGenericArguments().Length != method2.GetGenericArguments().Length)
53                     {
54                         return false; // Methods of different arity are not equivalent.
55                     }
56                 }
57 
58                 return method1 != method2
59                     && method1.CallingConvention == method2.CallingConvention
60                     && method1.Name == method2.Name
61                     && method1.DeclaringType.IsGenericallyEqual(method2.DeclaringType)
62                     && method1.ReturnType.IsGenericallyEquivalentTo(method2.ReturnType, method1, method2)
63                     && method1.AreParametersEquivalent(method2);
64             }
65 
66             if (mi1 is ConstructorInfo ctor1)
67             {
68                 return mi2 is ConstructorInfo ctor2
69                     && ctor1 != ctor2
70                     && ctor1.CallingConvention == ctor2.CallingConvention
71                     && ctor1.DeclaringType.IsGenericallyEqual(ctor2.DeclaringType)
72                     && ctor1.AreParametersEquivalent(ctor2);
73             }
74 
75             return mi1 is PropertyInfo prop1 && mi2 is PropertyInfo prop2
76                 && prop1 != prop2
77                 && prop1.Name == prop2.Name
78                 && prop1.DeclaringType.IsGenericallyEqual(prop2.DeclaringType)
79                 && prop1.PropertyType.IsGenericallyEquivalentTo(prop2.PropertyType, prop1, prop2)
80                 && prop1.GetGetMethod(true).IsEquivalentTo(prop2.GetGetMethod(true))
81                 && prop1.GetSetMethod(true).IsEquivalentTo(prop2.GetSetMethod(true));
82         }
83 
AreParametersEquivalent(this MethodBase method1, MethodBase method2)84         private static bool AreParametersEquivalent(this MethodBase method1, MethodBase method2)
85         {
86             ParameterInfo[] pis1 = method1.GetParameters();
87             ParameterInfo[] pis2 = method2.GetParameters();
88             if (pis1.Length != pis2.Length)
89             {
90                 return false;
91             }
92 
93             for (int i = 0; i < pis1.Length; ++i)
94             {
95                 if (!pis1[i].IsEquivalentTo(pis2[i], method1, method2))
96                 {
97                     return false;
98                 }
99             }
100 
101             return true;
102         }
103 
IsEquivalentTo(this ParameterInfo pi1, ParameterInfo pi2, MethodBase method1, MethodBase method2)104         private static bool IsEquivalentTo(this ParameterInfo pi1, ParameterInfo pi2, MethodBase method1, MethodBase method2)
105         {
106             if (pi1 == null || pi2 == null)
107             {
108                 return pi1 == null && pi2 == null;
109             }
110 
111             if (pi1.Equals(pi2))
112             {
113                 return true;
114             }
115 
116             return pi1.ParameterType.IsGenericallyEquivalentTo(pi2.ParameterType, method1, method2);
117         }
118 
IsGenericallyEqual(this Type t1, Type t2)119         private static bool IsGenericallyEqual(this Type t1, Type t2)
120         {
121             if (t1 == null || t2 == null)
122             {
123                 return t1 == null && t2 == null;
124             }
125 
126             if (t1.Equals(t2))
127             {
128                 return true;
129             }
130 
131             if (t1.IsConstructedGenericType || t2.IsConstructedGenericType)
132             {
133                 Type t1def = t1.IsConstructedGenericType ? t1.GetGenericTypeDefinition() : t1;
134                 Type t2def = t2.IsConstructedGenericType ? t2.GetGenericTypeDefinition() : t2;
135 
136                 return t1def.Equals(t2def);
137             }
138 
139             return false;
140         }
141 
142         // Compares two types and calls them equivalent if a type parameter equals a type argument.
143         // i.e if the inputs are (T, int, C<T>, C<int>) then this will return true.
IsGenericallyEquivalentTo(this Type t1, Type t2, MemberInfo member1, MemberInfo member2)144         private static bool IsGenericallyEquivalentTo(this Type t1, Type t2, MemberInfo member1, MemberInfo member2)
145         {
146             Debug.Assert(!(member1 is MethodBase) ||
147                          !((MethodBase)member1).IsGenericMethod ||
148                          (((MethodBase)member1).IsGenericMethodDefinition && ((MethodBase)member2).IsGenericMethodDefinition));
149 
150             if (t1.Equals(t2))
151             {
152                 return true;
153             }
154 
155             // If one of them is a type param and then the other is a real type, then get the type argument in the member
156             // or it's declaring type that corresponds to the type param and compare that to the other type.
157             if (t1.IsGenericParameter)
158             {
159                 if (t2.IsGenericParameter)
160                 {
161                     // If member's declaring type is not type parameter's declaring type, we assume that it is used as a type argument
162                     if (t1.DeclaringMethod == null && member1.DeclaringType.Equals(t1.DeclaringType))
163                     {
164                         if (!(t2.DeclaringMethod == null && member2.DeclaringType.Equals(t2.DeclaringType)))
165                         {
166                             return t1.IsTypeParameterEquivalentToTypeInst(t2, member2);
167                         }
168                     }
169                     else if (t2.DeclaringMethod == null && member2.DeclaringType.Equals(t2.DeclaringType))
170                     {
171                         return t2.IsTypeParameterEquivalentToTypeInst(t1, member1);
172                     }
173 
174                     // If both of these are type params but didn't compare to be equal then one of them is likely bound to another
175                     // open type. Simply disallow such cases.
176                     return false;
177                 }
178 
179                 return t1.IsTypeParameterEquivalentToTypeInst(t2, member2);
180             }
181             else if (t2.IsGenericParameter)
182             {
183                 return t2.IsTypeParameterEquivalentToTypeInst(t1, member1);
184             }
185 
186             // Recurse in for generic types arrays, byref and pointer types.
187             if (t1.IsGenericType && t2.IsGenericType)
188             {
189                 var args1 = t1.GetGenericArguments();
190                 var args2 = t2.GetGenericArguments();
191 
192                 if (args1.Length == args2.Length)
193                 {
194                     return t1.IsGenericallyEqual(t2) &&
195                            Enumerable.All(Enumerable.Zip(args1, args2, (ta1, ta2) => ta1.IsGenericallyEquivalentTo(ta2, member1, member2)), x => x);
196                 }
197             }
198 
199             if (t1.IsArray && t2.IsArray)
200             {
201                 return t1.GetArrayRank() == t2.GetArrayRank() &&
202                        t1.GetElementType().IsGenericallyEquivalentTo(t2.GetElementType(), member1, member2);
203             }
204 
205             if ((t1.IsByRef && t2.IsByRef) ||
206                 (t1.IsPointer && t2.IsPointer))
207             {
208                 return t1.GetElementType().IsGenericallyEquivalentTo(t2.GetElementType(), member1, member2);
209             }
210 
211             return false;
212         }
213 
IsTypeParameterEquivalentToTypeInst(this Type typeParam, Type typeInst, MemberInfo member)214         private static bool IsTypeParameterEquivalentToTypeInst(this Type typeParam, Type typeInst, MemberInfo member)
215         {
216             Debug.Assert(typeParam.IsGenericParameter);
217 
218             if (typeParam.DeclaringMethod != null)
219             {
220                 // The type param is from a generic method. Since only methods can be generic, anything else
221                 // here means they are not equivalent.
222                 if (!(member is MethodBase))
223                 {
224                     return false;
225                 }
226 
227                 MethodBase method = (MethodBase)member;
228                 int position = typeParam.GenericParameterPosition;
229                 Type[] args = method.IsGenericMethod ? method.GetGenericArguments() : null;
230 
231                 return args != null &&
232                        args.Length > position &&
233                        args[position].Equals(typeInst);
234             }
235             else
236             {
237                 return member.DeclaringType.GetGenericArguments()[typeParam.GenericParameterPosition].Equals(typeInst);
238             }
239         }
240 
241         // s_MemberEquivalence will replace itself with one version or another
242         // depending on what works at run time
243         private static Func<MemberInfo, MemberInfo, bool> s_MemberEquivalence = (m1, m2) =>
244             {
245                 try
246                 {
247                     Type memberInfo = typeof(MemberInfo);
248 
249                     // First, try the actual API. (Post .NetCore 2.0) The api is the only one that gets it completely right on frameworks without MetadataToken.
250                     MethodInfo apiMethod = memberInfo.GetMethod(
251                         "HasSameMetadataDefinitionAs",
252                         BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.ExactBinding,
253                         binder: null,
254                         types: new Type[] { typeof(MemberInfo) },
255                         modifiers: null);
256                     if (apiMethod != null)
257                     {
258                         Func<MemberInfo, MemberInfo, bool> apiDelegate = (Func<MemberInfo, MemberInfo, bool>)(apiMethod.CreateDelegate(typeof(Func<MemberInfo, MemberInfo, bool>)));
259                         try
260                         {
261                             bool result = apiDelegate(m1, m2);
262                             // it worked, so publish it
263                             s_MemberEquivalence = apiDelegate;
264                             return result;
265                         }
266                         catch
267                         {
268                             // Api found but apparently stubbed as not supported. Continue on to the next fallback...
269                         }
270                     }
271 
272                     // See if MetadataToken property is available.
273                     PropertyInfo property = memberInfo.GetProperty("MetadataToken", typeof(int), Array.Empty<Type>());
274 
275                     if ((object)property != null && property.CanRead)
276                     {
277                         // (parameter1, parameter2) => parameter1.MetadataToken == parameter2.MetadataToken
278                         var parameter1 = Expression.Parameter(memberInfo);
279                         var parameter2 = Expression.Parameter(memberInfo);
280                         var memberEquivalence = Expression.Lambda<Func<MemberInfo, MemberInfo, bool>>(
281                             Expression.Equal(
282                                 Expression.Property(parameter1, property),
283                                 Expression.Property(parameter2, property)),
284                                 parameter1, parameter2).Compile();
285 
286                         var result = memberEquivalence(m1, m2);
287                         // it worked, so publish it
288                         s_MemberEquivalence = memberEquivalence;
289 
290                         return result;
291                     }
292                 }
293                 catch
294                 {
295                     // Platform might not allow access to the property
296                 }
297 
298                 // MetadataToken is not available in some contexts. Looks like this is one of those cases.
299                 // fallback to "IsEquivalentTo"
300                 Func<MemberInfo, MemberInfo, bool> fallbackMemberEquivalence = (m1param, m2param) => m1param.IsEquivalentTo(m2param);
301 
302                 // fallback must work
303                 s_MemberEquivalence = fallbackMemberEquivalence;
304                 return fallbackMemberEquivalence(m1, m2);
305             };
306 
HasSameMetadataDefinitionAs(this MemberInfo mi1, MemberInfo mi2)307         public static bool HasSameMetadataDefinitionAs(this MemberInfo mi1, MemberInfo mi2)
308         {
309 #if UNSUPPORTEDAPI
310             return (mi1.MetadataToken == mi2.MetadataToken) && (mi1.Module == mi2.Module));
311 #else
312             return mi1.Module.Equals(mi2.Module) && s_MemberEquivalence(mi1, mi2);
313 #endif
314         }
315 
GetIndexerName(this Type type)316         public static string GetIndexerName(this Type type)
317         {
318             Debug.Assert(type != null);
319             string name = GetTypeIndexerName(type);
320             if (name == null && type.IsInterface)
321             {
322                 foreach (Type iface in type.GetInterfaces())
323                 {
324                     name = GetTypeIndexerName(iface);
325                     if (name != null)
326                     {
327                         break;
328                     }
329                 }
330             }
331 
332             return name;
333         }
334 
GetTypeIndexerName(Type type)335         private static string GetTypeIndexerName(Type type)
336         {
337             Debug.Assert(type != null);
338             string name = type.GetCustomAttribute<DefaultMemberAttribute>()?.MemberName;
339             if (name != null)
340             {
341                 if (type.GetProperties(
342                         BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)
343                     .Any(p => p.Name == name && p.GetIndexParameters().Length != 0))
344                 {
345                     return name;
346                 }
347             }
348 
349             return null;
350         }
351     }
352 }
353