Skip to content

Commit 6d00742

Browse files
committed
2 parents d5204fd + 8919415 commit 6d00742

File tree

11 files changed

+134
-71
lines changed

11 files changed

+134
-71
lines changed

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

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -270,21 +270,16 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo
270270
return true;
271271
}
272272

273-
private static Expression GetMemberExpression(Expression expression)
273+
public bool ExpressionQualifiesForNullPropagation(Expression expression)
274274
{
275-
if (expression is MemberExpression memberExpression)
276-
{
277-
return memberExpression;
278-
}
279-
280-
if (expression is ParameterExpression parameterExpression)
281-
{
282-
return parameterExpression;
283-
}
275+
return expression is MemberExpression || expression is ParameterExpression || expression is MethodCallExpression;
276+
}
284277

285-
if (expression is MethodCallExpression methodCallExpression)
278+
private Expression GetMemberExpression(Expression expression)
279+
{
280+
if (ExpressionQualifiesForNullPropagation(expression))
286281
{
287-
return methodCallExpression;
282+
return expression;
288283
}
289284

290285
if (expression is LambdaExpression lambdaExpression)
@@ -303,7 +298,7 @@ private static Expression GetMemberExpression(Expression expression)
303298
return null;
304299
}
305300

306-
private static List<Expression> CollectExpressions(bool addSelf, Expression sourceExpression)
301+
private List<Expression> CollectExpressions(bool addSelf, Expression sourceExpression)
307302
{
308303
Expression expression = GetMemberExpression(sourceExpression);
309304

@@ -317,24 +312,31 @@ private static List<Expression> CollectExpressions(bool addSelf, Expression sour
317312
}
318313
}
319314

320-
while (expression is MemberExpression memberExpression)
315+
bool expressionRecognized;
316+
do
321317
{
322-
expression = GetMemberExpression(memberExpression.Expression);
323-
if (expression is MemberExpression)
318+
switch (expression)
324319
{
325-
list.Add(expression);
320+
case MemberExpression memberExpression:
321+
expression = GetMemberExpression(memberExpression.Expression);
322+
expressionRecognized = true;
323+
break;
324+
325+
case MethodCallExpression methodCallExpression:
326+
expression = methodCallExpression.Arguments.First();
327+
expressionRecognized = true;
328+
break;
329+
330+
default:
331+
expressionRecognized = false;
332+
break;
326333
}
327-
}
328334

329-
if (expression is ParameterExpression)
330-
{
331-
list.Add(expression);
332-
}
333-
334-
if (expression is MethodCallExpression)
335-
{
336-
list.Add(expression);
337-
}
335+
if (expressionRecognized && ExpressionQualifiesForNullPropagation(expression))
336+
{
337+
list.Add(expression);
338+
}
339+
} while (expressionRecognized);
338340

339341
return list;
340342
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,20 +1135,20 @@ Expression ParseFunctionNullPropagation()
11351135
throw ParseError(errorPos, Res.NullPropagationRequiresCorrectArgs);
11361136
}
11371137

1138-
if (args[0] is MemberExpression memberExpression)
1138+
if (_expressionHelper.ExpressionQualifiesForNullPropagation(args[0]))
11391139
{
11401140
bool hasDefaultParameter = args.Length == 2;
11411141
Expression expressionIfFalse = hasDefaultParameter ? args[1] : Expression.Constant(null);
11421142

1143-
if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(memberExpression, hasDefaultParameter, out Expression generatedExpression))
1143+
if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(args[0], hasDefaultParameter, out Expression generatedExpression))
11441144
{
1145-
return GenerateConditional(generatedExpression, memberExpression, expressionIfFalse, errorPos);
1145+
return GenerateConditional(generatedExpression, args[0], expressionIfFalse, errorPos);
11461146
}
11471147

1148-
return memberExpression;
1148+
return args[0];
11491149
}
11501150

1151-
throw ParseError(errorPos, Res.NullPropagationRequiresMemberExpression);
1151+
throw ParseError(errorPos, Res.NullPropagationRequiresValidExpression);
11521152
}
11531153

11541154
// Is(...) function

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal interface IExpressionHelper
3030

3131
bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, bool addSelf, out Expression generatedExpression);
3232

33+
bool ExpressionQualifiesForNullPropagation(Expression expression);
34+
3335
void WrapConstantExpression(ref Expression argument);
3436
}
3537
}

src/System.Linq.Dynamic.Core/Res.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ internal static class Res
5252
public const string NoParentInScope = "No 'parent' is in scope";
5353
public const string NoRootInScope = "No 'root' is in scope";
5454
public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments";
55-
public const string NullPropagationRequiresMemberExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression";
55+
public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression";
5656
public const string OpenBracketExpected = "'[' expected";
5757
public const string OpenCurlyParenExpected = "'{' expected";
5858
public const string OpenParenExpected = "'(' expected";

