Skip to content

Commit 2aecd4b

Browse files
authored
Update function argument parsing for strings (part 2) (#760)
* Update function argument parsing for strings (part 2) * . * move test classes * ... * ok? * <PatchVersion>8-preview-04</PatchVersion>
1 parent 59e029b commit 2aecd4b

12 files changed

+271
-114
lines changed

System.Linq.Dynamic.Core.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
<s:Boolean x:Key="/Default/UserDictionary/Words/=DLL_0027s/@EntryIndexedValue">True</s:Boolean>
77
<s:Boolean x:Key="/Default/UserDictionary/Words/=Formattable/@EntryIndexedValue">True</s:Boolean>
88
<s:Boolean x:Key="/Default/UserDictionary/Words/=renamer/@EntryIndexedValue">True</s:Boolean>
9+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unescape/@EntryIndexedValue">True</s:Boolean>
910
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xunit/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

src/Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<Copyright>Copyright © ZZZ Projects</Copyright>
88
<DefaultLanguage>en-us</DefaultLanguage>
99
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10-
<LangVersion>11</LangVersion>
10+
<LangVersion>latest</LangVersion>
1111
<Nullable>enable</Nullable>
1212
<PackageIcon>logo.png</PackageIcon>
1313
<PackageReadmeFile>PackageReadme.md</PackageReadmeFile>

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

+16-9
Original file line numberDiff line numberDiff line change
@@ -889,25 +889,26 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
889889
{
890890
_textParser.ValidateToken(TokenId.StringLiteral);
891891

892-
var stringValue = StringParser.ParseString(_textParser.CurrentToken.Text);
892+
var text = _textParser.CurrentToken.Text;
893+
var parsedStringValue = StringParser.ParseString(_textParser.CurrentToken.Text);
893894

894895
if (_textParser.CurrentToken.Text[0] == '\'')
895896
{
896-
if (stringValue.Length > 1)
897+
if (parsedStringValue.Length > 1)
897898
{
898899
throw ParseError(Res.InvalidCharacterLiteral);
899900
}
900901

901902
_textParser.NextToken();
902-
return ConstantExpressionHelper.CreateLiteral(stringValue[0], stringValue);
903+
return ConstantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue);
903904
}
904905

905906
_textParser.NextToken();
906907

907-
if (_parsingConfig.SupportCastingToFullyQualifiedTypeAsString && !forceParseAsString && stringValue.Length > 2 && stringValue.Contains('.'))
908+
if (_parsingConfig.SupportCastingToFullyQualifiedTypeAsString && !forceParseAsString && parsedStringValue.Length > 2 && parsedStringValue.Contains('.'))
908909
{
909910
// Try to resolve this string as a type
910-
var type = _typeFinder.FindTypeByName(stringValue, null, false);
911+
var type = _typeFinder.FindTypeByName(parsedStringValue, null, false);
911912
if (type is { })
912913
{
913914
return type;
@@ -917,11 +918,13 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
917918
// While the next token is also a string, keep concatenating these strings and get next token
918919
while (_textParser.CurrentToken.Id == TokenId.StringLiteral)
919920
{
920-
stringValue += _textParser.CurrentToken.Text;
921+
text += _textParser.CurrentToken.Text;
921922
_textParser.NextToken();
922923
}
923-
924-
return ConstantExpressionHelper.CreateLiteral(stringValue, stringValue);
924+
925+
parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos);
926+
927+
return ConstantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
925928
}
926929

927930
private Expression ParseIntegerLiteral()
@@ -2170,15 +2173,19 @@ private Expression[] ParseArgumentList()
21702173
{
21712174
_textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected);
21722175
_textParser.NextToken();
2173-
Expression[] args = _textParser.CurrentToken.Id != TokenId.CloseParen ? ParseArguments() : new Expression[0];
2176+
2177+
var args = _textParser.CurrentToken.Id != TokenId.CloseParen ? ParseArguments() : new Expression[0];
2178+
21742179
_textParser.ValidateToken(TokenId.CloseParen, Res.CloseParenOrCommaExpected);
21752180
_textParser.NextToken();
2181+
21762182
return args;
21772183
}
21782184

