From 2aecd4b7da2e713d3aef13d869cc16e64e4e8e86 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 13 Jan 2024 11:37:11 +0100 Subject: [PATCH] Update function argument parsing for strings (part 2) (#760) * Update function argument parsing for strings (part 2) * . * move test classes * ... * ok? * 8-preview-04 --- System.Linq.Dynamic.Core.sln.DotSettings | 1 + src/Directory.Build.props | 2 +- .../Parser/ExpressionParser.cs | 25 ++-- .../Parser/StringParser.cs | 81 +++++++----- .../DynamicExpressionParserTests.cs | 121 ++++++++---------- .../Helpers/Models/User.cs | 6 + .../Parser/StringParserTests.cs | 2 +- .../CustomClassWithStaticMethod.cs | 7 + .../TestClasses/StaticHelper.cs | 73 +++++++++++ .../TestClasses/StaticHelperSqlExpression.cs | 10 ++ .../TestClasses/TestCustomTypeProvider.cs | 55 ++++++++ version.xml | 2 +- 12 files changed, 271 insertions(+), 114 deletions(-) create mode 100644 test/System.Linq.Dynamic.Core.Tests/TestClasses/CustomClassWithStaticMethod.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelperSqlExpression.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/TestClasses/TestCustomTypeProvider.cs diff --git a/System.Linq.Dynamic.Core.sln.DotSettings b/System.Linq.Dynamic.Core.sln.DotSettings index bc3691ec..fe4bacee 100644 --- a/System.Linq.Dynamic.Core.sln.DotSettings +++ b/System.Linq.Dynamic.Core.sln.DotSettings @@ -6,4 +6,5 @@ True True True + True True \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index bc2081c3..e34f3e2a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,7 +7,7 @@ Copyright © ZZZ Projects en-us true - 11 + latest enable logo.png PackageReadme.md diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 88f498ad..7e81a3ea 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -889,25 +889,26 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) { _textParser.ValidateToken(TokenId.StringLiteral); - var stringValue = StringParser.ParseString(_textParser.CurrentToken.Text); + var text = _textParser.CurrentToken.Text; + var parsedStringValue = StringParser.ParseString(_textParser.CurrentToken.Text); if (_textParser.CurrentToken.Text[0] == '\'') { - if (stringValue.Length > 1) + if (parsedStringValue.Length > 1) { throw ParseError(Res.InvalidCharacterLiteral); } _textParser.NextToken(); - return ConstantExpressionHelper.CreateLiteral(stringValue[0], stringValue); + return ConstantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue); } _textParser.NextToken(); - if (_parsingConfig.SupportCastingToFullyQualifiedTypeAsString && !forceParseAsString && stringValue.Length > 2 && stringValue.Contains('.')) + if (_parsingConfig.SupportCastingToFullyQualifiedTypeAsString && !forceParseAsString && parsedStringValue.Length > 2 && parsedStringValue.Contains('.')) { // Try to resolve this string as a type - var type = _typeFinder.FindTypeByName(stringValue, null, false); + var type = _typeFinder.FindTypeByName(parsedStringValue, null, false); if (type is { }) { return type; @@ -917,11 +918,13 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) // While the next token is also a string, keep concatenating these strings and get next token while (_textParser.CurrentToken.Id == TokenId.StringLiteral) { - stringValue += _textParser.CurrentToken.Text; + text += _textParser.CurrentToken.Text; _textParser.NextToken(); } - - return ConstantExpressionHelper.CreateLiteral(stringValue, stringValue); + + parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos); + + return ConstantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue); } private Expression ParseIntegerLiteral() @@ -2170,15 +2173,19 @@ private Expression[] ParseArgumentList() { _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); _textParser.NextToken(); - Expression[] args = _textParser.CurrentToken.Id != TokenId.CloseParen ? ParseArguments() : new Expression[0]; + + var args = _textParser.CurrentToken.Id != TokenId.CloseParen ? ParseArguments() : new Expression[0]; + _textParser.ValidateToken(TokenId.CloseParen, Res.CloseParenOrCommaExpected); _textParser.NextToken(); + return args; } private Expression[] ParseArguments() { var argList = new List(); + while (true) { var argumentExpression = ParseOutKeyword(); diff --git a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs index 3f22573d..18dcb2b2 100644 --- a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs @@ -2,40 +2,59 @@ using System.Linq.Dynamic.Core.Exceptions; using System.Text.RegularExpressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +/// +/// Parse a Double and Single Quoted string. +/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET +/// +internal static class StringParser { - /// - /// Parse a Double and Single Quoted string. - /// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET - /// - internal static class StringParser + private const string Pattern = @""""""; + private const string Replacement = "\""; + + public static string ParseString(string s, int pos = default) { - public static string ParseString(string s) + if (s == null || s.Length < 2) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringLength, s, 2), pos); + } + + if (s[0] != '"' && s[0] != '\'') + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringQuoteCharacter), pos); + } + + char quote = s[0]; // This can be single or a double quote + if (s.Last() != quote) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, s.Length, s), pos); + } + + try + { + return Regex.Unescape(s.Substring(1, s.Length - 2)); + } + catch (Exception ex) + { + throw new ParseException(ex.Message, pos, ex); + } + } + + public static string ParseStringAndReplaceDoubleQuotes(string s, int pos) + { + return ReplaceDoubleQuotes(ParseString(s, pos), pos); + } + + private static string ReplaceDoubleQuotes(string s, int pos) + { + try + { + return Regex.Replace(s, Pattern, Replacement); + } + catch (Exception ex) { - if (s == null || s.Length < 2) - { - throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringLength, s, 2), 0); - } - - if (s[0] != '"' && s[0] != '\'') - { - throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringQuoteCharacter), 0); - } - - char quote = s[0]; // This can be single or a double quote - if (s.Last() != quote) - { - throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, s.Length, s), s.Length); - } - - try - { - return Regex.Unescape(s.Substring(1, s.Length - 2)); - } - catch (Exception ex) - { - throw new ParseException(ex.Message, 0, ex); - } + throw new ParseException(ex.Message, pos, ex); } } } \ 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 82d987c1..fffdcca4 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -6,7 +6,6 @@ using System.Linq.Dynamic.Core.Tests.TestHelpers; using System.Linq.Expressions; using System.Reflection; -using System.Runtime.CompilerServices; using FluentAssertions; using Moq; using NFluent; @@ -77,11 +76,6 @@ public class ComplexParseLambda3Result public int TotalIncome { get; set; } } - public class CustomClassWithStaticMethod - { - public static int GetAge(int x) => x; - } - public class CustomClassWithMethod { public int GetAge(int x) => x; @@ -121,7 +115,7 @@ public CustomTextClass(string origin) public static implicit operator string(CustomTextClass customTextValue) { - return customTextValue?.Origin; + return customTextValue.Origin; } public static implicit operator CustomTextClass(string origin) @@ -256,67 +250,6 @@ public override string ToString() } } - public static class StaticHelper - { - public static Guid? GetGuid(string name) - { - return Guid.NewGuid(); - } - - public static string Filter(string filter) - { - return filter; - } - } - - public class TestCustomTypeProvider : AbstractDynamicLinqCustomTypeProvider, IDynamicLinkCustomTypeProvider - { - private HashSet _customTypes; - - public virtual HashSet GetCustomTypes() - { - if (_customTypes != null) - { - return _customTypes; - } - - _customTypes = new HashSet(FindTypesMarkedWithDynamicLinqTypeAttribute(new[] { GetType().GetTypeInfo().Assembly })) - { - typeof(CustomClassWithStaticMethod), - typeof(StaticHelper) - }; - return _customTypes; - } - - public Dictionary> GetExtensionMethods() - { - var types = GetCustomTypes(); - - var list = new List>(); - - foreach (var type in types) - { - var extensionMethods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) - .Where(x => x.IsDefined(typeof(ExtensionAttribute), false)).ToList(); - - extensionMethods.ForEach(x => list.Add(new Tuple(x.GetParameters()[0].ParameterType, x))); - } - - return list.GroupBy(x => x.Item1, tuple => tuple.Item2).ToDictionary(key => key.Key, methods => methods.ToList()); - } - - public Type ResolveType(string typeName) - { - return Type.GetType(typeName); - } - - public Type ResolveTypeBySimpleName(string typeName) - { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - return ResolveTypeBySimpleName(assemblies, typeName); - } - } - [Fact] public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQuery_false_String() { @@ -1405,7 +1338,7 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio var user = new User(); // Act : char - var expressionTextChar = "StaticHelper.Filter(\"C == 'x'\")"; + var expressionTextChar = "StaticHelper.Filter(\"C == 'c'\")"; var lambdaChar = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionTextChar, user); var funcChar = (Expression>)lambdaChar; @@ -1413,7 +1346,7 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio var resultChar = (string?)delegateChar.DynamicInvoke(user); // Assert : int - resultChar.Should().Be("C == 'x'"); + resultChar.Should().Be("C == 'c'"); // Act : int var expressionTextIncome = "StaticHelper.Filter(\"Income == 5\")"; @@ -1435,7 +1368,53 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio var resultUserName = (string?)delegateUserName.DynamicInvoke(user); // Assert : string - resultUserName.Should().Be(@"UserName == ""x"""""""); + resultUserName.Should().Be(@"UserName == ""x"""); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression1String() + { + // Arrange + var config = new ParsingConfig + { + CustomTypeProvider = new TestCustomTypeProvider() + }; + + var user = new User(); + + // Act + var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = 5"""", """"""""))"", """"))"; + var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user); + var func = (Expression>)lambda; + + var compile = func.Compile(); + var result = (bool?)compile.DynamicInvoke(user); + + // Assert + result.Should().Be(false); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression2String() + { + // Arrange + var config = new ParsingConfig + { + CustomTypeProvider = new TestCustomTypeProvider() + }; + + var user = new User(); + + // Act + var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = "" + StaticHelper.ToExpressionString(StaticHelper.Get(""CurrentPlace""), 2) + """""", """"""""))"", """"))"; + var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user); + var func = (Expression>)lambda; + + var compile = func.Compile(); + var result = (bool?)compile.DynamicInvoke(user); + + // Assert + result.Should().Be(false); } [Theory] diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs index 8b6bc5a9..d84f5eaa 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -6,6 +6,12 @@ public class User { public Guid Id { get; set; } + public Guid? ParentId { get; set; } + + public Guid? LegalPersonId { get; set; } + + public Guid? PointSiteTD { get; set; } + public SnowflakeId SnowflakeId { get; set; } public string UserName { get; set; } diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs index af42df75..cc8f782d 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs @@ -58,7 +58,7 @@ public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsExcepti parseException.Which.InnerException!.Message.Should().Contain("hexadecimal digits"); - parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseString(String s) in ").And.Contain("System.Linq.Dynamic.Core\\Parser\\StringParser.cs:line "); + parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseString(String s, Int32 pos) in ").And.Contain("System.Linq.Dynamic.Core\\Parser\\StringParser.cs:line "); } [Theory] diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/CustomClassWithStaticMethod.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/CustomClassWithStaticMethod.cs new file mode 100644 index 00000000..ab6bad96 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/CustomClassWithStaticMethod.cs @@ -0,0 +1,7 @@ +namespace System.Linq.Dynamic.Core.Tests +{ + public class CustomClassWithStaticMethod + { + public static int GetAge(int x) => x; + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs new file mode 100644 index 00000000..464fcdcd --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs @@ -0,0 +1,73 @@ +using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using System.Linq.Expressions; + +namespace System.Linq.Dynamic.Core.Tests +{ + public static class StaticHelper + { + public static Guid? GetGuid(string name) + { + return Guid.NewGuid(); + } + + public static string Filter(string filter) + { + return filter; + } + + public static StaticHelperSqlExpression SubSelect(string columnName, string objectClassName, string? filter, string order) + { + Expression>? expFilter = null; + + if (filter != null) + { + var config = new ParsingConfig + { + CustomTypeProvider = new TestCustomTypeProvider() + }; + + expFilter = DynamicExpressionParser.ParseLambda(config, true, filter); // Failed Here! + } + + return new StaticHelperSqlExpression + { + Filter = expFilter + }; + } + + public static bool In(Guid? value, StaticHelperSqlExpression expression) + { + return value != Guid.Empty; + } + + public static Guid First(StaticHelperSqlExpression staticHelperSqlExpression) + { + return Guid.NewGuid(); + } + + public static string ToExpressionString(Guid? value, int subQueryLevel) + { + if (value == null) + { + return "NULL"; + } + + var quote = GetQuote(subQueryLevel); + return $"Guid.Parse({quote}{value}{quote})"; + } + + public static Guid Get(string settingName) + { + return Guid.NewGuid(); + } + + private static string GetQuote(int subQueryLevel) + { + var quoteCount = (int)Math.Pow(2, subQueryLevel - 1); + + //var quote = string.Concat(Enumerable.Repeat("\"", quoteCount)); + //return quote; + return new string('"', quoteCount); + } + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelperSqlExpression.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelperSqlExpression.cs new file mode 100644 index 00000000..d381a240 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelperSqlExpression.cs @@ -0,0 +1,10 @@ +using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using System.Linq.Expressions; + +namespace System.Linq.Dynamic.Core.Tests +{ + public class StaticHelperSqlExpression + { + public Expression>? Filter { get; set; } + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/TestCustomTypeProvider.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/TestCustomTypeProvider.cs new file mode 100644 index 00000000..ccd6be9e --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/TestCustomTypeProvider.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.CustomTypeProviders; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace System.Linq.Dynamic.Core.Tests +{ + public class TestCustomTypeProvider : AbstractDynamicLinqCustomTypeProvider, IDynamicLinkCustomTypeProvider + { + private HashSet? _customTypes; + + public virtual HashSet GetCustomTypes() + { + if (_customTypes != null) + { + return _customTypes; + } + + _customTypes = new HashSet(FindTypesMarkedWithDynamicLinqTypeAttribute(new[] { GetType().GetTypeInfo().Assembly })) + { + typeof(CustomClassWithStaticMethod), + typeof(StaticHelper) + }; + return _customTypes; + } + + public Dictionary> GetExtensionMethods() + { + var types = GetCustomTypes(); + + var list = new List>(); + + foreach (var type in types) + { + var extensionMethods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.IsDefined(typeof(ExtensionAttribute), false)).ToList(); + + extensionMethods.ForEach(x => list.Add(new Tuple(x.GetParameters()[0].ParameterType, x))); + } + + return list.GroupBy(x => x.Item1, tuple => tuple.Item2).ToDictionary(key => key.Key, methods => methods.ToList()); + } + + public Type ResolveType(string typeName) + { + return Type.GetType(typeName)!; + } + + public Type ResolveTypeBySimpleName(string typeName) + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + return ResolveTypeBySimpleName(assemblies, typeName)!; + } + } +} \ No newline at end of file diff --git a/version.xml b/version.xml index 4fe8065c..9f0743a9 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 7 + 8-preview-04 \ No newline at end of file