Cleanup TypeHelper, add tests - IsAssignableFromGeneric may have issues?

This commit is contained in:
Stephan
2015-02-04 17:04:40 +01:00
parent c4c21b8b6e
commit c8087d1f68
3 changed files with 225 additions and 81 deletions

View File

@@ -330,94 +330,99 @@ namespace Umbraco.Core
//TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric
private static void ReduceGenericParameterCandidateTypes(ICollection<Type> allStuff, Type type)
// readings:
// http://stackoverflow.com/questions/2033912/c-sharp-variance-problem-assigning-listderived-as-listbase
// http://stackoverflow.com/questions/2208043/generic-variance-in-c-sharp-4-0
// http://stackoverflow.com/questions/8401738/c-sharp-casting-generics-covariance-and-contravariance
// http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class
// http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059
private static bool MatchGeneric(Type implementation, Type contract, IDictionary<string, Type> bindings)
{
var at1 = new List<Type>();
var t = type;
while (t != null)
// trying to match eg List<int> with List<T>
// or List<List<List<int>>> with List<ListList<T>>>
// classes are NOT invariant so List<string> does not match List<object>
if (implementation.IsGenericType == false) return false;
// must have the same generic type definition
var implDef = implementation.GetGenericTypeDefinition();
var contDef = contract.GetGenericTypeDefinition();
if (implDef != contDef) return false;
// must have the same number of generic arguments
var implArgs = implementation.GetGenericArguments();
var contArgs = contract.GetGenericArguments();
if (implArgs.Length != contArgs.Length) return false;
// generic arguments must match
// in insta we should have actual types (eg int, string...)
// in typea we can have generic parameters (eg <T>)
for (var i = 0; i < implArgs.Length; i++)
{
at1.Add(t);
t = t.BaseType;
}
var r = allStuff.Where(x => x.IsClass && at1.Contains(x) == false).ToArray();
foreach (var x in r) allStuff.Remove(x);
var ai1 = type.GetInterfaces();
if (type.IsInterface) ai1 = ai1.Union(new[] { type }).ToArray();
r = allStuff.Where(x => x.IsInterface && ai1.Contains(x) == false).ToArray();
foreach (var x in r) allStuff.Remove(x);
}
private static bool MatchGeneric(Type inst, Type type, IDictionary<string, List<Type>> bindings)
{
if (inst.IsGenericType == false) return false;
var instd = inst.GetGenericTypeDefinition();
var typed = type.GetGenericTypeDefinition();
if (instd != typed) return false;
var insta = inst.GetGenericArguments();
var typea = type.GetGenericArguments();
if (insta.Length != typea.Length) return false;
// but... there is no ZipWhile, and we have arrays anyway
//var x = insta.Zip<Type, Type, bool>(typea, (instax, typeax) => { ... });
for (var i = 0; i < insta.Length; i++)
if (MatchType(insta[i], typea[i], bindings) == false)
const bool variance = false; // classes are NOT invariant
if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false)
return false;
}
return true;
}
private static IEnumerable<Type> GetGenericParameterCandidateTypes(Type type)
public static bool MatchType(Type implementation, Type contract)
{
yield return type;
var t = type.BaseType;
while (t != null)
{
yield return t;
t = t.BaseType;
}
foreach (var i in type.GetInterfaces())
yield return i;
return MatchType(implementation, contract, new Dictionary<string, Type>());
}
public static bool MatchType(Type inst, Type type)
internal static bool MatchType(Type implementation, Type contract, IDictionary<string, Type> bindings, bool variance = true)
{
return MatchType(inst, type, new Dictionary<string, List<Type>>());
}
internal static bool MatchType(Type inst, Type type, IDictionary<string, List<Type>> bindings)
{
if (type.IsGenericType)
if (contract.IsGenericType)
{
if (MatchGeneric(inst, type, bindings)) return true;
var t = inst.BaseType;
// eg type is List<int> or List<T>
// if we have variance then List<int> can match IList<T>
// if we don't have variance it can't - must have exact type
// try to match implementation against contract
if (MatchGeneric(implementation, contract, bindings)) return true;
// if no variance, fail
if (variance == false) return false;
// try to match an ancestor of implementation against contract
var t = implementation.BaseType;
while (t != null)
{
if (MatchGeneric(t, type, bindings)) return true;
if (MatchGeneric(t, contract, bindings)) return true;
t = t.BaseType;
}
return inst.GetInterfaces().Any(i => MatchGeneric(i, type, bindings));
// try to match an interface of implementation against contract
return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings));
}
if (type.IsGenericParameter)
if (contract.IsGenericParameter)
{
if (bindings.ContainsKey(type.Name))
// eg <T>
if (bindings.ContainsKey(contract.Name))
{
ReduceGenericParameterCandidateTypes(bindings[type.Name], inst);
return bindings[type.Name].Count > 0;
// already bound: ensure it's compatible
return bindings[contract.Name] == implementation;
}
bindings[type.Name] = new List<Type>(GetGenericParameterCandidateTypes(inst));
// not already bound: bind
bindings[contract.Name] = implementation;
return true;
}
if (inst == type) return true;
if (type.IsClass && inst.IsClass && inst.IsSubclassOf(type)) return true;
if (type.IsInterface && inst.GetInterfaces().Contains(type)) return true;
// not a generic type, not a generic parameter
// so normal class or interface
// fixme structs? enums? array types?
// about primitive types, value types, etc:
// http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class
if (implementation == contract) return true;
if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) return true;
if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) return true;
return false;
}