21792185
private Expression[] ParseArguments()
21802186
{
21812187
var argList = new List<Expression>();
2188+
21822189
while (true)
21832190
{
21842191
var argumentExpression = ParseOutKeyword();

src/System.Linq.Dynamic.Core/Parser/StringParser.cs

+50-31
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,59 @@
22
using System.Linq.Dynamic.Core.Exceptions;
33
using System.Text.RegularExpressions;
44

5-
namespace System.Linq.Dynamic.Core.Parser
5+
namespace System.Linq.Dynamic.Core.Parser;
6+
7+
/// <summary>
8+
/// Parse a Double and Single Quoted string.
9+
/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET
10+
/// </summary>
11+
internal static class StringParser
612
{
7-
/// <summary>
8-
/// Parse a Double and Single Quoted string.
9-
/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET
10-
/// </summary>
11-
internal static class StringParser
13+
private const string Pattern = @"""""";
14+
private const string Replacement = "\"";
15+
16+
public static string ParseString(string s, int pos = default)
1217
{
13-
public static string ParseString(string s)
18+
if (s == null || s.Length < 2)
19+
{
20+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringLength, s, 2), pos);
21+
}
22+
23+
if (s[0] != '"' && s[0] != '\'')
24+
{
25+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringQuoteCharacter), pos);
26+
}
27+
28+
char quote = s[0]; // This can be single or a double quote
29+
if (s.Last() != quote)
30+
{
31+
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, s.Length, s), pos);
32+
}
33+
34+
try
35+
{
36+
return Regex.Unescape(s.Substring(1, s.Length - 2));
37+
}
38+
catch (Exception ex)
39+
{
40+
throw new ParseException(ex.Message, pos, ex);
41+
}
42+
}
43+
44+
public static string ParseStringAndReplaceDoubleQuotes(string s, int pos)
45+
{
46+
return ReplaceDoubleQuotes(ParseString(s, pos), pos);
47+
}
48+
49+
private static string ReplaceDoubleQuotes(string s, int pos)
50+
{
51+
try
52+
{
53+
return Regex.Replace(s, Pattern, Replacement);
54+
}
55+
catch (Exception ex)
1456
{
15-
if (s == null || s.Length < 2)
16-
{
17-
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringLength, s, 2), 0);
18-
}
19-
20-
if (s[0] != '"' && s[0] != '\'')
21-
{
22-
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.InvalidStringQuoteCharacter), 0);
23-
}
24-
25-
char quote = s[0]; // This can be single or a double quote
26-
if (s.Last() != quote)
27-
{
28-
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.UnexpectedUnclosedString, s.Length, s), s.Length);
29-
}
30-
31-
try
32-
{
33-
return Regex.Unescape(s.Substring(1, s.Length - 2));
34-
}
35-
catch (Exception ex)
36-
{
37-
throw new ParseException(ex.Message, 0, ex);
38-
}
57+
throw new ParseException(ex.Message, pos, ex);
3958
}
4059
}
4160
}

test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs

+50-71
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Linq.Dynamic.Core.Tests.TestHelpers;
77
using System.Linq.Expressions;
88
using System.Reflection;
9-
using System.Runtime.CompilerServices;
109
using FluentAssertions;
1110
using Moq;
1211
using NFluent;
@@ -77,11 +76,6 @@ public class ComplexParseLambda3Result
7776
public int TotalIncome { get; set; }
7877
}
7978

80-
public class CustomClassWithStaticMethod
81-
{
82-
public static int GetAge(int x) => x;
83-
}
84-
8579
public class CustomClassWithMethod
8680
{
8781
public int GetAge(int x) => x;
@@ -121,7 +115,7 @@ public CustomTextClass(string origin)
121115

122116
public static implicit operator string(CustomTextClass customTextValue)
123117
{
124-
return customTextValue?.Origin;
118+
return customTextValue.Origin;
125119
}
126120

127121
public static implicit operator CustomTextClass(string origin)
@@ -256,67 +250,6 @@ public override string ToString()
256250
}
257251
}
258252

