Skip to content

Commit

Permalink
Merge pull request #946 from riganti/feature/extension-methods-improv…
Browse files Browse the repository at this point in the history
…ements

Improved support for extension methods
  • Loading branch information
tomasherceg committed Apr 9, 2021
2 parents 25c2413 + be1e0e9 commit 2922ad6
Show file tree
Hide file tree
Showing 23 changed files with 196 additions and 96 deletions.
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

0 comments on commit 2922ad6

Please sign in to comment.