Skip to content

Commit

Permalink
Improve type parsing to support arrays and generics (#173)
Browse files Browse the repository at this point in the history
* ?. and ?[ operators now use the existing GenerateNullableTypeConversion that ensures a nullable type isn't converted to a nullable of nullable.
Fixes #169

* Promote operands before generating the equality check on ?. and ?[ operators.

* No longer consider ?. and ?[ as tokens, because it prevents the proper parsing of an array of a nullable type (e.g. int?[]).

* Improve type parsing to support arrays and generics.
Fixes #172

* No longer consider ?. and ?[ as tokens, because it prevents the proper parsing of an array of a nullable type (e.g. int?[]).

* Improve type parsing to support arrays and generics.
Fixes #172
  • Loading branch information
metoule authored Oct 27, 2021
1 parent a188ed8 commit 7e172a1
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 39 deletions.
147 changes: 114 additions & 33 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -646,28 +646,34 @@ private Expression ParsePrimary()
NextToken();
expr = ParseMemberAccess(null, expr);
}
else if (_token.id == TokenId.QuestionDot)
// special case for ?. and ?[ operators
else if (_token.id == TokenId.Question && (_parseChar == '.' || _parseChar == '['))
{
NextToken();

// ?. operator changes value types to nullable types
var memberAccess = GenerateNullableTypeConversion(ParseMemberAccess(null, expr));
var nullExpr = ParserConstants.NullLiteralExpression;
CheckAndPromoteOperands(typeof(ParseSignatures.IEqualitySignatures), ref expr, ref nullExpr);
expr = GenerateConditional(GenerateEqual(expr, nullExpr), ParserConstants.NullLiteralExpression, memberAccess, _token.pos);
if (_token.id == TokenId.Dot)
{
NextToken();

// ?. operator changes value types to nullable types
var memberAccess = GenerateNullableTypeConversion(ParseMemberAccess(null, expr));
var nullExpr = ParserConstants.NullLiteralExpression;
CheckAndPromoteOperands(typeof(ParseSignatures.IEqualitySignatures), ref expr, ref nullExpr);
expr = GenerateConditional(GenerateEqual(expr, nullExpr), ParserConstants.NullLiteralExpression, memberAccess, _token.pos);
}
else if (_token.id == TokenId.OpenBracket)
{
// ?[ operator changes value types to nullable types
var elementAccess = GenerateNullableTypeConversion(ParseElementAccess(expr));
var nullExpr = ParserConstants.NullLiteralExpression;
CheckAndPromoteOperands(typeof(ParseSignatures.IEqualitySignatures), ref expr, ref nullExpr);
expr = GenerateConditional(GenerateEqual(expr, nullExpr), ParserConstants.NullLiteralExpression, elementAccess, _token.pos);
}
}
else if (_token.id == TokenId.OpenBracket)
{
expr = ParseElementAccess(expr);
}
else if (_token.id == TokenId.QuestionOpenBracket)
{
// ?[ operator changes value types to nullable types
var elementAccess = GenerateNullableTypeConversion(ParseElementAccess(expr));
var nullExpr = ParserConstants.NullLiteralExpression;
CheckAndPromoteOperands(typeof(ParseSignatures.IEqualitySignatures), ref expr, ref nullExpr);
expr = GenerateConditional(GenerateEqual(expr, nullExpr), ParserConstants.NullLiteralExpression, elementAccess, _token.pos);
}
else if (_token.id == TokenId.OpenParen)
{
if (expr is LambdaExpression lambda)
Expand Down Expand Up @@ -1014,6 +1020,8 @@ private Expression ParseDefaultOperator()
var name = _token.text;
if (_arguments.TryGetKnownType(name, out var type))
type = ParseFullType(type);
else
throw new UnknownIdentifierException(_token.text, _token.pos);

ValidateToken(TokenId.CloseParen, ErrorMessages.CloseParenOrCommaExpected);
NextToken();
Expand Down Expand Up @@ -1204,9 +1212,99 @@ private Type ParseFullType(Type type)
if (!type.IsValueType || IsNullableType(type))
throw CreateParseException(errorPos, ErrorMessages.TypeHasNoNullableForm, GetTypeName(type));
type = typeof(Nullable<>).MakeGenericType(type);
type = ParseFullType(type);
}
else if (_token.id == TokenId.OpenBracket)
{
type = ParseArrayRankSpecifier(type);
}
else if (_token.id == TokenId.LessThan)
{
type = ParseTypeArgumentList(type);
type = ParseFullType(type);
}

return type;
}

private Type ParseArrayRankSpecifier(Type type)
{
ValidateToken(TokenId.OpenBracket);

// An array type of the form T[R][R1]...[Rn] is an array with rank R and an element type T[R1]...[Rn]
// => we need to parse all rank specifiers in one pass, and create the array from right to left
var ranks = new Stack<int>();
while (_token.id == TokenId.OpenBracket)
{
NextToken();
var rank = 1;
while (_token.id == TokenId.Comma)
{
rank++;
NextToken();
}

ValidateToken(TokenId.CloseBracket, ErrorMessages.CloseBracketOrCommaExpected);
ranks.Push(rank);
NextToken();
}

while (ranks.Count > 0)
{
var rank = ranks.Pop();
type = rank == 1 ? type.MakeArrayType() : type.MakeArrayType(rank);
}

return type;
}

private Type ParseTypeArgumentList(Type type)
{
ValidateToken(TokenId.LessThan);
NextToken();

if (_token.id == TokenId.Identifier)
type = ParseTypeArguments(type);
else
type = ParseUnboundType(type);

ValidateToken(TokenId.GreaterThan, ErrorMessages.CloseTypeArgumentListExpected);
return type;
}

private Type ParseTypeArguments(Type type)
{
var genericArguments = new List<Type>();
while (true)
{
ValidateToken(TokenId.Identifier);
var name = _token.text;
if (_arguments.TryGetKnownType(name, out var genericArgument))
genericArgument = ParseFullType(genericArgument);
else
throw new UnknownIdentifierException(_token.text, _token.pos);

genericArguments.Add(genericArgument);
if (_token.id != TokenId.Comma) break;
NextToken();
}

type = type.MakeGenericType(genericArguments.ToArray());
return type;
}

private Type ParseUnboundType(Type type)
{
var rank = 1;
while (_token.id == TokenId.Comma)
{
rank++;
NextToken();
}

if (rank != type.GetGenericArguments().Length)
throw new ArgumentException($"The number of generic arguments provided doesn't equal the arity of the generic type definition.");

return type;
}

Expand Down Expand Up @@ -1501,8 +1599,7 @@ private Expression[] ParseArguments()
private Expression ParseElementAccess(Expression expr)
{
var errorPos = _token.pos;
// expected tokens: either [ or ?[
ValidateToken(new[] { TokenId.OpenBracket, TokenId.QuestionOpenBracket }, ErrorMessages.OpenParenExpected);
ValidateToken(TokenId.OpenBracket, ErrorMessages.OpenParenExpected);
NextToken();
var args = ParseArguments();
ValidateToken(TokenId.CloseBracket, ErrorMessages.CloseBracketOrCommaExpected);
Expand Down Expand Up @@ -2614,17 +2711,7 @@ private void NextToken()
break;
case '?':
NextChar();
if (_parseChar == '.')
{
NextChar();
t = TokenId.QuestionDot;
}
else if (_parseChar == '[')
{
NextChar();
t = TokenId.QuestionOpenBracket;
}
else if (_parseChar == '?')
if (_parseChar == '?')
{
NextChar();
t = TokenId.QuestionQuestion;
Expand Down Expand Up @@ -2819,12 +2906,6 @@ private void ValidateToken(TokenId t, string errorMessage)
throw CreateParseException(_token.pos, errorMessage);
}

private void ValidateToken(IList<TokenId> t, string errorMessage)
{
if (!t.Contains(_token.id))
throw CreateParseException(_token.pos, errorMessage);
}

// ReSharper disable once UnusedParameter.Local
private void ValidateToken(TokenId t)
{
Expand Down
2 changes: 0 additions & 2 deletions src/DynamicExpresso.Core/Parsing/TokenId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ internal enum TokenId
Comma,
Minus,
Dot,
QuestionDot,
QuestionQuestion,
Slash,
Colon,
LessThan,
GreaterThan,
Question,
OpenBracket,
QuestionOpenBracket,
CloseBracket,
ExclamationEqual,
Amphersand,
Expand Down
13 changes: 12 additions & 1 deletion src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs

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

3 changes: 3 additions & 0 deletions src/DynamicExpresso.Core/Resources/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,7 @@
<data name="EqualExpected" xml:space="preserve">
<value>'=' expected</value>
</data>
<data name="CloseTypeArgumentListExpected" xml:space="preserve">
<value>'&gt;' expected</value>
</data>
</root>
37 changes: 34 additions & 3 deletions test/DynamicExpresso.UnitTest/OperatorsTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using DynamicExpresso.Exceptions;
Expand Down Expand Up @@ -68,7 +68,38 @@ public void Typeof_Operator()
{
var target = new Interpreter();

Assert.Throws<ParseException>(() => target.Eval("typeof(int??)"));
Assert.Throws<ParseException>(() => target.Eval("typeof(string?)"));

Assert.AreEqual(typeof(string), target.Eval("typeof(string)"));
Assert.AreEqual(typeof(int), target.Eval("typeof(int)"));
Assert.AreEqual(typeof(int?), target.Eval("typeof(int?)"));
}

[Test]
public void Typeof_Operator_Arrays()
{
var target = new Interpreter();

Assert.AreEqual(typeof(int[]), target.Eval("typeof(int[])"));
Assert.AreEqual(typeof(int?[]), target.Eval("typeof(int?[])"));
Assert.AreEqual(typeof(int?[,,][][,]), target.Eval("typeof(int?[,,][][,])"));
}

[Test]
public void Typeof_Operator_Generics()
{
var target = new Interpreter();
target.Reference(typeof(IEnumerable<>), "IEnumerable");
target.Reference(typeof(Dictionary<,>), "Dictionary");

Assert.AreEqual(typeof(IEnumerable<int>), target.Eval("typeof(IEnumerable<int>)"));
Assert.AreEqual(typeof(IEnumerable<IEnumerable<int?[]>>), target.Eval("typeof(IEnumerable<IEnumerable<int?[]>>)"));
Assert.AreEqual(typeof(IEnumerable<>), target.Eval("typeof(IEnumerable<>)"));

Assert.AreEqual(typeof(Dictionary<int, string>[,]), target.Eval("typeof(Dictionary<int,string>[,])"));
Assert.AreEqual(typeof(Dictionary<int, IEnumerable<int[]>>), target.Eval("typeof(Dictionary<int, IEnumerable<int[]>>)"));
Assert.AreEqual(typeof(Dictionary<,>), target.Eval("typeof(Dictionary<,>)"));
}

[Test]
Expand Down Expand Up @@ -765,4 +796,4 @@ public TypeWithoutOverloadedBinaryOperators(int value)
}
}
}
}
}

0 comments on commit 7e172a1

Please sign in to comment.