Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive resolving of generic arguments types in registration of methods in javascript translation #892

Merged
merged 16 commits into from Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 132 additions & 0 deletions src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs
Expand Up @@ -2,16 +2,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Runtime.InteropServices.ComTypes;
using DotVVM.Framework.Compilation.Binding;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Utils;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CSharp.RuntimeBinder;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DotVVM.Framework.Tests.Common.Binding
{
[TestClass]
public class ExpressionHelperTests
{
public TestContext TestContext { get; set; }

[TestMethod]
public void UpdateMember_GetValue()
{
Expand Down Expand Up @@ -40,5 +46,131 @@ public void UpdateMember_ReadOnlyProperty()
var updateExpr = ExpressionHelper.UpdateMember(ExpressionUtils.Replace((Tests.Binding.TestViewModel c) => c.LongArray, vmP), newValueP);
Assert.IsNull(updateExpr);
}

[TestMethod]
[DataRow(typeof(GenericTestResult1), typeof(int), new Type[0])]
[DataRow(typeof(GenericTestResult2), typeof(Uri), new Type[] { typeof(Uri) })]
[DataRow(typeof(GenericTestResult3), typeof(object), new Type[0])]
[DataRow(typeof(GenericTestResult5), typeof(GenericModelSampleObject<int>), new Type[0])]
public void Call_FindOverload_Generic_FirstLevel(Type resultIdentifierType, Type argType, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject), MethodsGenericArgumentsResolvingSampleObject.MethodName, new[] { argType }, resultIdentifierType, expectedGenericArgs);
}

private static void Call_FindOverload_Generic(Type targetType, string methodName, Type[] argTypes, Type resultIdentifierType, Type[] expectedGenericArgs)
{
Expression target = new MethodGroupExpression() {
MethodName = methodName,
Target = new StaticClassIdentifierExpression(targetType)
};

var j = 0;
var arguments = argTypes.Select(s => Expression.Parameter(s, $"param_{j++}")).ToArray();
var expression = ExpressionHelper.Call(target, arguments) as MethodCallExpression;
Assert.IsNotNull(expression);
Assert.AreEqual(resultIdentifierType, expression.Method.GetResultType());

var args = expression.Method.GetGenericArguments();
for (int i = 0; i < args.Length; i++)
{
Assert.AreEqual(expectedGenericArgs[i], args[i], message: "Order of resolved generic types is different then expected.");
}
}

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
[DataRow(typeof(GenericModelSampleObject<string>), typeof(GenericTestResult4), new Type[] { typeof(string) })]
public void Call_FindOverload_Generic_Ambiguous(Type argType, Type resultIdentifierType, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject), MethodsGenericArgumentsResolvingSampleObject.MethodName, new[] { argType }, resultIdentifierType, expectedGenericArgs);
}


[TestMethod]
[DataRow(typeof(GenericTestResult2), new Type[] { typeof(GenericModelSampleObject<GenericModelSampleObject<string>>), typeof(int) }, new Type[] { typeof(int), typeof(string) })]
public void Call_FindOverload_Generic_Ambiguous_Recursive(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject2), MethodsGenericArgumentsResolvingSampleObject2.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
[DataRow(typeof(GenericTestResult1), new Type[] { typeof(GenericModelSampleObject<GenericModelSampleObject<string>>), typeof(GenericModelSampleObject<GenericModelSampleObject<int>>) }, new Type[] { typeof(string), typeof(int) })]
public void Call_FindOverload_Generic_Ambiguous_Recursive_CannotDetermineResult(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
// This call should probably return method with result GenericTestResult1, but there is no certainty that it is what c# would resulted.
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject2), MethodsGenericArgumentsResolvingSampleObject2.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}

[TestMethod]
[DataRow(typeof(GenericTestResult1), new Type[] { typeof(int), typeof(string) }, new Type[] { typeof(string), typeof(int) })]
public void Call_FindOverload_Generic_Order_FirstLevel(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject3), MethodsGenericArgumentsResolvingSampleObject3.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}

