Skip to content
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
58 changes: 39 additions & 19 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,11 @@
{
var expr = ParseConditionalOperator();
var ascending = true;
if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending"))
if (TokenIsIdentifier("asc") || TokenIsIdentifier("ascending"))
{
_textParser.NextToken();
}
else if (TokenIdentifierIs("desc") || TokenIdentifierIs("descending"))
else if (TokenIsIdentifier("desc") || TokenIsIdentifier("descending"))
{
_textParser.NextToken();
ascending = false;
Expand Down Expand Up @@ -337,19 +337,34 @@
return left;
}

// in operator for literals - example: "x in (1,2,3,4)"
// in operator to mimic contains - example: "x in @0", compare to @0.Contains(x)
// Adapted from ticket submitted by github user mlewis9548
// "in" / "not in" / "not_in" operator for literals - example: "x in (1,2,3,4)"
// "in" / "not in" / "not_in" operator to mimic contains - example: "x in @0", compare to @0.Contains(x)
private Expression ParseIn()
{
Expression left = ParseLogicalAndOrOperator();
Expression accumulate = left;

while (TokenIdentifierIs("in"))
while (_textParser.TryGetToken(["in", "not_in", "not"], [TokenId.Exclamation], out var token))
{
var op = _textParser.CurrentToken;
var not = false;
if (token.Text == "not_in")
{
not = true;
}
else if (token.Text == "not" || token.Id == TokenId.Exclamation)
{
not = true;

_textParser.NextToken();

if (!TokenIsIdentifier("in"))
{
throw ParseError(token.Pos, Res.TokenExpected, "in");
}
}

_textParser.NextToken();

if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list)
{
while (_textParser.CurrentToken.Id != TokenId.CloseParen)
Expand All @@ -364,18 +379,18 @@
{
if (right is ConstantExpression constantExprRight)
{
right = ParseEnumToConstantExpression(op.Pos, left.Type, constantExprRight);
right = ParseEnumToConstantExpression(token.Pos, left.Type, constantExprRight);
}
else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight))
{
right = ParseEnumToConstantExpression(op.Pos, left.Type, unwrappedConstantExprRight);
right = ParseEnumToConstantExpression(token.Pos, left.Type, unwrappedConstantExprRight);
}
}

// else, check for direct type match
else if (left.Type != right.Type)
{
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, op.Pos);
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos);
}

if (accumulate.Type != typeof(bool))
Expand All @@ -389,7 +404,7 @@

if (_textParser.CurrentToken.Id == TokenId.End)
{
throw ParseError(op.Pos, Res.CloseParenOrCommaExpected);
throw ParseError(token.Pos, Res.CloseParenOrCommaExpected);
}
}

Expand All @@ -413,7 +428,12 @@
}
else
{
throw ParseError(op.Pos, Res.OpenParenOrIdentifierExpected);
throw ParseError(token.Pos, Res.OpenParenOrIdentifierExpected);
}

if (not)
{
accumulate = Expression.Not(accumulate);
}
}

