Skip to content

Commit

Permalink
Added support of collection initializer syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
holdenmai committed Aug 20, 2022
1 parent f0cbd31 commit c1d5b11
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 22 deletions.
130 changes: 110 additions & 20 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
Expand Down Expand Up @@ -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()
Expand All @@ -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<MemberBinding>();
var actions = new List<Expression>();
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<MemberBinding> bindingList, List<Expression> 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<MemberBinding> bindingList, List<Expression> 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)
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/DynamicExpresso.Core/Resources/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,10 @@
<data name="UnsupportedMultidimensionalArrays" xml:space="preserve">
<value>Multidimensional arrays are not supported</value>
</data>
<data name="InvalidInitializerMemberDeclarator" xml:space="preserve">
<value>Invalid initializer member declarator</value>
</data>
<data name="CollectionInitializationNotSupported" xml:space="preserve">
<value>Cannot initialize type '{0}' with a collection initializer because it does not implement '{1}'</value>
</data>
</root>
119 changes: 117 additions & 2 deletions test/DynamicExpresso.UnitTest/ConstructorTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using DynamicExpresso.Exceptions;
using NUnit.Framework;

Expand Down Expand Up @@ -121,7 +122,6 @@ public void Object_initializer_syntax_error()
{
var target = new Interpreter();
target.Reference(typeof(MyClass));

Assert.Throws<ParseException>(() => target.Parse("new MyClass() { StrProp }"));
Assert.Throws<ParseException>(() => target.Parse("new MyClass() { StrProp = }"));
Assert.Throws<ArgumentException>(() => target.Parse("new MyClass() { StrProp = 5 }")); // type mismatch
Expand All @@ -130,7 +130,7 @@ public void Object_initializer_syntax_error()
Assert.Throws<ParseException>(() => target.Parse("new MyClass() { StrProp ")); // no close bracket
Assert.Throws<ParseException>(() => target.Parse("new MyClass() StrProp }")); // no open bracket
Assert.Throws<ParseException>(() => target.Parse("new MyClass() {{IntField = 5}}")); // multiple bracket
Assert.Throws<ParseException>(() => target.Parse("new MyClass() {5}")); // no field name
Assert.Throws<ParseException>(() => target.Parse("new MyClass() {5}")); // collection initializer not supported
}

[Test]
Expand Down Expand Up @@ -179,6 +179,84 @@ public void Array_multi_dimension_constructor()
Assert.Throws<ParseException>(() => 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<System.Collections.Generic.Dictionary<int, string>>("new Dictionary<int, string>(){{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<MyClassAdder>("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<MyClassAdder>("new MyClassAdder(){{ 1, 2, 3, 4, 5},{StrProp = \"6\" },7}", new Parameter("StrProp", "0")));
Assert.DoesNotThrow(() => target.Eval<MyClassAdder>("new MyClassAdder(){{ 1, 2, 3, 4, 5},StrProp = \"6\",7}", new Parameter("StrProp", "0")));
Assert.DoesNotThrow(() => target.Eval<MyClassAdder>("new MyClassAdder(){{ 1, 2, 3, 4, 5},string.Empty, 7}"));
Assert.Throws<ParseException>(() => target.Eval<MyClassAdder>("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<System.Collections.Generic.List<int>>("new List<int>(){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<System.Collections.Generic.List<string>>("new List<string>(){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<System.Collections.Generic.List<string>>("new List<string>(){StrProp = string.Empty}", new Parameter("StrProp", "0")));
Assert.DoesNotThrow(() => target.Eval<System.Collections.Generic.List<string>>("new List<string>(){StrValue()}", new Parameter("StrValue", new Func<string>(() => "Func"))));
}


private class MyClass
{
public int IntField;
Expand Down Expand Up @@ -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;
}

}
}
}

0 comments on commit c1d5b11

Please sign in to comment.