View File

@@ -29,6 +29,7 @@ namespace Umbraco.Tests.DynamicsAndReflection
public void TestMethod6(int value) { }
public void TestMethod6(string value) { }
public void TestMethod7<T>(IList<T> value) { }
public void TestMethod8<T>(IDictionary<T, T> value) { }
public interface ITestDict<T> : IDictionary<T, T> { }

View File

@@ -75,8 +75,7 @@ namespace Umbraco.Tests.Plugins
.Single()
.ParameterType;
Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(genericEnumerableNonGenericDefinition, typeof(List<int>)));
Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(genericEnumerableNonGenericDefinition, typeof(List<int>)));
}
class Base<T> { }
@@ -144,47 +143,186 @@ namespace Umbraco.Tests.Plugins
[Test]
public void MatchTypesTest()
{
var bindings = new Dictionary<string, List<Type>>();
var bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(int), typeof(int), bindings));
Assert.AreEqual(0, bindings.Count);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsFalse(TypeHelper.MatchType(typeof(int), typeof(string), bindings));
Assert.AreEqual(0, bindings.Count);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(List<int>), typeof(System.Collections.IEnumerable), bindings));
Assert.AreEqual(0, bindings.Count);
var m = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod7");
var t1 = m.GetParameters()[0].ParameterType; // List<T>
var t2 = m.GetParameters()[0].ParameterType.GetGenericArguments()[0]; // <T>
var t1 = typeof(IList<>); // IList<>
var a1 = t1.GetGenericArguments()[0]; // <T>
t1 = t1.MakeGenericType(a1); // IList<T>
var t2 = a1;
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(int), t2, bindings));
Assert.AreEqual(1, bindings.Count);
Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault());
Assert.AreEqual(typeof(int), bindings["T"]);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(IList<int>), t1, bindings));
Assert.AreEqual(1, bindings.Count);
Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault());
Assert.AreEqual(typeof(int), bindings["T"]);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(List<int>), typeof(IList<int>), bindings));
Assert.AreEqual(0, bindings.Count);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(List<int>), t1, bindings));
Assert.AreEqual(1, bindings.Count);
Assert.AreEqual(typeof(int), bindings["T"].FirstOrDefault());
Assert.AreEqual(typeof(int), bindings["T"]);
bindings = new Dictionary<string, List<Type>>();
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(Dictionary<int, string>), typeof(IDictionary<,>), bindings));
Assert.AreEqual(2, bindings.Count);
Assert.AreEqual(typeof(int), bindings["TKey"].FirstOrDefault());
Assert.AreEqual(typeof(string), bindings["TValue"].FirstOrDefault());
Assert.AreEqual(typeof(int), bindings["TKey"]);
Assert.AreEqual(typeof(string), bindings["TValue"]);
t1 = typeof(IDictionary<,>); // IDictionary<,>
a1 = t1.GetGenericArguments()[0]; // <TKey>
t1 = t1.MakeGenericType(a1, a1); // IDictionary<TKey,TKey>
bindings = new Dictionary<string, Type>();
Assert.IsFalse(TypeHelper.MatchType(typeof(Dictionary<int, string>), t1, bindings));
bindings = new Dictionary<string, Type>();
Assert.IsTrue(TypeHelper.MatchType(typeof(Dictionary<int, int>), t1, bindings));
Assert.AreEqual(1, bindings.Count);
Assert.AreEqual(typeof(int), bindings["TKey"]);
}
[Test]
public void MatchType_Vs_IsAssignableFromGeneric()
{
// both are OK
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(List<int>), typeof(IEnumerable<>));
var t1 = typeof (IDictionary<,>); // IDictionary<,>
var a1 = t1.GetGenericArguments()[0];
t1 = t1.MakeGenericType(a1, a1); // IDictionary<T,T>
// both are OK
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(Dictionary<int, int>), t1);
// TODO FIXME - IsAssignableFromGeneric returns true! does not manage generic parameters binding
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(Dictionary<int, string>), t1);
// these are all of there from Is_Assignable_To_Generic_Type
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(Derived<int>), typeof(Base<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(List<int>), typeof(IEnumerable<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(Derived<int>), typeof(Derived<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(Derived2<int>), typeof(Base<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(DerivedI<int>), typeof(IBase<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(Derived2<int>), typeof(IBase<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(int?), typeof(Nullable<>));
// TODO FIXME - those that are marked below should actually be (true,...) but were false in Is_Assignable_To_Generic_Type
// TODO FIXME - why would IsAssignableFromGeneric returns false? Derived<T> inherits from Object
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(Derived<int>), typeof(Object));
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(Derived<int>), typeof(List<>));
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(Derived<int>), typeof(IEnumerable<>));
// TODO FIXME - why would IsAssignableFromGeneric returns false? Derived<T> inherits from Base<T>
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(Derived<int>), typeof(Base<int>));
// TODO FIXME - why would IsAssignableFromGeneric returns false? List<T> implements IEnumerable<T>
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(List<int>), typeof(IEnumerable<int>));
Assert_MatchType_Vs_IsAssignableFromGeneric(false, typeof(int), typeof(Nullable<>));
//This get's the "Type" from the Count extension method on IEnumerable<T>, however the type IEnumerable<T> isn't
// IEnumerable<> and it is not a generic definition, this attempts to explain that:
// http://blogs.msdn.com/b/haibo_luo/archive/2006/02/17/534480.aspx
var genericEnumerableNonGenericDefinition = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
.Single(x => x.Name == "Count" && x.GetParameters().Count() == 1)
.GetParameters()
.Single()
.ParameterType;
Assert_MatchType_Vs_IsAssignableFromGeneric(true, typeof(List<int>), genericEnumerableNonGenericDefinition);
}
private void Assert_MatchType_Vs_IsAssignableFromGeneric(bool expected, Type implementation, Type contract)
{
if (expected)
{
Assert.IsTrue(TypeHelper.MatchType(implementation, contract));
Assert.IsTrue(TypeHelper.IsAssignableFromGeneric(contract, implementation));
}
else
{
Assert.IsFalse(TypeHelper.MatchType(implementation, contract));
Assert.IsFalse(TypeHelper.IsAssignableFromGeneric(contract, implementation));
}
}
[Test]
public void CreateOpenGenericTypes()
{
// readings
// http://stackoverflow.com/questions/13466078/create-open-constructed-type-from-string
// http://stackoverflow.com/questions/6704722/c-sharp-language-how-to-get-type-of-bound-but-open-generic-class
// note that FullName returns "The fully qualified name of the type, including its namespace but not its
// assembly; or null if the current instance represents a generic type parameter, an array type, pointer
// type, or byref type based on a type parameter, or a generic type that is not a generic type definition
// but contains unresolved type parameters."
var t = Type.GetType("System.Collections.Generic.IList`1");
Assert.IsNotNull(t);
Assert.IsTrue(t.IsGenericTypeDefinition);
Assert.AreEqual("IList`1", t.Name);
Assert.AreEqual("System.Collections.Generic.IList`1", t.FullName);
Assert.AreEqual("System.Collections.Generic.IList`1[T]", t.ToString());
t = typeof (IList<>);
Assert.IsTrue(t.IsGenericTypeDefinition);
Assert.AreEqual("IList`1", t.Name);
Assert.AreEqual("System.Collections.Generic.IList`1", t.FullName);
Assert.AreEqual("System.Collections.Generic.IList`1[T]", t.ToString());
t = typeof(IDictionary<,>);
Assert.IsTrue(t.IsGenericTypeDefinition);
Assert.AreEqual("IDictionary`2", t.Name);
Assert.AreEqual("System.Collections.Generic.IDictionary`2", t.FullName);
Assert.AreEqual("System.Collections.Generic.IDictionary`2[TKey,TValue]", t.ToString());
t = typeof(IDictionary<,>);
t = t.MakeGenericType(t.GetGenericArguments()[0], t.GetGenericArguments()[1]);
Assert.AreEqual("IDictionary`2", t.Name);
Assert.AreEqual("System.Collections.Generic.IDictionary`2", t.FullName);
Assert.AreEqual("System.Collections.Generic.IDictionary`2[TKey,TValue]", t.ToString());
t = typeof(IDictionary<,>);
t = t.MakeGenericType(t.GetGenericArguments()[0], t.GetGenericArguments()[0]);
Assert.IsFalse(t.IsGenericTypeDefinition); // not anymore
Assert.AreEqual("IDictionary`2", t.Name);
Assert.IsNull(t.FullName); // see note above
Assert.AreEqual("System.Collections.Generic.IDictionary`2[TKey,TKey]", t.ToString());
t = typeof(IList<>);
Assert.IsTrue(t.IsGenericTypeDefinition);
t = t.MakeGenericType(t.GetGenericArguments()[0]);
Assert.AreEqual("IList`1", t.Name);
Assert.AreEqual("System.Collections.Generic.IList`1", t.FullName);
Assert.AreEqual("System.Collections.Generic.IList`1[T]", t.ToString());
t = typeof(IList<int>);
Assert.AreEqual("System.Collections.Generic.IList`1[System.Int32]", t.ToString());
t = typeof (IDictionary<,>);
t = t.MakeGenericType(typeof(int), t.GetGenericArguments()[1]);
Assert.IsFalse(t.IsGenericTypeDefinition); // not anymore
Assert.AreEqual("IDictionary`2", t.Name);
Assert.IsNull(t.FullName); // see note above
Assert.AreEqual("System.Collections.Generic.IDictionary`2[System.Int32,TValue]", t.ToString());
}
}
}