From c1d5b11a9cf09bb13aba1ca3163404ff46468907 Mon Sep 17 00:00:00 2001 From: Holden Mai Date: Sun, 14 Aug 2022 21:01:41 -0500 Subject: [PATCH] Added support of collection initializer syntax. --- src/DynamicExpresso.Core/Parsing/Parser.cs | 130 +++++++++++++++--- .../Resources/ErrorMessages.Designer.cs | 18 +++ .../Resources/ErrorMessages.resx | 6 + .../ConstructorTest.cs | 119 +++++++++++++++- 4 files changed, 251 insertions(+), 22 deletions(-) diff --git a/src/DynamicExpresso.Core/Parsing/Parser.cs b/src/DynamicExpresso.Core/Parsing/Parser.cs index b95d476..2fcae9c 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,126 @@ 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(); + ParameterExpression instance = Expression.Variable(newType); + actions.Add(Expression.Assign(instance, newExpr)); + 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(newExpr, newType, originalPos, bindingList, actions, instance, allowCollectionInit); + } + else + { + ParsePossibleMemberBinding(newExpr, newType, originalPos, bindingList, actions, instance, allowCollectionInit); + } + if (_token.id != TokenId.Comma) break; + NextToken(); + } + if (bindingList.Count == 0) + { + 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(NewExpression newExpr, Type newType, int originalPos, List bindingList, List actions, ParameterExpression instance, bool allowCollectionInit) + { + if (actions.Count > 1) + { + if ( + this._arguments.TryGetParameters(_token.text, out _) + || + this._arguments.TryGetIdentifier(_token.text, out _) + || + FindPropertyOrField(newType, _token.text, false) == null) + { + ParseCollectionInitalizer(newExpr, newType, originalPos, bindingList, actions, instance, allowCollectionInit); + return; + } + else + { + throw CreateParseException(originalPos, ErrorMessages.InvalidInitializerMemberDeclarator); + } + } + ValidateToken(TokenId.Identifier, ErrorMessages.IdentifierExpected); - NextToken(); + var propertyOrFieldName = _token.text; + var member = FindPropertyOrField(newType, propertyOrFieldName, false); + if (member == null) + { + var pos = _token.pos; + if (allowCollectionInit) + { + NextToken(); + if (_token.id != TokenId.Equal || _arguments.TryGetIdentifier(propertyOrFieldName, out _) || _arguments.TryGetParameters(propertyOrFieldName, out _)) + { + SetTextPos(pos); + NextToken(); + actions.Add(ParseNormalMethodInvocation(newType, instance, _token.pos, "Add", new[] { ParseExpressionSegment() })); + return; + } + } + throw CreateParseException(pos, ErrorMessages.UnknownPropertyOrField, propertyOrFieldName, GetTypeName(newType)); - ValidateToken(TokenId.Equal, ErrorMessages.EqualExpected); - NextToken(); + } + 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(NewExpression newExpr, 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) + { + if (!_arguments.TryGetIdentifier(_token.text, out var _) && !_arguments.TryGetParameters(_token.text, out var _)) + { + throw CreateParseException(_token.pos, ErrorMessages.InvalidInitializerMemberDeclarator); + } + } + SetTextPos(pos); + ParseExpressionSegment(); + actions.Add(ParseMethodInvocation(newType, instance, _token.pos, "Add", TokenId.OpenCurlyBracket, ErrorMessages.OpenCurlyBracketExpected, TokenId.CloseCurlyBracket, ErrorMessages.CloseCurlyBracketExpected)); + } + else + { + var args = new[] { ParseExpressionSegment() }; + actions.Add(ParseNormalMethodInvocation(newType, instance, _token.pos, "Add", args)); } - return bindingList.ToArray(); } private Expression ParseLambdaInvocation(LambdaExpression lambda, int errorPos) @@ -1573,7 +1658,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..369244f 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs @@ -531,5 +531,23 @@ 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); + } + } } } diff --git a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx index f72a79e..0eb359c 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx @@ -267,4 +267,10 @@ Multidimensional arrays are not supported + + Invalid initializer member declarator + + + Cannot initialize type '{0}' with a collection initializer because it does not implement '{1}' + \ No newline at end of file diff --git a/test/DynamicExpresso.UnitTest/ConstructorTest.cs b/test/DynamicExpresso.UnitTest/ConstructorTest.cs index 8889317..a399893 100644 --- a/test/DynamicExpresso.UnitTest/ConstructorTest.cs +++ b/test/DynamicExpresso.UnitTest/ConstructorTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using DynamicExpresso.Exceptions; using NUnit.Framework; @@ -121,7 +122,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 +130,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 +179,84 @@ 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() + { + string StrProp = ""; + var mc = new MyClassAdder() + { + {1,2,3 }, + {StrProp = "6" } + }; + if (StrProp == "6") + { + + } + var target = new Interpreter(); + target.Reference(typeof(MyClassAdder)); + Assert.DoesNotThrow(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},{StrProp = \"6\" },7}", new Parameter("StrProp", "0"))); + Assert.DoesNotThrow(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},StrProp = \"6\",7}", new Parameter("StrProp", "0"))); + Assert.DoesNotThrow(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},string.Empty, 7}")); + Assert.Throws(() => target.Eval("new MyClassAdder(){{ 1, 2, 3, 4, 5},{StrProp = \"6\" },7 }")); + } + [Test] + public void Ctor_NewListWithItems() + { + var target = new Interpreter(); + target.Reference(typeof(System.Collections.Generic.List<>)); + var intList = target.Eval>("new List(){1, 2, 3, 4, 5}"); + Assert.AreEqual(5, intList.Count); + for (int i = 0; i < intList.Count; ++i) + { + Assert.AreEqual(i + 1, intList[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 +296,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; + } + + } } }