From de18fe04d2c076942c55c5f6face128fed3c9cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 12 Dec 2023 12:00:58 +0100 Subject: [PATCH 1/2] lambda inferrer: support custom delegates resolves #1602 --- .../Binding/ExpressionBuildingVisitor.cs | 90 +++++++++++-- .../Compilation/Inference/InfererContext.cs | 4 +- .../Inference/Inferers/LambdaInferer.cs | 27 ++-- .../Compilation/Inference/TypeInferer.cs | 12 +- .../Framework/Utils/ReflectionUtils.cs | 126 +++++++++++++++++- src/Tests/Binding/BindingCompilationTests.cs | 48 +++++++ src/Tests/Runtime/ReflectionUtilsTests.cs | 46 +++++++ 7 files changed, 310 insertions(+), 43 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 3d7e5b7e90..b6608569d5 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -457,6 +457,8 @@ protected override Expression VisitLambda(LambdaBindingParserNode node) for (var paramIndex = 0; paramIndex < typeInferenceData.Parameters!.Length; paramIndex++) { var currentParamType = typeInferenceData.Parameters[paramIndex]; + if (currentParamType.ContainsGenericParameters) + throw new BindingCompilationException($"Internal bug: lambda parameter still contains generic arguments: parameters[{paramIndex}] = {currentParamType.ToCode()}", node); node.ParameterExpressions[paramIndex].SetResolvedType(currentParamType); } } @@ -506,26 +508,90 @@ protected override Expression VisitLambdaParameter(LambdaParameterBindingParserN private Expression CreateLambdaExpression(Expression body, ParameterExpression[] parameters, Type? delegateType) { - if (delegateType != null && delegateType.Namespace == "System") + if (delegateType is null || delegateType == typeof(object) || delegateType == typeof(Delegate)) + // Assume delegate is a System.Func<...> + return Expression.Lambda(body, parameters); + + if (!delegateType.IsDelegate(out var invokeMethod)) + throw new DotvvmCompilationException($"Cannot create lambda function, type '{delegateType.ToCode()}' is not a delegate type."); + + if (invokeMethod.ReturnType == typeof(void)) + { + // We must validate that lambda body contains a valid statement + if ((body.NodeType != ExpressionType.Default) && (body.NodeType != ExpressionType.Block) && (body.NodeType != ExpressionType.Call) && (body.NodeType != ExpressionType.Assign)) + throw new DotvvmCompilationException($"Only method invocations and assignments can be used as statements."); + + // Make sure the result type will be void by adding an empty expression + body = Expression.Block(body, Expression.Empty()); + } + + // convert body result to the delegate return type + if (invokeMethod.ReturnType.ContainsGenericParameters) { - if (delegateType.Name == "Action" || delegateType.Name == $"Action`{parameters.Length}") + if (invokeMethod.ReturnType.IsGenericType) { - // We must validate that lambda body contains a valid statement - if ((body.NodeType != ExpressionType.Default) && (body.NodeType != ExpressionType.Block) && (body.NodeType != ExpressionType.Call) && (body.NodeType != ExpressionType.Assign)) - throw new DotvvmCompilationException($"Only method invocations and assignments can be used as statements."); + // no fancy implicit conversions are supported, only inheritance + if (!ReflectionUtils.IsAssignableToGenericType(body.Type, invokeMethod.ReturnType.GetGenericTypeDefinition(), out var bodyReturnType)) + { + throw new DotvvmCompilationException($"Cannot convert lambda function body of type '{body.Type.ToCode()}' to the delegate return type '{invokeMethod.ReturnType.ToCode()}'."); + } + else + { + body = Expression.Convert(body, bodyReturnType); + } + } + else + { + // fine, we will unify it in the next step - // Make sure the result type will be void by adding an empty expression - return Expression.Lambda(Expression.Block(body, Expression.Empty()), parameters); + // Some complex conversions like Tuple> -> Tuple> + // will fail, but we don't have to support everything } - else if (delegateType.Name == "Predicate`1") + } + else + { + body = TypeConversion.EnsureImplicitConversion(body, invokeMethod.ReturnType); + } + + if (delegateType.ContainsGenericParameters) + { + var delegateTypeDef = delegateType.GetGenericTypeDefinition(); + // The delegate is either purely generic (Func) or only some of the generic arguments are known (Func) + // initialize generic args with the already known types + var genericArgs = + delegateTypeDef.GetGenericArguments().Zip( + delegateType.GetGenericArguments(), + (param, argument) => new KeyValuePair(param, argument) + ) + .Where(p => p.Value != p.Key) + .ToDictionary(p => p.Key, p => p.Value); + + var delegateParameters = invokeMethod.GetParameters(); + for (int i = 0; i < parameters.Length; i++) + { + if (!ReflectionUtils.TryUnifyGenericTypes(delegateParameters[i].ParameterType, parameters[i].Type, genericArgs)) + { + throw new DotvvmCompilationException($"Could not match lambda function parameter '{parameters[i].Type.ToCode()} {parameters[i].Name}' to delegate parameter '{delegateParameters[i].ParameterType.ToCode()} {delegateParameters[i].Name}'."); + } + } + if (!ReflectionUtils.TryUnifyGenericTypes(invokeMethod.ReturnType, body.Type, genericArgs)) { - var type = delegateType.GetGenericTypeDefinition().MakeGenericType(parameters.Single().Type); - return Expression.Lambda(type, body, parameters); + throw new DotvvmCompilationException($"Could not match lambda function return type '{body.Type.ToCode()}' to delegate return type '{invokeMethod.ReturnType.ToCode()}'."); } + ReflectionUtils.ExpandUnifiedTypes(genericArgs); + + if (!delegateTypeDef.GetGenericArguments().All(a => genericArgs.TryGetValue(a, out var v) && !v.ContainsGenericParameters)) + { + var missingGenericArgs = delegateTypeDef.GetGenericArguments().Where(genericArg => !genericArgs.ContainsKey(genericArg) || genericArgs[genericArg].ContainsGenericParameters); + throw new DotvvmCompilationException($"Could not infer all generic arguments ({string.Join(", ", missingGenericArgs)}) of delegate type '{delegateType.ToCode()}' from lambda expression '({string.Join(", ", parameters.Select(p => $"{p.Type.ToCode()} {p.Name}"))}) => ...'."); + } + + delegateType = delegateTypeDef.MakeGenericType( + delegateTypeDef.GetGenericArguments().Select(genericParam => genericArgs[genericParam]).ToArray() + ); } - // Assume delegate is a System.Func<...> - return Expression.Lambda(body, parameters); + return Expression.Lambda(delegateType, body, parameters); } protected override Expression VisitBlock(BlockBindingParserNode node) diff --git a/src/Framework/Framework/Compilation/Inference/InfererContext.cs b/src/Framework/Framework/Compilation/Inference/InfererContext.cs index 9db0571946..c6f3fe4a6b 100644 --- a/src/Framework/Framework/Compilation/Inference/InfererContext.cs +++ b/src/Framework/Framework/Compilation/Inference/InfererContext.cs @@ -9,7 +9,7 @@ internal class InfererContext { public MethodGroupExpression? Target { get; set; } public Expression[] Arguments { get; set; } - public Dictionary Generics { get; set; } + public Dictionary Generics { get; set; } public int CurrentArgumentIndex { get; set; } public bool IsExtensionCall { get; set; } @@ -17,7 +17,7 @@ public InfererContext(MethodGroupExpression? target, int argsCount) { this.Target = target; this.Arguments = new Expression[argsCount]; - this.Generics = new Dictionary(); + this.Generics = new(); } } } diff --git a/src/Framework/Framework/Compilation/Inference/Inferers/LambdaInferer.cs b/src/Framework/Framework/Compilation/Inference/Inferers/LambdaInferer.cs index fc633b6cf6..fc060b06ed 100644 --- a/src/Framework/Framework/Compilation/Inference/Inferers/LambdaInferer.cs +++ b/src/Framework/Framework/Compilation/Inference/Inferers/LambdaInferer.cs @@ -94,39 +94,32 @@ private bool TryMatchDelegate(InfererContext? context, int argsCount, Type deleg if (delegateParameters.Length != argsCount) return false; - var generics = (context != null) ? context.Generics : new Dictionary(); - if (!TryInstantiateDelegateParameters(delegateType, argsCount, generics, out parameters)) + var generics = (context != null) ? context.Generics : new Dictionary(); + if (!TryInstantiateDelegateParameters(delegateParameters.Select(p => p.ParameterType).ToArray(), argsCount, generics, out parameters)) return false; return true; } - private bool TryInstantiateDelegateParameters(Type generic, int argsCount, Dictionary generics, [NotNullWhen(true)] out Type[]? instantiation) + private bool TryInstantiateDelegateParameters(Type[] delegateParameters, int argsCount, Dictionary generics, [NotNullWhen(true)] out Type[]? instantiation) { - var genericArgs = generic.GetGenericArguments(); var substitutions = new Type[argsCount]; for (var argIndex = 0; argIndex < argsCount; argIndex++) { - var currentArg = genericArgs[argIndex]; + var currentArg = delegateParameters[argIndex]; + var assignedArg = ReflectionUtils.AssignGenericParameters(currentArg, generics); - if (!currentArg.IsGenericParameter) - { - // This is a known type - substitutions[argIndex] = currentArg; - } - else if (currentArg.IsGenericParameter && generics.ContainsKey(currentArg.Name)) - { - // This is a generic parameter - // But we already inferred its type - substitutions[argIndex] = generics[currentArg.Name]; - } - else + if (assignedArg.ContainsGenericParameters) { // This is an unknown type instantiation = null; return false; } + else + { + substitutions[argIndex] = assignedArg; + } } instantiation = substitutions; diff --git a/src/Framework/Framework/Compilation/Inference/TypeInferer.cs b/src/Framework/Framework/Compilation/Inference/TypeInferer.cs index 396a15b017..c890a03a02 100644 --- a/src/Framework/Framework/Compilation/Inference/TypeInferer.cs +++ b/src/Framework/Framework/Compilation/Inference/TypeInferer.cs @@ -74,11 +74,11 @@ private void RefineCandidates(int index) return; var newCandidates = new List(); - var newInstantiations = new Dictionary>(); + var newInstantiations = new Dictionary>(); // Check if we can remove some candidates // Also try to infer generics based on provided argument - var tempInstantiations = new Dictionary(); + var tempInstantiations = new Dictionary(); foreach (var candidate in context.Target.Candidates!.Where(c => c.GetParameters().Length > index)) { tempInstantiations.Clear(); @@ -87,12 +87,12 @@ private void RefineCandidates(int index) if (parameterType.IsGenericParameter) { - tempInstantiations.Add(parameterType.Name, argumentType); + tempInstantiations.Add(parameterType, argumentType); } else if (parameterType.ContainsGenericParameters) { // Check if we already inferred instantiation for these generics - if (!parameterType.GetGenericArguments().Any(param => !context.Generics.ContainsKey(param.Name))) + if (!parameterType.GetGenericArguments().Any(param => !context.Generics.ContainsKey(param))) continue; // Try to infer instantiation based on given argument @@ -119,7 +119,7 @@ private void RefineCandidates(int index) context.Target.Candidates = newCandidates; } - private bool TryInferInstantiation(Type generic, Type concrete, Dictionary generics) + private bool TryInferInstantiation(Type generic, Type concrete, Dictionary generics) { if (generic == concrete) return true; @@ -127,7 +127,7 @@ private bool TryInferInstantiation(Type generic, Type concrete, Dictionary GetAllMethods(this Type type, BindingFlags /// public static bool IsAssignableToGenericType(this Type givenType, Type genericType, [NotNullWhen(returnValue: true)] out Type? commonType) { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) + if (genericType.IsInterface) { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) + var interfaceTypes = givenType.GetInterfaces(); + + foreach (var it in interfaceTypes) { - commonType = it; - return true; + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) + { + commonType = it; + return true; + } } } @@ -654,5 +657,116 @@ public static IEnumerable GetBaseTypesAndInterfaces(Type type) type = baseType; } } + + + internal static bool TryUnifyGenericTypes(Type a, Type b, Dictionary genericAssignment) + { + if (a == b) + return true; + + if (a.IsGenericParameter) + { + if (genericAssignment.ContainsKey(a)) + return TryUnifyGenericTypes(genericAssignment[a], b, genericAssignment); + + genericAssignment.Add(a, b); + return true; + } + else if (b.IsGenericParameter) + { + if (genericAssignment.ContainsKey(b)) + return TryUnifyGenericTypes(a, genericAssignment[b], genericAssignment); + + genericAssignment.Add(b, a); + return true; + } + else if (a.IsGenericType && b.IsGenericType) + { + if (a.GetGenericTypeDefinition() != b.GetGenericTypeDefinition()) + return false; + + var aArgs = a.GetGenericArguments(); + var bArgs = b.GetGenericArguments(); + if (aArgs.Length != bArgs.Length) + return false; + + for (var i = 0; i < aArgs.Length; i++) + { + if (!TryUnifyGenericTypes(aArgs[i], bArgs[i], genericAssignment)) + return false; + } + + return true; + } + else + { + return false; + } + } + + internal static void ExpandUnifiedTypes(Dictionary genericAssignment) + { + // var dirty = true; + var iteration = 0; + bool dirty; + do + { + dirty = false; + iteration++; + if (iteration > 100) + throw new Exception("Too much recursion in ExpandUnifiedTypes"); + + foreach (var (key, value) in genericAssignment.ToArray()) + { + var expanded = AssignGenericParameters(value, genericAssignment); + if (expanded != value) + { + genericAssignment[key] = expanded; + dirty = true; + } + } + } + while (dirty); + } + + internal static Type AssignGenericParameters(Type t, IReadOnlyDictionary genericAssignment) + { + if (!t.ContainsGenericParameters) + return t; + + if (t.IsGenericParameter) + { + if (genericAssignment.TryGetValue(t, out var result)) + return result; + else + return t; + } + else if (t.IsGenericType) + { + var args = t.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + { + args[i] = AssignGenericParameters(args[i], genericAssignment); + } + if (args.SequenceEqual(t.GetGenericArguments())) + return t; + else + return t.GetGenericTypeDefinition().MakeGenericType(args); + } + else if (t.HasElementType) + { + var el = AssignGenericParameters(t.GetElementType()!, genericAssignment); + if (el == t.GetElementType()) + return t; + else if (t.IsArray) + return el.MakeArrayType(t.GetArrayRank()); + else + throw new NotSupportedException(); + } + else + { + return t; + } + } } } diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 8ad41f60e9..7041302d53 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -23,6 +23,7 @@ using CheckTestOutput; using DotVVM.Framework.Tests.Runtime; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; namespace DotVVM.Framework.Tests.Binding { @@ -429,12 +430,29 @@ public void BindingCompiler_Valid_LambdaParameter_PreferFunc(string expr) [DataRow("DelegateInvoker2('string', (i, a) => StringProp = (i + a))", "with int", "0string")] public void BindingCompiler_Valid_LambdaParameter_TypeFromOtherArg(string expr, string expectedResult, string stringPropResult) { + // while(!Debugger.IsAttached) + // { + // System.Threading.Thread.Sleep(100); + // } + var viewModel = new TestLambdaCompilation(); var result = ExecuteBinding(expr, viewModel); Assert.AreEqual(expectedResult, result, message: "Result mismatch"); Assert.AreEqual(stringPropResult, viewModel.StringProp, message: "StringProp mismatch"); } + [TestMethod] + [DataRow("Enumerable.Repeat(LongArray, 3).SelectMany(l => l.AsEnumerable())", typeof(IEnumerable))] + [DataRow("Enumerable.Repeat(LongArray, 3).SelectMany(l => l)", typeof(IEnumerable))] + [DataRow("Enumerable.Repeat(LongArray, 3).SelectMany(l => l.ToList())", typeof(IEnumerable))] + // SelectMany expects IEnumerable return type, but it might be List or T[] + public void BindingCompiler_Valid_Lambda_PolymorphicReturnType(string expr, Type expectedType) + { + var viewModel = new TestViewModel(); + var result = ExecuteBinding(expr, viewModel); + XAssert.IsAssignableFrom(expectedType, result); + } + [TestMethod] [DataRow("(int? arg) => arg.Value + 1", typeof(Func))] [DataRow("(double? arg) => arg.Value + 0.1", typeof(Func))] @@ -471,6 +489,25 @@ public void BindingCompiler_Valid_LambdaParameter_CombinedTypeModifies(string ex Assert.AreEqual(type, result.GetType()); } + [DataTestMethod] + [DataRow("_this.CustomDelegateInvoker((string a, int b) => $'{a}-{b}')", "a-1")] + [DataRow("_this.CustomDelegateInvoker((a, b) => $'{a}-{b}')", "a-1")] + [DataRow("_this.CustomListDelegateInvoker((List as) => as.Select(a => a + 'vv'))", "avv,bvv")] + [DataRow("_this.CustomListDelegateInvoker((as) => as.Select(a => a + 'vv'))", "avv,bvv")] + [DataRow("_this.CustomGenericDelegateInvoker(1, (List as) => as.Select(a => a + 1))", "2,2")] + [DataRow("_this.CustomGenericDelegateInvoker(1, as => as.Select(a => a + 1))", "2,2")] + [DataRow("_this.CustomGenericDelegateInvoker(true, as => as.Select(a => !a))", "False,False")] + public void BindingCompiler_Valid_LambdaParameter_CustomDelegate(string expr, string expectedResult) + { + // while(!Debugger.IsAttached) + // { + // System.Threading.Thread.Sleep(100); + // } + var viewModel = new TestLambdaCompilation(); + var result = ExecuteBinding(expr, viewModel); + Assert.AreEqual(expectedResult, result); + } + [TestMethod] [DataRow("(string? arg) => arg")] [DataRow("(int[]? arg) => arg")] @@ -1393,6 +1430,17 @@ class TestLambdaCompilation public string DelegateInvoker2(T x, Action func) { func(x); return "plain"; } public string DelegateInvoker2(T x, Action action) { action(0, x); return "with int"; } + + public delegate string CustomDelegate(string a, int b); + + public string CustomDelegateInvoker(CustomDelegate func) => func("a", 1); + + public delegate IEnumerable CustomGenericDelegate(List a); + + public string CustomListDelegateInvoker(CustomGenericDelegate func) => + string.Join(",", func(new List() { "a", "b" })); + public string CustomGenericDelegateInvoker(T item, CustomGenericDelegate func) => + string.Join(",", func(new List() { item, item })); } class TestViewModel2 diff --git a/src/Tests/Runtime/ReflectionUtilsTests.cs b/src/Tests/Runtime/ReflectionUtilsTests.cs index b5cf6e51bf..478da83892 100644 --- a/src/Tests/Runtime/ReflectionUtilsTests.cs +++ b/src/Tests/Runtime/ReflectionUtilsTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Reflection; using DotVVM.Framework.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -21,5 +22,50 @@ public void UnwrapTaskTypeTest(Type taskType, Type type) var actualType = ReflectionUtils.UnwrapTaskType(taskType); Assert.AreEqual(actualType, type); } + + + [DataTestMethod] + [UnificationDataSource] + public void TypeUnificationTest(Type a, Type b, Type[] expectedGenericTypes) + { + var unifiedTypes = new Dictionary(); + Assert.IsTrue(ReflectionUtils.TryUnifyGenericTypes(a, b, unifiedTypes)); + + // map dictionary to array of method generic arguments + var realResults = new Type[expectedGenericTypes.Length]; + foreach (var t in unifiedTypes) + realResults[t.Key.GenericParameterPosition] = t.Value; + + // null-out generic types which are expected to stay generic + for (int i = 0; i < expectedGenericTypes.Length; i++) + if (expectedGenericTypes[i].IsGenericParameter && expectedGenericTypes[i].GenericParameterPosition == i) + expectedGenericTypes[i] = null; + XAssert.Equal(expectedGenericTypes, realResults); + } + + class UnificationDataSource : Attribute, ITestDataSource + { + public IEnumerable GetData(MethodInfo methodInfo) => + from m in typeof(UnificationDataSource).GetMethods(BindingFlags.Public | BindingFlags.Instance) + where m.Name.StartsWith("Test") + let parameters = m.GetParameters() + select new object[] { + parameters[0].ParameterType, + parameters[1].ParameterType, + parameters.Skip(2).Select(p => p.ParameterType).ToArray() + }; + public string GetDisplayName(MethodInfo methodInfo, object[] data) => + $"{methodInfo.Name}({string.Join(", ", data.Select(d => d.ToString()))})"; + + // Test cases: first two arguments are unified together, + // the rest are the expected types unified into the type arguments + public void Test0(T a, string b, string expected) { } + public void Test1(List a, List b, string expected) { } + public void Test2(Func a, Func b, int expected, string expected2) { } + public void TestPartial0(T a, T b, T expected) { } + public void TestPartial1(Func a, Func b, T expected, string expected2) { } + public void TestPartial2(Tuple a, U b, T expected, Tuple expected2) { } + + } } } From dce21575424543dadcea8fbabff9d57ffbb3dc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 17 Jan 2024 10:30:24 +0100 Subject: [PATCH 2/2] Add test for type unification with the same generic type multiple times --- src/Framework/Framework/Utils/ReflectionUtils.cs | 1 - src/Tests/Binding/BindingCompilationTests.cs | 9 --------- src/Tests/Runtime/ReflectionUtilsTests.cs | 3 +++ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 409fbee33c..fc2a747af4 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -706,7 +706,6 @@ internal static bool TryUnifyGenericTypes(Type a, Type b, Dictionary internal static void ExpandUnifiedTypes(Dictionary genericAssignment) { - // var dirty = true; var iteration = 0; bool dirty; do diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 7041302d53..135cd18363 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -430,11 +430,6 @@ public void BindingCompiler_Valid_LambdaParameter_PreferFunc(string expr) [DataRow("DelegateInvoker2('string', (i, a) => StringProp = (i + a))", "with int", "0string")] public void BindingCompiler_Valid_LambdaParameter_TypeFromOtherArg(string expr, string expectedResult, string stringPropResult) { - // while(!Debugger.IsAttached) - // { - // System.Threading.Thread.Sleep(100); - // } - var viewModel = new TestLambdaCompilation(); var result = ExecuteBinding(expr, viewModel); Assert.AreEqual(expectedResult, result, message: "Result mismatch"); @@ -499,10 +494,6 @@ public void BindingCompiler_Valid_LambdaParameter_CombinedTypeModifies(string ex [DataRow("_this.CustomGenericDelegateInvoker(true, as => as.Select(a => !a))", "False,False")] public void BindingCompiler_Valid_LambdaParameter_CustomDelegate(string expr, string expectedResult) { - // while(!Debugger.IsAttached) - // { - // System.Threading.Thread.Sleep(100); - // } var viewModel = new TestLambdaCompilation(); var result = ExecuteBinding(expr, viewModel); Assert.AreEqual(expectedResult, result); diff --git a/src/Tests/Runtime/ReflectionUtilsTests.cs b/src/Tests/Runtime/ReflectionUtilsTests.cs index 478da83892..048ca01da1 100644 --- a/src/Tests/Runtime/ReflectionUtilsTests.cs +++ b/src/Tests/Runtime/ReflectionUtilsTests.cs @@ -62,6 +62,9 @@ public string GetDisplayName(MethodInfo methodInfo, object[] data) => public void Test0(T a, string b, string expected) { } public void Test1(List a, List b, string expected) { } public void Test2(Func a, Func b, int expected, string expected2) { } + public void TestTypeUsedMultipleTime0(Func> a, Func b, Func expected) { } + public void TestTypeUsedMultipleTime1(Func> a, Func> b, int expected) { } + public void TestTypeUsedMultipleTime2((IEnumerable, IEnumerable<(int, int)>) a, (IEnumerable>, IEnumerable) b, IEnumerable expected, (int, int) expected2) { } public void TestPartial0(T a, T b, T expected) { } public void TestPartial1(Func a, Func b, T expected, string expected2) { } public void TestPartial2(Tuple a, U b, T expected, Tuple expected2) { }