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

Improved support for extension methods #946

Merged
merged 11 commits into from Apr 9, 2021
Expand Up @@ -58,6 +58,11 @@ public object ExecuteBinding(string expression, params object[] contexts)
return ExecuteBinding(expression, contexts, null);
}

public object ExecuteBinding(string expression, NamespaceImport[] imports, params object[] contexts)
{
return ExecuteBinding(expression, contexts, null, imports);
}

[TestMethod]
public void BindingCompiler_FullNameResourceBinding()
{
Expand Down Expand Up @@ -204,7 +209,7 @@ public void BindingCompiler_Invalid_LambdaParameters(string expr)
public void BindingCompiler_Valid_ExtensionMethods()
{
var viewModel = new TestViewModel();
var result = (long[])ExecuteBinding("LongArray.Where((long item) => item % 2 != 0).ToArray()", viewModel);
var result = (long[])ExecuteBinding("LongArray.Where((long item) => item % 2 != 0).ToArray()", new[] { new NamespaceImport("System.Linq") }, viewModel);
CollectionAssert.AreEqual(viewModel.LongArray.Where(item => item % 2 != 0).ToArray(), result);
}

Expand Down
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq.Expressions;
using System.Text;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Compilation.Binding;
using DotVVM.Framework.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -12,14 +14,19 @@ namespace DotVVM.Framework.Tests.Common.Binding
[TestClass]
public class CustomExtensionMethodTests
{
private DotvvmConfiguration configuration;
private MemberExpressionFactory memberExpressionFactory;
private ExtensionMethodsCache extensionsMethodCache;

[TestInitialize]
public void Init()
{
this.configuration = DotvvmTestHelper.CreateConfiguration(services => services.AddScoped<IExtensionsProvider, TestExtensionsProvider>());
this.memberExpressionFactory = configuration.ServiceProvider.GetRequiredService<MemberExpressionFactory>();
var configuration = DotvvmTestHelper.CreateConfiguration();
extensionsMethodCache = configuration.ServiceProvider.GetRequiredService<ExtensionMethodsCache>();
}

private Expression CreateCall(MethodGroupExpression target, Expression[] args, NamespaceImport[] imports)
{
var memberExpressionFactory = new MemberExpressionFactory(extensionsMethodCache, imports);
return memberExpressionFactory.Call(target, args);
}

[TestMethod]
Expand All @@ -31,23 +38,79 @@ public void Call_FindCustomExtensionMethod()
Target = Expression.Constant(11)
};

var expression = memberExpressionFactory.Call(target, Array.Empty<Expression>());
var expression = CreateCall(target, Array.Empty<Expression>(), new[] { new NamespaceImport("DotVVM.Framework.Tests.Common.Binding") });
var result = Expression.Lambda<Func<int>>(expression).Compile().Invoke();
Assert.AreEqual(12, result);
}

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Call_AmbiguousExtensionMethodsThrows()
{
var nonAmbiguousTarget = new MethodGroupExpression() {
MethodName = nameof(AmbiguousExtensions.Extensions1.Decrement),
Target = Expression.Constant(11)
};

// Non-ambiguous
var expression = CreateCall(nonAmbiguousTarget, Array.Empty<Expression>(), new[] { new NamespaceImport("DotVVM.Framework.Tests.Common.Binding.AmbiguousExtensions") });
var result = Expression.Lambda<Func<int>>(expression).Compile().Invoke();
Assert.AreEqual(10, result);

var ambiguousTarget = new MethodGroupExpression() {
MethodName = nameof(AmbiguousExtensions.Extensions1.Increment),
Target = Expression.Constant(11)
};

// Ambiguous
CreateCall(ambiguousTarget, Array.Empty<Expression>(), new[] { new NamespaceImport("DotVVM.Framework.Tests.Common.Binding.AmbiguousExtensions") });
}

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Call_NotImportedExtensionMethodThrows()
{
var importedTarget = new MethodGroupExpression() {
MethodName = nameof(AmbiguousExtensions.Extensions1.Decrement),
Target = Expression.Constant(11)
};

// Imported extension
var expression = CreateCall(importedTarget, Array.Empty<Expression>(), new[] { new NamespaceImport("DotVVM.Framework.Tests.Common.Binding.AmbiguousExtensions") });
var result = Expression.Lambda<Func<int>>(expression).Compile().Invoke();
Assert.AreEqual(10, result);

var notImportedTarget = new MethodGroupExpression() {
MethodName = nameof(AmbiguousExtensions.Extensions1.Decrement),
Target = Expression.Constant(11)
};

// Not imported extension
CreateCall(notImportedTarget, Array.Empty<Expression>(), new[] { new NamespaceImport("DotVVM.Framework.Tests.Common.Binding") });
}
}

