diff --git a/src/DynamicExpresso.Core/Parsing/Parser.cs b/src/DynamicExpresso.Core/Parsing/Parser.cs index 50206036..eea0ba9c 100644 --- a/src/DynamicExpresso.Core/Parsing/Parser.cs +++ b/src/DynamicExpresso.Core/Parsing/Parser.cs @@ -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) @@ -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(); @@ -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(); + 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(); + 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; } @@ -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); @@ -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; @@ -2819,12 +2906,6 @@ private void ValidateToken(TokenId t, string errorMessage) throw CreateParseException(_token.pos, errorMessage); } - private void ValidateToken(IList t, string errorMessage) - { - if (!t.Contains(_token.id)) - throw CreateParseException(_token.pos, errorMessage); - } - // ReSharper disable once UnusedParameter.Local private void ValidateToken(TokenId t) { diff --git a/src/DynamicExpresso.Core/Parsing/TokenId.cs b/src/DynamicExpresso.Core/Parsing/TokenId.cs index ee9b69a4..46dd511f 100644 --- a/src/DynamicExpresso.Core/Parsing/TokenId.cs +++ b/src/DynamicExpresso.Core/Parsing/TokenId.cs @@ -18,7 +18,6 @@ internal enum TokenId Comma, Minus, Dot, - QuestionDot, QuestionQuestion, Slash, Colon, @@ -26,7 +25,6 @@ internal enum TokenId GreaterThan, Question, OpenBracket, - QuestionOpenBracket, CloseBracket, ExclamationEqual, Amphersand, diff --git a/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs b/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs index e7c6869f..37e4f973 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -405,6 +405,17 @@ internal static string CloseCurlyBracketExpected } } + /// + /// Looks up a localized string similar to '>' expected. + /// + internal static string CloseTypeArgumentListExpected + { + get + { + return ResourceManager.GetString("CloseTypeArgumentListExpected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Syntax error. /// diff --git a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx index 646907b7..829cc3da 100644 --- a/src/DynamicExpresso.Core/Resources/ErrorMessages.resx +++ b/src/DynamicExpresso.Core/Resources/ErrorMessages.resx @@ -252,4 +252,7 @@ '=' expected + + '>' expected + \ No newline at end of file diff --git a/test/DynamicExpresso.UnitTest/OperatorsTest.cs b/test/DynamicExpresso.UnitTest/OperatorsTest.cs index e0db871b..d8f984a1 100644 --- a/test/DynamicExpresso.UnitTest/OperatorsTest.cs +++ b/test/DynamicExpresso.UnitTest/OperatorsTest.cs @@ -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; @@ -68,7 +68,38 @@ public void Typeof_Operator() { var target = new Interpreter(); + Assert.Throws(() => target.Eval("typeof(int??)")); + Assert.Throws(() => 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), target.Eval("typeof(IEnumerable)")); + Assert.AreEqual(typeof(IEnumerable>), target.Eval("typeof(IEnumerable>)")); + Assert.AreEqual(typeof(IEnumerable<>), target.Eval("typeof(IEnumerable<>)")); + + Assert.AreEqual(typeof(Dictionary[,]), target.Eval("typeof(Dictionary[,])")); + Assert.AreEqual(typeof(Dictionary>), target.Eval("typeof(Dictionary>)")); + Assert.AreEqual(typeof(Dictionary<,>), target.Eval("typeof(Dictionary<,>)")); } [Test] @@ -765,4 +796,4 @@ public TypeWithoutOverloadedBinaryOperators(int value) } } } -} \ No newline at end of file +}