Skip to content

Commit 0ee9f0e

Browse files
VS-152: Support Null-Forgiving Operators (#75)
1 parent 626bb3f commit 0ee9f0e

File tree

5 files changed

+192
-3
lines changed

5 files changed

+192
-3
lines changed

src/MongoDB.Analyzer/Core/Utilities/SyntaxFactoryUtilities.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ public static SimpleNameSyntax GetUnderlyingNameSyntax(SyntaxNode syntax)
107107
{
108108
while (true)
109109
{
110-
var trimmedExpression = TrimElementAccessAndInvocationSyntax(memberAccessExpressionSyntax.Expression);
110+
var trimmedExpression = TrimElementAccessNullOperatorAndInvocationSyntax(memberAccessExpressionSyntax.Expression);
111111
if (trimmedExpression is not MemberAccessExpressionSyntax nextMemberAccessExpressionSyntax)
112112
{
113-
underlyingNameSyntax = memberAccessExpressionSyntax.Expression as SimpleNameSyntax;
113+
underlyingNameSyntax = trimmedExpression as SimpleNameSyntax;
114114

115115
break;
116116
}
@@ -145,7 +145,7 @@ public static MemberAccessExpressionSyntax SimpleMemberAccess(string source, str
145145
SyntaxFactory.IdentifierName(source),
146146
SyntaxFactory.IdentifierName(member));
147147

148-
public static ExpressionSyntax TrimElementAccessAndInvocationSyntax(ExpressionSyntax expressionSyntax)
148+
public static ExpressionSyntax TrimElementAccessNullOperatorAndInvocationSyntax(ExpressionSyntax expressionSyntax)
149149
{
150150
var result = expressionSyntax;
151151

@@ -159,6 +159,11 @@ public static ExpressionSyntax TrimElementAccessAndInvocationSyntax(ExpressionSy
159159
{
160160
result = invocationExpressionSyntax.Expression;
161161
}
162+
else if (result is PostfixUnaryExpressionSyntax postfixUnaryExpressionSyntax &&
163+
postfixUnaryExpressionSyntax.Kind() == SyntaxKind.SuppressNullableWarningExpression)
164+
{
165+
result = postfixUnaryExpressionSyntax.Operand;
166+
}
162167
else
163168
{
164169
break;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2021-present MongoDB Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License")
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using MongoDB.Analyzer.Tests.Common.DataModel;
16+
using MongoDB.Driver;
17+
18+
namespace MongoDB.Analyzer.Tests.Common.TestCases.Builders
19+
{
20+
public sealed class BuildersNullForgivingOperators : TestCasesBase
21+
{
22+
[BuildersMQL("{ \"StringField\" : \"value\" }")]
23+
[BuildersMQL("{ \"Age\" : 1 }")]
24+
public void Identifier()
25+
{
26+
_ = Builders<ClassWithObjectId>.Filter.Eq(c => c!.StringField, "value");
27+
_ = Builders<User>.IndexKeys.Ascending(x => x!.Age);
28+
}
29+
30+
[BuildersMQL("{ \"Name\" : \"name\" }")]
31+
[BuildersMQL("{ \"Name\" : null }")]
32+
public void Literals()
33+
{
34+
_ = Builders<Person>.Filter.Eq(p => p.Name, "name"!);
35+
_ = Builders<Person>.Filter.Eq(p => p.Name, null!);
36+
}
37+
38+
[BuildersMQL("{ \"Name\" : GetNullableString() }")]
39+
[BuildersMQL("{ \"Name\" : name }")]
40+
public void Methods()
41+
{
42+
_ = Builders<Person>.Filter.Eq(p => p.Name, GetNullableString()!);
43+
44+
string? name = GetNullableString();
45+
_ = Builders<Person>.Filter.Eq(p => p.Name, name!);
46+
}
47+
48+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
49+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
50+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
51+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
52+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
53+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
54+
[BuildersMQL("{ \"Address.City\" : \"Boston\" }")]
55+
public void Nested()
56+
{
57+
_ = Builders<Person>.Filter.Eq(p => p!.Address!.City!, "Boston");
58+
_ = Builders<Person>.Filter.Eq(p => p!.Address.City!, "Boston");
59+
_ = Builders<Person>.Filter.Eq(p => p!.Address!.City, "Boston");
60+
_ = Builders<Person>.Filter.Eq(p => p.Address!.City!, "Boston");
61+
_ = Builders<Person>.Filter.Eq(p => p.Address!.City, "Boston");
62+
_ = Builders<Person>.Filter.Eq(p => p.Address.City!, "Boston");
63+
_ = Builders<Person>.Filter.Eq(p => p!.Address.City, "Boston");
64+
}
65+
66+
[BuildersMQL("{ \"Age\" : 1 }")]
67+
public void Property()
68+
{
69+
_ = Builders<User>.IndexKeys.Ascending(x => x.Age!);
70+
}
71+
72+
[BuildersMQL("{ \"Name\" : name }")]
73+
[BuildersMQL("{ \"LastName\" : lastName }")]
74+
public void Variables()
75+
{
76+
string? name = "name";
77+
_ = Builders<Person>.Filter.Eq(p => p.Name, name!);
78+
79+
string? lastName = null;
80+
_ = Builders<Person>.Filter.Eq(p => p.LastName, lastName!);
81+
}
82+
83+
private string? GetNullableString() => "string";
84+
}
85+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2021-present MongoDB Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License")
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
using System.Linq;
17+
using System.Xml.Linq;
18+
using MongoDB.Analyzer.Tests.Common.DataModel;
19+
using MongoDB.Driver.Linq;
20+
21+
namespace MongoDB.Analyzer.Tests.Common.TestCases.Linq
22+
{
23+
public sealed class LinqNullForgivingOperators : TestCasesBase
24+
{
25+
[MQL("aggregate([{ \"$match\" : { \"StringField\" : \"value\" } }])")]
26+
[MQL("aggregate([{ \"$match\" : { \"Name\" : \"Bob\" } }])")]
27+
public void Identifier()
28+
{
29+
_ = GetMongoQueryable<ClassWithObjectId>().Where(c => c!.StringField == "value");
30+
_ = GetMongoQueryable<User>().Where(u => u!.Name == "Bob");
31+
}
32+
33+
[MQL("aggregate([{ \"$match\" : { \"FieldString\" : \"Bob\", \"PropertyArray.0\" : 1 } }, { \"$match\" : { \"FieldMixedDataMembers.FieldString\" : \"Alice\" } }])")]
34+
[MQL("aggregate([{ \"$match\" : { \"StringField\" : null } }])")]
35+
public void Literals()
36+
{
37+
_ = GetMongoQueryable<MixedDataMembers>()
38+
.Where(u => u.FieldString == "Bob"! && u.PropertyArray[0] == 1)
39+
.Where(u => u.FieldMixedDataMembers.FieldString == "Alice"!);
40+
41+
_ = GetMongoQueryable<ClassWithObjectId>().Where(c => c.StringField == null!);
42+
}
43+
44+
[MQL("aggregate([{ \"$match\" : { \"Name\" : GetNullableString() } }])")]
45+
[MQL("aggregate([{ \"$match\" : { \"Name\" : name } }])")]
46+
public void Methods()
47+
{
48+
_ = GetMongoQueryable<User>().Where(u => u.Name == GetNullableString()!);
49+
50+
string? name = GetNullableString();
51+
_ = GetMongoQueryable<User>().Where(u => u.Name == name!);
52+
}
53+
54+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
55+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
56+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
57+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
58+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
59+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
60+
[MQL("aggregate([{ \"$match\" : { \"Address.City\" : \"Boston\" } }])")]
61+
public void Nested()
62+
{
63+
_ = GetMongoQueryable<Person>().Where(p => p!.Address!.City! == "Boston");
64+
_ = GetMongoQueryable<Person>().Where(p => p!.Address!.City == "Boston");
65+
_ = GetMongoQueryable<Person>().Where(p => p!.Address.City! == "Boston");
66+
_ = GetMongoQueryable<Person>().Where(p => p.Address!.City! == "Boston");
67+
_ = GetMongoQueryable<Person>().Where(p => p.Address.City! == "Boston");
68+
_ = GetMongoQueryable<Person>().Where(p => p.Address!.City == "Boston");
69+
_ = GetMongoQueryable<Person>().Where(p => p!.Address.City == "Boston");
70+
}
71+
72+
[MQL("aggregate([{ \"$match\" : { \"Name\" : \"Bob\" } }])")]
73+
public void Property()
74+
{
75+
_ = GetMongoQueryable<User>().Where(u => u.Name! == "Bob");
76+
}
77+
78+
[MQL("aggregate([{ \"$match\" : { \"Name\" : name } }])")]
79+
[MQL("aggregate([{ \"$match\" : { \"LastName\" : lastName } }])")]
80+
public void Variables()
81+
{
82+
string? name = "name";
83+
_ = GetMongoQueryable<Person>().Where(p => p.Name == name!);
84+
85+
string? lastName = null;
86+
_ = GetMongoQueryable<Person>().Where(p => p.LastName == lastName!);
87+
}
88+
89+
private string? GetNullableString() => "string";
90+
}
91+
}

tests/MongoDB.Analyzer.Tests/Builders/BuildersTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public sealed class BuildersTests : DiagnosticsTestCasesRunner
7878
[CodeBasedTestCasesSource(typeof(BuildersNullables))]
7979
public Task Nullables(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
8080

81+
[DataTestMethod]
82+
[CodeBasedTestCasesSource(typeof(BuildersNullForgivingOperators))]
83+
public Task NullForgivingOperators(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
84+
8185
[DataTestMethod]
8286
[CodeBasedTestCasesSource(typeof(BuildersProjection))]
8387
public Task Projection(DiagnosticTestCase testCase) => VerifyTestCase(testCase);

tests/MongoDB.Analyzer.Tests/Linq/LinqTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public sealed class LinqTests : DiagnosticsTestCasesRunner
6666
[CodeBasedTestCasesSource(typeof(LinqNullables))]
6767
public Task Nullables(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
6868

69+
[DataTestMethod]
70+
[CodeBasedTestCasesSource(typeof(LinqNullForgivingOperators))]
71+
public Task NullForgivingOperators(DiagnosticTestCase testCase) => VerifyTestCase(testCase);
72+
6973
[DataTestMethod]
7074
[CodeBasedTestCasesSource(typeof(LinqQualifiedNames))]
7175
public Task QualifiedNames(DiagnosticTestCase testCase) => VerifyTestCase(testCase);

0 commit comments

Comments
 (0)