Skip to content

Commit 29167bb

Browse files
authored
Add support of method invocation with dynamic arguments (#347)
Add support of method invocation with dynamic arguments Fix #295
1 parent 0a34264 commit 29167bb

File tree

5 files changed

+110
-17
lines changed

5 files changed

+110
-17
lines changed

src/DynamicExpresso.Core/Identifier.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal void AddOverload(Delegate overload)
3737

3838
/// <summary>
3939
/// Custom expression that simulates a method group (ie. a group of methods with the same name).
40+
/// It's used when custom functions are added to the interpreter via <see cref="Interpreter.SetFunction(string, Delegate)"/>.
4041
/// </summary>
4142
internal class MethodGroupExpression : Expression
4243
{

src/DynamicExpresso.Core/Parsing/Parser.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,7 +1377,17 @@ private Expression ParseMethodGroupInvocation(MethodGroupExpression methodGroup,
13771377
applicableMethods = MethodResolution.FindBestMethod(candidates.Select(_ => _.InvokeMethod), args);
13781378

13791379
if (applicableMethods.Length == 0)
1380+
{
1381+
if (args.Any(IsDynamicExpression))
1382+
{
1383+
// TODO: we could try to find the best method by using the dynamic binder
1384+
var candidatesWithSameArgumentCount = candidates.Where(_ => _.Method.GetParameters().Length == args.Length).ToList();
1385+
if (candidatesWithSameArgumentCount.Count == 1)
1386+
return ParseDynamicMethodGroupInvocation(candidatesWithSameArgumentCount[0].Delegate, args);
1387+
}
1388+
13801389
throw ParseException.Create(errorPos, ErrorMessages.ArgsIncompatibleWithDelegate);
1390+
}
13811391

13821392
if (applicableMethods.Length > 1)
13831393
throw ParseException.Create(errorPos, ErrorMessages.AmbiguousDelegateInvocation);
@@ -1691,21 +1701,21 @@ private Expression ParseMethodInvocation(Type type, Expression instance, int err
16911701
if (methodInvocationExpression != null)
16921702
return methodInvocationExpression;
16931703

1694-
if (TypeUtils.IsDynamicType(type) || IsDynamicExpression(instance))
1704+
if (TypeUtils.IsDynamicType(type) || IsDynamicExpression(instance) || args.Any(IsDynamicExpression))
16951705
return ParseDynamicMethodInvocation(type, instance, methodName, args);
16961706

16971707
throw new NoApplicableMethodException(methodName, TypeUtils.GetTypeName(type), errorPos);
16981708
}
16991709

1700-
private Expression ParseExtensionMethodInvocation(Type type, Expression instance, int errorPos, string id, Expression[] args)
1710+
private Expression ParseExtensionMethodInvocation(Type type, Expression instance, int errorPos, string methodName, Expression[] args)
17011711
{
17021712
var extensionMethodsArguments = new Expression[args.Length + 1];
17031713
extensionMethodsArguments[0] = instance;
17041714
args.CopyTo(extensionMethodsArguments, 1);
17051715

1706-
var extensionMethods = _memberFinder.FindExtensionMethods(id, extensionMethodsArguments);
1716+
var extensionMethods = _memberFinder.FindExtensionMethods(methodName, extensionMethodsArguments);
17071717
if (extensionMethods.Length > 1)
1708-
throw ParseException.Create(errorPos, ErrorMessages.AmbiguousMethodInvocation, id, TypeUtils.GetTypeName(type));
1718+
throw ParseException.Create(errorPos, ErrorMessages.AmbiguousMethodInvocation, methodName, TypeUtils.GetTypeName(type));
17091719

17101720
if (extensionMethods.Length == 1)
17111721
{
@@ -1719,11 +1729,11 @@ private Expression ParseExtensionMethodInvocation(Type type, Expression instance
17191729
return null;
17201730
}
17211731

1722-
private Expression ParseNormalMethodInvocation(Type type, Expression instance, int errorPos, string id, Expression[] args)
1732+
private Expression ParseNormalMethodInvocation(Type type, Expression instance, int errorPos, string methodName, Expression[] args)
17231733
{
1724-
var applicableMethods = _memberFinder.FindMethods(type, id, instance == null, args);
1734+
var applicableMethods = _memberFinder.FindMethods(type, methodName, instance == null, args);
17251735
if (applicableMethods.Length > 1)
1726-
throw ParseException.Create(errorPos, ErrorMessages.AmbiguousMethodInvocation, id, TypeUtils.GetTypeName(type));
1736+
throw ParseException.Create(errorPos, ErrorMessages.AmbiguousMethodInvocation, methodName, TypeUtils.GetTypeName(type));
17271737

17281738
if (applicableMethods.Length == 1)
17291739
{
@@ -1791,8 +1801,16 @@ private static Expression ParseDynamicProperty(Type type, Expression instance, s
17911801
private static Expression ParseDynamicMethodInvocation(Type type, Expression instance, string methodName, Expression[] args)
17921802
{
17931803
var argsDynamic = args.ToList();
1794-
argsDynamic.Insert(0, instance);
1795-
return Expression.Dynamic(new LateInvokeMethodCallSiteBinder(methodName), typeof(object), argsDynamic);
1804+
var isStatic = instance == null;
1805+
argsDynamic.Insert(0, !isStatic ? instance : Expression.Constant(type));
1806+
return Expression.Dynamic(new LateInvokeMethodCallSiteBinder(methodName, isStatic), typeof(object), argsDynamic);
1807+
}
1808+
1809+
private Expression ParseDynamicMethodGroupInvocation(Delegate @delegate, Expression[] args)
1810+
{
1811+
var argsDynamic = args.ToList();
1812+
argsDynamic.Insert(0, Expression.Constant(@delegate));
1813+
return Expression.Dynamic(new LateInvokeDelegateCallSiteBinder(), typeof(object), argsDynamic);
17961814
}
17971815

17981816
private static Expression ParseDynamicIndex(Type type, Expression instance, Expression[] args)

src/DynamicExpresso.Core/Resolution/LateBinders.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.ObjectModel;
23
using System.Linq;
34
using System.Linq.Expressions;
@@ -34,20 +35,59 @@ public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpre
3435
internal class LateInvokeMethodCallSiteBinder : CallSiteBinder
3536
{
3637
private readonly string _methodName;
38+
private readonly bool _isStatic;
3739

38-
public LateInvokeMethodCallSiteBinder(string methodName)
40+
public LateInvokeMethodCallSiteBinder(string methodName, bool isStatic)
3941
{
4042
_methodName = methodName;
43+
_isStatic = isStatic;
4144
}
4245

4346
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
4447
{
48+
// if the method is static, the first argument is the type containing the method,
49+
// otherwise it's the instance on which the method is called
50+
var context = _isStatic ? (Type)args[0] : args[0]?.GetType();
51+
var argumentInfo = parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)).ToArray();
52+
if (_isStatic)
53+
{
54+
// instruct the compiler that we already know the containing type of the method
55+
argumentInfo[0] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, null);
56+
}
57+
4558
var binderM = Binder.InvokeMember(
4659
CSharpBinderFlags.None,
4760
_methodName,
4861
null,
49-
TypeUtils.RemoveArrayType(args[0]?.GetType()),
50-
parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null))
62+
TypeUtils.RemoveArrayType(context),
63+
argumentInfo
64+
);
65+
return binderM.Bind(args, parameters, returnLabel);
66+
}
67+
}
68+
69+
/// <summary>
70+
/// Binds to a delegate invocation as late as possible. This allows the use of delegates with dynamic arguments.
71+
/// </summary>
72+
internal class LateInvokeDelegateCallSiteBinder : CallSiteBinder
73+
{
74+
public LateInvokeDelegateCallSiteBinder()
75+
{
76+
}
77+
78+
public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel)
79+
{
80+
// the first argument is the delegate to invoke
81+
var _delegate = (Delegate)args[0];
82+
var argumentInfo = parameters.Select(x => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)).ToArray();
83+
84+
// instruct the compiler that we already know the delegate's type
85+
argumentInfo[0] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null);
86+
87+
var binderM = Binder.Invoke(
88+
CSharpBinderFlags.None,
89+
null,
90+
argumentInfo
5191
);
5292
return binderM.Bind(args, parameters, returnLabel);
5393
}

test/DynamicExpresso.UnitTest/DynamicTest.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,44 @@ public void Unary_with_dynamic_properties()
435435

436436
Assert.That(interpreter.Eval("!dyn.Foo"), Is.EqualTo(!dyn.Foo));
437437
Assert.That(interpreter.Eval("-dyn.Bar"), Is.EqualTo(-dyn.Bar));
438+
}
439+
440+
[Test]
441+
public void Static_method_call_with_dynamic_arg()
442+
{
443+
dynamic dyn = new ExpandoObject();
444+
dyn.Foo = "test";
445+
446+
var myClass = new MyClass();
447+
448+
var interpreter = new Interpreter()
449+
.SetVariable("dyn", (object)dyn);
450+
451+
Assert.That(interpreter.Eval("string.IsNullOrEmpty(dyn.Foo)"), Is.EqualTo(string.IsNullOrEmpty(dyn.Foo)));
452+
}
438453

