diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index d1ea7a90..1f1daf78 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -802,8 +802,7 @@ private Expression ParsePrimaryStart() return ParseIdentifier(); case TokenId.StringLiteral: - var expressionOrType = ParseStringLiteral(false); - return expressionOrType.IsFirst ? expressionOrType.First : ParseTypeAccess(expressionOrType.Second, false); + return ParseStringLiteralAsStringExpressionOrTypeExpression(); case TokenId.IntegerLiteral: return ParseIntegerLiteral(); @@ -819,6 +818,32 @@ private Expression ParsePrimaryStart() } } + private Expression ParseStringLiteralAsStringExpressionOrTypeExpression() + { + var clonedTextParser = _textParser.Clone(); + clonedTextParser.NextToken(); + + // Check if next token is a "(" or a "?(". + // Used for casting like $"\"System.DateTime\"(Abc)" or $"\"System.DateTime\"?(Abc)". + // In that case, the string value is NOT forced to stay a string. + bool forceParseAsString = true; + if (clonedTextParser.CurrentToken.Id == TokenId.OpenParen) + { + forceParseAsString = false; + } + else if (clonedTextParser.CurrentToken.Id == TokenId.Question) + { + clonedTextParser.NextToken(); + if (clonedTextParser.CurrentToken.Id == TokenId.OpenParen) + { + forceParseAsString = false; + } + } + + var expressionOrType = ParseStringLiteral(forceParseAsString); + return expressionOrType.IsFirst ? expressionOrType.First : ParseTypeAccess(expressionOrType.Second, false); + } + private AnyOf ParseStringLiteral(bool forceParseAsString) { _textParser.ValidateToken(TokenId.StringLiteral); diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index 83839d82..82284d7a 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -2,526 +2,536 @@ using System.Globalization; using System.Linq.Dynamic.Core.Exceptions; -namespace System.Linq.Dynamic.Core.Tokenizer +namespace System.Linq.Dynamic.Core.Tokenizer; + +/// +/// TextParser which can be used to parse a text into tokens. +/// +public class TextParser { + private const char DefaultNumberDecimalSeparator = '.'; + private static readonly char[] EscapeCharacters = { '\\', 'a', 'b', 'f', 'n', 'r', 't', 'v' }; + + // These aliases are supposed to simply the where clause and make it more human readable + private static readonly Dictionary PredefinedOperatorAliases = new(StringComparer.OrdinalIgnoreCase) + { + { "eq", TokenId.Equal }, + { "equal", TokenId.Equal }, + { "ne", TokenId.ExclamationEqual }, + { "notequal", TokenId.ExclamationEqual }, + { "neq", TokenId.ExclamationEqual }, + { "lt", TokenId.LessThan }, + { "LessThan", TokenId.LessThan }, + { "le", TokenId.LessThanEqual }, + { "LessThanEqual", TokenId.LessThanEqual }, + { "gt", TokenId.GreaterThan }, + { "GreaterThan", TokenId.GreaterThan }, + { "ge", TokenId.GreaterThanEqual }, + { "GreaterThanEqual", TokenId.GreaterThanEqual }, + { "and", TokenId.DoubleAmpersand }, + { "AndAlso", TokenId.DoubleAmpersand }, + { "or", TokenId.DoubleBar }, + { "OrElse", TokenId.DoubleBar }, + { "not", TokenId.Exclamation }, + { "mod", TokenId.Percent } + }; + + private readonly char _numberDecimalSeparator; + private readonly string _text; + private readonly int _textLen; + private readonly ParsingConfig _parsingConfig; + + private int _textPos; + private char _ch; + /// - /// TextParser which can be used to parse a text into tokens. + /// The current parsed . /// - public class TextParser + public Token CurrentToken; + + /// + /// Constructor for TextParser + /// + /// + /// + public TextParser(ParsingConfig config, string text) { - private const char DefaultNumberDecimalSeparator = '.'; - private static readonly char[] EscapeCharacters = { '\\', 'a', 'b', 'f', 'n', 'r', 't', 'v' }; + _parsingConfig = config; - // These aliases are supposed to simply the where clause and make it more human readable - private static readonly Dictionary PredefinedOperatorAliases = new(StringComparer.OrdinalIgnoreCase) - { - { "eq", TokenId.Equal }, - { "equal", TokenId.Equal }, - { "ne", TokenId.ExclamationEqual }, - { "notequal", TokenId.ExclamationEqual }, - { "neq", TokenId.ExclamationEqual }, - { "lt", TokenId.LessThan }, - { "LessThan", TokenId.LessThan }, - { "le", TokenId.LessThanEqual }, - { "LessThanEqual", TokenId.LessThanEqual }, - { "gt", TokenId.GreaterThan }, - { "GreaterThan", TokenId.GreaterThan }, - { "ge", TokenId.GreaterThanEqual }, - { "GreaterThanEqual", TokenId.GreaterThanEqual }, - { "and", TokenId.DoubleAmpersand }, - { "AndAlso", TokenId.DoubleAmpersand }, - { "or", TokenId.DoubleBar }, - { "OrElse", TokenId.DoubleBar }, - { "not", TokenId.Exclamation }, - { "mod", TokenId.Percent } - }; - - private readonly char _numberDecimalSeparator; - private readonly string _text; - private readonly int _textLen; - - private int _textPos; - private char _ch; - - /// - /// The current parsed . - /// - public Token CurrentToken; - - /// - /// Constructor for TextParser - /// - /// - /// - public TextParser(ParsingConfig config, string text) - { - _numberDecimalSeparator = config.NumberParseCulture?.NumberFormat.NumberDecimalSeparator[0] ?? DefaultNumberDecimalSeparator; + _numberDecimalSeparator = config.NumberParseCulture?.NumberFormat.NumberDecimalSeparator[0] ?? DefaultNumberDecimalSeparator; - _text = text; - _textLen = _text.Length; + _text = text; + _textLen = _text.Length; - SetTextPos(0); - NextToken(); - } + SetTextPos(0); + NextToken(); + } - private void SetTextPos(int pos) - { - _textPos = pos; - _ch = _textPos < _textLen ? _text[_textPos] : '\0'; - } + public TextParser Clone() + { + var clone = new TextParser(_parsingConfig, _text); + clone.SetTextPos(_textPos); - private void NextChar() - { - if (_textPos < _textLen) - { - _textPos++; - } - _ch = _textPos < _textLen ? _text[_textPos] : '\0'; - } + return clone; + } + + /// + /// Peek the next character. + /// + /// The next character, or \0 if end of string. + public char PeekNextChar() + { + return _textPos + 1 < _textLen ? _text[_textPos + 1] : '\0'; + } - /// - /// Peek the next character. - /// - /// The next character, or \0 if end of string. - public char PeekNextChar() + /// + /// Go to the next token. + /// + public void NextToken() + { + while (char.IsWhiteSpace(_ch)) { - return _textPos + 1 < _textLen ? _text[_textPos + 1] : '\0'; + NextChar(); } - /// - /// Go to the next token. - /// - public void NextToken() + // ReSharper disable once RedundantAssignment + TokenId tokenId = TokenId.Unknown; + int tokenPos = _textPos; + + switch (_ch) { - while (char.IsWhiteSpace(_ch)) - { + case '!': NextChar(); - } - - // ReSharper disable once RedundantAssignment - TokenId tokenId = TokenId.Unknown; - int tokenPos = _textPos; - - switch (_ch) - { - case '!': + if (_ch == '=') + { NextChar(); - if (_ch == '=') - { - NextChar(); - tokenId = TokenId.ExclamationEqual; - } - else - { - tokenId = TokenId.Exclamation; - } - break; + tokenId = TokenId.ExclamationEqual; + } + else + { + tokenId = TokenId.Exclamation; + } + break; + + case '%': + NextChar(); + tokenId = TokenId.Percent; + break; - case '%': + case '&': + NextChar(); + if (_ch == '&') + { NextChar(); - tokenId = TokenId.Percent; - break; + tokenId = TokenId.DoubleAmpersand; + } + else + { + tokenId = TokenId.Ampersand; + } + break; + + case '(': + NextChar(); + tokenId = TokenId.OpenParen; + break; - case '&': - NextChar(); - if (_ch == '&') - { - NextChar(); - tokenId = TokenId.DoubleAmpersand; - } - else - { - tokenId = TokenId.Ampersand; - } - break; + case ')': + NextChar(); + tokenId = TokenId.CloseParen; + break; - case '(': - NextChar(); - tokenId = TokenId.OpenParen; - break; + case '{': + NextChar(); + tokenId = TokenId.OpenCurlyParen; + break; - case ')': - NextChar(); - tokenId = TokenId.CloseParen; - break; + case '}': + NextChar(); + tokenId = TokenId.CloseCurlyParen; + break; - case '{': - NextChar(); - tokenId = TokenId.OpenCurlyParen; - break; + case '*': + NextChar(); + tokenId = TokenId.Asterisk; + break; - case '}': - NextChar(); - tokenId = TokenId.CloseCurlyParen; - break; + case '+': + NextChar(); + tokenId = TokenId.Plus; + break; - case '*': - NextChar(); - tokenId = TokenId.Asterisk; - break; + case ',': + NextChar(); + tokenId = TokenId.Comma; + break; - case '+': - NextChar(); - tokenId = TokenId.Plus; - break; + case '-': + NextChar(); + tokenId = TokenId.Minus; + break; - case ',': - NextChar(); - tokenId = TokenId.Comma; - break; + case '.': + NextChar(); + tokenId = TokenId.Dot; + break; - case '-': - NextChar(); - tokenId = TokenId.Minus; - break; + case '/': + NextChar(); + tokenId = TokenId.Slash; + break; - case '.': - NextChar(); - tokenId = TokenId.Dot; - break; + case ':': + NextChar(); + tokenId = TokenId.Colon; + break; - case '/': + case '<': + NextChar(); + if (_ch == '=') + { NextChar(); - tokenId = TokenId.Slash; - break; - - case ':': + tokenId = TokenId.LessThanEqual; + } + else if (_ch == '>') + { NextChar(); - tokenId = TokenId.Colon; - break; - - case '<': + tokenId = TokenId.LessGreater; + } + else if (_ch == '<') + { NextChar(); - if (_ch == '=') - { - NextChar(); - tokenId = TokenId.LessThanEqual; - } - else if (_ch == '>') - { - NextChar(); - tokenId = TokenId.LessGreater; - } - else if (_ch == '<') - { - NextChar(); - tokenId = TokenId.DoubleLessThan; - } - else - { - tokenId = TokenId.LessThan; - } - break; - - case '=': + tokenId = TokenId.DoubleLessThan; + } + else + { + tokenId = TokenId.LessThan; + } + break; + + case '=': + NextChar(); + if (_ch == '=') + { NextChar(); - if (_ch == '=') - { - NextChar(); - tokenId = TokenId.DoubleEqual; - } - else if (_ch == '>') - { - NextChar(); - tokenId = TokenId.Lambda; - } - else - { - tokenId = TokenId.Equal; - } - break; - - case '>': + tokenId = TokenId.DoubleEqual; + } + else if (_ch == '>') + { NextChar(); - if (_ch == '=') - { - NextChar(); - tokenId = TokenId.GreaterThanEqual; - } - else if (_ch == '>') - { - NextChar(); - tokenId = TokenId.DoubleGreaterThan; - } - else - { - tokenId = TokenId.GreaterThan; - } - break; - - case '?': + tokenId = TokenId.Lambda; + } + else + { + tokenId = TokenId.Equal; + } + break; + + case '>': + NextChar(); + if (_ch == '=') + { NextChar(); - if (_ch == '?') - { - NextChar(); - tokenId = TokenId.NullCoalescing; - } - else if (_ch == '.') - { - NextChar(); - tokenId = TokenId.NullPropagation; - } - else - { - tokenId = TokenId.Question; - } - break; - - case '[': + tokenId = TokenId.GreaterThanEqual; + } + else if (_ch == '>') + { NextChar(); - tokenId = TokenId.OpenBracket; - break; - - case ']': + tokenId = TokenId.DoubleGreaterThan; + } + else + { + tokenId = TokenId.GreaterThan; + } + break; + + case '?': + NextChar(); + if (_ch == '?') + { NextChar(); - tokenId = TokenId.CloseBracket; - break; - - case '|': + tokenId = TokenId.NullCoalescing; + } + else if (_ch == '.') + { NextChar(); - if (_ch == '|') - { - NextChar(); - tokenId = TokenId.DoubleBar; - } - else - { - tokenId = TokenId.Bar; - } - break; + tokenId = TokenId.NullPropagation; + } + else + { + tokenId = TokenId.Question; + } + break; + + case '[': + NextChar(); + tokenId = TokenId.OpenBracket; + break; - case '"': - case '\'': - bool balanced = false; - char quote = _ch; + case ']': + NextChar(); + tokenId = TokenId.CloseBracket; + break; + case '|': + NextChar(); + if (_ch == '|') + { NextChar(); + tokenId = TokenId.DoubleBar; + } + else + { + tokenId = TokenId.Bar; + } + break; + + case '"': + case '\'': + bool balanced = false; + char quote = _ch; - while (_textPos < _textLen && _ch != quote) - { - char next = PeekNextChar(); + NextChar(); - if (_ch == '\\') + while (_textPos < _textLen && _ch != quote) + { + char next = PeekNextChar(); + + if (_ch == '\\') + { + if (EscapeCharacters.Contains(next)) { - if (EscapeCharacters.Contains(next)) - { - NextChar(); - } - - if (next == '"') - { - NextChar(); - } + NextChar(); } - NextChar(); - - if (_ch == quote) + if (next == '"') { - balanced = !balanced; + NextChar(); } } - if (_textPos == _textLen && !balanced) + NextChar(); + + if (_ch == quote) { - throw ParseError(_textPos, Res.UnterminatedStringLiteral); + balanced = !balanced; } + } - NextChar(); + if (_textPos == _textLen && !balanced) + { + throw ParseError(_textPos, Res.UnterminatedStringLiteral); + } + + NextChar(); - tokenId = TokenId.StringLiteral; + tokenId = TokenId.StringLiteral; + break; + + default: + if (char.IsLetter(_ch) || _ch == '@' || _ch == '_' || _ch == '$' || _ch == '^' || _ch == '~') + { + do + { + NextChar(); + } while (char.IsLetterOrDigit(_ch) || _ch == '_'); + tokenId = TokenId.Identifier; break; + } - default: - if (char.IsLetter(_ch) || _ch == '@' || _ch == '_' || _ch == '$' || _ch == '^' || _ch == '~') + if (char.IsDigit(_ch)) + { + tokenId = TokenId.IntegerLiteral; + do { - do - { - NextChar(); - } while (char.IsLetterOrDigit(_ch) || _ch == '_'); - tokenId = TokenId.Identifier; - break; - } + NextChar(); + } while (char.IsDigit(_ch)); - if (char.IsDigit(_ch)) + bool binaryInteger = false; + if (_ch == 'B' || _ch == 'b') { - tokenId = TokenId.IntegerLiteral; + NextChar(); + ValidateBinaryChar(); do { NextChar(); - } while (char.IsDigit(_ch)); + } while (IsZeroOrOne(_ch)); - bool binaryInteger = false; - if (_ch == 'B' || _ch == 'b') - { - NextChar(); - ValidateBinaryChar(); - do - { - NextChar(); - } while (IsZeroOrOne(_ch)); - - binaryInteger = true; - } + binaryInteger = true; + } - if (binaryInteger) - { - break; - } + if (binaryInteger) + { + break; + } - bool hexInteger = false; - if (_ch == 'X' || _ch == 'x') + bool hexInteger = false; + if (_ch == 'X' || _ch == 'x') + { + NextChar(); + ValidateHexChar(); + do { NextChar(); - ValidateHexChar(); - do - { - NextChar(); - } while (IsHexChar(_ch)); + } while (IsHexChar(_ch)); - hexInteger = true; - } + hexInteger = true; + } - if (_ch == 'U' || _ch == 'L') + if (_ch == 'U' || _ch == 'L') + { + NextChar(); + if (_ch == 'L') { - NextChar(); - if (_ch == 'L') - { - if (_text[_textPos - 1] == 'U') NextChar(); - else throw ParseError(_textPos, Res.InvalidIntegerQualifier, _text.Substring(_textPos - 1, 2)); - } - ValidateExpression(); - break; + if (_text[_textPos - 1] == 'U') NextChar(); + else throw ParseError(_textPos, Res.InvalidIntegerQualifier, _text.Substring(_textPos - 1, 2)); } + ValidateExpression(); + break; + } - if (hexInteger) - { - break; - } + if (hexInteger) + { + break; + } - if (_ch == _numberDecimalSeparator) + if (_ch == _numberDecimalSeparator) + { + tokenId = TokenId.RealLiteral; + NextChar(); + ValidateDigit(); + do { - tokenId = TokenId.RealLiteral; NextChar(); - ValidateDigit(); - do - { - NextChar(); - } while (char.IsDigit(_ch)); - } + } while (char.IsDigit(_ch)); + } - if (_ch == 'E' || _ch == 'e') + if (_ch == 'E' || _ch == 'e') + { + tokenId = TokenId.RealLiteral; + NextChar(); + if (_ch == '+' || _ch == '-') NextChar(); + ValidateDigit(); + do { - tokenId = TokenId.RealLiteral; NextChar(); - if (_ch == '+' || _ch == '-') NextChar(); - ValidateDigit(); - do - { - NextChar(); - } while (char.IsDigit(_ch)); - } - - if (_ch == 'F' || _ch == 'f') NextChar(); - if (_ch == 'D' || _ch == 'd') NextChar(); - if (_ch == 'M' || _ch == 'm') NextChar(); - break; + } while (char.IsDigit(_ch)); } - if (_textPos == _textLen) - { - tokenId = TokenId.End; - break; - } + if (_ch == 'F' || _ch == 'f') NextChar(); + if (_ch == 'D' || _ch == 'd') NextChar(); + if (_ch == 'M' || _ch == 'm') NextChar(); + break; + } - throw ParseError(_textPos, Res.InvalidCharacter, _ch); - } + if (_textPos == _textLen) + { + tokenId = TokenId.End; + break; + } - CurrentToken.Pos = tokenPos; - CurrentToken.Text = _text.Substring(tokenPos, _textPos - tokenPos); - CurrentToken.OriginalId = tokenId; - CurrentToken.Id = GetAliasedTokenId(tokenId, CurrentToken.Text); + throw ParseError(_textPos, Res.InvalidCharacter, _ch); } - /// - /// Check if the current token is the specified . - /// - /// The tokenId to check. - /// The (optional) error message. - public void ValidateToken(TokenId tokenId, string? errorMessage = null) - { - if (CurrentToken.Id != tokenId) - { - throw ParseError(errorMessage ?? Res.SyntaxError); - } - } + CurrentToken.Pos = tokenPos; + CurrentToken.Text = _text.Substring(tokenPos, _textPos - tokenPos); + CurrentToken.OriginalId = tokenId; + CurrentToken.Id = GetAliasedTokenId(tokenId, CurrentToken.Text); + } - private void ValidateExpression() + /// + /// Check if the current token is the specified . + /// + /// The tokenId to check. + /// The (optional) error message. + public void ValidateToken(TokenId tokenId, string? errorMessage = null) + { + if (CurrentToken.Id != tokenId) { - if (char.IsLetterOrDigit(_ch)) - { - throw ParseError(_textPos, Res.ExpressionExpected); - } + throw ParseError(errorMessage ?? Res.SyntaxError); } + } - private void ValidateDigit() - { - if (!char.IsDigit(_ch)) - { - throw ParseError(_textPos, Res.DigitExpected); - } - } + private void SetTextPos(int pos) + { + _textPos = pos; + _ch = _textPos < _textLen ? _text[_textPos] : '\0'; + } - private void ValidateHexChar() + private void NextChar() + { + if (_textPos < _textLen) { - if (!IsHexChar(_ch)) - { - throw ParseError(_textPos, Res.HexCharExpected); - } + _textPos++; } + _ch = _textPos < _textLen ? _text[_textPos] : '\0'; + } - private void ValidateBinaryChar() + private void ValidateExpression() + { + if (char.IsLetterOrDigit(_ch)) { - if (!IsZeroOrOne(_ch)) - { - throw ParseError(_textPos, Res.BinraryCharExpected); - } + throw ParseError(_textPos, Res.ExpressionExpected); } + } - private Exception ParseError(string format, params object[] args) + private void ValidateDigit() + { + if (!char.IsDigit(_ch)) { - return ParseError(CurrentToken.Pos, format, args); + throw ParseError(_textPos, Res.DigitExpected); } + } - private static Exception ParseError(int pos, string format, params object[] args) + private void ValidateHexChar() + { + if (!IsHexChar(_ch)) { - return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos); + throw ParseError(_textPos, Res.HexCharExpected); } + } - private static TokenId GetAliasedTokenId(TokenId tokenId, string alias) + private void ValidateBinaryChar() + { + if (!IsZeroOrOne(_ch)) { - return tokenId == TokenId.Identifier && PredefinedOperatorAliases.TryGetValue(alias, out TokenId id) ? id : tokenId; + throw ParseError(_textPos, Res.BinraryCharExpected); } + } - private static bool IsHexChar(char c) + private Exception ParseError(string format, params object[] args) + { + return ParseError(CurrentToken.Pos, format, args); + } + + private static Exception ParseError(int pos, string format, params object[] args) + { + return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos); + } + + private static TokenId GetAliasedTokenId(TokenId tokenId, string alias) + { + return tokenId == TokenId.Identifier && PredefinedOperatorAliases.TryGetValue(alias, out TokenId id) ? id : tokenId; + } + + private static bool IsHexChar(char c) + { + if (char.IsDigit(c)) { - if (char.IsDigit(c)) - { - return true; - } - - if (c <= '\x007f') - { - c |= (char)0x20; - return c is >= 'a' and <= 'f'; - } - - return false; + return true; } - private static bool IsZeroOrOne(char c) + if (c <= '\x007f') { - return c is '0' or '1'; + c |= (char)0x20; + return c is >= 'a' and <= 'f'; } + + return false; + } + + private static bool IsZeroOrOne(char c) + { + return c is '0' or '1'; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index b2c589f5..818037e1 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -813,7 +813,7 @@ public class EntityDbo } [Fact] - public void DynamicExpressionParser_ParseLambda_StringLiteral_WithADot() + public void DynamicExpressionParser_ParseLambda_StringLiteral_WithADot_As_Arg() { // Act var expression = DynamicExpressionParser.ParseLambda(typeof(EntityDbo), typeof(bool), "Name == @0", "System.Int32"); @@ -824,6 +824,18 @@ public void DynamicExpressionParser_ParseLambda_StringLiteral_WithADot() result.Should().Be(true); } + [Fact] + public void DynamicExpressionParser_ParseLambda_StringLiteral_WithADot_In_Expression() + { + // Act + var expression = DynamicExpressionParser.ParseLambda(typeof(EntityDbo), typeof(bool), "Name == \"System.Int32\""); + var del = expression.Compile(); + var result = del.DynamicInvoke(new EntityDbo { Name = "System.Int32" }); + + // Assert + result.Should().Be(true); + } + [Fact] public void DynamicExpressionParser_ParseLambda_StringLiteral_ReturnsBooleanLambdaExpression() {