[TestMethod]
[DataRow(typeof(GenericTestResult1), new Type[] { typeof(int[]), typeof(string[]) }, new Type[] { typeof(string[]), typeof(int[]) })]
public void Call_FindOverload_Generic_Array_Order(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject3), MethodsGenericArgumentsResolvingSampleObject3.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}
[TestMethod]
[DataRow(typeof(int[]), new Type[] { typeof(int[]) }, new Type[] { typeof(int) })]
public void Call_FindOverload_Generic_Array(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject4), MethodsGenericArgumentsResolvingSampleObject4.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}
[TestMethod]
[DataRow(typeof(GenericTestResult1), new Type[] { typeof(GenericModelSampleObject<int[]>) }, new Type[] { typeof(int) })]
[DataRow(typeof(GenericTestResult2), new Type[] { typeof(List<int>[]) }, new Type[] { typeof(int) })]
public void Call_FindOverload_Generic_Array_Recursive(Type resultIdentifierType, Type[] argTypes, Type[] expectedGenericArgs)
{
Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject5), MethodsGenericArgumentsResolvingSampleObject5.MethodName, argTypes, resultIdentifierType, expectedGenericArgs);
}
}

public static class MethodsGenericArgumentsResolvingSampleObject5
{
public const string MethodName = nameof(TestMethod);
public static GenericTestResult1 TestMethod<T1>(GenericModelSampleObject<T1[]> a) => null;
public static GenericTestResult2 TestMethod<T1>(List<T1>[] a) => null;
}
public static class MethodsGenericArgumentsResolvingSampleObject4
{
public const string MethodName = nameof(TestMethod);
public static T1[] TestMethod<T1>(T1[] a) => null;
}
public static class MethodsGenericArgumentsResolvingSampleObject3
{
public const string MethodName = nameof(TestMethod);
public static GenericTestResult1 TestMethod<T1, T2>(T2 a, T1 b) => null;
}
public static class MethodsGenericArgumentsResolvingSampleObject2
{
public const string MethodName = nameof(TestMethod);
public static GenericTestResult1 TestMethod<T1, T2>(GenericModelSampleObject<GenericModelSampleObject<T1>> a, GenericModelSampleObject<GenericModelSampleObject<T2>> b) => null;
public static GenericTestResult2 TestMethod<T2, T1>(GenericModelSampleObject<GenericModelSampleObject<T1>> a, T2 b) => null;
}
public static class MethodsGenericArgumentsResolvingSampleObject
{
public const string MethodName = nameof(TestMethod);
public static GenericTestResult1 TestMethod(int a) => null;
public static GenericTestResult2 TestMethod<T>(T a) => null;
public static GenericTestResult3 TestMethod(object a) => null;
public static GenericTestResult4 TestMethod<T>(GenericModelSampleObject<T> a) => null;
public static GenericTestResult5 TestMethod(GenericModelSampleObject<int> a) => null;
}

public class GenericModelSampleObject<T>
{
public T Prop { get; set; }
}

public class GenericTestResult1 { }
public class GenericTestResult2 { }
public class GenericTestResult3 { }
public class GenericTestResult4 { }
public class GenericTestResult5 { }

}
61 changes: 52 additions & 9 deletions src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs
Expand Up @@ -5,11 +5,14 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Runtime;
using DotVVM.Framework.Utils;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CSharp.RuntimeBinder;

namespace DotVVM.Framework.Compilation.Binding
Expand Down Expand Up @@ -158,10 +161,22 @@ public static Expression CallMethod(Type target, BindingFlags flags, string name

private static MethodRecognitionResult FindValidMethodOveloads(Type type, string name, BindingFlags flags, Type[] typeArguments, Expression[] arguments, IDictionary<string, Expression> namedArgs)
{
var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType<MethodInfo>().Where(m => m.Name == name), typeArguments, arguments, namedArgs);
var method = methods.FirstOrDefault();
if (method == null) throw new InvalidOperationException($"Could not find overload of method '{name}'.");
return method;
var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType<MethodInfo>().Where(m => m.Name == name), typeArguments, arguments, namedArgs).ToList();

if (methods.Count == 1) return methods.FirstOrDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: This special case is not needed.

if (methods.Count == 0) throw new InvalidOperationException($"Could not find overload of method '{name}'.");
else
{
methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ToList();
var method = methods.FirstOrDefault();
var method2 = methods.Skip(1).FirstOrDefault();
if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount)
{
// TODO: this behavior is not completed. Implement the same behavior as in roslyn.
throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'.");
}
return method;
}
}

