Skip to content

Commit

Permalink
Update function argument parsing for strings (part 2) (#760)
Browse files Browse the repository at this point in the history
* Update function argument parsing for strings (part 2)

* .

* move test classes

* ...

* ok?

* <PatchVersion>8-preview-04</PatchVersion>
  • Loading branch information
StefH authored Jan 13, 2024
1 parent 59e029b commit 2aecd4b
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 114 deletions.
1 change: 1 addition & 0 deletions System.Linq.Dynamic.Core.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=DLL_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Formattable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=renamer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unescape/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xunit/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Copyright>Copyright © ZZZ Projects</Copyright>
<DefaultLanguage>en-us</DefaultLanguage>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>11</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>PackageReadme.md</PackageReadmeFile>
Expand Down
25 changes: 16 additions & 9 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -889,25 +889,26 @@ private AnyOf<Expression, Type> 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;
Expand All @@ -917,11 +918,13 @@ private AnyOf<Expression, Type> 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()
Expand Down Expand Up @@ -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<Expression>();

while (true)
{
var argumentExpression = ParseOutKeyword();
Expand Down
81 changes: 50 additions & 31 deletions src/System.Linq.Dynamic.Core/Parser/StringParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Parse a Double and Single Quoted string.
/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET
/// </summary>
internal static class StringParser
{
/// <summary>
/// Parse a Double and Single Quoted string.
/// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET
/// </summary>
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);
}
}
}
121 changes: 50 additions & 71 deletions test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Type> _customTypes;

public virtual HashSet<Type> GetCustomTypes()
{
if (_customTypes != null)
{
return _customTypes;
}

_customTypes = new HashSet<Type>(FindTypesMarkedWithDynamicLinqTypeAttribute(new[] { GetType().GetTypeInfo().Assembly }))
{
typeof(CustomClassWithStaticMethod),
typeof(StaticHelper)
};
return _customTypes;
}

public Dictionary<Type, List<MethodInfo>> GetExtensionMethods()
{
var types = GetCustomTypes();

var list = new List<Tuple<Type, MethodInfo>>();

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<Type, MethodInfo>(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()
{
Expand Down Expand Up @@ -1405,15 +1338,15 @@ 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<Func<User, string>>)lambdaChar;

var delegateChar = funcChar.Compile();
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\")";
Expand All @@ -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<Func<User, bool>>)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<Func<User, bool>>)lambda;

var compile = func.Compile();
var result = (bool?)compile.DynamicInvoke(user);

// Assert
result.Should().Be(false);
}

[Theory]
Expand Down
6 changes: 6 additions & 0 deletions test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace System.Linq.Dynamic.Core.Tests
{
public class CustomClassWithStaticMethod
{
public static int GetAge(int x) => x;
}
}
Loading

0 comments on commit 2aecd4b

Please sign in to comment.