src/System.Linq.Dynamic.Core/Validation/Check.cs

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
using System.Collections.Generic;
55
using System.Diagnostics;
66
using JetBrains.Annotations;
7-
using System.Reflection;
7+
// using System.Reflection;
88

99
// Copied from https://github.com/aspnet/EntityFramework/blob/dev/src/Shared/Check.cs
1010
namespace System.Linq.Dynamic.Core.Validation
1111
{
1212
[DebuggerStepThrough]
1313
internal static class Check
1414
{
15-
public static T Condition<T>([NoEnumeration] T value, [NotNull] Predicate<T> condition, [InvokerParameterName] [NotNull] string parameterName)
15+
public static T Condition<T>([ValidatedNotNull, NoEnumeration] T value, [ValidatedNotNull, NotNull] Predicate<T> condition, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
1616
{
1717
NotNull(condition, nameof(condition));
1818

@@ -27,7 +27,7 @@ public static T Condition<T>([NoEnumeration] T value, [NotNull] Predicate<T> con
2727
}
2828

2929
[ContractAnnotation("value:null => halt")]
30-
public static T NotNull<T>([NoEnumeration] T value, [InvokerParameterName] [NotNull] string parameterName)
30+
public static T NotNull<T>([ValidatedNotNull, NoEnumeration] T value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
3131
{
3232
if (ReferenceEquals(value, null))
3333
{
@@ -42,8 +42,8 @@ public static T NotNull<T>([NoEnumeration] T value, [InvokerParameterName] [NotN
4242
[ContractAnnotation("value:null => halt")]
4343
public static T NotNull<T>(
4444
[NoEnumeration] T value,
45-
[InvokerParameterName] [NotNull] string parameterName,
46-
[NotNull] string propertyName)
45+
[InvokerParameterName, ValidatedNotNull, NotNull] string parameterName,
46+
[ValidatedNotNull, NotNull] string propertyName)
4747
{
4848
if (ReferenceEquals(value, null))
4949
{
@@ -56,23 +56,23 @@ public static T NotNull<T>(
5656
return value;
5757
}
5858

59-
[ContractAnnotation("value:null => halt")]
60-
public static IList<T> NotEmpty<T>(IList<T> value, [InvokerParameterName] [NotNull] string parameterName)
61-
{
62-
NotNull(value, parameterName);
59+
//[ContractAnnotation("value:null => halt")]
60+
//public static IList<T> NotEmpty<T>(IList<T> value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
61+
//{
62+
// NotNull(value, parameterName);
6363

64-
if (value.Count == 0)
65-
{
66-
NotEmpty(parameterName, nameof(parameterName));
64+
// if (value.Count == 0)
65+
// {
66+
// NotEmpty(parameterName, nameof(parameterName));
6767

68-
throw new ArgumentException(CoreStrings.CollectionArgumentIsEmpty(parameterName));
69-
}
68+
// throw new ArgumentException(CoreStrings.CollectionArgumentIsEmpty(parameterName));
69+
// }
7070

71-
return value;
72-
}
71+
// return value;
72+
//}
7373

7474
[ContractAnnotation("value:null => halt")]
75-
public static string NotEmpty(string value, [InvokerParameterName] [NotNull] string parameterName)
75+
public static string NotEmpty(string value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
7676
{
7777
Exception e = null;
7878
if (ReferenceEquals(value, null))
@@ -94,20 +94,19 @@ public static string NotEmpty(string value, [InvokerParameterName] [NotNull] str
9494
return value;
9595
}
9696

97-
public static string NullButNotEmpty(string value, [InvokerParameterName] [NotNull] string parameterName)
98-
{
99-
if (!ReferenceEquals(value, null)
100-
&& (value.Length == 0))
101-
{
102-
NotEmpty(parameterName, nameof(parameterName));
97+
//public static string NullButNotEmpty(string value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
98+
//{
99+
// if (!ReferenceEquals(value, null) && value.Length == 0)
100+
// {
101+
// NotEmpty(parameterName, nameof(parameterName));
103102

104-
throw new ArgumentException(CoreStrings.ArgumentIsEmpty(parameterName));
105-
}
103+
// throw new ArgumentException(CoreStrings.ArgumentIsEmpty(parameterName));
104+
// }
106105

107-
return value;
108-
}
106+
// return value;
107+
//}
109108

110-
public static IList<T> HasNoNulls<T>(IList<T> value, [InvokerParameterName] [NotNull] string parameterName)
109+
public static IList<T> HasNoNulls<T>(IList<T> value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
111110
where T : class
112111
{
113112
NotNull(value, parameterName);
@@ -122,16 +121,16 @@ public static IList<T> HasNoNulls<T>(IList<T> value, [InvokerParameterName] [Not
122121
return value;
123122
}
124123

125-
public static Type ValidEntityType(Type value, [InvokerParameterName] [NotNull] string parameterName)
126-
{
127-
if (!value.GetTypeInfo().IsClass)
128-
{
129-
NotEmpty(parameterName, nameof(parameterName));
124+
//public static Type ValidEntityType(Type value, [InvokerParameterName, ValidatedNotNull, NotNull] string parameterName)
125+
//{
126+
// if (!value.GetTypeInfo().IsClass)
127+
// {
128+
// NotEmpty(parameterName, nameof(parameterName));
130129

131-
throw new ArgumentException(CoreStrings.InvalidEntityType(value, parameterName));
132-
}
130+
// throw new ArgumentException(CoreStrings.InvalidEntityType(value, parameterName));
131+
// }
133132

134-
return value;
135-
}
133+
// return value;
134+
//}
136135
}
137136
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace System.Linq.Dynamic.Core.Validation
2+
{
3+
/// <summary>
4+
/// To fix 'xxx' is null on at least one execution path. See also https://rules.sonarsource.com/csharp/RSPEC-3900.
5+
/// </summary>
6+
internal class ValidatedNotNullAttribute : Attribute
7+
{
8+
}
9+
}

test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
4444
<HintPath>..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll</HintPath>
4545
</Reference>
46+
<Reference Include="FluentAssertions, Version=5.10.3.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
47+
<HintPath>..\..\packages\FluentAssertions.5.10.3\lib\net45\FluentAssertions.dll</HintPath>
48+
</Reference>
4649
<Reference Include="Linq.PropertyTranslator.Core, Version=1.0.3.0, Culture=neutral, processorArchitecture=MSIL">
4750
<HintPath>..\..\packages\Linq.PropertyTranslator.Core.1.0.3.0\lib\net452\Linq.PropertyTranslator.Core.dll</HintPath>
4851
</Reference>

test/EntityFramework.DynamicLinq.Tests.net452/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<packages>
33
<package id="Castle.Core" version="4.4.0" targetFramework="net452" />
44
<package id="EntityFramework" version="6.1.3" targetFramework="net452" />
5+
<package id="FluentAssertions" version="5.10.3" targetFramework="net452" />
56
<package id="Linq.PropertyTranslator.Core" version="1.0.3.0" targetFramework="net452" />
67
<package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net452" />
78
<package id="Microsoft.AspNet.Identity.EntityFramework" version="2.2.1" targetFramework="net452" />

test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
3536
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
3637
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
3738
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
66
using System.Linq.Expressions;
77
using System.Reflection;
8+
using FluentAssertions;
89
using Xunit;
910
using User = System.Linq.Dynamic.Core.Tests.Helpers.Models.User;
1011

@@ -14,10 +15,18 @@ public class DynamicExpressionParserTests
1415
{
1516
private class MyClass
1617
{
18+
public List<string> MyStrings { get; set; }
19+
20+
public List<MyClass> MyClasses { get; set; }
21+
1722
public int Foo()
1823
{
1924
return 42;
2025
}
26+
27+
public string Name { get; set; }
28+
29+
public MyClass Child { get; set; }
2130
}
2231

2332
private class ComplexParseLambda1Result
@@ -1044,5 +1053,39 @@ public void DynamicExpressionParser_ParseLambda_SupportEnumerationStringComparis
10441053
// Assert
10451054
Check.That(result).IsEqualTo(expectedResult);
10461055
}
1056+
1057+
[Fact]
1058+
public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression()
1059+
{
1060+
// Arrange
1061+
var dataSource = new MyClass();
1062+
1063+
var expressionText = "np(MyClasses.FirstOrDefault())";
1064+
1065+
// Act
1066+
LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(MyClass), expressionText);
1067+
Delegate del = expression.Compile();
1068+
MyClass result = del.DynamicInvoke(dataSource) as MyClass;
1069+
1070+
// Assert
1071+
result.Should().BeNull();
1072+
}
1073+
1074+
[Theory]
1075+
[InlineData("np(MyClasses.FirstOrDefault().Name)")]
1076+
[InlineData("np(MyClasses.FirstOrDefault(Name == \"a\").Name)")]
1077+
public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression_With_Property(string expressionText)
1078+
{
1079+
// Arrange
1080+
var dataSource = new MyClass();
1081+
1082+
// Act
1083+
LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(string), expressionText);
1084+
Delegate del = expression.Compile();
1085+
string result = del.DynamicInvoke(dataSource) as string;
1086+
1087+
// Assert
1088+
result.Should().BeNull();
1089+
}
10471090
}
10481091
}

0 commit comments

Comments
 (0)