Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update function argument parsing for strings (part 2) #760

Merged
merged 6 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading