diff --git a/src/DynamicExpresso.Core/Lambda.cs b/src/DynamicExpresso.Core/Lambda.cs index e89edbb..78a5279 100644 --- a/src/DynamicExpresso.Core/Lambda.cs +++ b/src/DynamicExpresso.Core/Lambda.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -83,23 +83,63 @@ public object Invoke(params object[] args) var parameters = new List(); var declaredParameters = DeclaredParameters.ToArray(); - if (args != null) + int[] actualArgOrdering = null; + object[] orderedArgs = args; + var argsAreReordered = false; + if (args != null && args.Length > 0) { if (declaredParameters.Length != args.Length) throw new InvalidOperationException("Arguments count mismatch."); - for (var i = 0; i < args.Length; i++) + actualArgOrdering = new int[args.Length]; + var usedParametersIndex = new Dictionary(_parserArguments.Settings.KeyComparer); + foreach (var v in UsedParameters) { - var parameter = new Parameter( - declaredParameters[i].Name, - declaredParameters[i].Type, - args[i]); + if (declaredParameters.Any(x => string.Equals(x.Name, v.Name, _parserArguments.Settings.KeyComparison))) + { + usedParametersIndex[v.Name] = usedParametersIndex.Count; + } + } - parameters.Add(parameter); + for (var i = 0; i < args.Length; i++) + { + if (!usedParametersIndex.TryGetValue(declaredParameters[i].Name, out var actualArgIndex)) + { + actualArgIndex = -1; + } + if (actualArgIndex != i) + { + if (!argsAreReordered) + { + orderedArgs = (object[])orderedArgs.Clone(); + argsAreReordered = true; + } + if (actualArgIndex == -1) + { + Array.Resize(ref orderedArgs, args.Length - 1); + } + else + { + orderedArgs[actualArgIndex] = args[i]; + } + } + actualArgOrdering[i] = actualArgIndex; } } - return Invoke(parameters); + var result = InvokeWithUsedParameters(orderedArgs); + if (argsAreReordered) + { + for (var i = 0; i < actualArgOrdering.Length; i++) + { + var pullFrom = actualArgOrdering[i]; + if (pullFrom >= 0) + { + args[i] = orderedArgs[pullFrom]; + } + } + } + return result; } private object InvokeWithUsedParameters(object[] orderedArgs) diff --git a/src/DynamicExpresso.Core/Parsing/Parser.cs b/src/DynamicExpresso.Core/Parsing/Parser.cs index b95d476..3f19c9e 100644 --- a/src/DynamicExpresso.Core/Parsing/Parser.cs +++ b/src/DynamicExpresso.Core/Parsing/Parser.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Dynamic; using System.Globalization; @@ -1193,11 +1194,10 @@ private Expression ParseNew() var constructor = applicableConstructors[0]; var newExpr = Expression.New((ConstructorInfo)constructor.MethodBase, constructor.PromotedParameters); - var memberBindings = new MemberBinding[0]; if (_token.id == TokenId.OpenCurlyBracket) - memberBindings = ParseObjectInitializer(newType); + return ParseWithObjectInitializer(newExpr, newType); - return Expression.MemberInit(newExpr, memberBindings); + return newExpr; } private Expression[] ParseArrayInitializerList() @@ -1207,41 +1207,134 @@ private Expression[] ParseArrayInitializerList() allowTrailingComma: true); } - private MemberBinding[] ParseObjectInitializer(Type newType) + private Expression ParseWithObjectInitializer(NewExpression newExpr, Type newType) { ValidateToken(TokenId.OpenCurlyBracket, ErrorMessages.OpenCurlyBracketExpected); NextToken(); - var bindings = ParseMemberInitializerList(newType); + var initializedInstance = ParseMemberAndInitializerList(newExpr, newType); ValidateToken(TokenId.CloseCurlyBracket, ErrorMessages.CloseCurlyBracketExpected); NextToken(); - return bindings; + return initializedInstance; } - private MemberBinding[] ParseMemberInitializerList(Type newType) + private Expression ParseMemberAndInitializerList(NewExpression newExpr, Type newType) { + var originalPos = _token.pos; var bindingList = new List(); + var actions = new List(); + var instance = Expression.Variable(newType); + var allowCollectionInit = typeof(IEnumerable).IsAssignableFrom(newType); while (true) { if (_token.id == TokenId.CloseCurlyBracket) break; - ValidateToken(TokenId.Identifier, ErrorMessages.IdentifierExpected); + if (_token.id != TokenId.Identifier) + { + ParseCollectionInitalizer(newType, originalPos, bindingList, actions, instance, allowCollectionInit); + } + else + { + ParsePossibleMemberBinding(newType, originalPos, bindingList, actions, instance, allowCollectionInit); + } + if (_token.id != TokenId.Comma) break; + NextToken(); + } + if (bindingList.Count == 0) + { + actions.Insert(0, Expression.Assign(instance, newExpr)); + actions.Add(instance); + return Expression.Block(new ParameterExpression[] { instance }, actions); + } + return Expression.MemberInit(newExpr, bindingList.ToArray()); + } - var propertyOrFieldName = _token.text; - var member = FindPropertyOrField(newType, propertyOrFieldName, false); - if (member == null) - throw CreateParseException(_token.pos, ErrorMessages.UnknownPropertyOrField, propertyOrFieldName, GetTypeName(newType)); + private void ParsePossibleMemberBinding(Type newType, int originalPos, List bindingList, List actions, ParameterExpression instance, bool allowCollectionInit) + { + ValidateToken(TokenId.Identifier, ErrorMessages.IdentifierExpected); + var propertyOrFieldName = _token.text; + var member = FindPropertyOrField(newType, propertyOrFieldName, false); + var pos = _token.pos; + if (allowCollectionInit) + { NextToken(); - - ValidateToken(TokenId.Equal, ErrorMessages.EqualExpected); + //new T(){Prop = 1} + //new T(){variable = 2} + if (_token.id == TokenId.Equal && member != null) + { + if (actions.Count > 0) + { + throw CreateParseException(pos, ErrorMessages.InvalidInitializerMemberDeclarator); + } + } + else if (_token.id != TokenId.Equal || _arguments.TryGetIdentifier(propertyOrFieldName, out _) || _arguments.TryGetParameters(propertyOrFieldName, out _)) + { + SetTextPos(pos); + NextToken(); + ParseCollectionInitalizer(newType, pos, bindingList, actions, instance, allowCollectionInit); + return; + } + SetTextPos(pos); NextToken(); + } + if (member == null) + { + throw CreateParseException(pos, ErrorMessages.UnknownPropertyOrField, propertyOrFieldName, GetTypeName(newType)); + } + NextToken(); - var value = ParseExpressionSegment(); - bindingList.Add(Expression.Bind(member, value)); + ValidateToken(TokenId.Equal, ErrorMessages.EqualExpected); + NextToken(); - if (_token.id != TokenId.Comma) break; + var value = ParseExpressionSegment(); + bindingList.Add(Expression.Bind(member, value)); + } + + private void ParseCollectionInitalizer(Type newType, int originalPos, List bindingList, List actions, ParameterExpression instance, bool allowCollectionInit) + { + if (!allowCollectionInit) + { + throw CreateParseException(_token.pos, ErrorMessages.CollectionInitializationNotSupported, newType, typeof(IEnumerable)); + } + if (bindingList.Count > 0) + { + throw CreateParseException(originalPos, ErrorMessages.InvalidInitializerMemberDeclarator); + } + if (_token.id == TokenId.OpenCurlyBracket) + { + var pos = _token.pos; NextToken(); + + if (_token.id == TokenId.Identifier) + { + var identifierName = _token.text; + NextToken(); + if (_token.id == TokenId.Equal && !_arguments.TryGetIdentifier(identifierName, out _) && !_arguments.TryGetParameters(identifierName, out _)) + { + throw CreateParseException(_token.pos, ErrorMessages.InvalidInitializerMemberDeclarator); + } + else + { + SetTextPos(pos); + NextToken(); + } + } + else + { + SetTextPos(pos); + ParseExpressionSegment(); + } + actions.Add(ParseMethodInvocation(newType, instance, _token.pos, "Add", TokenId.OpenCurlyBracket, ErrorMessages.OpenCurlyBracketExpected, TokenId.CloseCurlyBracket, ErrorMessages.CloseCurlyBracketExpected)); + } + else + { + var args = new[] { ParseExpressionSegment() }; + var addMethod = ParseNormalMethodInvocation(newType, instance, _token.pos, "Add", args); + if (addMethod == null) + { + throw CreateParseException(_token.pos, ErrorMessages.UnableToFindAppropriateAddMethod, GetTypeName(newType)); + } + actions.Add(addMethod); } - return bindingList.ToArray(); } private Expression ParseLambdaInvocation(LambdaExpression lambda, int errorPos) @@ -1573,7 +1666,12 @@ private Expression GeneratePropertyOrFieldExpression(Type type, Expression insta private Expression ParseMethodInvocation(Type type, Expression instance, int errorPos, string methodName) { - var args = ParseArgumentList(); + return ParseMethodInvocation(type, instance, errorPos, methodName, TokenId.OpenParen, ErrorMessages.OpenParenExpected, TokenId.CloseParen, ErrorMessages.CloseParenOrCommaExpected); + + } + private Expression ParseMethodInvocation(Type type, Expression instance, int errorPos, string methodName, TokenId open, string openExpected, TokenId close, string closeExpected) + { + var args = ParseArgumentList(open, openExpected, close, closeExpected); var methodInvocationExpression = ParseNormalMethodInvocation(type, instance, errorPos, methodName, args); if (methodInvocationExpression == null && instance != null) diff --git a/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs b/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs index d23ce39..7e0643d 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs @@ -31,11 +31,11 @@ internal class ErrorMessages { [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal ErrorMessages() { } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { @@ -531,5 +531,33 @@ internal static string UnsupportedMultidimensionalArrays return ResourceManager.GetString("UnsupportedMultidimensionalArrays", resourceCulture); } } + + /// + /// Looks up a localized string similar to Invalid initializer member declarator. + /// + internal static string InvalidInitializerMemberDeclarator { + get { + return ResourceManager.GetString("InvalidInitializerMemberDeclarator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot initialize type '{0}' with a collection initializer because it does not implement '{1}'. + /// + internal static string CollectionInitializationNotSupported { + get { + return ResourceManager.GetString("CollectionInitializationNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The best overloaded Add method '{0}.Add' for the collection initializer has some invalid arguments. + /// + internal static string UnableToFindAppropriateAddMethod { + get { + return ResourceManager.GetString("UnableToFindAppropriateAddMethod", resourceCulture); + } + } + } } diff --git a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx index f72a79e..9a3ea95 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx @@ -267,4 +267,13 @@ Multidimensional arrays are not supported + + Invalid initializer member declarator + + + Cannot initialize type '{0}' with a collection initializer because it does not implement '{1}' + + + The best overloaded Add method '{0}.Add' for the collection initializer has some invalid arguments + \ No newline at end of file diff --git a/test/DynamicExpresso.UnitTest/ConstructorTest.cs b/test/DynamicExpresso.UnitTest/ConstructorTest.cs index 8889317..0690158 100644 --- a/test/DynamicExpresso.UnitTest/ConstructorTest.cs +++ b/test/DynamicExpresso.UnitTest/ConstructorTest.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using DynamicExpresso.Exceptions; using NUnit.Framework; +using System.Linq; namespace DynamicExpresso.UnitTest { @@ -121,7 +123,6 @@ public void Object_initializer_syntax_error() { var target = new Interpreter(); target.Reference(typeof(MyClass)); - Assert.Throws(() => target.Parse("new MyClass() { StrProp }")); Assert.Throws(() => target.Parse("new MyClass() { StrProp = }")); Assert.Throws(() => target.Parse("new MyClass() { StrProp = 5 }")); // type mismatch @@ -130,7 +131,7 @@ public void Object_initializer_syntax_error() Assert.Throws(() => target.Parse("new MyClass() { StrProp ")); // no close bracket Assert.Throws(() => target.Parse("new MyClass() StrProp }")); // no open bracket Assert.Throws(() => target.Parse("new MyClass() {{IntField = 5}}")); // multiple bracket - Assert.Throws(() => target.Parse("new MyClass() {5}")); // no field name + Assert.Throws(() => target.Parse("new MyClass() {5}")); // collection initializer not supported } [Test] @@ -179,6 +180,121 @@ public void Array_multi_dimension_constructor() Assert.Throws(() => target.Parse("new int[,] { { 1 }, { 2 } }")); } + [Test] + public void Ctor_NewDictionaryWithItems() + { + var target = new Interpreter(); + target.Reference(typeof(System.Collections.Generic.Dictionary<,>)); + var l = target.Eval>("new Dictionary(){{1, \"1\"}, {2, \"2\"}, {3, \"3\"}, {4, \"4\"}, {5, \"5\"}}"); + Assert.AreEqual(5, l.Count); + for (int i = 0; i < l.Count; ++i) + { + Assert.AreEqual(i + 1 + "", l[i + 1]); + } + } + + [Test] + public void Ctor_NewMyClassWithItems() + { + var target = new Interpreter(); + target.Reference(typeof(MyClassAdder)); + var l = target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},{\"6\" },7 }.Add(true)"); + Assert.AreEqual(5, l.MyArr.Length); + for (int i = 0; i < l.MyArr.Length; ++i) + { + Assert.AreEqual(i + 1, l.MyArr[i]); + } + Assert.AreEqual("6", l.StrProp); + Assert.AreEqual(7, l.IntField); + } + + + [Test] + public void Ctor_NewMyClassWithCross() + { + var StrProp = string.Empty; + + new MyClassAdder() { StrProp = StrProp = "6" }; + + var target = new Interpreter(); + target.Reference(typeof(MyClassAdder)); + target.Reference(typeof(MyClass)); + var strProp = new Parameter("StrProp", typeof(string).MakeByRefType(), "0"); + var args = new object[] + { + strProp.Value + }; + Assert.AreEqual(new MyClassAdder() { { 1, 2, 3, 4, 5 }, "6", 7 }, target.Parse("new MyClassAdder(){{ 1, 2, 3, 4, 5},{StrProp = \"6\" },7}", strProp).Invoke(args)); + Assert.AreEqual("6", args[0]); + args[0] = "0"; + Assert.AreEqual(new MyClassAdder() { { 1, 2, 3, 4, 5 }, string.Empty, 7 }, target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},string.Empty, 7}")); + Assert.AreEqual(new MyClassAdder() {StrProp = string.Empty, MyArr = new[] { 1, 2, 3, 4, 5 }, IntField = int.MinValue }, target.Eval("new MyClassAdder() {StrProp = string.Empty, MyArr = new int[] {1, 2, 3, 4, 5}, IntField = int.MinValue }")); + Assert.Throws(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},{StrProp = \"6\" },7 }")); + Assert.Throws(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},StrProp = \"6\" ,7 }")); + Assert.Throws(() => target.Eval("new MyClassAdder(){StrProp = \"6\" ,{ 1, 2, 3, 4, 5},7 }")); + Assert.Throws(() => target.Eval("new MyClass(){ 1, 2, 3, 4, 5}")); + } + [Test] + public void Ctor_NewListWithItems() + { + Ctor_NewListGeneric("\"1\"", "\"2\"", "\"3\"", "{string.Empty}", "string.Empty", "int.MaxValue.ToString()", "{int.MinValue.ToString()}"); + Ctor_NewListGeneric("1", "2", "3", "int.MinValue", "int.MaxValue", "{int.MinValue}", "{int.MaxValue}"); + Ctor_NewListGeneric("string.Empty", "int.MinValue"); + } + + [Test] + public void Ctor_NewListCantFindAddMethod() + { + var target = new Interpreter(); + target.Reference(typeof(System.Collections.Generic.List<>)); + Assert.Throws(() => target.Eval>("string.Empty")); + Assert.Throws(() => target.Eval>("int.MaxValue")); + } + + public void Ctor_NewListGeneric(params string[] items) + { + var target = new Interpreter(); + target.Reference(typeof(System.Collections.Generic.List<>)); + target.Reference(typeof(TList)); + target.Reference(typeof(TObject)); + //Create a random list of values to test. + var actual = new System.Collections.Generic.List(); + foreach (var v in items) + { + actual.Add(target.Eval(v.Trim('}', '{'))); + } + for (var min = 0; min < actual.Count; ++min) + { + for (var count = Math.Min(min, 1); count <= actual.Count - min; ++count) + { + var evalText = $"new List<{typeof(TList).Name}>(){{{string.Join(",", items.Skip(min).Take(count))}}}"; + System.Collections.Generic.List eval = null; + Assert.DoesNotThrow(() => eval = target.Eval>(evalText), evalText); + Assert.AreEqual(count, eval.Count); + for (var i = 0; i < count; ++i) + { + Assert.AreEqual(actual[i + min], eval[i]); + } + } + } + } + + [Test] + public void Ctor_NewListWithString() + { + var target = new Interpreter(); + target.Reference(typeof(System.Collections.Generic.List<>)); + var list = target.Eval>("new List(){string.Empty}"); + Assert.AreEqual(1, list.Count); + for (int i = 0; i < list.Count; ++i) + { + Assert.AreSame(string.Empty, list[i]); + } + Assert.DoesNotThrow(() => target.Eval>("new List(){StrProp = string.Empty}", new Parameter("StrProp", "0"))); + Assert.DoesNotThrow(() => target.Eval>("new List(){StrValue()}", new Parameter("StrValue", new Func(() => "Func")))); + } + + private class MyClass { public int IntField; @@ -218,5 +334,42 @@ public override int GetHashCode() return 0; } } + + private class MyClassAdder : MyClass, System.Collections.IEnumerable + { + + public MyClassAdder Add(string s) + { + StrProp = s; + return this; + } + + public MyClassAdder Add(int intValue) + { + IntField = intValue; + return this; + } + + public MyClassAdder Add(params int[] intValues) + { + MyArr = intValues; + return this; + } + + public MyClassAdder Add(bool returnMe) + { + if (returnMe) + { + return this; + } + return null; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + + } } } diff --git a/test/DynamicExpresso.UnitTest/ParametersTest.cs b/test/DynamicExpresso.UnitTest/ParametersTest.cs index 040c219..a222c1a 100644 --- a/test/DynamicExpresso.UnitTest/ParametersTest.cs +++ b/test/DynamicExpresso.UnitTest/ParametersTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using NUnit.Framework; using System.Globalization; using System.Reflection; @@ -332,6 +332,86 @@ public void When_lambda_is_invoked_input_parameters_must_follow_in_the_same_orde Assert.AreEqual(4, lambda.Invoke(1, 5)); } + [Test] + public void When_lambda_is_invoked_byref_parameters_are_updated_on_invoke() + { + var a = 1; + var b = 2; + var c = 3; + var d = 4; + var e = 5; + + var parameters = new[]{ + new Parameter(nameof(a), typeof(int).MakeByRefType()), + new Parameter(nameof(b), typeof(int).MakeByRefType()), + new Parameter(nameof(c), typeof(int).MakeByRefType()), + new Parameter(nameof(d), typeof(int).MakeByRefType()), + new Parameter(nameof(e), typeof(int).MakeByRefType()), + }; + + var args = new object[] + { + a, + b, + c, + d, + e + }; + + a += b *= c -= d /= e %= 3; + + var target = new Interpreter(); + + var lambda = target.Parse("a = a + (b = b * (c = c - (d = d / (e = e % 3))))", parameters); + + Assert.AreEqual(a, lambda.Invoke(args)); + Assert.AreEqual(a, args[0]); + Assert.AreEqual(b, args[1]); + Assert.AreEqual(c, args[2]); + Assert.AreEqual(d, args[3]); + Assert.AreEqual(e, args[4]); + } + + [Test] + public void When_lambda_is_invoked_byref_parameters_are_updated_on_invoke_and_I_can_omit_parameters_not_used() + { + var target = new Interpreter(); + + var a = 1; + var b = 2; + var c = 3; + var d = 4; + var e = 5; + + var parameters = new[]{ + new Parameter(nameof(a), typeof(int).MakeByRefType()), + new Parameter(nameof(b), typeof(int).MakeByRefType()), + new Parameter(nameof(c), typeof(int).MakeByRefType()), + new Parameter(nameof(d), typeof(int).MakeByRefType()), + new Parameter(nameof(e), typeof(int).MakeByRefType()), + }; + + var args = new object[] + { + a, + b, + c, + d, + e + }; + + a += b *= d /= e %= 3; + + var lambda = target.Parse("a = a + (b = b * (d = d / (e = e % 3)))", parameters); + + Assert.AreEqual(a, lambda.Invoke(args)); + Assert.AreEqual(a, args[0]); + Assert.AreEqual(b, args[1]); + Assert.AreEqual(c, args[2]); + Assert.AreEqual(d, args[3]); + Assert.AreEqual(e, args[4]); + } + [Test] public void When_lambda_is_invoked_I_can_omit_parameters_not_used() {