Expand Down Expand Up @@ -759,7 +779,7 @@
private Expression ParseArithmetic()
{
Expression left = ParseUnary();
while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIdentifierIs("mod"))
while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIsIdentifier("mod"))
{
Token op = _textParser.CurrentToken;
_textParser.NextToken();
Expand Down Expand Up @@ -787,11 +807,11 @@
// -, !, not unary operators
private Expression ParseUnary()
{
if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIdentifierIs("not"))
if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIsIdentifier("not"))
{
Token op = _textParser.CurrentToken;
_textParser.NextToken();
if (op.Id == TokenId.Minus && (_textParser.CurrentToken.Id == TokenId.IntegerLiteral || _textParser.CurrentToken.Id == TokenId.RealLiteral))
if (op.Id == TokenId.Minus && _textParser.CurrentToken.Id is TokenId.IntegerLiteral or TokenId.RealLiteral)
{
_textParser.CurrentToken.Text = "-" + _textParser.CurrentToken.Text;
_textParser.CurrentToken.Pos = op.Pos;
Expand Down Expand Up @@ -1445,7 +1465,7 @@
if (!arrayInitializer)
{
string? propName;
if (TokenIdentifierIs("as"))
if (TokenIsIdentifier("as"))
{
_textParser.NextToken();
propName = GetIdentifierAs();
Expand Down Expand Up @@ -1930,7 +1950,7 @@
switch (member)
{
case PropertyInfo property:
var propertyIsStatic = property?.GetGetMethod().IsStatic ?? property?.GetSetMethod().IsStatic ?? false;

Check warning on line 1953 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1953 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.
propertyOrFieldExpression = propertyIsStatic ? Expression.Property(null, property) : Expression.Property(expression, property);
return true;

Expand Down Expand Up @@ -2527,11 +2547,11 @@
#endif
}

private bool TokenIdentifierIs(string id)
private bool TokenIsIdentifier(string id)
{
return _textParser.CurrentToken.Id == TokenId.Identifier && string.Equals(id, _textParser.CurrentToken.Text, StringComparison.OrdinalIgnoreCase);
return _textParser.TokenIsIdentifier(id);
}

private string GetIdentifier()
{
_textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected);
Expand Down
34 changes: 34 additions & 0 deletions src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,40 @@ public void ValidateToken(TokenId tokenId, string? errorMessage = null)
}
}

/// <summary>
/// Check if the current token is an <see cref="TokenId.Identifier"/> with the provided id .
/// </summary>
/// <param name="id">The id</param>
public bool TokenIsIdentifier(string id)
{
return CurrentToken.Id == TokenId.Identifier && string.Equals(id, CurrentToken.Text, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Try to get a token based on the id or <see cref="TokenId"/>.
/// </summary>
/// <param name="ids">The ids.</param>
/// <param name="tokenIds">The tokenIds.</param>
/// <param name="token">The found token, or default when not found.</param>
public bool TryGetToken(string[] ids, TokenId[] tokenIds, out Token token)
{
token = default;

if (ids.Any(TokenIsIdentifier))
{
token = CurrentToken;
return true;
}

if (tokenIds.Any(tokenId => tokenId == CurrentToken.Id))
{
token = CurrentToken;
return true;
}

return false;
}

private void SetTextPos(int pos)
{
_textPos = pos;
Expand Down
68 changes: 52 additions & 16 deletions test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1307,28 +1307,44 @@ public void ExpressionTests_In_Short()
public void ExpressionTests_In_String()
{
// Arrange
var testRange = Enumerable.Range(1, 100).ToArray();
var testModels = User.GenerateSampleModels(10);
var testModelByUsername = string.Format("Username in (\"{0}\",\"{1}\",\"{2}\")", testModels[0].UserName, testModels[1].UserName, testModels[2].UserName);
var testModelByUsername = $"Username in (\"{testModels[0].UserName}\",\"{testModels[1].UserName}\",\"{testModels[2].UserName}\")";

// Act
var result1 = testModels.AsQueryable().Where(testModelByUsername).ToArray();
var result2 = testModels.AsQueryable().Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id).ToArray();

// Assert
Assert.Equal(testModels.Take(3).ToArray(), result1);
Assert.Equal(testModels.Take(3).ToArray(), result2);
}

[Fact]
public void ExpressionTests_In_IntegerArray()
{
// Arrange
var testRange = Enumerable.Range(1, 10).ToArray();
var testInExpression = new[] { 2, 4, 6, 8 };

// Act
var result1a = testRange.AsQueryable().Where("it in (2,4,6,8)").ToArray();
var result1b = testRange.AsQueryable().Where("it in (2, 4, 6, 8)").ToArray();
// https://github.com/NArnott/System.Linq.Dynamic/issues/52
var result2 = testModels.AsQueryable().Where(testModelByUsername).ToArray();
var result3 =
testModels.AsQueryable()
.Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id)
.ToArray();
var result4 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray();
var result2 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray();

// Assert
Assert.Equal(new[] { 2, 4, 6, 8 }, result1a);
Assert.Equal(new[] { 2, 4, 6, 8 }, result1b);
Assert.Equal(testModels.Take(3).ToArray(), result2);
Assert.Equal(testModels.Take(3).ToArray(), result3);
Assert.Equal(new[] { 2, 4, 6, 8 }, result4);
Assert.Equal([2, 4, 6, 8], result1a);
Assert.Equal([2, 4, 6, 8], result1b);
Assert.Equal([2, 4, 6, 8], result2);
}

[Fact]
public void ExpressionTests_InvalidNotIn_ThrowsException()
{
// Arrange
var testRange = Enumerable.Range(1, 10).ToArray();

// Act + Assert
Check.ThatCode(() => testRange.AsQueryable().Where("it not not in (2,4,6,8)").ToArray()).Throws<ParseException>();
}

[Fact]
Expand Down Expand Up @@ -1519,6 +1535,26 @@ public void ExpressionTests_Multiply_Number()
Check.That(result).ContainsExactly(expected);
}

[Fact]
public void ExpressionTests_NotIn_IntegerArray()
{
// Arrange
var testRange = Enumerable.Range(1, 9).ToArray();
var testInExpression = new[] { 2, 4, 6, 8 };

// Act
var result1a = testRange.AsQueryable().Where("it not in (2,4,6,8)").ToArray();
var result1b = testRange.AsQueryable().Where("it not_in (2, 4, 6, 8)").ToArray();
var result2 = testRange.AsQueryable().Where("it not in @0", testInExpression).ToArray();
var result3 = testRange.AsQueryable().Where("it not_in @0", testInExpression).ToArray();

// Assert
Assert.Equal([1, 3, 5, 7, 9], result1a);
Assert.Equal([1, 3, 5, 7, 9], result1b);
Assert.Equal([1, 3, 5, 7, 9], result2);
Assert.Equal([1, 3, 5, 7, 9], result3);
}

[Fact]
public void ExpressionTests_NullCoalescing()
{
Expand Down Expand Up @@ -1699,7 +1735,7 @@ public void ExpressionTests_NullPropagating_Config_Has_UseDefault(string test, s
queryAsString = queryAsString.Substring(queryAsString.IndexOf(".Select") + 1).TrimEnd(']');
Check.That(queryAsString).Equals(query);
}

[Fact]
public void ExpressionTests_NullPropagation_Method()
{
Expand Down Expand Up @@ -2103,7 +2139,7 @@ public void ExpressionTests_StringEscaping()

// Act
var result = baseQuery.Where("it.Value == \"ab\\\"cd\"").ToList();

// Assert
Assert.Single(result);
Assert.Equal("ab\"cd", result[0].Value);
Expand Down
Loading
Loading