static class TestExtensions
public static class TestExtensions
{
public static int Increment(this int number)
=> ++number;
}

class TestExtensionsProvider : DefaultExtensionsProvider
namespace AmbiguousExtensions
{
public TestExtensionsProvider()
public static class Extensions1
{
public static int Increment(this int number)
=> ++number;

public static int Decrement(this int number)
=> --number;
}

public static class Extensions2
{
AddTypeForExtensionsLookup(typeof(TestExtensions));
public static int Increment(this int number)
=> ++number;
}
}
}
Expand Up @@ -4,6 +4,7 @@
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Runtime.InteropServices.ComTypes;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Compilation.Binding;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Utils;
Expand All @@ -24,7 +25,8 @@ public class ExpressionHelperTests
public void Init()
{
var configuration = DotvvmTestHelper.CreateConfiguration();
memberExpressionFactory = configuration.ServiceProvider.GetRequiredService<MemberExpressionFactory>();
var extensionsCache = configuration.ServiceProvider.GetRequiredService<ExtensionMethodsCache>();
memberExpressionFactory = new MemberExpressionFactory(extensionsCache);
}

[TestMethod]
Expand Down
Expand Up @@ -35,7 +35,8 @@ public void Init()
this.bindingService = configuration.ServiceProvider.GetRequiredService<BindingCompilationService>();
}
public string CompileBinding(string expression, params Type[] contexts) => CompileBinding(expression, contexts, expectedType: typeof(object));
public string CompileBinding(string expression, Type[] contexts, Type expectedType)
public string CompileBinding(string expression, NamespaceImport[] imports, params Type[] contexts) => CompileBinding(expression, contexts, expectedType: typeof(object), imports);
public string CompileBinding(string expression, Type[] contexts, Type expectedType, NamespaceImport[] imports = null)
{
var context = DataContextStack.Create(contexts.FirstOrDefault() ?? typeof(object), extensionParameters: new BindingExtensionParameter[]{
new CurrentCollectionIndexExtensionParameter(),
Expand All @@ -47,8 +48,8 @@ public string CompileBinding(string expression, Type[] contexts, Type expectedTy
{
context = DataContextStack.Create(contexts[i], context);
}
var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService<CompiledAssemblyCache>(), configuration.ServiceProvider.GetRequiredService<MemberExpressionFactory>());
var parsedExpression = parser.ParseWithLambdaConversion(expression, context, BindingParserOptions.Create<ValueBindingExpression>(), expectedType);
var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService<CompiledAssemblyCache>(), configuration.ServiceProvider.GetRequiredService<ExtensionMethodsCache>());
var parsedExpression = parser.ParseWithLambdaConversion(expression, context, BindingParserOptions.Create<ValueBindingExpression>(importNs: imports), expectedType);
var expressionTree =
TypeConversion.MagicLambdaConversion(parsedExpression, expectedType) ??
TypeConversion.ImplicitConversion(parsedExpression, expectedType, true, true);
Expand Down Expand Up @@ -372,29 +373,31 @@ public void JsTranslator_ArrayIndexer()
[DataRow("LongArray.Where((long item) => item % 2 == 0)", DisplayName = "Syntax sugar - extension method")]
public void JsTranslator_EnumerableWhere(string binding)
{
var result = CompileBinding(binding, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().filter(function(item){return ko.unwrap(item)%2==0;})", result);
}
[TestMethod]
public void JsTranslator_NestedEnumerableMethods()
{
var result = CompileBinding("Enumerable.Where(Enumerable.Where(LongArray, (long item) => item % 2 == 0), (long item) => item % 3 == 0)", new[] { typeof(TestViewModel) });
var result = CompileBinding("Enumerable.Where(Enumerable.Where(LongArray, (long item) => item % 2 == 0), (long item) => item % 3 == 0)",
new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });

Assert.AreEqual("LongArray().filter(function(item){return ko.unwrap(item)%2==0;}).filter(function(item){return ko.unwrap(item)%3==0;})", result);
}
[TestMethod]
[DataRow("Enumerable.Select(LongArray, (long item) => -item)", DisplayName = "Regular call of Enumerable.Select")]
[DataRow("LongArray.Select((long item) => -item)", DisplayName = "Syntax sugar - extension method")]
public void JsTranslator_EnumerableSelect(string binding)
{
var result = CompileBinding(binding, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().map(function(item){return -ko.unwrap(item);})", result);
}

[TestMethod]
public void JsTranslator_ValidMethod_UnsupportedTranslation()
{
Assert.ThrowsException<NotSupportedException>(() =>
CompileBinding("Enumerable.Skip<long>(LongArray, 2)", new[] { typeof(TestViewModel) }));
CompileBinding("Enumerable.Skip<long>(LongArray, 2)", new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) }));
}

