From 478d32cb13838a6ee9be442504e7dc689ff05fec Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 12:11:18 +0200 Subject: [PATCH 1/8] wip --- .../DynamicQueryableExtensions.cs | 2 + .../Parser/ExpressionParser.cs | 92 +++++++++++++---- .../Parser/SupportedMethods/MethodFinder.cs | 28 ++++-- .../ExpressionTests.MethodCall.cs | 17 ++++ .../Helpers/Models/User.cs | 99 ++++++++++--------- .../Parser/MethodFinderTest.cs | 23 +++-- .../System.Linq.Dynamic.Core.Tests.csproj | 1 + 7 files changed, 180 insertions(+), 82 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 8908434dd..78f822b75 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -1807,6 +1807,8 @@ public static IQueryable Select(this IQueryable source, Parsin new[] { source.ElementType, typeof(TResult) }, source.Expression, Expression.Quote(lambda))); + + return source.Provider.CreateQuery(optimized); } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c16ec5bc0..28ddf497a 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -20,6 +20,9 @@ namespace System.Linq.Dynamic.Core.Parser; /// public class ExpressionParser { + private static readonly string[] OutKeywords = { "out", "$out" }; + private const string DiscardVariable = "_"; + private const string MethodOrderBy = nameof(Queryable.OrderBy); private const string MethodOrderByDescending = nameof(Queryable.OrderByDescending); private const string MethodThenBy = nameof(Queryable.ThenBy); @@ -150,7 +153,7 @@ public Expression Parse(Type? resultType, bool createParameterCtor = true) _createParameterCtor = createParameterCtor; int exprPos = _textParser.CurrentToken.Pos; - Expression? expr = ParseConditionalOperator(); + Expression? expr = ParseFirstAsConditionalOperator(); if (resultType != null) { @@ -165,13 +168,37 @@ public Expression Parse(Type? resultType, bool createParameterCtor = true) return expr; } + // out keyword + private Expression ParseOutKeyword() + { + if (_textParser.CurrentToken.Id == TokenId.Identifier && OutKeywords.Contains(_textParser.CurrentToken.Text)) + { + // Go to next token (which should be a '_') + _textParser.NextToken(); + + var variableName = _textParser.CurrentToken.Text; + if (variableName != DiscardVariable) + { + throw ParseError(_textParser.CurrentToken.Pos, "out !!!"); + } + + // Advance to next token + _textParser.NextToken(); + + // Use MakeByRefType() to indicate that it's a by-reference type. C# uses this for both 'ref' and 'out' parameters. + return Expression.Parameter(typeof(string).MakeByRefType(), variableName); + } + + return ParseFirstAsConditionalOperator(); + } + #pragma warning disable 0219 internal IList ParseOrdering(bool forceThenBy = false) { var orderings = new List(); while (true) { - Expression expr = ParseConditionalOperator(); + Expression expr = ParseFirstAsConditionalOperator(); bool ascending = true; if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending")) { @@ -209,17 +236,17 @@ internal IList ParseOrdering(bool forceThenBy = false) #pragma warning restore 0219 // ?: operator - private Expression ParseConditionalOperator() + private Expression ParseFirstAsConditionalOperator() { int errorPos = _textParser.CurrentToken.Pos; Expression expr = ParseNullCoalescingOperator(); if (_textParser.CurrentToken.Id == TokenId.Question) { _textParser.NextToken(); - Expression expr1 = ParseConditionalOperator(); + Expression expr1 = ParseFirstAsConditionalOperator(); _textParser.ValidateToken(TokenId.Colon, Res.ColonExpected); _textParser.NextToken(); - Expression expr2 = ParseConditionalOperator(); + Expression expr2 = ParseFirstAsConditionalOperator(); expr = GenerateConditional(expr, expr1, expr2, false, errorPos); } return expr; @@ -232,7 +259,7 @@ private Expression ParseNullCoalescingOperator() if (_textParser.CurrentToken.Id == TokenId.NullCoalescing) { _textParser.NextToken(); - Expression right = ParseConditionalOperator(); + Expression right = ParseFirstAsConditionalOperator(); expr = Expression.Coalesce(expr, right); } return expr; @@ -247,7 +274,7 @@ private Expression ParseLambdaOperator() _textParser.NextToken(); if (_textParser.CurrentToken.Id == TokenId.Identifier || _textParser.CurrentToken.Id == TokenId.OpenParen) { - var right = ParseConditionalOperator(); + var right = ParseFirstAsConditionalOperator(); return Expression.Lambda(right, new[] { (ParameterExpression)expr }); } _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); @@ -919,7 +946,7 @@ private Expression ParseParenExpression() { _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); _textParser.NextToken(); - Expression e = ParseConditionalOperator(); + Expression e = ParseFirstAsConditionalOperator(); _textParser.ValidateToken(TokenId.CloseParen, Res.CloseParenOrOperatorExpected); _textParser.NextToken(); return e; @@ -931,9 +958,9 @@ private Expression ParseIdentifier() var isValidKeyWord = _keywordsHelper.TryGetValue(_textParser.CurrentToken.Text, out var value); - + bool shouldPrioritizeType = true; - + if (_parsingConfig.PrioritizePropertyOrFieldOverTheType && value is Type) { bool isPropertyOrField = _it != null && FindPropertyOrField(_it.Type, _textParser.CurrentToken.Text, false) != null; @@ -1374,7 +1401,7 @@ private Expression ParseNew() while (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) { int exprPos = _textParser.CurrentToken.Pos; - Expression expr = ParseConditionalOperator(); + Expression expr = ParseFirstAsConditionalOperator(); if (!arrayInitializer) { string? propName; @@ -1774,16 +1801,42 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) throw ParseError(errorPos, Res.MethodsAreInaccessible, TypeHelper.GetTypeName(method.DeclaringType!)); } - if (method.IsGenericMethod) + MethodInfo methodToCall; + if (!method.IsGenericMethod) + { + methodToCall = method; + } + else { var genericParameters = method.GetParameters().Where(p => p.ParameterType.IsGenericParameter); var typeArguments = genericParameters.Select(a => args[a.Position].Type); - var constructedMethod = method.MakeGenericMethod(typeArguments.ToArray()); - - return Expression.Call(expression, constructedMethod, args); + methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } - - return Expression.Call(expression, method, args); + + return Expression.Call(expression, methodToCall, args); + // stef +//#if NET35 +// return Expression.Call(expression, methodToCall, args); +//#else +// var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); +// if (outParameters.Any()) +// { +// var variablesInScope = outParameters.Select(p => Expression.Variable(p.Type, p.Name)).ToArray(); + +// var block = Expression.Block( +// variablesInScope, // Declare variables used inside the block +// Expression.Call(expression, methodToCall, args) // Expression body +// // Expression.Variable(methodToCall.ReturnType) +// //inScope[0] +// //variablesInScope[0] +// // +// ); + +// return Expression.Lambda(block); +// } + +// return Expression.Call(expression, methodToCall, args); +//#endif default: throw ParseError(errorPos, Res.AmbiguousMethodInvocation, id, TypeHelper.GetTypeName(type)); @@ -1864,7 +1917,7 @@ private Expression ParseAsLambda(string id) _textParser.NextToken(); LastLambdaItName = ItName; - var exp = ParseConditionalOperator(); + var exp = ParseFirstAsConditionalOperator(); // Restore previous context and clear internals _internals.Remove(id); @@ -2088,7 +2141,8 @@ private Expression[] ParseArguments() var argList = new List(); while (true) { - var argumentExpression = ParseConditionalOperator(); + var argumentExpression = ParseOutKeyword(); + // var argumentExpression = ParseFirstAsConditionalOperator(); _expressionHelper.WrapConstantExpression(ref argumentExpression); diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 3a926a2e3..59cf6d564 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -221,18 +221,32 @@ bool IsApplicable(MethodData method, Expression[] args) } else { - ParameterInfo pi = method.Parameters[i]; - if (pi.IsOut) + // stef + var methodParameter = method.Parameters[i]; + if (methodParameter.IsOut && args[i] is ParameterExpression parameterExpression) { +#if NET35 return false; - } +#else + if (!parameterExpression.IsByRef) + { + return false; + } - var promotedExpression = _parsingConfig.ExpressionPromoter.Promote(args[i], pi.ParameterType, false, method.MethodBase.DeclaringType != typeof(IEnumerableSignatures)); - if (promotedExpression == null) + //promotedArgs[i] = Expression.Parameter(parameterExpression.Type.MakeByRefType(), methodParameter.Name); + promotedArgs[i] = Expression.Parameter(methodParameter.ParameterType, methodParameter.Name); +#endif + } + else { - return false; + var promotedExpression = _parsingConfig.ExpressionPromoter.Promote(args[i], methodParameter.ParameterType, false, method.MethodBase.DeclaringType != typeof(IEnumerableSignatures)); + if (promotedExpression == null) + { + return false; + } + + promotedArgs[i] = promotedExpression; } - promotedArgs[i] = promotedExpression; } } diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index b858dc5c8..76885f3a5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using System.Linq.Expressions; using FluentAssertions; using Moq; using Xunit; @@ -33,6 +34,22 @@ private static ParsingConfig CreateParsingConfigForStaticMethodCallTests() }; } + [Fact] + public void ExpressionTests_MethodCall_Out() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + string? un = null; + var expected = users.Select(u => u.TryGetUserName(out un)); + var result = users.AsQueryable().Select(config, "TryGetUserName($out _)"); + + // Assert + result.Should().BeEquivalentTo(expected); + } + [Fact] public void ExpressionTests_MethodCall_NoParams() { 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 eec7384cb..d7a6e4844 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -1,70 +1,81 @@ using System.Collections.Generic; -namespace System.Linq.Dynamic.Core.Tests.Helpers.Models +namespace System.Linq.Dynamic.Core.Tests.Helpers.Models; + +public class User { - public class User - { - public Guid Id { get; set; } + public Guid Id { get; set; } - public SnowflakeId SnowflakeId { get; set; } + public SnowflakeId SnowflakeId { get; set; } - public string UserName { get; set; } + public string UserName { get; set; } - public int? NullableInt { get; set; } + public int? NullableInt { get; set; } - public int Income { get; set; } + public int Income { get; set; } - public UserProfile Profile { get; set; } + public UserProfile Profile { get; set; } - public UserState State { get; set; } + public UserState State { get; set; } - public List Roles { get; set; } + public List Roles { get; set; } - public bool TestMethod1() - { - return true; - } + public bool TestMethod1() + { + return true; + } + + public bool TestMethod2(User other) + { + return true; + } + + public bool TestMethod3(User other) + { + return Id == other.Id; + } - public bool TestMethod2(User other) + public bool TryGetUserName(out string? username) + { + if (UserName.EndsWith("1") || UserName.EndsWith("2")) { + username = UserName; return true; } - public bool TestMethod3(User other) - { - return Id == other.Id; - } + username = null; + return false; + } - public static IList GenerateSampleModels(int total, bool allowNullableProfiles = false) + public static IList GenerateSampleModels(int total, bool allowNullableProfiles = false) + { + var list = new List(); + + for (int i = 0; i < total; i++) { - var list = new List(); + var user = new User + { + Id = Guid.NewGuid(), + SnowflakeId = new SnowflakeId(((ulong)long.MaxValue + (ulong)i + 2UL)), + UserName = "User" + i, + Income = 1 + (i % 15) * 100 + }; - for (int i = 0; i < total; i++) + if (!allowNullableProfiles || (i % 8) != 5) { - var user = new User + user.Profile = new UserProfile { - Id = Guid.NewGuid(), - SnowflakeId = new SnowflakeId(((ulong)long.MaxValue + (ulong)i + 2UL)), - UserName = "User" + i, - Income = 1 + (i % 15) * 100 + FirstName = "FirstName" + i, + LastName = "LastName" + i, + Age = (i % 50) + 18 }; - - if (!allowNullableProfiles || (i % 8) != 5) - { - user.Profile = new UserProfile - { - FirstName = "FirstName" + i, - LastName = "LastName" + i, - Age = (i % 50) + 18 - }; - } - - user.Roles = new List(Role.StandardRoles); - - list.Add(user); } - return list.ToArray(); + user.Roles = new List(Role.StandardRoles); + + list.Add(user); } + + return list.ToArray(); } -} +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs index 16564d945..a1afadd49 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs @@ -3,21 +3,20 @@ using Xunit; using static System.Linq.Expressions.Expression; -namespace System.Linq.Dynamic.Core.Tests.Parser +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public class MethodFinderTest { - public class MethodFinderTest + [Fact] + public void MethodsOfDynamicLinqAndSystemLinqShouldBeEqual() { - [Fact] - public void MethodsOfDynamicLinqAndSystemLinqShouldBeEqual() - { - Expression> expr = x => x.ToString(); + Expression> expr = x => x.ToString(); - var selector = "ToString()"; - var prm = Parameter(typeof(int?)); - var parser = new ExpressionParser(new[] { prm }, selector, new object[] { }, ParsingConfig.Default); - var expr1 = parser.Parse(null); + var selector = "ToString()"; + var prm = Parameter(typeof(int?)); + var parser = new ExpressionParser(new[] { prm }, selector, new object[] { }, ParsingConfig.Default); + var expr1 = parser.Parse(null); - Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expr1).Method.DeclaringType); - } + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expr1).Method.DeclaringType); } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj index 1f82ea88a..3a27dfd4a 100644 --- a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj @@ -6,6 +6,7 @@ full True latest + enable ../../src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.snk {912FBF24-3CAE-4A50-B5EA-E525B9FAEC80} From 547f0ce42aee84d19dcbb90dd5c614c932f49cd9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 16:57:10 +0200 Subject: [PATCH 2/8] ok! --- .../DynamicQueryableExtensions.cs | 11 ++- .../Parser/ExpressionParser.cs | 84 ++++++++++++++----- .../ExpressionTests.MethodCall.cs | 6 +- .../Helpers/Models/User.cs | 8 +- 4 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 78f822b75..7ba188d26 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -1802,10 +1802,15 @@ public static IQueryable Select(this IQueryable source, Parsin bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(TResult), selector, args); - var optimized = OptimizeExpression(Expression.Call( - typeof(Queryable), nameof(Queryable.Select), + var e = Expression.Call( + typeof(Queryable), + nameof(Queryable.Select), new[] { source.ElementType, typeof(TResult) }, - source.Expression, Expression.Quote(lambda))); + source.Expression, + Expression.Quote(lambda) + ); + + var optimized = OptimizeExpression(e); diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 28ddf497a..80e364957 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1813,30 +1813,70 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } - return Expression.Call(expression, methodToCall, args); + //return Expression.Call(expression, methodToCall, args); // stef -//#if NET35 -// return Expression.Call(expression, methodToCall, args); -//#else -// var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); -// if (outParameters.Any()) -// { -// var variablesInScope = outParameters.Select(p => Expression.Variable(p.Type, p.Name)).ToArray(); +#if NET35 + return Expression.Call(expression, methodToCall, args); +#else + var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); + if (outParameters.Any()) + { + //var a = new List(); + var outVars = new List(); + var inP = new List(); + + foreach (var a in args) + { + if (a is ParameterExpression parameterExpression && parameterExpression.IsByRef) + { + var outputVar = Expression.Variable(parameterExpression.Type, parameterExpression.Name); + // a.Add(outputVar); + outVars.Add(outputVar); + } + else + { + // a.Add(parameterExpression); + inP.Add(a); + } + } + + // Create a method call expression + //var methodCall = Expression.Call(expression, methodToCall, args); + + // Create a variable expression to hold the 'out' parameter. + var outVar = Expression.Variable(typeof(string), "xxx"); + + // Create a method call expression to call User.TryParse. + var methodCall = Expression.Call( + expression, + methodToCall, + new Expression[] { args[0], outVar } + ); + + + // Create a variable to hold the return value + var returnValue = Expression.Variable(methodToCall.ReturnType, "returnValue"); + + var userParam = (ParameterExpression) expression;//Expression.Parameter(expression!.Type, "user"); -// var block = Expression.Block( -// variablesInScope, // Declare variables used inside the block -// Expression.Call(expression, methodToCall, args) // Expression body -// // Expression.Variable(methodToCall.ReturnType) -// //inScope[0] -// //variablesInScope[0] -// // -// ); - -// return Expression.Lambda(block); -// } - -// return Expression.Call(expression, methodToCall, args); -//#endif + // Create the block to return the boolean value. + var block = Expression.Block( + new[] { outVar, returnValue }, + Expression.Assign(returnValue, methodCall), + returnValue + ); + + // Create the lambda expression + var lambda = Expression.Lambda( + block, + userParam + ); + + return lambda; + } + + return Expression.Call(expression, methodToCall, args); +#endif default: throw ParseError(errorPos, Res.AmbiguousMethodInvocation, id, TypeHelper.GetTypeName(type)); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index 76885f3a5..10545a433 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -42,9 +42,9 @@ public void ExpressionTests_MethodCall_Out() var users = User.GenerateSampleModels(5); // Act - string? un = null; - var expected = users.Select(u => u.TryGetUserName(out un)); - var result = users.AsQueryable().Select(config, "TryGetUserName($out _)"); + string un = ""; + var expected = users.Select(u => u.TryParseWithArgument(u.UserName, out un)); + var result = users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out _)"); // Assert result.Should().BeEquivalentTo(expected); 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 d7a6e4844..7138c0997 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -35,15 +35,15 @@ public bool TestMethod3(User other) return Id == other.Id; } - public bool TryGetUserName(out string? username) + public bool TryParseWithArgument(string s, out string xxx) { - if (UserName.EndsWith("1") || UserName.EndsWith("2")) + if (s.EndsWith("1") || s.EndsWith("2")) { - username = UserName; + xxx = UserName; return true; } - username = null; + xxx = ""; return false; } From 093bfc8e35ed638a60e49e835ff1bc944c4b34fb Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 17:14:32 +0200 Subject: [PATCH 3/8] . --- .../Parser/ExpressionParser.cs | 66 ++++++++----------- .../Parser/SupportedMethods/MethodFinder.cs | 4 +- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 80e364957..e8b6bc684 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -179,14 +179,15 @@ private Expression ParseOutKeyword() var variableName = _textParser.CurrentToken.Text; if (variableName != DiscardVariable) { - throw ParseError(_textParser.CurrentToken.Pos, "out !!!"); + throw ParseError(_textParser.CurrentToken.Pos, "stef todo"); } // Advance to next token _textParser.NextToken(); - // Use MakeByRefType() to indicate that it's a by-reference type. C# uses this for both 'ref' and 'out' parameters. - return Expression.Parameter(typeof(string).MakeByRefType(), variableName); + // Use MakeByRefType() to indicate that it's a by-reference type because C# uses this for both 'ref' and 'out' parameters. + // The "typeof(object).MakeByRefType()" is used, this will be changed later in the flow to the real type. + return Expression.Parameter(typeof(object).MakeByRefType(), variableName); } return ParseFirstAsConditionalOperator(); @@ -1813,64 +1814,51 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } - //return Expression.Call(expression, methodToCall, args); // stef #if NET35 return Expression.Call(expression, methodToCall, args); #else var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); - if (outParameters.Any()) + + if (outParameters.Length == 1) { - //var a = new List(); - var outVars = new List(); - var inP = new List(); + // Create a new list which is used to store all method arguments. + var newList = new List(); + var blockList = new List(); - foreach (var a in args) + foreach (var arg in args) { - if (a is ParameterExpression parameterExpression && parameterExpression.IsByRef) + if (arg is ParameterExpression { IsByRef: true } parameterExpression) { - var outputVar = Expression.Variable(parameterExpression.Type, parameterExpression.Name); - // a.Add(outputVar); - outVars.Add(outputVar); + // Create a variable expression to hold the 'out' parameter. + var variable = Expression.Variable(parameterExpression.Type, parameterExpression.Name); + newList.Add(variable); + blockList.Add(variable); } else { - // a.Add(parameterExpression); - inP.Add(a); + newList.Add(arg); } } - // Create a method call expression - //var methodCall = Expression.Call(expression, methodToCall, args); - - // Create a variable expression to hold the 'out' parameter. - var outVar = Expression.Variable(typeof(string), "xxx"); - - // Create a method call expression to call User.TryParse. - var methodCall = Expression.Call( - expression, - methodToCall, - new Expression[] { args[0], outVar } - ); - + // Create a method call expression to call the method + var methodCall = Expression.Call(expression, methodToCall, newList); // Create a variable to hold the return value - var returnValue = Expression.Variable(methodToCall.ReturnType, "returnValue"); + var returnValue = Expression.Variable(methodToCall.ReturnType); + + // Define a parameter list which contains the variable expression for the 'out' parameter, and contains the returnValue variable. + blockList.Add(returnValue); - var userParam = (ParameterExpression) expression;//Expression.Parameter(expression!.Type, "user"); - // Create the block to return the boolean value. var block = Expression.Block( - new[] { outVar, returnValue }, + blockList.ToArray(), Expression.Assign(returnValue, methodCall), returnValue ); // Create the lambda expression - var lambda = Expression.Lambda( - block, - userParam - ); + var lambda = Expression.Lambda(block, (ParameterExpression)expression!); return lambda; } @@ -2182,7 +2170,6 @@ private Expression[] ParseArguments() while (true) { var argumentExpression = ParseOutKeyword(); - // var argumentExpression = ParseFirstAsConditionalOperator(); _expressionHelper.WrapConstantExpression(ref argumentExpression); @@ -2196,6 +2183,11 @@ private Expression[] ParseArguments() _textParser.NextToken(); } + if (argList.OfType().Count() > 1) + { + throw new ParseException("stef todo", _textParser.CurrentToken.Pos); + } + return argList.ToArray(); } diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 59cf6d564..d5d667b97 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -170,7 +170,7 @@ public int FindIndexer(Type type, Expression[] args, out MethodBase? method) return 0; } - bool IsApplicable(MethodData method, Expression[] args) + private bool IsApplicable(MethodData method, Expression[] args) { bool isParamArray = method.Parameters.Length > 0 && method.Parameters.Last().IsDefined(typeof(ParamArrayAttribute), false); @@ -221,7 +221,6 @@ bool IsApplicable(MethodData method, Expression[] args) } else { - // stef var methodParameter = method.Parameters[i]; if (methodParameter.IsOut && args[i] is ParameterExpression parameterExpression) { @@ -233,7 +232,6 @@ bool IsApplicable(MethodData method, Expression[] args) return false; } - //promotedArgs[i] = Expression.Parameter(parameterExpression.Type.MakeByRefType(), methodParameter.Name); promotedArgs[i] = Expression.Parameter(methodParameter.ParameterType, methodParameter.Name); #endif } From b911a1e835d2d7ee83d787e0bd6c254d3e31389e Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 17:17:30 +0200 Subject: [PATCH 4/8] . --- .../ExpressionTests.MethodCall.cs | 19 +++++++++++++++++-- .../Helpers/Models/User.cs | 5 +++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index 10545a433..cc1922bc4 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Tests.Helpers.Models; -using System.Linq.Expressions; using FluentAssertions; using Moq; using Xunit; @@ -35,7 +34,7 @@ private static ParsingConfig CreateParsingConfigForStaticMethodCallTests() } [Fact] - public void ExpressionTests_MethodCall_Out() + public void ExpressionTests_MethodCall_WithArgument_And_OutArgument() { // Arrange var config = CreateParsingConfigForMethodCallTests(); @@ -50,6 +49,22 @@ public void ExpressionTests_MethodCall_Out() result.Should().BeEquivalentTo(expected); } + [Fact] + public void ExpressionTests_MethodCall_WithoutArgument_And_OutArgument() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + string un = ""; + var expected = users.Select(u => u.TryParseWithoutArgument(out un)); + var result = users.AsQueryable().Select(config, "TryParseWithoutArgument(out _)"); + + // Assert + result.Should().BeEquivalentTo(expected); + } + [Fact] public void ExpressionTests_MethodCall_NoParams() { 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 7138c0997..bfe370dbb 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -35,6 +35,11 @@ public bool TestMethod3(User other) return Id == other.Id; } + public bool TryParseWithoutArgument(out string xxx) + { + return TryParseWithArgument(UserName, out xxx); + } + public bool TryParseWithArgument(string s, out string xxx) { if (s.EndsWith("1") || s.EndsWith("2")) From a660326811a42676af451c3195c76066349b2ef6 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 17:29:14 +0200 Subject: [PATCH 5/8] res --- .../DynamicQueryableExtensions.cs | 6 +- .../Parser/ExpressionParser.cs | 19 +-- src/System.Linq.Dynamic.Core/Res.cs | 161 +++++++++--------- .../Tokenizer/TextParser.cs | 2 +- 4 files changed, 93 insertions(+), 95 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 7ba188d26..fc1268654 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -1802,7 +1802,7 @@ public static IQueryable Select(this IQueryable source, Parsin bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(TResult), selector, args); - var e = Expression.Call( + var methodCallExpression = Expression.Call( typeof(Queryable), nameof(Queryable.Select), new[] { source.ElementType, typeof(TResult) }, @@ -1810,9 +1810,7 @@ public static IQueryable Select(this IQueryable source, Parsin Expression.Quote(lambda) ); - var optimized = OptimizeExpression(e); - - + var optimized = OptimizeExpression(methodCallExpression); return source.Provider.CreateQuery(optimized); } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index e8b6bc684..7e23ef805 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -179,7 +179,7 @@ private Expression ParseOutKeyword() var variableName = _textParser.CurrentToken.Text; if (variableName != DiscardVariable) { - throw ParseError(_textParser.CurrentToken.Pos, "stef todo"); + throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableRequireDiscard); } // Advance to next token @@ -1814,16 +1814,16 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } - // stef #if NET35 return Expression.Call(expression, methodToCall, args); #else var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); - if (outParameters.Length == 1) { - // Create a new list which is used to store all method arguments. + // A list which is used to store all method arguments. var newList = new List(); + + // A list which contains the variable expression for the 'out' parameter, and also contains the returnValue variable. var blockList = new List(); foreach (var arg in args) @@ -1832,6 +1832,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) { // Create a variable expression to hold the 'out' parameter. var variable = Expression.Variable(parameterExpression.Type, parameterExpression.Name); + newList.Add(variable); blockList.Add(variable); } @@ -1847,7 +1848,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) // Create a variable to hold the return value var returnValue = Expression.Variable(methodToCall.ReturnType); - // Define a parameter list which contains the variable expression for the 'out' parameter, and contains the returnValue variable. + // Add this return variable to the blockList blockList.Add(returnValue); // Create the block to return the boolean value. @@ -1857,10 +1858,8 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) returnValue ); - // Create the lambda expression - var lambda = Expression.Lambda(block, (ParameterExpression)expression!); - - return lambda; + // Create the lambda expression (note that expression must be a ParameterExpression !) + return Expression.Lambda(block, (ParameterExpression)expression!); } return Expression.Call(expression, methodToCall, args); @@ -2185,7 +2184,7 @@ private Expression[] ParseArguments() if (argList.OfType().Count() > 1) { - throw new ParseException("stef todo", _textParser.CurrentToken.Pos); + throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableSingleRequired); } return argList.ToArray(); diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 2402f710b..44d9f024c 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -1,81 +1,82 @@ -namespace System.Linq.Dynamic.Core +namespace System.Linq.Dynamic.Core; + +internal static class Res { - internal static class Res - { - public const string AmbiguousConstructorInvocation = "Ambiguous invocation of '{0}' constructor"; - public const string AmbiguousIndexerInvocation = "Ambiguous invocation of indexer in type '{0}'"; - public const string AmbiguousMethodInvocation = "Ambiguous invocation of method '{0}' in type '{1}'"; - public const string ArgsIncompatibleWithLambda = "Argument list incompatible with lambda expression"; - public const string BinraryCharExpected = "Binary character expected"; - public const string BothTypesConvertToOther = "Both of the types '{0}' and '{1}' convert to the other"; - public const string CannotConvertValue = "A value of type '{0}' cannot be converted to type '{1}'"; - public const string CannotIndexMultiDimArray = "Indexing of multi-dimensional arrays is not supported"; - public const string CloseBracketExpected = "']' expected"; - public const string CloseBracketOrCommaExpected = "']' or ',' expected"; - public const string CloseParenOrCommaExpected = "')' or ',' expected"; - public const string CloseParenOrOperatorExpected = "')' or operator expected"; - public const string ColonExpected = "':' expected"; - public const string DigitExpected = "Digit expected"; - public const string DotExpected = "'.' expected"; - public const string DotOrOpenParenExpected = "'.' or '(' expected"; - public const string DotOrOpenParenOrStringLiteralExpected = "'.' or '(' or string literal expected"; - public const string DynamicExpandoObjectIsNotSupported = "Dynamic / ExpandoObject is not supported in .NET 3.5, UAP and .NETStandard 1.3"; - public const string DuplicateIdentifier = "The identifier '{0}' was defined more than once"; - public const string EnumTypeNotFound = "Enum type '{0}' not found"; - public const string EnumValueExpected = "Enum value expected"; - public const string EnumValueNotDefined = "Enum value '{0}' is not defined in enum type '{1}'"; - public const string ExpressionExpected = "Expression expected"; - public const string ExpressionTypeMismatch = "Expression of type '{0}' expected"; - public const string FirstExprMustBeBool = "The first expression must be of type 'Boolean'"; - public const string FunctionRequiresOneArg = "The '{0}' function requires one argument"; - public const string FunctionRequiresOneNotNullArg = "The '{0}' function requires one argument which is not null."; - public const string FunctionRequiresNotNullArgOfType = "The '{0}' function requires the {1}argument to be not null and of type {2}."; - public const string FunctionRequiresOneOrTwoArgs = "The '{0}' function requires 1 or 2 arguments"; - public const string HexCharExpected = "Hexadecimal character expected"; - public const string IQueryableProviderNotAsync = "The provider for the source IQueryable doesn't implement IAsyncQueryProvider/IDbAsyncQueryProvider. Only providers that implement IAsyncQueryProvider/IDbAsyncQueryProvider can be used for Entity Framework asynchronous operations."; - public const string IdentifierExpected = "Identifier expected"; - public const string IdentifierImplementingInterfaceExpected = "Identifier implementing interface '{0}' expected"; - public const string IifRequiresThreeArgs = "The 'iif' function requires three arguments"; - public const string IncompatibleOperand = "Operator '{0}' incompatible with operand type '{1}'"; - public const string IncompatibleOperands = "Operator '{0}' incompatible with operand types '{1}' and '{2}'"; - public const string IncompatibleTypes = "Types '{0}' and '{1}' are incompatible"; - public const string InvalidBinaryIntegerLiteral = "Invalid binary integer literal '{0}'"; - public const string InvalidCharacter = "Syntax error '{0}'"; - public const string InvalidCharacterLiteral = "Character literal must contain exactly one character"; - public const string InvalidIndex = "Array index must be an integer expression"; - public const string InvalidIntegerLiteral = "Invalid integer literal '{0}'"; - public const string InvalidIntegerQualifier = "Invalid integer literal qualifier '{0}'"; - public const string InvalidRealLiteral = "Invalid real literal '{0}'"; - public const string InvalidStringQuoteCharacter = "An escaped string should start with a double (\") or a single (') quote."; - public const string InvalidStringLength = "String '{0}' should have at least {1} characters."; - public const string IsNullRequiresTwoArgs = "The 'isnull' function requires two arguments"; - public const string MethodIsVoid = "Method '{0}' in type '{1}' does not return a value"; - public const string MethodsAreInaccessible = "Methods on type '{0}' are not accessible"; - public const string MinusCannotBeAppliedToUnsignedInteger = "'-' cannot be applied to unsigned integers."; - public const string MissingAsClause = "Expression is missing an 'as' clause"; - public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; - public const string NoApplicableAggregate = "No applicable aggregate method '{0}({1})' exists"; - public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; - public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; - public const string NoItInScope = "No 'it' is in scope"; - public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; - public const string NoParentInScope = "No 'parent' is in scope"; - public const string NoRootInScope = "No 'root' is in scope"; - public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments"; - public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression"; - public const string OpenBracketExpected = "'[' expected"; - public const string OpenCurlyParenExpected = "'{' expected"; - public const string OpenParenExpected = "'(' expected"; - public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; - public const string ParseExceptionFormat = "{0} (at index {1})"; - public const string SyntaxError = "Syntax error"; - public const string TokenExpected = "{0} expected"; - public const string TypeHasNoNullableForm = "Type '{0}' has no nullable form"; - public const string TypeNotFound = "Type '{0}' not found"; - public const string UnexpectedUnclosedString = "Unexpected end of string with unclosed string at position {0} near '{1}'."; - public const string UnexpectedUnrecognizedEscapeSequence = "Unexpected unrecognized escape sequence at position {0} near '{1}'."; - public const string UnknownIdentifier = "Unknown identifier '{0}'"; - public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'"; - public const string UnterminatedStringLiteral = "Unterminated string literal"; - } -} + public const string AmbiguousConstructorInvocation = "Ambiguous invocation of '{0}' constructor"; + public const string AmbiguousIndexerInvocation = "Ambiguous invocation of indexer in type '{0}'"; + public const string AmbiguousMethodInvocation = "Ambiguous invocation of method '{0}' in type '{1}'"; + public const string ArgsIncompatibleWithLambda = "Argument list incompatible with lambda expression"; + public const string BinaryCharExpected = "Binary character expected"; + public const string BothTypesConvertToOther = "Both of the types '{0}' and '{1}' convert to the other"; + public const string CannotConvertValue = "A value of type '{0}' cannot be converted to type '{1}'"; + public const string CannotIndexMultiDimArray = "Indexing of multi-dimensional arrays is not supported"; + public const string CloseBracketExpected = "']' expected"; + public const string CloseBracketOrCommaExpected = "']' or ',' expected"; + public const string CloseParenOrCommaExpected = "')' or ',' expected"; + public const string CloseParenOrOperatorExpected = "')' or operator expected"; + public const string ColonExpected = "':' expected"; + public const string DigitExpected = "Digit expected"; + public const string DotExpected = "'.' expected"; + public const string DotOrOpenParenExpected = "'.' or '(' expected"; + public const string DotOrOpenParenOrStringLiteralExpected = "'.' or '(' or string literal expected"; + public const string DynamicExpandoObjectIsNotSupported = "Dynamic / ExpandoObject is not supported in .NET 3.5, UAP and .NETStandard 1.3"; + public const string DuplicateIdentifier = "The identifier '{0}' was defined more than once"; + public const string EnumTypeNotFound = "Enum type '{0}' not found"; + public const string EnumValueExpected = "Enum value expected"; + public const string EnumValueNotDefined = "Enum value '{0}' is not defined in enum type '{1}'"; + public const string ExpressionExpected = "Expression expected"; + public const string ExpressionTypeMismatch = "Expression of type '{0}' expected"; + public const string FirstExprMustBeBool = "The first expression must be of type 'Boolean'"; + public const string FunctionRequiresOneArg = "The '{0}' function requires one argument"; + public const string FunctionRequiresOneNotNullArg = "The '{0}' function requires one argument which is not null."; + public const string FunctionRequiresNotNullArgOfType = "The '{0}' function requires the {1}argument to be not null and of type {2}."; + public const string FunctionRequiresOneOrTwoArgs = "The '{0}' function requires 1 or 2 arguments"; + public const string HexCharExpected = "Hexadecimal character expected"; + public const string IQueryableProviderNotAsync = "The provider for the source IQueryable doesn't implement IAsyncQueryProvider/IDbAsyncQueryProvider. Only providers that implement IAsyncQueryProvider/IDbAsyncQueryProvider can be used for Entity Framework asynchronous operations."; + public const string IdentifierExpected = "Identifier expected"; + public const string IdentifierImplementingInterfaceExpected = "Identifier implementing interface '{0}' expected"; + public const string IifRequiresThreeArgs = "The 'iif' function requires three arguments"; + public const string IncompatibleOperand = "Operator '{0}' incompatible with operand type '{1}'"; + public const string IncompatibleOperands = "Operator '{0}' incompatible with operand types '{1}' and '{2}'"; + public const string IncompatibleTypes = "Types '{0}' and '{1}' are incompatible"; + public const string InvalidBinaryIntegerLiteral = "Invalid binary integer literal '{0}'"; + public const string InvalidCharacter = "Syntax error '{0}'"; + public const string InvalidCharacterLiteral = "Character literal must contain exactly one character"; + public const string InvalidIndex = "Array index must be an integer expression"; + public const string InvalidIntegerLiteral = "Invalid integer literal '{0}'"; + public const string InvalidIntegerQualifier = "Invalid integer literal qualifier '{0}'"; + public const string InvalidRealLiteral = "Invalid real literal '{0}'"; + public const string InvalidStringQuoteCharacter = "An escaped string should start with a double (\") or a single (') quote."; + public const string InvalidStringLength = "String '{0}' should have at least {1} characters."; + public const string IsNullRequiresTwoArgs = "The 'isnull' function requires two arguments"; + public const string MethodIsVoid = "Method '{0}' in type '{1}' does not return a value"; + public const string MethodsAreInaccessible = "Methods on type '{0}' are not accessible"; + public const string MinusCannotBeAppliedToUnsignedInteger = "'-' cannot be applied to unsigned integers."; + public const string MissingAsClause = "Expression is missing an 'as' clause"; + public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; + public const string NoApplicableAggregate = "No applicable aggregate method '{0}({1})' exists"; + public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; + public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; + public const string NoItInScope = "No 'it' is in scope"; + public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; + public const string NoParentInScope = "No 'parent' is in scope"; + public const string NoRootInScope = "No 'root' is in scope"; + public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments"; + public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression"; + public const string OpenBracketExpected = "'[' expected"; + public const string OpenCurlyParenExpected = "'{' expected"; + public const string OpenParenExpected = "'(' expected"; + public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; + public const string OutVariableRequireDiscard = "When using an out variable, a discard '_' is required."; + public const string OutVariableSingleRequired = "Only a single out variable is supported."; + public const string ParseExceptionFormat = "{0} (at index {1})"; + public const string SyntaxError = "Syntax error"; + public const string TokenExpected = "{0} expected"; + public const string TypeHasNoNullableForm = "Type '{0}' has no nullable form"; + public const string TypeNotFound = "Type '{0}' not found"; + public const string UnexpectedUnclosedString = "Unexpected end of string with unclosed string at position {0} near '{1}'."; + public const string UnexpectedUnrecognizedEscapeSequence = "Unexpected unrecognized escape sequence at position {0} near '{1}'."; + public const string UnknownIdentifier = "Unknown identifier '{0}'"; + public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'"; + public const string UnterminatedStringLiteral = "Unterminated string literal"; +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index 82284d7a3..7369a3ddf 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -495,7 +495,7 @@ private void ValidateBinaryChar() { if (!IsZeroOrOne(_ch)) { - throw ParseError(_textPos, Res.BinraryCharExpected); + throw ParseError(_textPos, Res.BinaryCharExpected); } } From c0f8ab7ddbb102be55be3f77b822d1e8d565e3e9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 19:14:02 +0200 Subject: [PATCH 6/8] tests --- .../ExpressionTests.MethodCall.cs | 29 +++++++++++++++++++ .../Helpers/Models/User.cs | 6 ++++ 2 files changed, 35 insertions(+) diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index cc1922bc4..ee9209818 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; +using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers.Models; using FluentAssertions; using Moq; @@ -65,6 +66,34 @@ public void ExpressionTests_MethodCall_WithoutArgument_And_OutArgument() result.Should().BeEquivalentTo(expected); } + [Fact] + public void ExpressionTests_MethodCall_WithArgument_And_NoDiscardForOutArgument_ThrowsException() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + Action action = () => users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out x)"); + + // Assert + action.Should().Throw().WithMessage("When using an out variable, a discard '_' is required."); + } + + [Fact] + public void ExpressionTests_MethodCall_WithArgument_And_MultipleOutArgument_ThrowsException() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + Action action = () => users.AsQueryable().Select(config, "TryParseWithArgumentAndTwoOut(it.UserName, $out _, $out _)"); + + // Assert + action.Should().Throw().WithMessage("Only a single out variable is supported."); + } + [Fact] public void ExpressionTests_MethodCall_NoParams() { 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 bfe370dbb..801f8099b 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -52,6 +52,12 @@ public bool TryParseWithArgument(string s, out string xxx) return false; } + public bool TryParseWithArgumentAndTwoOut(string s, out string xxx, out int x) + { + x = 0; + return TryParseWithArgument(s, out xxx) && int.TryParse(s, out x); + } + public static IList GenerateSampleModels(int total, bool allowNullableProfiles = false) { var list = new List(); From 12c540419f74ad5b1e615587c7ab277b2096d360 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Sep 2023 21:03:27 +0200 Subject: [PATCH 7/8] ... --- .../Parser/ExpressionParser.cs | 36 +++++++++---------- src/System.Linq.Dynamic.Core/Res.cs | 3 +- .../ExpressionTests.MethodCall.cs | 29 ++++++++------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 7e23ef805..101bdf7b1 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -153,7 +153,7 @@ public Expression Parse(Type? resultType, bool createParameterCtor = true) _createParameterCtor = createParameterCtor; int exprPos = _textParser.CurrentToken.Pos; - Expression? expr = ParseFirstAsConditionalOperator(); + Expression? expr = ParseConditionalOperator(); if (resultType != null) { @@ -179,7 +179,7 @@ private Expression ParseOutKeyword() var variableName = _textParser.CurrentToken.Text; if (variableName != DiscardVariable) { - throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableRequireDiscard); + throw ParseError(_textParser.CurrentToken.Pos, Res.OutKeywordRequiresDiscard); } // Advance to next token @@ -190,7 +190,7 @@ private Expression ParseOutKeyword() return Expression.Parameter(typeof(object).MakeByRefType(), variableName); } - return ParseFirstAsConditionalOperator(); + return ParseConditionalOperator(); } #pragma warning disable 0219 @@ -199,7 +199,7 @@ internal IList ParseOrdering(bool forceThenBy = false) var orderings = new List(); while (true) { - Expression expr = ParseFirstAsConditionalOperator(); + Expression expr = ParseConditionalOperator(); bool ascending = true; if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending")) { @@ -237,17 +237,17 @@ internal IList ParseOrdering(bool forceThenBy = false) #pragma warning restore 0219 // ?: operator - private Expression ParseFirstAsConditionalOperator() + private Expression ParseConditionalOperator() { int errorPos = _textParser.CurrentToken.Pos; Expression expr = ParseNullCoalescingOperator(); if (_textParser.CurrentToken.Id == TokenId.Question) { _textParser.NextToken(); - Expression expr1 = ParseFirstAsConditionalOperator(); + Expression expr1 = ParseConditionalOperator(); _textParser.ValidateToken(TokenId.Colon, Res.ColonExpected); _textParser.NextToken(); - Expression expr2 = ParseFirstAsConditionalOperator(); + Expression expr2 = ParseConditionalOperator(); expr = GenerateConditional(expr, expr1, expr2, false, errorPos); } return expr; @@ -260,7 +260,7 @@ private Expression ParseNullCoalescingOperator() if (_textParser.CurrentToken.Id == TokenId.NullCoalescing) { _textParser.NextToken(); - Expression right = ParseFirstAsConditionalOperator(); + Expression right = ParseConditionalOperator(); expr = Expression.Coalesce(expr, right); } return expr; @@ -275,7 +275,7 @@ private Expression ParseLambdaOperator() _textParser.NextToken(); if (_textParser.CurrentToken.Id == TokenId.Identifier || _textParser.CurrentToken.Id == TokenId.OpenParen) { - var right = ParseFirstAsConditionalOperator(); + var right = ParseConditionalOperator(); return Expression.Lambda(right, new[] { (ParameterExpression)expr }); } _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); @@ -947,7 +947,7 @@ private Expression ParseParenExpression() { _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); _textParser.NextToken(); - Expression e = ParseFirstAsConditionalOperator(); + Expression e = ParseConditionalOperator(); _textParser.ValidateToken(TokenId.CloseParen, Res.CloseParenOrOperatorExpected); _textParser.NextToken(); return e; @@ -1402,7 +1402,7 @@ private Expression ParseNew() while (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) { int exprPos = _textParser.CurrentToken.Pos; - Expression expr = ParseFirstAsConditionalOperator(); + Expression expr = ParseConditionalOperator(); if (!arrayInitializer) { string? propName; @@ -1818,7 +1818,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) return Expression.Call(expression, methodToCall, args); #else var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); - if (outParameters.Length == 1) + if (outParameters.Any()) { // A list which is used to store all method arguments. var newList = new List(); @@ -1858,7 +1858,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) returnValue ); - // Create the lambda expression (note that expression must be a ParameterExpression !) + // Create the lambda expression (note that expression must be a ParameterExpression). return Expression.Lambda(block, (ParameterExpression)expression!); } @@ -1944,7 +1944,7 @@ private Expression ParseAsLambda(string id) _textParser.NextToken(); LastLambdaItName = ItName; - var exp = ParseFirstAsConditionalOperator(); + var exp = ParseConditionalOperator(); // Restore previous context and clear internals _internals.Remove(id); @@ -2182,10 +2182,10 @@ private Expression[] ParseArguments() _textParser.NextToken(); } - if (argList.OfType().Count() > 1) - { - throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableSingleRequired); - } + //if (argList.OfType().Count() > 1) + //{ + // throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableSingleRequired); + //} return argList.ToArray(); } diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 44d9f024c..0b897a856 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -67,8 +67,7 @@ internal static class Res public const string OpenCurlyParenExpected = "'{' expected"; public const string OpenParenExpected = "'(' expected"; public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; - public const string OutVariableRequireDiscard = "When using an out variable, a discard '_' is required."; - public const string OutVariableSingleRequired = "Only a single out variable is supported."; + public const string OutKeywordRequiresDiscard = "When using an out variable, a discard '_' is required."; public const string ParseExceptionFormat = "{0} (at index {1})"; public const string SyntaxError = "Syntax error"; public const string TokenExpected = "{0} expected"; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index ee9209818..0c51ea52e 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -35,15 +35,15 @@ private static ParsingConfig CreateParsingConfigForStaticMethodCallTests() } [Fact] - public void ExpressionTests_MethodCall_WithArgument_And_OutArgument() + public void ExpressionTests_MethodCall_WithArgument_And_1_OutArgument() { // Arrange var config = CreateParsingConfigForMethodCallTests(); var users = User.GenerateSampleModels(5); // Act - string un = ""; - var expected = users.Select(u => u.TryParseWithArgument(u.UserName, out un)); + string s = ""; + var expected = users.Select(u => u.TryParseWithArgument(u.UserName, out s)); var result = users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out _)"); // Assert @@ -51,47 +51,50 @@ public void ExpressionTests_MethodCall_WithArgument_And_OutArgument() } [Fact] - public void ExpressionTests_MethodCall_WithoutArgument_And_OutArgument() + public void ExpressionTests_MethodCall_WithArgument_And_2_OutArguments() { // Arrange var config = CreateParsingConfigForMethodCallTests(); var users = User.GenerateSampleModels(5); // Act - string un = ""; - var expected = users.Select(u => u.TryParseWithoutArgument(out un)); - var result = users.AsQueryable().Select(config, "TryParseWithoutArgument(out _)"); + string s = "?"; + int i = -1; + var expected = users.Select(u => u.TryParseWithArgumentAndTwoOut(u.UserName, out s, out i)); + var result = users.AsQueryable().Select(config, "TryParseWithArgumentAndTwoOut(UserName, out _, out _)"); // Assert result.Should().BeEquivalentTo(expected); } [Fact] - public void ExpressionTests_MethodCall_WithArgument_And_NoDiscardForOutArgument_ThrowsException() + public void ExpressionTests_MethodCall_WithoutArgument_And_OutArgument() { // Arrange var config = CreateParsingConfigForMethodCallTests(); var users = User.GenerateSampleModels(5); // Act - Action action = () => users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out x)"); + string s = ""; + var expected = users.Select(u => u.TryParseWithoutArgument(out s)); + var result = users.AsQueryable().Select(config, "TryParseWithoutArgument(out _)"); // Assert - action.Should().Throw().WithMessage("When using an out variable, a discard '_' is required."); + result.Should().BeEquivalentTo(expected); } [Fact] - public void ExpressionTests_MethodCall_WithArgument_And_MultipleOutArgument_ThrowsException() + public void ExpressionTests_MethodCall_WithArgument_And_NoDiscardForOutArgument_ThrowsException() { // Arrange var config = CreateParsingConfigForMethodCallTests(); var users = User.GenerateSampleModels(5); // Act - Action action = () => users.AsQueryable().Select(config, "TryParseWithArgumentAndTwoOut(it.UserName, $out _, $out _)"); + Action action = () => users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out x)"); // Assert - action.Should().Throw().WithMessage("Only a single out variable is supported."); + action.Should().Throw().WithMessage("When using an out variable, a discard '_' is required."); } [Fact] From 3cb4e20ce67e2b803de3b56958726a07e69d0e3a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 10 Sep 2023 10:29:04 +0200 Subject: [PATCH 8/8] func --- .../Parser/ExpressionParser.cs | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 101bdf7b1..ada4b2b95 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -193,7 +193,6 @@ private Expression ParseOutKeyword() return ParseConditionalOperator(); } -#pragma warning disable 0219 internal IList ParseOrdering(bool forceThenBy = false) { var orderings = new List(); @@ -234,7 +233,6 @@ internal IList ParseOrdering(bool forceThenBy = false) _textParser.ValidateToken(TokenId.End, Res.SyntaxError); return orderings; } -#pragma warning restore 0219 // ?: operator private Expression ParseConditionalOperator() @@ -1814,56 +1812,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } -#if NET35 - return Expression.Call(expression, methodToCall, args); -#else - var outParameters = args.OfType().Where(p => p.IsByRef).ToArray(); - if (outParameters.Any()) - { - // A list which is used to store all method arguments. - var newList = new List(); - - // A list which contains the variable expression for the 'out' parameter, and also contains the returnValue variable. - var blockList = new List(); - - foreach (var arg in args) - { - if (arg is ParameterExpression { IsByRef: true } parameterExpression) - { - // Create a variable expression to hold the 'out' parameter. - var variable = Expression.Variable(parameterExpression.Type, parameterExpression.Name); - - newList.Add(variable); - blockList.Add(variable); - } - else - { - newList.Add(arg); - } - } - - // Create a method call expression to call the method - var methodCall = Expression.Call(expression, methodToCall, newList); - - // Create a variable to hold the return value - var returnValue = Expression.Variable(methodToCall.ReturnType); - - // Add this return variable to the blockList - blockList.Add(returnValue); - - // Create the block to return the boolean value. - var block = Expression.Block( - blockList.ToArray(), - Expression.Assign(returnValue, methodCall), - returnValue - ); - - // Create the lambda expression (note that expression must be a ParameterExpression). - return Expression.Lambda(block, (ParameterExpression)expression!); - } - - return Expression.Call(expression, methodToCall, args); -#endif + return CallMethod(expression, methodToCall, args); default: throw ParseError(errorPos, Res.AmbiguousMethodInvocation, id, TypeHelper.GetTypeName(type)); @@ -1928,6 +1877,59 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) throw ParseError(errorPos, Res.UnknownPropertyOrField, id, TypeHelper.GetTypeName(type)); } + private static Expression CallMethod(Expression? expression, MethodInfo methodToCall, Expression[] args) + { +#if NET35 + return Expression.Call(expression, methodToCall, args); +#else + if (!args.OfType().Any(p => p.IsByRef)) + { + return Expression.Call(expression, methodToCall, args); + } + + // A list which is used to store all method arguments. + var newList = new List(); + + // A list which contains the variable expression for the 'out' parameter, and also contains the returnValue variable. + var blockList = new List(); + + foreach (var arg in args) + { + if (arg is ParameterExpression { IsByRef: true } parameterExpression) + { + // Create a variable expression to hold the 'out' parameter. + var variable = Expression.Variable(parameterExpression.Type, parameterExpression.Name); + + newList.Add(variable); + blockList.Add(variable); + } + else + { + newList.Add(arg); + } + } + + // Create a method call expression to call the method + var methodCall = Expression.Call(expression, methodToCall, newList); + + // Create a variable to hold the return value + var returnValue = Expression.Variable(methodToCall.ReturnType); + + // Add this return variable to the blockList + blockList.Add(returnValue); + + // Create the block to return the boolean value. + var block = Expression.Block( + blockList.ToArray(), + Expression.Assign(returnValue, methodCall), + returnValue + ); + + // Create the lambda expression (note that expression must be a ParameterExpression). + return Expression.Lambda(block, (ParameterExpression)expression!); +#endif + } + private Expression ParseAsLambda(string id) { // This might be an internal variable for use within a lambda expression, so store it as such @@ -2101,7 +2103,7 @@ private Expression ParseEnumerable(Expression instance, Type elementType, string private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression, int? arguments = null) { - string argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; + var argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; switch (argumentExpression) {