439454

455+
[Test]
456+
public void Method_call_with_dynamic_arg()
457+
{
458+
dynamic dyn = new ExpandoObject();
459+
dyn.Foo = "test";
460+
461+
var myClass = new MyClass();
462+
463+
var interpreter = new Interpreter()
464+
.SetVariable("dyn", (object)dyn)
465+
.SetVariable("myClass", myClass);
440466

467+
Assert.That(interpreter.Eval("myClass.MyMethod(dyn.Foo)"), Is.EqualTo(myClass.MyMethod(dyn.Foo)));
468+
}
469+
470+
public class MyClass
471+
{
472+
public string MyMethod(string input)
473+
{
474+
return input;
475+
}
441476
}
442477

443478
public class TestDynamicClass : DynamicObject

test/DynamicExpresso.UnitTest/GithubIssues.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,6 @@ public void GitHub_Issue_292()
775775
}
776776

777777
[Test]
778-
[Ignore("The fix suggested in #296 break other use cases, so let's ignore this test for now")]
779778
public void GitHub_Issue_295()
780779
{
781780
var evaluator = new Interpreter();
@@ -789,11 +788,11 @@ public void GitHub_Issue_295()
789788
globalSettings.MyTestPath = "C:\\delme\\";
790789
evaluator.SetVariable("GlobalSettings", globalSettings);
791790

792-
var works = (string)evaluator.Eval("StringConcat((string)GlobalSettings.MyTestPath,\"test.txt\")");
793-
Assert.That(works, Is.EqualTo("C:\\delme\\test.txt"));
791+
var worksWithCast = (string)evaluator.Eval("StringConcat((string)GlobalSettings.MyTestPath,\"test.txt\")");
792+
Assert.That(worksWithCast, Is.EqualTo("C:\\delme\\test.txt"));
794793

795-
var doesntWork = (string)evaluator.Eval("StringConcat(GlobalSettings.MyTestPath,\"test.txt\")");
796-
Assert.That(doesntWork, Is.EqualTo("C:\\delme\\test.txt"));
794+
var worksWithoutCast = (string)evaluator.Eval("StringConcat(GlobalSettings.MyTestPath,\"test.txt\")");
795+
Assert.That(worksWithoutCast, Is.EqualTo("C:\\delme\\test.txt"));
797796
}
798797

799798
#region GitHub_Issue_305

0 commit comments

Comments
 (0)