[TestMethod]
Expand Down
Expand Up @@ -70,7 +70,7 @@ public string CompileBinding(string expression, bool niceMode, Type[] contexts,
var options = BindingParserOptions.Create<ValueBindingExpression>()
.AddImports(configuration.Markup.ImportedNamespaces);

var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService<CompiledAssemblyCache>(), configuration.ServiceProvider.GetRequiredService<MemberExpressionFactory>());
var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService<CompiledAssemblyCache>(), configuration.ServiceProvider.GetRequiredService<ExtensionMethodsCache>());
var expressionTree = parser.ParseWithLambdaConversion(expression, context, options, expectedType);
var jsExpression =
configuration.ServiceProvider.GetRequiredService<StaticCommandBindingCompiler>().CompileToJavascript(context, expressionTree);
Expand Down
Expand Up @@ -4,6 +4,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -3,6 +3,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -22,6 +22,9 @@
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
},
{
"namespace": "System"
},
Expand Down
Expand Up @@ -3,6 +3,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"defaultExtensionParameters": [
Expand Down
Expand Up @@ -10,6 +10,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -3,6 +3,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -3,6 +3,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -3,6 +3,9 @@
"importedNamespaces": [
{
"namespace": "DotVVM.Framework.Binding.HelperNamespace"
},
{
"namespace": "System.Linq"
}
],
"ViewCompilation": {
Expand Down
Expand Up @@ -17,18 +17,21 @@ namespace DotVVM.Framework.Compilation.Binding
public class BindingExpressionBuilder : IBindingExpressionBuilder
{
private readonly CompiledAssemblyCache compiledAssemblyCache;
private readonly MemberExpressionFactory memberExpressionFactory;
private readonly ExtensionMethodsCache extensionMethodsCache;
private MemberExpressionFactory memberExpressionFactory;

public BindingExpressionBuilder(CompiledAssemblyCache compiledAssemblyCache, MemberExpressionFactory memberExpressionFactory)
public BindingExpressionBuilder(CompiledAssemblyCache compiledAssemblyCache, ExtensionMethodsCache extensionMethodsCache)
{
this.compiledAssemblyCache = compiledAssemblyCache;
this.memberExpressionFactory = memberExpressionFactory;
this.extensionMethodsCache = extensionMethodsCache;
}

public Expression Parse(string expression, DataContextStack dataContexts, BindingParserOptions options, params KeyValuePair<string, Expression>[] additionalSymbols)
{
try
{
memberExpressionFactory = new MemberExpressionFactory(extensionMethodsCache, options.ImportNamespaces);

var tokenizer = new BindingTokenizer();
tokenizer.Tokenize(expression);

Expand Down

This file was deleted.

Expand Up @@ -28,15 +28,15 @@ public class BindingPropertyResolvers
private readonly IBindingExpressionBuilder bindingParser;
private readonly StaticCommandBindingCompiler staticCommandBindingCompiler;
private readonly JavascriptTranslator javascriptTranslator;
private readonly MemberExpressionFactory memberExpressionFactory;
private readonly ExtensionMethodsCache extensionsMethodCache;

public BindingPropertyResolvers(IBindingExpressionBuilder bindingParser, StaticCommandBindingCompiler staticCommandBindingCompiler, JavascriptTranslator javascriptTranslator, DotvvmConfiguration configuration, MemberExpressionFactory memberExpressionFactory)
public BindingPropertyResolvers(IBindingExpressionBuilder bindingParser, StaticCommandBindingCompiler staticCommandBindingCompiler, JavascriptTranslator javascriptTranslator, DotvvmConfiguration configuration, ExtensionMethodsCache extensionsCache)
{
this.configuration = configuration;
this.bindingParser = bindingParser;
this.staticCommandBindingCompiler = staticCommandBindingCompiler;
this.javascriptTranslator = javascriptTranslator;
this.memberExpressionFactory = memberExpressionFactory;
this.extensionsMethodCache = extensionsCache;
}

public ActionFiltersBindingProperty GetActionFilters(ParsedExpressionBindingProperty parsedExpression)
Expand Down Expand Up @@ -73,7 +73,7 @@ public Expression<BindingUpdateDelegate> CompileToUpdateDelegate(ParsedExpressio
{
var valueParameter = Expression.Parameter(typeof(object), "value");
var body = BindingCompiler.ReplaceParameters(binding.Expression, dataContext);
body = memberExpressionFactory.UpdateMember(body, valueParameter);
body = new MemberExpressionFactory(extensionsMethodCache, dataContext.NamespaceImports).UpdateMember(body, valueParameter);
if (body == null)
{
return null;
Expand Down