private static IEnumerable<MethodRecognitionResult> FindValidMethodOveloads(IEnumerable<MethodInfo> methods, Type[] typeArguments, Expression[] arguments, IDictionary<string, Expression> namedArgs)
Expand Down Expand Up @@ -210,20 +225,21 @@ private static MethodRecognitionResult TryCallMethod(MethodInfo method, Type[] t
// resolve generic parameters
if (method.ContainsGenericParameters)
{
var typeArgs = new Type[method.GetGenericArguments().Length];
var genericArguments = method.GetGenericArguments();
var typeArgs = new Type[genericArguments.Length];
if (typeArguments != null)
{
if (typeArguments.Length > typeArgs.Length) return null;
Array.Copy(typeArguments, typeArgs, typeArgs.Length);
}
for (int i = 0; i < typeArgs.Length; i++)
for (int genericArgumentPosition = 0; genericArgumentPosition < typeArgs.Length; genericArgumentPosition++)
{
if (typeArgs[i] == null)
if (typeArgs[genericArgumentPosition] == null)
{
// try to resolve from arguments
var arg = Array.FindIndex(parameters, p => p.ParameterType.IsGenericParameter && p.ParameterType.GenericParameterPosition == i);
var argType = GetGenericParameterType(genericArguments[genericArgumentPosition], parameters.Select(s => s.ParameterType).ToArray(), args.Select(s => s.Type).ToArray());
automaticTypeArgs++;
if (arg >= 0) typeArgs[i] = args[arg].Type;
if (argType != null) typeArgs[genericArgumentPosition] = argType;
else return null;
}
}
Expand Down Expand Up @@ -252,6 +268,33 @@ private static MethodRecognitionResult TryCallMethod(MethodInfo method, Type[] t
};
}

private static Type GetGenericParameterType(Type genericArg, Type[] searchedGenericTypes, Type[] expressionTypes)
{
for (var i = 0; i < searchedGenericTypes.Length; i++)
{
if (expressionTypes.Length <= i) return null;
var sgt = searchedGenericTypes[i];
if (sgt == genericArg)
{
return expressionTypes[i];
}
if (sgt.IsArray)
{
var elementType = sgt.GetElementType();
var expressionElementType = expressionTypes[i].GetElementType();
if (elementType == genericArg)
return expressionElementType;
else
return GetGenericParameterType(genericArg, searchedGenericTypes[i].GetGenericArguments(), expressionTypes[i].GetGenericArguments());
}
else if (sgt.IsGenericType)
{
var value = GetGenericParameterType(genericArg, sgt.GetGenericArguments(), expressionTypes[i].GetGenericArguments());
if (value is Type) return value;
}
}
return null;
}

public static Expression EqualsMethod(Expression left, Expression right)
{
Expand Down
1 change: 1 addition & 0 deletions src/DotVVM.Samples.Common/DotVVM.Samples.Common.csproj
Expand Up @@ -49,6 +49,7 @@
<None Remove="Views\Errors\InvalidLocationFallback.dothtml" />
<None Remove="Views\Errors\ResourceCircularDependency.dothtml" />
<None Remove="Views\FeatureSamples\Caching\CachedValues.dothtml" />
<None Remove="Views\FeatureSamples\JavascriptTranslation\GenericMethodTranslation.dothtml" />
<None Remove="Views\FeatureSamples\Localization\Globalize.dothtml" />
<None Remove="Views\FeatureSamples\PostbackConcurrency\RedirectPostbackQueue.dothtml" />
<None Remove="Views\FeatureSamples\PostbackConcurrency\StressTest.dothtml" />
Expand Down
12 changes: 12 additions & 0 deletions src/DotVVM.Samples.Common/DotvvmStartup.cs
Expand Up @@ -22,6 +22,10 @@
using DotVVM.Samples.Common.Api.AspNetCore;
using DotVVM.Samples.Common.Api.Owin;
using DotVVM.Samples.Common.Controls;
using DotVVM.Framework.Utils;
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Compilation.Javascript.Ast;
using DotVVM.Samples.Common.ViewModels.FeatureSamples.JavascriptTranslation;

namespace DotVVM.Samples.BasicSamples
{
Expand Down Expand Up @@ -55,6 +59,14 @@ public void Configure(DotvvmConfiguration config, string applicationPath)
config.RegisterApiClient(typeof(AzureFunctionsApi.Client), "https://dotvvmazurefunctionstest.azurewebsites.net/", "Scripts/AzureFunctionsApiClient.js", "_azureFuncApi");

LoadSampleConfiguration(config, applicationPath);

config.Markup.JavascriptTranslator.MethodCollection.AddMethodTranslator(typeof(JavascriptTranslationTestMethods),
nameof(JavascriptTranslationTestMethods.Unwrap),
new GenericMethodCompiler((a) =>
new JsIdentifierExpression("unwrap")
.Invoke(a[1])
), allowGeneric: true, allowMultipleMethods: true);

}

private void LoadSampleConfiguration(DotvvmConfiguration config, string applicationPath)
Expand Down
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.ViewModel;
using DotVVM.Samples.BasicSamples;

namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.JavascriptTranslation
{
public class GenericMethodTranslationViewModel : DotVVM.Samples.BasicSamples.ViewModels.ComplexSamples.SPA.SiteViewModel
{
public JavascriptTranslationModel<string, JavascriptTranslationInnerTestModel<JavascriptTranslationInnerTestModel<int>>> Model { get; set; } = new JavascriptTranslationModel<string, JavascriptTranslationInnerTestModel<JavascriptTranslationInnerTestModel<int>>>() { VModel2 = "Some data", VModel = new JavascriptTranslationInnerTestModel<JavascriptTranslationInnerTestModel<int>>() { Value = new JavascriptTranslationInnerTestModel<int>() { Value = 20 } } };
public JavascriptTranslationModel<bool, JavascriptTranslationInnerTestModel<double[]>> Model2 { get; set; }
= new JavascriptTranslationModel<bool, JavascriptTranslationInnerTestModel<double[]>>() {
VModel2 = true,
VModel = new JavascriptTranslationInnerTestModel<double[]>() {
Value = new[] { 20d } } };

public JavascriptTranslationModel<bool> Model3 { get; set; } = new JavascriptTranslationModel<bool>() { Model = true };

public JavascriptTranslationModel<JavascriptTranslationInnerModel<bool>> Model4 { get; set; }
= new JavascriptTranslationModel<JavascriptTranslationInnerModel<bool>>() {
Model = new JavascriptTranslationInnerModel<bool>() {
Model = false } };

public JavascriptTranslationModel<JavascriptTranslationInnerModel<string[]>[]> Model5 { get; set; }
= new JavascriptTranslationModel<JavascriptTranslationInnerModel<string[]>[]>() {
Model = new[]{
new JavascriptTranslationInnerModel<string[]>() { Model = new[] { "str1", "str2" } },
new JavascriptTranslationInnerModel<string[]>() { Model = new[] { "str3", "str4" } }
}
};

public override Task Load()
{
var w1 = JavascriptTranslationTestMethods.Unwrap(Model, Model2);
var w3 = JavascriptTranslationTestMethods.Unwrap(Model3);
var w4 = JavascriptTranslationTestMethods.Unwrap(Model4);
var w5 = JavascriptTranslationTestMethods.Unwrap(Model5);
return base.Load();
}

}
public class JavascriptTranslationInnerTestModel<Tmodel>
{
public Tmodel Value { get; set; }
}
public static class JavascriptTranslationTestMethods
{
public static T2 Unwrap<T3, T2, T4, T>(JavascriptTranslationModel<T2, T> transferModel, JavascriptTranslationModel<T3, T4> transferModel2)
{
return transferModel.VModel2;
}

public static T Unwrap<T>(JavascriptTranslationModel<T> transferModel)
{
return transferModel.Model;
}

public static T Unwrap<T>(JavascriptTranslationModel<JavascriptTranslationInnerModel<T>> transferModel)
{
return transferModel.Model.Model;
}
}
public class JavascriptTranslationModel<T>
{
public T Model { get; set; }

}
public class JavascriptTranslationInnerModel<T>
{
public T Model { get; set; }
}
public class JavascriptTranslationInner2Model<T>
{
public T Model { get; set; }
}
public class JavascriptTranslationModel<Tx, Ty>
{
public Ty VModel { get; set; }
public Tx VModel2 { get; set; }
}
}