259-
public static class StaticHelper
260-
{
261-
public static Guid? GetGuid(string name)
262-
{
263-
return Guid.NewGuid();
264-
}
265-
266-
public static string Filter(string filter)
267-
{
268-
return filter;
269-
}
270-
}
271-
272-
public class TestCustomTypeProvider : AbstractDynamicLinqCustomTypeProvider, IDynamicLinkCustomTypeProvider
273-
{
274-
private HashSet<Type> _customTypes;
275-
276-
public virtual HashSet<Type> GetCustomTypes()
277-
{
278-
if (_customTypes != null)
279-
{
280-
return _customTypes;
281-
}
282-
283-
_customTypes = new HashSet<Type>(FindTypesMarkedWithDynamicLinqTypeAttribute(new[] { GetType().GetTypeInfo().Assembly }))
284-
{
285-
typeof(CustomClassWithStaticMethod),
286-
typeof(StaticHelper)
287-
};
288-
return _customTypes;
289-
}
290-
291-
public Dictionary<Type, List<MethodInfo>> GetExtensionMethods()
292-
{
293-
var types = GetCustomTypes();
294-
295-
var list = new List<Tuple<Type, MethodInfo>>();
296-
297-
foreach (var type in types)
298-
{
299-
var extensionMethods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
300-
.Where(x => x.IsDefined(typeof(ExtensionAttribute), false)).ToList();
301-
302-
extensionMethods.ForEach(x => list.Add(new Tuple<Type, MethodInfo>(x.GetParameters()[0].ParameterType, x)));
303-
}
304-
305-
return list.GroupBy(x => x.Item1, tuple => tuple.Item2).ToDictionary(key => key.Key, methods => methods.ToList());
306-
}
307-
308-
public Type ResolveType(string typeName)
309-
{
310-
return Type.GetType(typeName);
311-
}
312-
313-
public Type ResolveTypeBySimpleName(string typeName)
314-
{
315-
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
316-
return ResolveTypeBySimpleName(assemblies, typeName);
317-
}
318-
}
319-
320253
[Fact]
321254
public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQuery_false_String()
322255
{
@@ -1405,15 +1338,15 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
14051338
var user = new User();
14061339

14071340
// Act : char
1408-
var expressionTextChar = "StaticHelper.Filter(\"C == 'x'\")";
1341+
var expressionTextChar = "StaticHelper.Filter(\"C == 'c'\")";
14091342
var lambdaChar = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionTextChar, user);
14101343
var funcChar = (Expression<Func<User, string>>)lambdaChar;
14111344

14121345
var delegateChar = funcChar.Compile();
14131346
var resultChar = (string?)delegateChar.DynamicInvoke(user);
14141347

14151348
// Assert : int
1416-
resultChar.Should().Be("C == 'x'");
1349+
resultChar.Should().Be("C == 'c'");
14171350

14181351
// Act : int
14191352
var expressionTextIncome = "StaticHelper.Filter(\"Income == 5\")";
@@ -1435,7 +1368,53 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
14351368
var resultUserName = (string?)delegateUserName.DynamicInvoke(user);
14361369

14371370
// Assert : string
1438-
resultUserName.Should().Be(@"UserName == ""x""""""");
1371+
resultUserName.Should().Be(@"UserName == ""x""");
1372+
}
1373+
1374+
[Fact]
1375+
public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression1String()
1376+
{
1377+
// Arrange
1378+
var config = new ParsingConfig
1379+
{
1380+
CustomTypeProvider = new TestCustomTypeProvider()
1381+
};
1382+
1383+
var user = new User();
1384+
1385+
// Act
1386+
var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = 5"""", """"""""))"", """"))";
1387+
var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
1388+
var func = (Expression<Func<User, bool>>)lambda;
1389+
1390+
var compile = func.Compile();
1391+
var result = (bool?)compile.DynamicInvoke(user);
1392+
1393+
// Assert
1394+
result.Should().Be(false);
1395+
}
1396+
1397+
[Fact]
1398+
public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression2String()
1399+
{
1400+
// Arrange
1401+
var config = new ParsingConfig
1402+
{
1403+
CustomTypeProvider = new TestCustomTypeProvider()
1404+
};
1405+
1406+
var user = new User();
1407+
1408+
// Act
1409+
var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = "" + StaticHelper.ToExpressionString(StaticHelper.Get(""CurrentPlace""), 2) + """""", """"""""))"", """"))";
1410+
var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
1411+
var func = (Expression<Func<User, bool>>)lambda;
1412+
1413+
var compile = func.Compile();
1414+
var result = (bool?)compile.DynamicInvoke(user);
1415+
1416+
// Assert
1417+
result.Should().Be(false);
14391418
}
14401419

14411420
[Theory]

test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ public class User
66
{
77
public Guid Id { get; set; }
88

9+
public Guid? ParentId { get; set; }
10+
11+
public Guid? LegalPersonId { get; set; }
12+
13+
public Guid? PointSiteTD { get; set; }
14+
915
public SnowflakeId SnowflakeId { get; set; }
1016

1117
public string UserName { get; set; }

test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsExcepti
5858

5959
parseException.Which.InnerException!.Message.Should().Contain("hexadecimal digits");
6060

61-
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 ");
61+
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 ");
6262
}
6363

6464
[Theory]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace System.Linq.Dynamic.Core.Tests
2+
{
3+
public class CustomClassWithStaticMethod
4+
{
5+
public static int GetAge(int x) => x;
6+
}
7+
}

0 commit comments

Comments
 (0)